[Diario de aprendizaje de algoritmos 02] Siete búsquedas

Tabla de contenido

Referencia: búsqueda en tabla de árbol
PD: todos los códigos de este artículo se han verificado con 704 preguntas (algunos algoritmos de búsqueda expirarán): 704 preguntas

1. Información general

1.1 Clasificación

Categoría de búsqueda aproximada

1.2 Métodos de búsqueda comunes

Existen muchos métodos de búsqueda, los más comunes son: búsqueda secuencial, búsqueda binaria, búsqueda por interpolación, búsqueda de Fibonacci, búsqueda de bloques, búsqueda de hash y búsqueda de tabla de árbol.

1.3 Longitud media de búsqueda (ASL)

Longitud promedio de búsqueda (ASL): el valor esperado de la cantidad de palabras clave que deben compararse con la clave especificada, que se denomina longitud de búsqueda promedio del algoritmo de búsqueda cuando la búsqueda es exitosa.

Para una tabla de búsqueda que contiene n elementos de datos, la longitud de búsqueda promedio para una búsqueda exitosa es: ASL = la suma de Pi*Ci.

Pi: la probabilidad del i-ésimo elemento de datos en la tabla de búsqueda.
Ci: El número de comparaciones que se han realizado al encontrar el i-ésimo elemento de datos.

2 Búsqueda secuencial

2.1 Descripción del algoritmo

La búsqueda secuencial es un método de búsqueda simple, también llamado búsqueda lineal. Su idea básica es comenzar desde un extremo de la lista, escanear la lista lineal secuencialmente y comparar los elementos escaneados con el valor dado uno por uno hasta encontrar elementos iguales o escanear toda la lista lineal.

2.2 Implementación del código

int SequentialSearch(int[] nums,int target){
    
    
	for(int i=0;i<nums.Length;i++){
    
    
		if(nums[i]==target) return i;
	}
	return -1;
}

2.3 Análisis de desempeño

Suponiendo que la probabilidad de cada elemento de datos es igual, la longitud promedio de la búsqueda cuando la búsqueda es exitosa es: ASL = 1/n*(1+2+3+…+n) = (n+1)/2. Cuando la búsqueda no tiene éxito, se requieren n + 1 comparaciones y la complejidad del tiempo es O (n).

Longitud promedio de búsqueda ASL complejidad del tiempo
( norte + 1 ) / 2 ( norte + 1 )/2( n.+1 ) /2 O (n) O(n)O ( n )

3 búsqueda binaria

3.1 Descripción del algoritmo

La búsqueda binaria, también conocida como búsqueda binaria, es un algoritmo de búsqueda ordenado.

Su idea básica es: usar el valor dado k para compararlo primero con la palabra clave del nodo intermedio. El nodo intermedio divide la tabla lineal en dos subtablas. Si son iguales, la búsqueda es exitosa; si no son iguales Luego se comparan k y el nodo intermedio. El resultado de la comparación de la palabra clave determina qué subtabla buscar a continuación, y así sucesivamente hasta que se encuentra la búsqueda o la búsqueda finaliza y se descubre que no existe tal nodo en el mesa.

3.2 Implementación del código

int BinarySearch(int[] nums,int target){
    
    
    if(nums.Length==0) return -1;
    int left = 0;
    int right = nums.Length-1;
    while(left<=right){
    
    
        int mid = (left+right)/2;
        if(nums[mid]==target) return mid;
        else if(nums[mid]<target) left = mid+1;
        else right = mid-1;
    }
    return -1;
}

3.3 Análisis de desempeño

Longitud promedio de búsqueda ASL complejidad del tiempo
Iniciar sesión 2 (norte + 1) Iniciar sesión_2{(n+1)}iniciar sesión _2( n.+1 ) O(iniciar sesión)

4 Búsqueda por interpolación

4.1 Descripción del algoritmo

El método de búsqueda de diferencias es un algoritmo mejorado basado en el método de dicotomía. A diferencia del método de dicotomía, el método de diferencias no selecciona el valor intermedio. En cambio, la selección adaptativa se realiza en función del número que se va a encontrar, de modo que el valor medio esté más cerca de la clave de la palabra clave, lo que indirectamente puede reducir el número de comparaciones.

int mid = left+(right-left)*(target-nums[left])/(nums[right]-nums[left]);

4.2 Implementación del código

int InterpolationSearch(int[] nums,int target){
    
    
    if(nums.Length==0) return -1;
    int left = 0;
    int right = nums.Length-1;
    while(left<=right){
    
    
        if(left==right){
    
    
            if(nums[left]==target) return left;
            else return -1;
        }
        int mid = left+(right-left)*(target-nums[left])/(nums[right]-nums[left]);
        if(nums[mid]==target) return mid;
        else if(nums[mid]<target) left = mid+1;
        else right = mid-1;
    }
    return -1;
}

4.3 Análisis de desempeño

La complejidad temporal promedio de la búsqueda por interpolación es Θ (loglogn). Si la matriz no está distribuida uniformemente, la complejidad de la búsqueda por interpolación degenerará a complejidad lineal.

Longitud promedio de búsqueda ASL complejidad del tiempo
levemente O(iniciar sesión)

5 hallazgos de Fibonacci

5.1 Descripción del algoritmo

La secuencia de Fibonacci es 1,1,2,3,5,8,13..., a partir del tercer número, los siguientes números son iguales a la suma de los dos primeros números, y la búsqueda de Fibonacci utiliza Fibonacci.La secuencia de Bonacci se utiliza para buscar.

La búsqueda de Fibonacci se distingue de la búsqueda binaria y la búsqueda por interpolación. Las ideas específicas son similares, la principal diferencia radica en la selección de la interpolación.

parte de inferencia

F(k) = F(k-1)+F(k-2);
F(k)-1 = (F(k-1)-1)+(F(k-2)-1)+1;
即数组长度=左半部分+右半部分+mid
所以如果待查找数组长度为F(k)-1,mid=left+F(k-1)-1;

parte del juicio

if(target==nums[mid]) return mid;
else if(target<nums[mid]) return FibnacciSearch(left,mid-1,k-1);
else return FibnacciSearch(mid+1,right,k-2);

5.2 Implementación del código

int FibonacciSearch(int[] nums,int target){
    
    
    if(nums.Length==0) return -1;
    int left = 0;
    int right = nums.Length-1;
    int len = nums.Length;
    int k = 0;
    while(len>(Fib(k)-1)) k++;
    int[] temp = new int[Fib(k)-1];
    nums.CopyTo(temp,0);
    for(int i=len;i<temp.Length;i++) temp[i] = nums[right];
    return FibonacciSearch(temp,target,left,right,k-1);
}
int FibonacciSearch(int[] temp,int target,int left,int right,int k){
    
    
    while(left<=right){
    
    
        int mid = left+(Fib(k)-1);
        if(temp[mid]==target){
    
    
            if(mid<=right) return mid;
            else return right;  //说明temp[mid]是扩充部分。
        }else if(temp[mid]<target){
    
    
            left = mid+1;
            k-=2;  //右半部分
        }else{
    
    
            right = mid-1;
            k-=1;  //左半部分
        }
    }
    return -1;
}
int Fib(int n){
    
    
    if(n<=1) return n;
    int left = 0;
    int right = 1;
    for(int i=2;i<=n;i++){
    
    
        int cur = left+right;
        left = right;
        right = cur;
    }
    return right;
}

5.3 Análisis de desempeño

Longitud promedio de búsqueda ASL complejidad del tiempo
levemente O ( iniciar sesión ) O ( iniciar sesión )O ( iniciar sesión ) _ _

5.4 Diferencias entre búsqueda binaria, búsqueda por interpolación y búsqueda de Fibonacci

La búsqueda binaria, la búsqueda por interpolación y la búsqueda de Fibonacci son algoritmos de búsqueda para matrices ordenadas, pero sus métodos de búsqueda son diferentes. Entre ellos, la búsqueda binaria es la más básica y su punto medio es la posición media de la matriz, es decir, medio = (bajo + alto) / 2. La búsqueda por interpolación utiliza el pensamiento humano para optimizar la mitad de la búsqueda, de modo que la posición de búsqueda esté lo más cerca posible de la posición objetivo en lugar de comenzar desde el medio de la matriz. La búsqueda de Fibonacci es un método de búsqueda que determina el punto de la sección áurea basándose en la secuencia de Fibonacci, lo que puede mejorar la eficiencia hasta cierto punto.

La búsqueda binaria y la búsqueda por interpolación son adecuadas para tablas estáticas, mientras que la búsqueda de Fibonacci es adecuada para tablas dinámicas.

La complejidad temporal de la búsqueda binaria y la búsqueda por interpolación es O (logn), la complejidad temporal de la búsqueda por interpolación es O (loglogn) cuando la distribución de datos es uniforme y es O (n) cuando la distribución de datos es desigual.

La complejidad temporal de la búsqueda de Fibonacci es la misma que la de la búsqueda binaria, que es O (logn), pero su ventaja es que solo implica operaciones de suma y resta, en lugar de división, lo que llevará más tiempo. Por lo tanto, la búsqueda de Fibonacci se ejecuta El tiempo de esa búsqueda es teóricamente menor que el de la búsqueda binaria.

búsqueda de 6 bloques

6.1 Descripción del algoritmo

La búsqueda bloqueada también se denomina búsqueda secuencial por índice, que es un método mejorado de búsqueda secuencial.

La idea de la búsqueda de bloques es dividir n elementos de datos en m bloques (m ≤ n) "en orden de bloques". Los nodos de cada bloque no tienen que estar en orden, pero los bloques deben estar "ordenados por bloque", es decir, la clave de cualquier elemento del primer bloque debe ser menor que la clave de cualquier elemento del segundo bloque; y Cualquier elemento en el bloque 2 debe ser más pequeño que cualquier elemento en el bloque 3,...

Proceso algorítmico:

1. Primero seleccione la palabra clave más grande en cada bloque para formar una tabla de índice.
2. La búsqueda se divide en dos partes: primero realizar una búsqueda binaria o secuencial en la tabla de índice para determinar en qué bloque se encuentra el registro a buscar, luego utilizar el método secuencial para buscar en el bloque determinado.

Buscar en trozos

6.2 Implementación del código

public struct Block{
    
    
    public int start;
    public int max;
}
const int BLOCK_COUNT = 3;  //在分块的选择上,一般让分块的大小尽量大,而分块的数量尽量少。
public int Search(int[] nums, int target) {
    
    
    //0.处理特殊情况
    if(nums.Length<3){
    
    
        for(int i=0;i<nums.Length;i++) if(target==nums[i]) return i;
        return -1;
    }
    //1.初始化
    int len = nums.Length;
    int blockSize = len/BLOCK_COUNT;
    int remain = len%BLOCK_COUNT;
    Block[] blocks = new Block[BLOCK_COUNT];
    for(int i=0;i<BLOCK_COUNT;i++){
    
    
        int size = 0;
        if(i==0){
    
    
            blocks[i].start = i*blockSize;
            size = blockSize+remain;
        }
        else{
    
    
            blocks[i].start = i*blockSize+remain;
            size = blockSize;
        }
        blocks[i].max = FindMax(nums,blocks[i].start,size);
    }
    return BlockSearch(nums,target,blocks,blockSize,remain);
}
int BlockSearch(int[] nums,int target,Block[] blocks,int blockSize,int remain){
    
    
    int i=0;
    while(i<blocks.Length&&target>blocks[i].max) i++;
    if(i==blocks.Length) return -1;
    if(i==0) blockSize+=remain;
    for(int j=blocks[i].start;j<nums.Length&&j<(blocks[i].start+blockSize);j++){
    
    
	    if(nums[j]==target) return j;
    }
    return -1;
}
int FindMax(int[] nums,int start,int size){
    
    
    int max = nums[start];
    for(int i=start+1;i<nums.Length&&i<(start+size);i++){
    
    
        if(nums[i]>max) max = nums[i];
    }
    return max;
}

7 Búsqueda en tabla de árbol

7.1 Búsqueda de árbol binario (BST)

7.1.1 Árbol de búsqueda binaria

Un árbol de búsqueda binario (también llamado árbol de búsqueda binario) es un árbol vacío o un árbol binario con las siguientes propiedades:

1. Si el subárbol izquierdo de cualquier nodo no está vacío, entonces los valores de todos los nodos en el subárbol izquierdo son menores que el valor de su nodo raíz.
2. Si el subárbol derecho de cualquier nodo no está vacío, entonces los valores de todos los nodos en el subárbol derecho son mayores que el valor de su nodo raíz.
3. Los subárboles izquierdo y derecho de cualquier nodo también son árboles de búsqueda binarios, respectivamente.

Propiedades de los árboles de búsqueda binarios: al realizar un recorrido en orden en un árbol de búsqueda binario, se puede obtener una secuencia ordenada.
árbol de búsqueda binaria

7.1.2 Descripción del algoritmo

El algoritmo de búsqueda de árbol binario es un algoritmo de búsqueda basado en árboles binarios. Su idea es construir una secuencia ordenada en un árbol binario. Al comparar la relación de tamaño entre el elemento de búsqueda y el nodo actual, el rango de búsqueda se reduce continuamente hasta que el objetivo elemento encontrado o determinado no existe. La ventaja del algoritmo de búsqueda de árbol binario es que puede encontrar rápidamente el elemento objetivo en una secuencia ordenada, pero es menos eficiente cuando busca el elemento objetivo en una secuencia desordenada.

7.1.3 Implementación del código

class TreeNode{
    
    
    public int val;
    public int index;
    public TreeNode left;
    public TreeNode right;
    public TreeNode(int val,int index){
    
    
        this.val = val;
        this.index = index;
    }
}
public int Search(int[] nums, int target) {
    
    
    //1.建树
    TreeNode root = null;
    for(int i=0;i<nums.Length;i++){
    
    
        root = CreateBST(root,nums[i],i);
    }
    //2.搜索
    return BSTSearch(root,target);
}
TreeNode CreateBST(TreeNode node,int val,int index){
    
    
    if(node==null) return new TreeNode(val,index);
    if(val<node.val) node.left = CreateBST(node.left,val,index);
    else node.right = CreateBST(node.right,val,index);
    return node;
}
int BSTSearch(TreeNode node,int target){
    
    
    if(node==null) return -1;
    if(target==node.val) return node.index;
    else if(target<node.val) return BSTSearch(node.left,target);
    else return BSTSearch(node.right,target);
}

7.1.4 Análisis de desempeño

Al igual que la búsqueda binaria, la complejidad temporal tanto de la inserción como de la búsqueda es O (logn), pero en el peor de los casos seguirá habiendo una complejidad temporal O (n). La razón es que el árbol no está equilibrado cuando se insertan y eliminan elementos.

A partir de la optimización de los árboles de búsqueda binarios, se pueden obtener otros algoritmos de búsqueda de tablas de árboles, como árboles equilibrados, árboles rojo-negro y otros algoritmos eficientes.

7.2 Árbol de búsqueda equilibrado Árbol AVL

PD: Los árboles AVL generalmente no se utilizan, pero se agregan aquí para popularizarlos.

7.2.1 árbol AVL

El árbol AVL es el primer árbol de búsqueda binario autoequilibrado. Debido a que el valor absoluto de la diferencia de altura entre los subárboles izquierdo y derecho de cualquier nodo en el árbol AVL no excede 1, el árbol AVL también se denomina árbol de altura equilibrada. . Un árbol AVL es esencialmente un árbol de búsqueda binario con condiciones equilibradas.

Cada nodo del árbol AVL tiene un factor de equilibrio, que puede ser -1, 0 o 1. El factor de equilibrio es la altura del subárbol izquierdo menos la altura del subárbol derecho.

Las características de los árboles AVL son:

1. La ruta más larga desde el nodo raíz hasta el nodo hoja no es más del doble de la ruta más corta.
2. La diferencia de altura entre los dos subárboles de cualquier nodo no supera 1.

7.2.2 Autoequilibrio del árbol AVL

El autoequilibrio del árbol AVL se logra mediante el comportamiento de rotación. Cuando la diferencia de altura entre los subárboles izquierdo y derecho de un nodo en el árbol AVL es mayor que 1, se requiere una operación de rotación para mantener el equilibrio del árbol AVL. El comportamiento de rotación generalmente ocurre durante la inserción y eliminación.

La operación de rotación del árbol AVL se divide en dos tipos: rotación a la izquierda y rotación a la derecha. La rotación a la izquierda se refiere a tomar un nodo como punto de apoyo y rotar su subárbol derecho hacia la izquierda para que se convierta en el nodo padre del nodo y el nodo se convierta en el nodo raíz de su subárbol izquierdo. La rotación hacia la derecha es lo contrario.

Rotación a la derecha

7.2.3 Análisis de desempeño

El rendimiento equilibrado del árbol AVL es muy bueno y la complejidad temporal de las operaciones de inserción, eliminación y búsqueda es todas O (logn).

7.3 Árbol de búsqueda equilibrado Árbol de búsqueda 2-3

7.3.1 Árbol de búsqueda 2-3

Un árbol de búsqueda 2-3 (árbol 2-3) es un árbol vacío o un árbol con las siguientes propiedades:

1. Para el nodo 2, el nodo almacena una clave y el valor correspondiente, así como dos nodos que apuntan a los nodos izquierdo y derecho. El nodo izquierdo también es un nodo 2-3 y todos los valores son más pequeños que la clave. El nodo derecho El punto también es un nodo 2-3 y todos los valores son mayores que la clave.
2. Para 3 nodos, el nodo almacena dos claves y sus valores correspondientes, así como tres nodos que apuntan a la izquierda, al medio y a la derecha. El nodo izquierdo también es un nodo 2-3 y todos los valores son más pequeños que la clave más pequeña entre las dos claves; el nodo del medio también es un nodo 2-3 y el valor clave del nodo medio está entre los dos siguientes nodos punto entre valores clave, el nodo derecho también es un nodo 2-3, y todos los valores clave del nodo son mayores que la clave más grande entre las dos claves.

2-3 árbol
2-3 Encuentra las propiedades de los árboles:

1. Si recorre el árbol de búsqueda 2-3 en el orden medio, puede obtener la secuencia ordenada.
2. En un árbol de búsqueda 2-3 completamente equilibrado, la distancia desde el nodo raíz hasta cada nodo vacío es la misma. (Este es también el concepto de la palabra "equilibrio" en el árbol equilibrado. La distancia más larga desde el nodo raíz hasta el nodo hoja corresponde al peor caso del algoritmo de búsqueda, mientras que la distancia desde el nodo raíz hasta el nodo hoja en el árbol equilibrado es el mismo. El caso malo también tiene complejidad logarítmica).

Árbol equilibrado 2-3

7.3.2 Autoequilibrio de 2-3 árboles

El autoequilibrio de 2 o 3 árboles se logra dividiendo y fusionando nodos. Cuando hay tres elementos en un nodo, se divide en dos nodos, uno que contiene los dos elementos más pequeños y el otro que contiene el elemento más grande. Cuando solo hay un elemento en un nodo, se fusionará con sus nodos hermanos en un solo nodo, asegurando así el equilibrio del árbol 2-3.

7.3.3 Búsqueda e inserción de 2-3 árboles

Soy vago, déjame publicar un enlace: Árbol de búsqueda equilibrado 2-3 árbol

7.3.4 Análisis de desempeño

2-3 La eficiencia de búsqueda del árbol de búsqueda está estrechamente relacionada con la altura:

1. En el peor de los casos, es decir, todos los nodos son nodos de 2 nodos y la eficiencia de búsqueda es O (logn).
2. En el mejor de los casos, todos los nodos son nodos de 3 nodos y la eficiencia de búsqueda es aproximadamente igual a O (0,631 logn).

7.3.5 La diferencia entre árboles AVL y 2-3 árboles

Los árboles AVL y 2-3 árboles son árboles equilibrados, pero se implementan de manera diferente. El árbol AVL es un árbol de búsqueda binario con condiciones equilibradas. La diferencia de altura entre los subárboles izquierdo y derecho de cada nodo no supera 1. El árbol 2-3 es un árbol de búsqueda multidireccional y cada nodo puede tener dos o tres nodos secundarios.

El autoequilibrio de los árboles AVL se logra mediante operaciones de rotación, mientras que el árbol 2-3 mantiene el equilibrio mediante la división y fusión de nodos.

7.4 Árbol de búsqueda equilibrado árbol rojo-negro

7.4.1 Árboles rojo-negros

El árbol de búsqueda 2-3 puede garantizar que el estado equilibrado del árbol se pueda mantener después de insertar elementos. En el peor de los casos, todos los nodos secundarios son de 2 nodos, lo que garantiza la complejidad del tiempo en el peor de los casos. Sin embargo, el árbol 2-3 es más complicado de implementar, por lo que existe una estructura de datos que simplemente implementa el árbol 2-3, es decir, el árbol Rojo-Negro.

La idea del árbol rojo-negro es codificar el árbol de búsqueda 2-3, especialmente para agregar información adicional a los nodos de 3 nodos en el árbol de búsqueda 2-3. En los árboles rojo-negro, los enlaces entre nodos se dividen en dos tipos diferentes: los enlaces rojos se utilizan para vincular dos nodos de 2 nodos para representar un nodo de 3 nodos. Los enlaces negros se utilizan para conectar 2-3 nodos normales. En particular, dos nodos de 2 enlazados en rojo se utilizan para representar un nodo de 3 nodos y están inclinados hacia la izquierda, es decir, un nodo de 2 es el nodo secundario izquierdo de otro nodo de 2. La ventaja de este enfoque es que no es necesario realizar modificaciones durante la búsqueda, que es lo mismo que un árbol de búsqueda binario normal.

árbol negro rojo
Un árbol rojo-negro es un árbol de búsqueda binario autoequilibrado con enlaces rojos y negros que satisface ambos:

1. El nodo rojo se inclina hacia la izquierda.
2. Un nodo no puede tener dos enlaces rojos.
3. La ruta más larga desde el nodo raíz hasta el nodo hoja no excede el doble de la ruta más corta. Para cumplir con esta característica, el árbol rojo-negro requiere que el número de enlaces negros en la ruta desde el nodo raíz hasta todas las hojas Los nodos son iguales. Esto asegura el equilibrio del árbol rojo-negro.

Como se puede ver en la figura siguiente, el árbol rojo-negro es en realidad otra forma de árbol 2-3: si dibujamos la conexión roja horizontalmente, entonces los dos nodos de 2 nodos que vincula son uno de los 3 en el árbol 2-3. 3 árbol.-nodo nodo.
árbol negro rojo 2

7.4.2 Implementación del código

Soy vago, aquí hay un enlace: Árbol de búsqueda equilibrado - Árbol rojo-negro

7.4.3 Autoequilibrio de árboles rojo-negros

Los árboles rojo-negro logran el autoequilibrio mediante nodos codificados por colores y operaciones de rotación. Cuando se inserta o elimina un nodo, si se destruye la naturaleza equilibrada del árbol rojo-negro, la posición y el color del nodo deben ajustarse mediante operaciones de rotación para mantener el equilibrio del árbol rojo-negro.

7.4.4 Aplicación de árboles rojo-negros

1 aplicación

1...SortedDictionary, SortedSet, etc. en NET.
2.java.util.TreeMap, java.util.TreeSet en Java.
3. En C++ STL: mapa, multimapa, multiconjunto;

2 Diccionario ordenado

La capa inferior de SortedDictionary es un árbol rojo-negro, por lo que su complejidad de tiempo de búsqueda es O (logn). Su ventaja sobre el Diccionario es que sus claves están ordenadas. Si necesita ordenar el diccionario, o si necesita realizar operaciones de inserción o eliminación más rápidas en datos no ordenados, SortedDictionary es una mejor opción.

7.4.5 Análisis de desempeño

La altura promedio del árbol rojo-negro es aproximadamente logn. El peor de los casos es que, excepto el camino más a la izquierda, todos están compuestos por nodos de 3 nodos, es decir, la longitud del camino rojo-negro es el doble de la longitud de el camino completamente negro.

El árbol rojo-negro puede considerarse como una implementación del árbol de búsqueda 2-3, que puede garantizar que todavía tenga una complejidad de tiempo logarítmica en el peor de los casos.

7.4.6 Comparación de rendimiento de BST, árbol 2-3 y árbol rojo-negro

Comparación de rendimiento

7.5 Árbol B y árbol B+

7.5.1 Árbol B/Árbol B+

1 árbol B

En informática, un árbol B es una estructura de datos similar a un árbol que puede almacenar datos, ordenarlos y permitir que se ejecuten búsquedas, lecturas secuenciales e inserciones con una complejidad temporal O (log n) y estructuras de datos eliminados. El árbol B, en resumen, es un árbol de búsqueda binario en el que un nodo puede tener más de 2 nodos secundarios. A diferencia de los árboles de búsqueda binarios autoequilibrados, los árboles B optimizan la lectura y escritura de grandes bloques de datos para el sistema. El algoritmo B-tree reduce el proceso intermedio que se experimenta al localizar registros, acelerando así el acceso. Comúnmente utilizado en bases de datos y sistemas de archivos.
árbol B

Árbol 2 B+

El árbol B+ es un árbol deformado del árbol B. Las características del árbol B son:

1. Los nodos que no son nodos hoja solo sirven como índices y la información relacionada con los registros se almacena en los nodos hoja.
2. Todos los nodos de hoja del árbol forman una lista vinculada ordenada y todos los registros se pueden recorrer en el orden de clasificación del código clave.
árbol+b

3 La diferencia entre el árbol B y el árbol B+

La ventaja del árbol B es que, dado que cada nodo del árbol B almacena datos, es posible que la consulta no requiera complejidad O (logn). En el mejor de los casos, los datos se pueden encontrar en O (1).

Las ventajas de los árboles B+ son:

1. Dado que el árbol B+ no contiene información de datos sobre los nodos internos, se pueden almacenar más claves en la página de memoria. Los datos se almacenan más estrechamente y tienen una mejor localidad espacial. Por lo tanto, acceder a los datos asociados con los nodos hoja también tiene una mejor tasa de aciertos de caché.
2. Todos los nodos de hoja del árbol B + están conectados, por lo que atravesar todo el árbol solo requiere un recorrido lineal de los nodos de hoja. Y debido a que los datos están organizados secuencialmente y conectados, es fácil encontrar y buscar intervalos. El árbol B requiere un recorrido recursivo de cada nivel. Es posible que los elementos adyacentes no lo sean en la memoria, por lo que el rendimiento de los accesos al caché no es tan bueno como el del árbol B+.

7.5.2 Aplicación

Los árboles B/B+ se utilizan a menudo en sistemas de archivos y sistemas de bases de datos. Al expandir el número de almacenamiento de cada nodo, se pueden ubicar y acceder a datos continuos más rápido, lo que puede reducir efectivamente el tiempo de búsqueda y mejorar el rendimiento del espacio de almacenamiento, reduciendo así las operaciones de IO. Es ampliamente utilizado en sistemas de archivos y bases de datos, tales como:

1.Windows: sistema de archivos HPFS.
2.Mac: sistema de archivos HFS, HFS+.
3.Linux: sistema de archivos ResiserFS, XFS, Ext3FS, JFS.
4. Base de datos: ORACLE, MYSQL, SQLSERVER, etc.

8 búsqueda de hash

Se han introducido muchos métodos de búsqueda antes. Se puede encontrar que el árbol rojo-negro ha alcanzado una complejidad de tiempo O (logn) para inserción, búsqueda y eliminación en circunstancias promedio. Entonces, ¿existe alguna estructura de datos con mayor eficiencia de búsqueda?
La respuesta es una tabla hash.

8.1 Descripción del algoritmo

La búsqueda hash es un método que utiliza una tabla hash (tabla hash) para encontrar el elemento de destino. Cuando la eficiencia de la búsqueda es más alta, la complejidad de tiempo correspondiente es O (1). El algoritmo de búsqueda hash es adecuado para la mayoría de escenarios y admite tanto la búsqueda de elementos objetivo en secuencias ordenadas como en secuencias desordenadas. La idea del algoritmo de búsqueda hash es encontrar directamente la dirección del elemento realizando alguna operación en el valor de la palabra clave del elemento, es decir, utilizando el método de conversión directa de palabra clave a dirección sin comparaciones repetidas.

8.2 Tabla hash

Una tabla hash es una estructura que almacena datos en un formato indexado por clave, solo necesitamos ingresar el valor a encontrar, que es la clave, para encontrar su valor correspondiente.

Hay dos pasos para utilizar una búsqueda de hash:

1. Utilice una función hash para convertir la clave que se busca en un índice de matriz. En un mundo ideal, diferentes claves se convertirían en diferentes valores de índice, pero en algunos casos debemos lidiar con varias claves que se convierten en hash al mismo valor de índice. Entonces, el segundo paso en la búsqueda de hash es lidiar con los conflictos.
2. Manejar los conflictos de colisión de hash. Hay muchas formas de lidiar con los conflictos de colisión de hash. El método de cremallera y el método de detección lineal se presentarán más adelante en este artículo.

Las tablas hash son un ejemplo clásico de equilibrio entre tiempo y espacio. Si no hay restricciones de memoria, entonces la clave se puede usar directamente como índice de la matriz. Entonces toda la complejidad del tiempo de búsqueda es O (1). Si no hay límite de tiempo, entonces podemos usar una matriz desordenada y realizar una búsqueda secuencial, que requiere muy poca memoria. Las tablas hash encuentran un equilibrio entre estos dos extremos utilizando una cantidad modesta de tiempo y espacio. Simplemente ajuste el algoritmo de la función hash para hacer concesiones en el tiempo y el espacio.

8.3 Funciones hash

El primer paso en una búsqueda de hash es utilizar una función hash para asignar claves a índices. Esta función de mapeo es una función hash. Si tenemos una matriz que contiene 0-M, entonces necesitamos una función hash que pueda convertir cualquier clave en un índice en el rango de la matriz (0 ~ M-1). La función hash debe ser fácil de calcular y poder distribuir todas las claves de manera uniforme.

Para diferentes tipos de claves, necesitamos implementar diferentes funciones hash.

8.3.1 Enteros positivos

La forma más común de obtener un valor hash entero positivo es utilizar el método de división dejando resto. Es decir, para una matriz de tamaño M, para cualquier entero positivo k, calcule k%M. Para evitar conflictos de hash tanto como sea posible, M generalmente es un número primo.

8.3.2 Cadena

Cuando usamos una cadena como clave, también podemos usarla como un número entero grande para usar el método de división y resto. También puedes tomar el valor de cada carácter que compone la cadena y luego aplicar un hash.

El siguiente es el método de Horner para calcular el valor hash de cadena. Tomemos un ejemplo para ilustrar: por ejemplo, para obtener el valor hash de "call", el Unicode correspondiente a la cadena c es 99, el Unicode correspondiente a a es 97 y el Unicode correspondiente para L es Unicode es 108, entonces hash = 108 + 31 · (108 + 31 · (97 + 31 · (99))).

int GetHashCode(string str){
    
    
	char[] s = str.ToCharArray();
	int hash = 0;
	for(int i=0;i<s.Length;i++){
    
    
		hash = s[i]+31*hash;
	}
	return hash;
}

Si la cadena es larga, puede llevar mucho tiempo procesar cada carácter, por lo que puede ahorrar tiempo tomando N caracteres a intervalos para obtener el valor hash. Por ejemplo, puede obtener cada 8-9 caracteres para obtener el valor hash. Valor de esperanza:

int GetHashCode(string str){
    
    
	char[] s = str.ToCharArray();
	int hash = 0;
	int skip = Math.Max(1,s.Length/8);
	for(int i=0;i<s.Length;i+=skip){
    
    
		hash = s[i]+(31*hash);
	}
	return hash;
}

8.4 Evitar colisiones de hash

8.4.1 Método de cremallera

A través de la función hash, podemos convertir la clave en el índice de la matriz (0-M-1), pero en el caso de que dos o más claves tengan el mismo valor de índice, necesitamos tener una manera de manejar este conflicto.

Un método más directo es apuntar cada elemento de una matriz de tamaño M a una lista vinculada, y cada nodo de la lista vinculada almacena el par clave-valor cuyo valor hash es el índice. Este es el método de cremallera.
método de cremallera
La idea básica de este método es elegir M lo suficientemente grande como para que todas las listas vinculadas sean lo más cortas posible para garantizar la eficiencia de la búsqueda. La búsqueda de la implementación de hash utilizando el método zip se divide en dos pasos: primero, se encuentra la lista vinculada correspondiente de acuerdo con el valor hash y luego se encuentra la clave correspondiente secuencialmente a lo largo de la lista vinculada.

//注意:基于力扣704题
public class HashTable{
    
    
    private List<int[]>[] data;
    private int size;
    public HashTable(int size){
    
    
        this.size = size;
        data = new List<int[]>[size];
        for(int i=0;i<size;i++){
    
    
            data[i] = new List<int[]>();
        }
    }
    private int GetHashCode(int key)=>Math.Abs(key)%size;
    //这里key和value相同,所以就不传入value了
    public void Put(int key,int index){
    
    
        int hash = GetHashCode(key);
        data[hash].Add(new int[]{
    
    key,index});
    }
    public int GetIndex(int key){
    
    
        int hash = GetHashCode(key);
        for(int i=0;i<data[hash].Count;i++){
    
    
            if(data[hash][i][0]==key) return data[hash][i][1];
        }
        return -1;
    }
}
public class Solution {
    
    
    public int Search(int[] nums, int target) {
    
    
        HashTable hashTable = new HashTable(10);
        for(int i=0;i<nums.Length;i++){
    
    
            hashTable.Put(nums[i],i);
        }
        return hashTable.GetIndex(target);
    }
}

8.4.2 Método de detección lineal

El método de detección lineal es un método de direccionamiento abierto para resolver conflictos hash. El principio básico es usar una matriz de tamaño M para guardar N pares clave-valor, donde M>N, necesitamos usar las vacantes en la matriz para resolver conflictos de colisión. . Como se muestra en la siguiente figura:
Método de detección lineal
El método de direccionamiento abierto más simple es el método de detección lineal: cuando ocurre una colisión, es decir, cuando el valor hash de una clave está ocupado por otra clave, se verifica directamente la siguiente posición en la tabla hash. es decir, el valor del índice aumenta en 1, dicha detección lineal producirá tres resultados:

1. Presione, la clave en esta posición es la misma que la clave que se está buscando
2. Falló, la clave está vacía
3. Continúe buscando, la clave en esta posición es diferente de la clave que se está buscando.

//注意:基于力扣704题
public class HashTable{
    
    
    private int size;
    private int[] keys;
    private int[] values;
    private bool[] types; 
    public HashTable(int size){
    
    
        this.size = size;
        keys = new int[size];
        values = new int[size];
        types = new bool[size];
    } 
    private int GetHash(int key)=>Math.Abs(key)%size;
    public void Put(int key,int index){
    
    
        int hash = GetHash(key);
        while(types[hash]) hash = (hash+1)%size;
        keys[hash] = key;
        values[hash] = index;
        types[hash] = true;
    }
    public int GetIndex(int key){
    
    
        int hash = GetHash(key);
        int cur = hash;
        while(keys[cur]!=key){
    
    
            cur = (cur+1)%size;
            if(cur==hash) return -1;
        }
        return values[cur];
    }
}
public class Solution {
    
    
    public int Search(int[] nums, int target) {
    
    
        HashTable hashTable = new HashTable(nums.Length);
        for(int i=0;i<nums.Length;i++){
    
    
            hashTable.Put(nums[i],i);
        }
        return hashTable.GetIndex(target);
    }
}

Aunque Linear Probing es simple, tiene algunos problemas: conducirá a la agregación de hashes similares. Hay un conflicto al guardar y el conflicto aún existe al buscar.

8.5 Ataque de colisión de hash

Un ataque de tabla hash consiste en construir cuidadosamente una función hash de modo que todas las claves se asignen al mismo índice o a varios índices después de pasar por la función hash, y la tabla hash se reduzca a una lista enlazada individualmente. De esta manera, varias operaciones en la Las tablas hash, como la inserción y la búsqueda, han degenerado de O(1) a operaciones de búsqueda de listas vinculadas, lo que consumirá una gran cantidad de recursos de la CPU y provocará que el sistema no pueda responder, logrando así el propósito de denegación de servicio (Dos). ).
colisión de hash

8.6 Implementación de Hash en .NET

Cuando se agrega cualquier valor como clave al Diccionario, primero se obtendrá el código hash de la clave y luego se asignará a diferentes depósitos:

public Dictionary(int capacity, IEqualityComparer<TKey> comparer) {
    
    
    if (capacity < 0) ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.capacity);
    if (capacity > 0) Initialize(capacity);
    this.comparer = comparer ?? EqualityComparer<TKey>.Default;
}

Cuando se inicializa el Diccionario, si se pasa el tamaño, el depósito se inicializará llamando al método Inicializar:

private void Initialize(int capacity) {
    
    
    int size = HashHelpers.GetPrime(capacity);
    buckets = new int[size];
    for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;
    entries = new Entry[size];
    freeList = -1;
}

Podemos mirar el método Agregar del Diccionario, el método Agregar llama internamente al método Insertar:

private void Insert(TKey key, TValue value, bool add) 
{
    
    
        if( key == null ) {
    
    
            ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
        }
 
        if (buckets == null) Initialize(0);
        int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
        int targetBucket = hashCode % buckets.Length;
 
#if FEATURE_RANDOMIZED_STRING_HASHING
        int collisionCount = 0;
#endif
 
        for (int i = buckets[targetBucket]; i >= 0; i = entries[i].next) {
    
    
            if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) {
    
    
                if (add) {
    
     
                    ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_AddingDuplicate);
                }
                entries[i].value = value;
                version++;
                return;
            } 
 
#if FEATURE_RANDOMIZED_STRING_HASHING
            collisionCount++;
#endif
        }
        int index;
        if (freeCount > 0) {
    
    
            index = freeList;
            freeList = entries[index].next;
            freeCount--;
        }
        else {
    
    
            if (count == entries.Length)
            {
    
    
                Resize();
                targetBucket = hashCode % buckets.Length;
            }
            index = count;
            count++;
        }
 
        entries[index].hashCode = hashCode;
        entries[index].next = buckets[targetBucket];
        entries[index].key = key;
        entries[index].value = value;
        buckets[targetBucket] = index;
        version++;
 
#if FEATURE_RANDOMIZED_STRING_HASHING
        if(collisionCount > HashHelpers.HashCollisionThreshold && HashHelpers.IsWellKnownEqualityComparer(comparer)) 
        {
    
    
            comparer = (IEqualityComparer<TKey>) HashHelpers.GetRandomizedEqualityComparer(comparer);
            Resize(entries.Length, true);
        }
#endif
 
    }

Primero, obtenga su código hash basado en la clave, luego divida el código hash por el tamaño del backet y asígnelo al backet de destino, y luego recorra la lista vinculada almacenada en el depósito. Si se encuentra el mismo valor que la clave, si No se permite que la clave agregada más tarde sea la misma que la existente. Si la clave es la misma y el valor se reemplaza (agrega), se generará una excepción. Si se permite, el valor anterior se reemplazará y luego se devolverá.

Si no se encuentra, el valor recién agregado se colocará en un nuevo depósito. Cuando no haya suficiente espacio libre, se realizará una operación de expansión (Redimensionar) y luego el valor se volverá a aplicar hash al depósito de destino. Lo que hay que tener en cuenta aquí es que la operación de cambio de tamaño consume más recursos.

8.7 Análisis de desempeño

actuación

9 ejercicios

Las preguntas de práctica son muy interesantes.

69 raíz cuadrada de x

1 dicotomía

public class Solution {
    
    
    public int MySqrt(int x) {
    
    
        int left = 0;
        int right = x;
        int cur = 0;    //保存当前的最好答案
        while(left<=right){
    
    
            int mid = (left+right)/2;
            //这里用long是考虑了乘积的大数情况
            if((long)mid*(long)mid==x) return mid;
            else if((long)mid*(long)mid<x){
    
    
                cur = mid; 
                left = mid+1;
            }else right = mid-1;
        }
        return cur;
    }
}

Método de iteración de 2 Newton*

introducir

El método de iteración de Newton es un método que se puede utilizar para resolver rápidamente los puntos cero. Para facilitar la descripción, aquí usamos C para representar el número entero cuya raíz cuadrada se va a encontrar, entonces podemos obtener fácilmente esta fórmula: f (x) = x 2 − C f(x)=x^2-Cf ( x )=X2C._ _ El punto cero de la función es el resultado que requerimos.

La esencia del método de iteración de Newton es utilizar series de Taylor para acercarse rápidamente al punto cero a partir del valor inicial. Escogemos cualquier x 0 x_0X0Como valor inicial, en cada iteración, encontramos el punto en la gráfica de la función ( xi x_iXyo, f (xi) f(x_i)f ( xyo) ), dibuja una pendiente que pase por el punto para que sea la derivadaf ′ ( xi ) f'(x_i)F (xyo) , el punto de intersección con el eje horizontal se denota comoxi + 1 x_{i+1}Xyo + 1xi + 1 x_{i+1}Xyo + 1Comparado con xi x_iXyoestá más cerca del punto cero. Después de muchas iteraciones, podemos obtener un punto de intersección que está muy cerca del punto cero. La siguiente figura da el valor de x 0 x_0X0Comience a iterar dos veces y obtenga x 1 x_1X1y x 2 x_2X2el proceso de. Detalles
El método de iteración de Newton.
del cálculo de la fórmula
Cálculo de fórmulas

1. Selección del valor inicial:
elegimos x 0 x_0X0=C como valor inicial porque es la opción más segura. Debido a que el punto cero se acerca de derecha a izquierda, si el valor inicial seleccionado es demasiado pequeño, puede ubicarse en el lado izquierdo del punto cero, lo que hace que la iteración no alcance el punto cero.
2. Cuándo termina la iteración:
Después de cada iteración, estaremos más lejos del punto cero, por lo que cuando los puntos de intersección obtenidos por dos iteraciones adyacentes están muy cerca, podemos concluir que el resultado en este momento es suficiente para que podamos obtener la respuesta. En términos generales, se puede juzgar si la diferencia entre los resultados de dos iteraciones adyacentes es menor que un número muy pequeño no negativo, que generalmente puede ser 1 0 − 6 10^{-6}1 06 o1 0 − 7 10^{-7}1 0−7._ _ _
3. ¿Cómo obtener la respuesta final a través de los puntos cero aproximados obtenidos por iteración?
El resultado de cada iteraciónxi x_iXyosiempre será mayor o igual que la raíz cuadrada, por lo que siempre que la diferencia seleccionada sea lo suficientemente pequeña, puede asegurarse de que el resultado final sea solo ligeramente mayor que cero y luego guardar la parte decimal mediante int.

código

public class Solution {
    
    
    public int MySqrt(int x) {
    
    
        int C = x;
        double dif = 1e-7;
        double a = x;
        double b = (a+C/a)/2;
        while((a-b)>=dif){
    
    
            a = b;
            b = (a+C/a)/2;
        }
        return (int)a;
    }
}

300 subsecuencia creciente más larga

1 programación dinámica

public class Solution {
    
    
    public int LengthOfLIS(int[] nums) {
    
    
        //dp[n]:以数组下标为n的元素结尾的最长递增子序列长度
        //dp[n] = Math.Max(dp[n],dp[m]+1); 其中nums[m]<nums[n]
        int[] dp = new int[nums.Length];
        for(int i=0;i<dp.Length;i++) dp[i] = 1;
        int max = 1;
        for(int i=1;i<nums.Length;i++){
    
    
            for(int j=i-1;j>=0;j--){
    
    
                if(nums[j]<nums[i]){
    
    
                    dp[i] = Math.Max(dp[i],dp[j]+1);
                }
            }
            max = Math.Max(max,dp[i]);
        }
        return max;
    }
}

2 avaricia + dos puntos*

La complejidad temporal de la solución de programación dinámica es O ( n 2 ) O(n^2)O ( n.2 ), debemos pensar en una forma de reducir la complejidad del tiempo. Hay muchas explicaciones y soluciones específicas, por lo que no entraré en detalles, solo mostraré el código.

public class Solution {
    
    
    public int LengthOfLIS(int[] nums) {
    
    
        int[] tails = new int[nums.Length];
        int len = 0;
        foreach(int item in nums){
    
    
            int left = 0;
            int right = len;
            while(left<right){
    
    
                int mid = (left+right)/2;
                if(tails[mid]<item) left = mid+1;
                else right = mid;
            }
            tails[left] = item;
            if(left==len) len++;
        }
        return len;
    }
}

4 Encuentra la mediana de dos matrices positivas.

1 késimo número más pequeño*

Supongamos que las longitudes de las dos matrices son len1 y len2 respectivamente. Luego, encontrar la mediana significa encontrar el k-ésimo número o el promedio de los k-ésimo y k+1. Luego, el problema se transforma en cómo encontrar el k-ésimo número.

Supongamos que ahora tenemos dos matrices, a=[2,3,4,5], b=[1,6,7], y nuestro objetivo es encontrar el k=4to número.

1. Primer paso: primero tomamos el k/2=2º número de cada una de las dos matrices para comparar, y encontramos que 3<6, entonces podemos saber que 3 y el número anterior a 3 definitivamente no son el cuarto dígito de la matriz Después de la eliminación, necesitamos encontrar el k-2 = 2º número entre los números restantes.
2. La segunda pasada: lo mismo, tomamos el k/2=1º número de cada una de las dos matrices para comparar y encontramos que 1<4, entonces podemos saber que 1 no es el segundo dígito de los números restantes, y excluirlo Después de la eliminación, necesitamos encontrar el k-1 = 1º dígito en los números restantes.
3. El tercer paso, porque en este momento solo necesitamos encontrar el primer número entre los números restantes, por lo que podemos generar directamente el número menor del primer número en las dos matrices.

Por supuesto, tenemos que considerar algunos casos límite, por ejemplo, cuando a = [1], b = [2,3,4], podemos excluir fácilmente todos los números en a, por lo que solo necesitamos continuar buscando en b .

public class Solution {
    
    
    private int[] nums1;
    private int[] nums2;
    public double FindMedianSortedArrays(int[] nums1, int[] nums2) {
    
    
        this.nums1 = nums1;
        this.nums2 = nums2;
        int len = nums1.Length+nums2.Length;
        if(len%2!=0) return (double)(FindKNum(len/2+1));
        else return (double)(FindKNum(len/2)+FindKNum(len/2+1))/2;
    }
    //方法:返回第k大的数
    private int FindKNum(int k){
    
    
        int len1 = nums1.Length,len2 = nums2.Length;
        int index1 = 0,index2 = 0;
        while(true){
    
    
            //边界情况
            if(index1==len1) return nums2[index2+k-1];
            if(index2==len2) return nums1[index1+k-1];
            if(k==1) return Math.Min(nums1[index1],nums2[index2]);
            //普通情况
            int half = k/2;
            int newIndex1 = Math.Min(index1+half,len1)-1;
            int newIndex2 = Math.Min(index2+half,len2)-1;
            if(nums1[newIndex1]<=nums2[newIndex2]){
    
    
                k-=(newIndex1-index1+1);
                index1 = newIndex1+1;
            }else{
    
    
                k-=(newIndex2-index2+1);
                index2 = newIndex2+1;
            }
        }
    }
}

2 Divida la matriz*

algoritmo

public class Solution {
    
    
    public double FindMedianSortedArrays(int[] nums1, int[] nums2) {
    
    
        int m = nums1.Length;
        int n = nums2.Length;
        if(m>n) return FindMedianSortedArrays(nums2,nums1);
        int i=0,j=0;          //两个数组的划分指针
        int left=0,right=m;   //对第一个数组执行二分查找,查找最佳划分点
        while(true){
    
    
            i = (left+right)/2;
            j = (m+n+1)/2-i;
            if(i!=0&&j!=n&&nums1[i-1]>nums2[j]){
    
             //i左移->i在左边
                right = i-1;
            }else if(i!=m&&j!=0&&nums2[j-1]>nums1[i]){
    
       //i右移->i在右边
                left = i+1;
            }else{
    
                                           //i到合适位置了
                int maxLeft = 0;
                if(i==0) maxLeft = nums2[j-1];
                else if(j==0) maxLeft = nums1[i-1];
                else maxLeft = Math.Max(nums1[i-1],nums2[j-1]);
                //奇数
                if((m+n)%2!=0) return maxLeft;
                //偶数
                int minRight = 0;
                if(i==m) minRight = nums2[j];
                else if(j==n) minRight = nums1[i];
                else minRight = Math.Min(nums1[i],nums2[j]);
                return (double)(maxLeft+minRight)/2;
            }
        }
        return -1;
    }
}

Supongo que te gusta

Origin blog.csdn.net/manpi/article/details/129871736
Recomendado
Clasificación