Explicación práctica de la estructura de datos y el código del algoritmo: estructura de datos avanzada

Autor: Zen y el arte de la programación informática

1. Introducción a los antecedentes

Estructuras de datos y sus aplicaciones.

La estructura de datos juega un papel importante en la programación de computadoras: se refiere a una colección de elementos de datos que tienen una o más relaciones entre sí y algunas reglas y métodos para operar estos elementos. Como tablas lineales, estructuras de árbol, estructuras de gráficos, colas, pilas, tablas hash, conjuntos, montones, algoritmos de clasificación, etc. Las diferentes estructuras de datos también tienen diferentes estándares de medición en términos de rendimiento, capacidad de almacenamiento, dificultad de implementación, etc. Por lo tanto, elegir una estructura de datos adecuada es de gran importancia para mejorar la eficiencia del programa y reducir el consumo de recursos del programa.

De hecho, la estructura de datos no solo afecta directamente el rendimiento y la eficiencia del programa, sino que también es una parte indispensable del diseño del programa. Por ejemplo, elegir la estructura de datos incorrecta puede hacer que el programa se ejecute incorrectamente, se ejecute lentamente o no logre los resultados esperados. Por lo tanto, antes de aplicar la estructura de datos, debe considerar completamente el tamaño de los datos, los escenarios de aplicación y la complejidad del algoritmo. , lectura y escritura simultáneas, etc. Muchos factores. Además, dominar diversas estructuras de datos puede ayudarnos a comprender más profundamente los principios de funcionamiento de varios algoritmos y utilizarlos mejor para resolver problemas prácticos.

Qué estructura de datos elegir

En términos generales, si ya tiene algo de experiencia, puede decidir qué estructura de datos utilizar analizando el código existente o la lógica empresarial abstracta. Pero la mayoría de las veces, solo cuando encontramos un problema específico profundizaremos en las ventajas y desventajas, los escenarios aplicables y la implementación de una estructura de datos específica. Por ejemplo, cuando necesita procesar cantidades masivas de datos, para encontrar rápidamente una palabra clave, usar una tabla hash es una buena opción; cuando necesita insertar, eliminar o modificar elementos de datos con frecuencia, puede usar una estructura de árbol (como un árbol rojo-negro, árbol B); cuando necesita administrar dinámicamente la memoria, puede usar una pila o una cola de dos extremos; y cuando el tamaño de los datos es relativamente pequeño (como cientos de elementos de datos ) y se requiere acceso aleatorio, usar una matriz es una buena opción. En definitiva, elegir la estructura de datos adecuada puede mejorar eficazmente el rendimiento del programa, ahorrar espacio en la memoria y reducir el consumo de recursos.

Además, cuando encuentre nuevos requisitos, también puede sopesar los principios del algoritmo relevante y la complejidad del tiempo de acuerdo con la situación real, y luego elegir la estructura de datos óptima más adecuada según el escenario. Esto no solo puede satisfacer las necesidades tanto como sea posible, sino también garantizar la corrección y eficiencia del programa.

2. Conceptos centrales y conexiones

tipo de datos

El tipo de datos generalmente se refiere a las características de los datos, incluido el tamaño, la representación, el rango, la unidad, la precisión, el valor que acepta valores NULL, el método de codificación, etc. Los tipos de datos comunes incluyen entero, punto flotante, carácter, booleano, fecha, matriz, puntero, estructura, unión, etc.

tabla de secuencia

Una tabla de secuencia es una estructura de almacenamiento que almacena variables del mismo tipo juntas y las organiza en una estructura lineal. Cada elemento de datos de la tabla de secuencia se almacena en secuencia. Las tablas secuenciales normalmente pueden realizar dos operaciones básicas: inserción y eliminación de elementos.

tabla de secuencia estática

La tabla de secuencia estática es una matriz, es decir, solo se almacena un elemento en cada posición. Las tablas secuenciales estáticas pueden realizar fácilmente operaciones de inserción y eliminación, pero debido a la alta sobrecarga de asignar y liberar espacio de almacenamiento, su velocidad de consulta será limitada cuando haya muchos elementos de datos.

tabla de secuencia dinámica

La tabla de secuencia dinámica es similar a la tabla de secuencia estática, pero permite la expansión y contracción dinámica, es decir, cuando los elementos agregados exceden la capacidad de almacenamiento actual, la tabla reasigna automáticamente más espacio de almacenamiento. Las tablas secuenciales dinámicas pueden proporcionar velocidades de consulta más altas que las tablas secuenciales estáticas en algunos casos.

lista enlazada

Una lista vinculada es una estructura de almacenamiento no continua. Además de almacenar el valor del elemento de datos, cada nodo de la lista vinculada también tiene un campo de puntero que apunta al siguiente nodo. Las operaciones de inserción y eliminación de listas vinculadas son más problemáticas, pero proporcionan operaciones flexibles. La inserción y eliminación de listas vinculadas se puede completar con una complejidad de tiempo O (1), lo cual es muy crítico para ciertas aplicaciones que requieren inserción y eliminación de alta velocidad.

tabla de picadillo

Hash Table es una estructura de datos que asigna pares clave-valor y calcula el valor del índice para obtener la ubicación del elemento en el área de almacenamiento, lo que reduce en gran medida el tiempo de búsqueda. Una función hash puede convertir un valor binario de cualquier longitud en un valor de índice. Las funciones hash de uso común incluyen: método de división dejando resto, método de producto, método de detección de cuadrados, método de dirección en cadena, método de direccionamiento abierto, etc.

pila

La pila es una lista lineal especial que solo puede realizar operaciones de inserción y eliminación en la parte superior. El elemento en la parte superior de la pila se llama parte superior de la pila y el elemento en la parte inferior de la pila se llama parte inferior de la pila. La pila es una estructura de datos de último en entrar, primero en salir (LIFO).

cola

Una cola es una tabla lineal especial que solo se puede insertar al final de la cola y eliminar al principio de la cola. El elemento a la cabeza del equipo se llama cabeza del equipo y el elemento al final del equipo se llama cola. Una cola es una estructura de datos primero en entrar, primero en salir (FIFO).

recolectar

Un conjunto es una colección desordenada que no contiene elementos duplicados. Se utiliza principalmente para realizar algunas operaciones básicas sobre un conjunto de elementos, como determinar si un elemento pertenece a un conjunto, encontrar la diferencia y unión entre dos conjuntos, etc.

montón

El montón es una estructura de datos de árbol especial. El nodo raíz tiene el valor más grande (mínimo). Todos los demás nodos deben cumplir con la definición de un árbol binario y los valores del subárbol izquierdo son menores o iguales que el valor del subárbol derecho. Existen muchas aplicaciones del montón, como algoritmos de programación, colas de prioridad, etc.

cola de prioridad

Una cola prioritaria es una cola especial que se utiliza para procesar datos con prioridad. La cola de prioridad clasifica los elementos según su prioridad: cada vez que se elimina el elemento principal, es el elemento con mayor prioridad. Hay dos colas de prioridad comunes: montón mínimo y montón máximo.

3. Explicación detallada de los principios básicos del algoritmo, pasos operativos específicos y fórmulas del modelo matemático.

Algoritmo de clasificación

El algoritmo de clasificación se refiere a un algoritmo utilizado para reorganizar colecciones de registros. Los algoritmos de clasificación comunes incluyen: clasificación por burbujas, clasificación por inserción, clasificación por selección, clasificación Hill, clasificación por fusión, clasificación rápida, clasificación por montón, clasificación por conteo, clasificación por cubo, clasificación por base, etc.

tipo de inserción

Insertion Sort es un algoritmo de clasificación simple. Su idea central es que en un conjunto de números a ordenar, suponiendo que se hayan ordenado los n-1 números anteriores, ahora es necesario insertar el enésimo número en él., de modo que estos n Los números todavía están ordenados, el algoritmo es el siguiente:

  1. A partir del primer elemento, se puede considerar que los elementos han sido ordenados.
  2. Saque el siguiente elemento y escanee de atrás hacia adelante en la secuencia ordenada de elementos.
  3. Si el elemento (ordenado) es más grande que el nuevo elemento, mueva el elemento a la siguiente posición
  4. Repita el paso 3 hasta encontrar una posición donde el elemento ordenado sea menor o igual que el nuevo elemento
  5. Después de insertar el nuevo elemento en esa posición
  6. Repita los pasos 2 ~ 5

Código de muestra:

public void insertionSort(int[] arr){
    int n = arr.length;
    for(int i=1;i<n;i++){
        int key = arr[i]; // 待插入元素
        int j = i - 1; // 有序序列最后一个元素下标
        while(j>=0 && arr[j]>key){
            arr[j+1] = arr[j]; // 移动元素至后一个位置
            j--; // 更新下标
        }
        arr[j+1] = key; // 插入新元素
    }
}

Complejidad de tiempo promedio: O (n ^ 2)

Ordenamiento de burbuja

Bubble Sort es un algoritmo de clasificación simple y estable. Su idea central es visitar repetidamente la secuencia que se va a ordenar, comparar dos elementos a la vez e intercambiar sus posiciones hasta que no sea necesario comparar ningún par de números, lo que significa que se ha ordenado. . El algoritmo es como sigue:

  1. Compara elementos adyacentes. Si el primero es mayor que el segundo, intercámbialos ambos.
  2. Haga lo mismo para cada par de elementos adyacentes, desde el primer par al principio hasta el último par al final, de modo que el elemento al final sea el número más grande.
  3. Repita los pasos anteriores para todos los elementos excepto el último.
  4. Siga repitiendo los pasos anteriores para cada vez menos elementos cada vez hasta que no haya pares de números para comparar.

Código de muestra:

public void bubbleSort(int[] arr){
    int n = arr.length;
    for(int i=0;i<n-1;i++){
        for(int j=0;j<n-i-1;j++){
            if(arr[j]>arr[j+1]){
                // swap arr[j] and arr[j+1]
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
}

Complejidad de tiempo promedio: O (n ^ 2)

clasificación de selección

Selection Sort es un algoritmo de clasificación simple e intuitivo. Su idea central es encontrar el elemento más pequeño (más grande) y colocarlo primero, y luego continuar buscando el elemento más pequeño (más grande) de los elementos restantes y colocarlo en la segunda posición. y así sucesivamente hasta completar la clasificación. El algoritmo es como sigue:

  1. Encuentre el elemento más pequeño (más grande) en la secuencia no ordenada y guárdelo al comienzo de la secuencia ordenada
  2. Luego continúe buscando el elemento más pequeño (más grande) de los elementos restantes sin clasificar y luego colóquelo al final de la secuencia ordenada.
  3. Repita el paso dos hasta que todos los elementos estén ordenados.

Código de muestra:

public void selectionSort(int[] arr){
    int n = arr.length;
    for(int i=0;i<n-1;i++){
        int minIndex = i;
        for(int j=i+1;j<n;j++){
            if(arr[minIndex]>arr[j]){
                minIndex = j;
            }
        }
        // swap arr[i] and arr[minIndex]
        int temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
}

Complejidad de tiempo promedio: O (n ^ 2)

clasificación de colinas

Shell Sort es una versión más eficiente y mejorada de la ordenación por inserción. Su idea principal es agrupar registros por un cierto incremento del subíndice y ordenar cada grupo utilizando el algoritmo de ordenación por inserción directa. A medida que los incrementos disminuyan gradualmente, todo el proceso de clasificación será más ordenado y eficiente. El algoritmo es como sigue:

  1. Establezca una secuencia de espacio y establezca el último elemento de la secuencia de espacio en la mitad de la longitud de la matriz
  2. Inserte elementos de los espacios respectivos en las posiciones correspondientes a través de bucles
  3. La brecha se reduce y se establece una nueva brecha.
  4. Cuando el espacio es 1, se ha ordenado toda la secuencia y el bucle sale.

Código de muestra:

public void shellSort(int[] arr){
    int n = arr.length;
    // 计算间隔序列
    int d = 1;
    while((d/9)*7 < n/3){
        d = (d*3 + 1)/2;
    }

    // 进行希尔排序
    for(int g=d;g>0;g/=3){
        for(int i=g;i<n;i+=g){
            int temp = arr[i];
            int j;
            for(j=i;j>=g && arr[j-g]>temp;j-=g){
                arr[j] = arr[j-g];
            }
            arr[j] = temp;
        }
    }
}

Complejidad de tiempo promedio: O(n^(3/2))

fusionar ordenar

Merge Sort es un algoritmo de clasificación eficaz basado en operaciones de fusión, tiene un rendimiento estable y es un algoritmo de clasificación recursivo. Combinar orden primero divide recursivamente la secuencia actual en dos mitades, luego ordena ambos lados por separado y luego combina los dos resultados ordenados. El algoritmo es como sigue:

  1. Divida la secuencia de entrada de longitud n en dos subsecuencias de longitud n/2
  2. Llame a la ordenación por combinación en estas dos subsecuencias
  3. Fusionar dos subsecuencias ordenadas en una secuencia ordenada final

Código de muestra:

public void mergeSort(int[] arr){
    int n = arr.length;
    if(n<=1){
        return;
    }
    int mid = n / 2;
    int[] leftArr = new int[mid];
    int[] rightArr = new int[n - mid];

    System.arraycopy(arr, 0, leftArr, 0, mid);
    System.arraycopy(arr, mid, rightArr, 0, n - mid);

    mergeSort(leftArr);
    mergeSort(rightArr);

    merge(arr, leftArr, rightArr);
}

private static void merge(int[] arr, int[] leftArr, int[] rightArr){
    int i = 0;
    int j = 0;
    int k = 0;
    while(i<leftArr.length&&j<rightArr.length){
        if(leftArr[i]<rightArr[j]){
            arr[k++] = leftArr[i++];
        }else{
            arr[k++] = rightArr[j++];
        }
    }
    while(i<leftArr.length){
        arr[k++] = leftArr[i++];
    }
    while(j<rightArr.length){
        arr[k++] = rightArr[j++];
    }
}

Complejidad de tiempo promedio: O (nlogn)

Ordenación rápida

Quick Sort es una mejora de la clasificación de burbujas. Utiliza la estrategia Divide And Conquer para dividir una matriz (o lista) en serie en dos subsecuencias, en las que el código de clasificación de algunos elementos es mayor que El código de clasificación de otra parte de los elementos es pequeño. Luego reordene de esta manera hasta que toda la secuencia esté en orden. El algoritmo es como sigue:

  1. Elija un elemento de la secuencia, llamado "pivote".
  2. Reordene la secuencia para que todos los elementos menores que el valor base se coloquen a la izquierda y todos los elementos mayores que el valor base se coloquen a la derecha (el mismo número puede ir a cualquier lado). Después de esta división, el lado izquierdo o derecho del elemento de referencia es un subarreglo ordenado.
  3. Repita el paso dos para los subarreglos de cada lado hasta que todos los subarreglos estén ordenados.
  4. Repita el primer paso para toda la matriz hasta que toda la matriz esté en orden.

Código de muestra:

public void quickSort(int[] arr, int start, int end){
    if(start >= end){
        return;
    }
    int pivotIdx = partition(arr, start, end);
    quickSort(arr, start, pivotIdx-1);
    quickSort(arr, pivotIdx+1, end);
}

// 返回arr[l...r]的第k大元素的索引
private static int partition(int[] arr, int l, int r){
    int v = arr[l];
    int idx = l;
    for(int i=l+1;i<=r;i++){
        if(arr[i] > v){
            idx++;
            swap(arr, idx, i);
        }
    }
    swap(arr, idx, l);
    return idx;
}

private static void swap(int[] arr, int a, int b){
    int t = arr[a];
    arr[a] = arr[b];
    arr[b] = t;
}

Complejidad de tiempo promedio: O (nlogn), peor caso O (n ^ 2)

clasificación de montón

Heap Sort se refiere a un algoritmo de clasificación implementado utilizando una estructura de datos como un montón. El montón es una estructura que es aproximadamente un árbol binario completo. Cada nodo del árbol satisface que el valor del nodo principal sea menor o igual que el valor del nodo secundario y, para montones, esta característica permite que los montones se implementen mediante matrices y se mantengan dinámicamente. El algoritmo es como sigue:

  1. Crear un montón (Montón máximo o Montón mínimo)
  2. Coloque el elemento superior del montón (valor máximo o mínimo) al final de la matriz y ajuste el montón
  3. Repita el paso 2 hasta que el montón esté vacío.
  4. Ordenación del montón completada

Código de muestra:

public void heapSort(int[] arr){
    int len = arr.length;
    buildMaxHeap(arr, len); // 创建最大堆
    for(int i=len-1;i>0;i--){
        swap(arr, 0, i); // 堆顶元素放到末尾
        siftDown(arr, 0, i); // 堆调整
    }
}

private static void buildMaxHeap(int[] arr, int len){
    for(int i=(len-2)/2;i>=0;i--){
        siftDown(arr, i, len);
    }
}

private static void siftDown(int[] arr, int i, int len){
    int child;
    int tmp = arr[i];
    for(child=2*i+1;child<len;child=2*child+1){
        if(child!=len-1 && arr[child]<arr[child+1]){ // 若有右孩子且右孩子大于左孩子
            child++;
        }
        if(tmp<arr[child]){
            break;
        }
        arr[i] = arr[child];
        i = child;
    }
    arr[i] = tmp;
}

Complejidad de tiempo promedio: O (nlogn)

contando ordenar

Counting Sort es un algoritmo de clasificación no comparativo. Su idea central es contar el número de apariciones de cada elemento en la matriz y luego crear una matriz del tamaño correspondiente de acuerdo con la situación real, de modo que cada elemento pueda estar en O ( 1) Asigne valores a la matriz de salida dentro del tiempo. El algoritmo es como sigue:

  1. Determine el rango de elementos de la matriz que se ordenarán. Suponga que el valor mínimo del elemento de la matriz es my el valor máximo es M. Luego cree una matriz C con un tamaño de M-m+1.
  2. Inicialice todos los elementos de la matriz C a 0
  3. Recorra la matriz A, use el valor de cada elemento como índice y agregue 1 a su valor C [x] correspondiente.
  4. Reconstruir la matriz B basándose en los valores de C[x]
  5. Matriz de retorno B

Código de muestra:

public void countingSort(int[] arr){
    int maxVal = Integer.MIN_VALUE;
    int minVal = Integer.MAX_VALUE;

    for(int val : arr){
        maxVal = Math.max(maxVal, val);
        minVal = Math.min(minVal, val);
    }

    int countArraySize = maxVal - minVal + 1;
    int[] countArray = new int[countArraySize];

    for(int val : arr){
        countArray[val - minVal]++;
    }

    int pos = 0;
    for(int i=0;i<countArraySize;i++){
        while(countArray[i]>0){
            arr[pos++] = i + minVal;
            countArray[i]--;
        }
    }
}

Complejidad de tiempo promedio: O (n+k)

ordenar cubos

Bucket Sort es un algoritmo extendido de clasificación por conteo que utiliza la relación de mapeo de funciones para dividir los datos que se van a clasificar en varios depósitos diferentes y luego clasifica los datos en cada depósito por separado. El algoritmo es como sigue:

  1. Defina un número fijo de depósitos, suponiendo que hay n depósitos, recorra la matriz A original y divida el elemento e en el depósito A [(e-min)/(max-min)*(n-1)].
  2. Recorra el depósito i y ordene los elementos en el depósito i. Puede utilizar cualquier algoritmo de clasificación, como ordenar por inserción, ordenar por selección, ordenar por burbujas, etc.
  3. Fusione todos los depósitos para obtener una matriz ordenada completa.

Código de muestra:

public void bucketSort(int[] arr){
    int numBuckets = 10;

    // Determine minimum and maximum values in the array
    int minValue = Integer.MAX_VALUE;
    int maxValue = Integer.MIN_VALUE;
    for(int value : arr){
        minValue = Math.min(minValue, value);
        maxValue = Math.max(maxValue, value);
    }

    // Create list of empty buckets to hold elements
    List<List<Integer>> bucketList = new ArrayList<>();
    for(int i=0; i<numBuckets; i++) {
        bucketList.add(new ArrayList<>());
    }

    // Distribute each element into its appropriate bucket based on its value range
    double bucketSize = ((double)(maxValue - minValue + 1))/numBuckets;
    for(int value : arr) {
        int index = (int)Math.floor(((value - minValue)/bucketSize));
        if(index == numBuckets) {
            index--;
        }
        bucketList.get(index).add(value);
    }

    // Sort each non-empty bucket using an Insertion Sort
    for(List<Integer> bucket : bucketList) {
        Collections.sort(bucket);
    }

    // Merge all sorted buckets together into one sorted array
    int index = 0;
    for(List<Integer> bucket : bucketList) {
        for(int value : bucket) {
            arr[index++] = value;
        }
    }
}

Complejidad de tiempo promedio: O (n+k), donde k es el número de depósitos.

Ordenación por base

Radix Sort es un algoritmo de clasificación sin comparación. Su idea central es cortar números enteros por dígitos y luego ordenarlos en el orden de los números de cada dígito. El algoritmo es como sigue:

  1. Obtenga el elemento más grande de la matriz y determine el número máximo de dígitos d
  2. Usando d como índice de bucle, comenzando desde el bit más bajo, realice una clasificación estable en la secuencia que se va a ordenar.
  3. Al final de cada ciclo, el valor de bit máximo de la secuencia cambia, es decir, el rango del valor de bit máximo se vuelve más pequeño.
  4. Se ejecutan un total de d rondas de bucles.

Código de muestra:

public void radixSort(int[] arr) {
    int m = getMax(arr);
    boolean negative = false;

    if(m < 0) {
        negative = true;
        m *= -1;
    }

    for(int exp = 1; m/exp > 0; exp*=10) {
        countingSort(arr, exp, digitAtPosition(arr, arr[0], exp), negative);
    }

    if(negative) {
        reverse(arr, 0, arr.length-1);
    }
}

private static int getMax(int[] arr) {
    int max = arr[0];
    for(int i = 1; i < arr.length; i++) {
        if(arr[i] > max) {
            max = arr[i];
        }
    }
    return max;
}

private static void countingSort(int[] arr, int exp, int divisor, boolean negative) {
    int size = 10;
    int[] count = new int[size];

    // Count frequencies of digits at position 'exp'
    for(int i = 0; i < arr.length; i++) {
        int digit = getDigit(arr[i], exp, negative);

        if(digit == 0) {
            continue;
        }

        count[digit]++;
    }

    // Calculate prefix sums of counts
    for(int i = 1; i < size; i++) {
        count[i] += count[i-1];
    }

    // Place each element in its correct place in output array
    int[] output = new int[arr.length];
    for(int i = arr.length-1; i >= 0; i--) {
        int digit = getDigit(arr[i], exp, negative);

        if(digit!= 0) {
            output[count[digit]-1] = arr[i];

            count[digit]--;
        }
    }

    // Copy sorted elements back into original array
    System.arraycopy(output, 0, arr, 0, arr.length);
}

private static int getDigit(int number, int exp, boolean negative) {
    int digit = (number/exp)%10;

    if(!negative) {
        return digit;
    } else {
        if(digit == 0) {
            return 10;
        } else {
            return digit * (-1);
        }
    }
}

private static void reverse(int[] arr, int low, int high) {
    while(low < high) {
        int temp = arr[low];
        arr[low] = arr[high];
        arr[high] = temp;

        low++;
        high--;
    }
}

Complejidad de tiempo promedio: O (nk), donde k es el número máximo de dígitos de elementos en la matriz.

Supongo que te gusta

Origin blog.csdn.net/universsky2015/article/details/133593795
Recomendado
Clasificación