Clasificación universal. Método de Hoare - Clasificación rápida (Quick-sort)

Si usted es un programador y su código va más allá de escribir una calculadora, a menudo encontrará o se habrá enfrentado a la necesidad de clasificar este o aquel conjunto de datos. Hay muchas formas de ordenar. En este artículo, analizaremos los principales y nos centraremos en el ordenamiento rápido.

Comprender la ordenación rápida

Ordenación rápida - Ordenación rápida o qsort. Por el nombre queda claro qué es y por qué. Pero si no está claro, entonces este es un algoritmo para ordenar rápidamente una matriz, el algoritmo tiene una eficiencia de O (n log n) en promedio. ¿Qué significa? Esto significa que el tiempo promedio de ejecución del algoritmo aumenta a lo largo de la misma trayectoria que el gráfico de esta función. Algunos lenguajes populares tienen bibliotecas integradas con este algoritmo, y esto ya indica que es extremadamente efectivo. Estos son lenguajes como Java, C++, C#.

Algoritmo

El método quicksort utiliza recursividad y una estrategia Divide and Conquer.

1. Se busca un determinado elemento de referencia en la matriz, por simplicidad es mejor tomar el central, pero si desea trabajar en la optimización, deberá probar diferentes opciones.

2. A la izquierda del soporte, se busca un elemento más grande que la referencia, a la derecha, más pequeño que la referencia, luego los intercambiamos. Hacemos esto hasta que el máximo a la derecha sea menor que el mínimo a la izquierda. Así, lanzamos todos los elementos pequeños al principio, los grandes al final.

3. Aplique recursivamente este algoritmo a las partes izquierda y derecha de nuestro algoritmo por separado, luego una y otra vez, hasta que se alcance un elemento o una cierta cantidad de elementos. ¿Cuál es el número de elementos? Hay otra forma de optimizar este algoritmo. Cuando la parte que se va a clasificar se vuelve aproximadamente igual a 8 o 16, puede procesarla con una clasificación normal, como la clasificación de burbujas. Entonces aumentaremos la eficiencia de nuestro algoritmo, porque. no procesa arreglos pequeños tan rápido como nos gustaría.

Por lo tanto, toda la matriz será procesada y ordenada. Ahora, echemos un vistazo más de cerca a este algoritmo.

Eficiencia de clasificación rápida

¿Quicksort es el algoritmo de clasificación más rápido? Definitivamente no. Ahora hay más y más clasificaciones, en este momento la clasificación más rápida es Timsort, funciona extremadamente rápido para arreglos inicialmente ordenados de manera diferente. Pero no olvide que el método de ordenación rápida es uno de los más fáciles de escribir, esto es muy importante porque, por regla general, para un proyecto ordinario, solo necesita una escritura simple y no un algoritmo enorme que usted mismo pueda hacer. no escribir Timsort tampoco es el algoritmo más complejo, pero el título del más simple definitivamente no brilla.

Implementación del algoritmo

Bueno, aquí llegamos a los más "deliciosos". Ahora veamos cómo se implementa este algoritmo. Como se mencionó anteriormente, no es demasiado complicado de implementar, más bien, incluso simple. Pero aún analizaremos completamente cada paso de nuestro código para que comprenda cómo funciona la ordenación rápida.

Nuestro método se llama QuickSort. Inicia el algoritmo principal, al que pasamos la matriz, su primer y último elemento. Recordamos el primer y último elemento del segmento ordenado en las variables i y k para no cambiar estas variables, ya que las necesitamos. Luego verificamos la distancia entre el primero y el último verificado: ¿es mayor o igual a uno? Si no, entonces hemos llegado al centro y necesitamos salir de la clasificación de este segmento, y si es así, entonces continuamos clasificando.

Luego tomamos el primer elemento del segmento a ordenar como el elemento de referencia. Hacemos el siguiente ciclo hasta llegar al centro. En él, hacemos dos ciclos más: el primero, para el lado izquierdo y el segundo, para el derecho. Los ejecutamos hasta que haya elementos que coincidan con la condición, o hasta llegar al elemento pivote. Luego, si el elemento mínimo todavía está a la derecha y el elemento máximo está a la izquierda, los intercambiamos. Cuando termina el bucle, cambiamos el primer elemento y el pivote si el pivote es más pequeño. Luego hacemos recursivamente nuestro algoritmo para las secciones derecha e izquierda de la matriz y continuamos así hasta llegar a un segmento con una longitud de 1 elemento. Luego, todos nuestros algoritmos recursivos regresarán y saldremos completamente del ordenamiento. También en la parte inferior hay un método de intercambio, un método completamente estándar cuando se ordena una matriz por reemplazos. Para no escribir el reemplazo de elementos varias veces, escribimos una vez y cambiamos los elementos en esta matriz.

En conclusión, podemos decir que en términos de la relación "calidad-complejidad", la ordenación rápida ocupa la posición de liderazgo entre todos los algoritmos, por lo que definitivamente debe tomar nota del método y usarlo en sus proyectos si es necesario.

Hasta ahora, la única publicación de clasificación en mi blog ha sido . ¡Es hora de arreglarlo! No construiré planes grandiosos para conquistar todos los tipos de clasificación, pero comenzaré con uno de los más populares: la clasificación rápida.

No seré el primero ni el último en decir que es "rápido" solo en el nombre, y ahora hay muchos análogos y, en general, cada tipo de datos necesita su propia clasificación. Sí, todo esto es cierto, pero esta verdad no niega el simple hecho de que implementación manuscrita de quicksort perfecciona las habilidades de programación en general y siempre será en los cursos universitarios, estoy seguro. Por las mismas razones, se eligió un lenguaje de programación para la implementación, ya que puede practicar de inmediato el uso de punteros en C / C ++.

Propongo poner manos a la obra y primero considerar brevemente la esencia del algoritmo.

Qué tan rápido funciona la ordenación

El esquema del algoritmo se puede describir de la siguiente manera:

  1. Elegir referencia un elemento en una matriz: a menudo se encuentra una variante con un elemento central.
  2. Dividir matriz en dos partes como sigue: todos los elementos de izquierda partes que mayor que o igual a la base, lo echamos en Correcto , del mismo modo, todos los elementos de Correcto , cual menor o igual lanzamos la referencia a izquierda parte.
  3. Como resultado del paso anterior, en el lado izquierdo de la matriz habrá elementos que son menores o iguales que el central, y en el lado derecho, mayores o iguales.
    Esto se puede mostrar visualmente de esta manera:
    |———————|—————————|———————|
    | más[yo]<= mid | mid = mas | mas[i] >= medio |
    |———————-|—————————|———————|
  4. Repita recursivamente la acción para las partes izquierda y derecha de la matriz.

La recursividad se detiene cuando el tamaño de ambas partes es menor o igual a uno.

Tomé prestada una ilustración de un paso del algoritmo, es dolorosamente visual.

Implementación recursiva de quicksort

Como entrada, la función toma la matriz en sí (puntero al principio) y su tamaño.

Void qsortRecursive(int *mas, int size) ( //Puntero al principio y al final del arreglo int i = 0; int j = tamaño - 1; //El elemento central del arreglo int mid = mas; //Dividiendo the array do ( // Corremos a través de los elementos, buscando aquellos que necesitan ser transferidos a otra parte // En el lado izquierdo de la matriz, omita (deje en su lugar) los elementos que son menores que el central while (mas [i]< mid) { i++; } //В правой части пропускаем элементы, которые больше центрального while(mas[j] >mid) ( j--; ) // Intercambiar elementos si (i<= j) { int tmp = mas[i]; mas[i] = mas[j]; mas[j] = tmp; i++; j--; } } while (i <= j); //Рекурсивные вызовы, если осталось, что сортировать if(j >0) ( //"Pieza izquierda" qsortRecursive(mas, j + 1); ) if (i< size) { //"Првый кусок" qsortRecursive(&mas[i], size - i); } }

Conclusión

Esta tarea ayuda simultáneamente a comprender cómo funciona la recursividad y le enseña cómo realizar un seguimiento de los cambios de datos durante la ejecución del algoritmo. Se recomienda "continuar" y escribirlo usted mismo, pero aún aquí está mi implementación, puede ser útil para mí y para mí. Eso es todo para mí, ¡gracias por su atención!

O( norte) auxiliar
O(registro norte) auxiliares (Sedgwick 1978)

Ordenación rápida, tipo de Hoare(ing. quicksort), a menudo llamado ordenar(por su nombre en la biblioteca estándar C) es un conocido algoritmo de clasificación desarrollado por el informático inglés Charles Hoare durante su trabajo en la Universidad Estatal de Moscú en 1960.

algoritmo clasificación rápida (A, lo, hola) es si hola< hi después p:= partición(A, bajo, hola) ordenación rápida(A, bajo, p – 1) ordenación rápida(A, p + 1, hola) algoritmo partición (A, lo, hola) es pivote:= A i:= lo - 1 por j:= bajo a hola - 1 hacer si A[j] ≤ pivote después i:= i + 1 intercambiar A[i] con A[j] intercambiar A con A devolver yo + 1

Se puede ordenar una matriz completa haciendo quicksort(A, 1, length(A)) .

División de Hoare

Este esquema utiliza dos índices (uno al principio de la matriz, el otro al final), que se aproximan entre sí hasta que hay un par de elementos donde uno es mayor que el pivote y está ubicado antes de él, y el segundo es más pequeño y ubicado después de este. Estos elementos se intercambian. El intercambio ocurre hasta que los índices se cruzan. El algoritmo devuelve el último índice. . El esquema de Hoare es más eficiente que el esquema de Lomuto, ya que en promedio hay tres veces menos intercambios (swap) de elementos, y la partición es más eficiente incluso cuando todos los elementos son iguales. Al igual que el esquema Lomuto, este esquema también muestra eficiencia en O(norte 2) cuando la matriz de entrada ya está ordenada. Ordenar usando este esquema es inestable. Tenga en cuenta que la posición final del elemento ancla no es necesariamente la misma que el índice devuelto. Pseudocódigo:

algoritmo clasificación rápida (A, lo, hola) es si hola< hi después p:= partición(A, bajo, hola) ordenación rápida(A, bajo, p) ordenación rápida(A, p + 1, hola) algoritmo partición (A, lo, hola) es pivote:= A i:= bajo - 1 j:= alto + 1 bucle para siempre hacer yo:= yo + 1 tiempo Ai]< pivot hacer j:= j-1 tiempo A[j] > pivote si yo >= j después devolver j intercambiar A[i] con A[j]

Elementos repetitivos

Para mejorar el rendimiento con una gran cantidad de elementos idénticos en la matriz, se puede aplicar el procedimiento para dividir la matriz en tres grupos: elementos menores que la referencia, iguales y mayores. (Bentley y McIlroy llaman a esto una "partición gruesa". Esta partición se usa en la función ordenar en la séptima versión de Unix. ). Pseudocódigo:

algoritmo clasificación rápida (A, lo, hola) es si hola< hi después p:= pivote(A, lo, hola) izquierda, derecha:= partición(A, p, lo, hola) // devuelve dos valores clasificación rápida (A, baja, izquierda) clasificación rápida (A, derecha, hola)

Estimación de la complejidad del algoritmo

Está claro que la operación de dividir una matriz en dos partes con respecto al elemento de referencia lleva tiempo. Dado que todas las operaciones de partición realizadas a la misma profundidad de recursión procesan diferentes partes del arreglo original, cuyo tamaño es constante, en total en cada nivel de recursión también será necesario O(n) (\displaystyle O(n)) operaciones. Por lo tanto, la complejidad general del algoritmo está determinada únicamente por el número de divisiones, es decir, la profundidad de la recursividad. La profundidad de la recursividad, a su vez, depende de la combinación de datos de entrada y de cómo se determina el pivote.

Mejor caso. En la versión más equilibrada, en cada operación de división, la matriz se divide en dos partes idénticas (más o menos un elemento), por lo tanto, la profundidad de recursión máxima a la que los tamaños de las subarreglas procesadas alcanzan 1 será iniciar sesión 2 ⁡ norte (\displaystyle \log _(2)n). Como resultado, el número de comparaciones realizadas por quicksort sería igual al valor de la expresión recursiva C norte = 2 ⋅ C norte / 2 + norte (\displaystyle C_(n)=2\cdot C_(n/2)+n), que da la complejidad general del algoritmo O (n ⋅ log 2 ⁡ n) (\displaystyle O(n\cdot \log _(2)n)). Promedio. La complejidad promedio con una distribución aleatoria de datos de entrada solo puede estimarse probabilísticamente. En primer lugar, debe tenerse en cuenta que en realidad no es necesario que el elemento pivote siempre divida la matriz en dos idéntico partes. Por ejemplo, si en cada etapa habrá una división en arreglos con una longitud de 75% y 25% del original, la profundidad de recursión será igual a , y esto aún da complejidad. En general, para cualquier fijado relación entre las partes izquierda y derecha de la división, la complejidad del algoritmo será la misma, solo que con diferentes constantes. Consideraremos una división “exitosa” tal que el elemento de referencia estará entre el 50% central de los elementos de la parte compartida del arreglo; claramente, la probabilidad de suerte con una distribución aleatoria de elementos es 0.5. Con una separación exitosa, los tamaños de los subarreglos seleccionados serán al menos el 25 % y no más del 75 % del original. Dado que cada subarreglo seleccionado también tendrá una distribución aleatoria, todas estas consideraciones se aplican a cualquier etapa de clasificación y cualquier fragmento de arreglo inicial. Una división exitosa da una profundidad de recursión de como máximo Iniciar sesión 4/3 ⁡ norte (\displaystyle \log _(4/3)n). Como la probabilidad de suerte es 0.5, para obtener k (\ estilo de visualización k) separaciones exitosas en promedio requerirán 2 ⋅ k (\displaystyle 2\cdot k) llamadas recursivas al elemento pivote k veces estaba entre el 50% central de la matriz. Aplicando estas consideraciones, podemos concluir que, en promedio, la profundidad de recursión no excederá 2 ⋅ log 4 / 3 ⁡ norte (\displaystyle 2\cdot \log _(4/3)n), que es igual a O (log ⁡ n) (\displaystyle O(\log n)) Y dado que cada nivel de recursividad todavía lo hace como máximo O(n) (\displaystyle O(n)) operaciones, la complejidad media será O (n Iniciar sesión ⁡ norte) (\ Displaystyle O (n \ iniciar sesión n)). Peor de los casos. En la versión más desequilibrada, cada división produce dos subarreglos de tamaños 1 y , lo que significa que con cada llamada recursiva, el arreglo más grande será 1 más corto que la vez anterior. Esto puede suceder si se selecciona el más pequeño o el más grande de todos los elementos procesados ​​como elemento de referencia en cada etapa. Con la elección más simple de un elemento de referencia, el primero o el último en la matriz, dicho efecto lo dará una matriz ya ordenada (en orden directo o inverso), para el medio o cualquier otro elemento fijo, la "matriz del peor caso". " también se puede seleccionar especialmente. En este caso, necesitarás norte - 1 (\ estilo de visualización n-1) operaciones de separación, y el tiempo total de ejecución será ∑ yo = 0 norte (n − i) = O (n 2) (\displaystyle \textstyle \sum _(i=0)^(n)(n-i)=O(n^(2))) operaciones, es decir, la ordenación se realizará en tiempo cuadrático. Pero la cantidad de intercambios y, en consecuencia, el tiempo de funcionamiento no es su mayor inconveniente. Peor aún, en este caso, la profundidad de recursión durante la ejecución del algoritmo alcanzará n, lo que significará un ahorro de n veces de la dirección de retorno y las variables locales del procedimiento de partición de la matriz. Para valores grandes de n, el peor de los casos puede ser el agotamiento de la memoria (desbordamiento de pila) mientras se ejecuta el programa.

Ventajas y desventajas

ventajas:

Defectos:

Mejoras

Las mejoras del algoritmo están destinadas principalmente a eliminar o mitigar las desventajas anteriores, por lo que todas ellas se pueden dividir en tres grupos: hacer que el algoritmo sea estable, eliminar la degradación del rendimiento mediante una elección especial del elemento pivote y proteger contra la pila de llamadas. desbordamiento debido a la gran profundidad de recursión en caso de datos de entrada fallidos.

  • El problema de la inestabilidad se resuelve expandiendo la clave con el índice inicial del elemento en el arreglo. En el caso de igualdad de las claves principales, la comparación se realiza por índice, excluyendo así la posibilidad de cambiar la posición relativa de elementos iguales. Esta modificación no es gratuita: requiere una memoria O(n) adicional y un pase completo a través de la matriz para guardar los índices originales.
  • La degradación de la velocidad en el caso de un conjunto fallido de datos de entrada se resuelve en dos direcciones diferentes: reduciendo la probabilidad de que ocurra el peor de los casos mediante una elección especial del elemento de referencia y el uso de varias técnicas que garantizan un funcionamiento estable en la entrada fallida datos. Para la primera dirección:
  • Selección del elemento central. Elimina la degradación de los datos preordenados, pero deja la posibilidad de ocurrencia aleatoria o selección deliberada de una matriz "mala".
  • Elegir una mediana de tres elementos: primero, medio y último. Reduce la probabilidad de que ocurra el peor de los casos en comparación con elegir el elemento intermedio.
  • Selección aleatoria. La probabilidad de que ocurra al azar el peor de los casos se vuelve extremadamente pequeña y la selección deliberada se vuelve prácticamente imposible. El tiempo de ejecución esperado del algoritmo de clasificación es O( norte lg norte).
La desventaja de todos los métodos complicados para seleccionar el elemento de referencia es la sobrecarga adicional; sin embargo, no son tan buenos.
  • Para evitar fallas en el programa debido a una gran profundidad de recursión, se pueden usar los siguientes métodos:

Pseudocódigo.
quickSort (matriz a, límite superior N) (Seleccione el elemento pivote p - medio de la matriz Divida la matriz por este elemento Si la subarreglo a la izquierda de p contiene más de un elemento, llame a quickSort en él. Si el subarreglo a la derecha de p contiene más de un elemento, llame a quickSort en él). Implementación en C.
modelo void quickSortR(T* a, long N) ( // Entrada - matriz a, a[N] - su último elemento. largo i = 0, j = N-1; // poner punteros a lugares originales Ttemp,p; p = a[N>>1]; // elemento central // procedimiento de separación hacer (mientras que (a[i]< p) i++; while (a[j] >p) j--; si yo<= j) { temp = a[i]; a[i] = a[j]; a[j] = temp; i++; j--; } } while (i<=j); // llamadas recursivas si hay algo que ordenar si (j > 0) quickSortR(a, j); si (N > i) quickSortR(a+i, N-i); )

Cada división obviamente requiere operaciones Theta(n). El número de pasos de división (profundidad de recurrencia) es aproximadamente log n si la matriz se divide en partes más o menos iguales. Así, el rendimiento global es: O(n log n), que es lo que sucede en la práctica.

Sin embargo, es posible el caso de tales datos de entrada, en los que el algoritmo funcionará en operaciones O (n 2). Esto sucede si cada vez se elige como elemento central el máximo o el mínimo de la secuencia de entrada. Si los datos se toman al azar, la probabilidad de esto es 2/n. Y esta probabilidad debe realizarse en cada paso... En términos generales, una situación poco realista.

El método es inestable. El comportamiento es bastante natural, teniendo en cuenta que con el ordenamiento parcial, aumentan las posibilidades de dividir la matriz en partes más iguales.

La clasificación utiliza memoria adicional porque la profundidad de recursión aproximada es O (log n) y las subllamadas recursivas se insertan en la pila cada vez.

Modificaciones de código y método

    Debido a la recursividad y otros gastos generales, es posible que Quicksort no sea tan rápido para arreglos cortos. Por lo tanto, si hay menos elementos CUTOFF en la matriz (una constante dependiente de la implementación, generalmente entre 3 y 40), se llama a la ordenación por inserción. El aumento de velocidad puede ser de hasta un 15%.

    Para dar vida al método, puede modificar la función quickSortR reemplazando las últimas 2 líneas con

    Si (j > CUTOFF) quickSortR(a, j); if (N > i + CUTOFF) quickSortR(a+i, N-i);

    Por lo tanto, las matrices de elementos CUTOFF y menos no se ordenarán, y al final de la operación quickSortR(), la matriz se dividirá en partes sucesivas de<=CUTOFF элементов, отсортированные друг относительно друга. Близкие элементы имеют близкие позиции, поэтому, аналогично сортировке Шелла, вызывается insertSort(), которая доводит процесс до конца.

    Modelo void qsortR(T *a, tamaño largo) ( quickSortR(a, tamaño-1); insertSort(a, tamaño); // insertSortGuarded es más rápido, pero necesita una función setmax()}

  1. En el caso de la recursividad explícita, como en el programa anterior, no solo se almacenan en la pila los límites de los subarreglos, sino también una serie de parámetros completamente innecesarios, como las variables locales. Si emula la pila mediante programación, su tamaño se puede reducir varias veces.
  2. Cuantas más partes iguales se divida la matriz, mejor. Por lo tanto, es recomendable tomar el promedio de tres como referencia, y si la matriz es lo suficientemente grande, entonces de nueve elementos arbitrarios.
  3. Deje que las secuencias de entrada sean muy malas para el algoritmo. Por ejemplo, se seleccionan especialmente para que el elemento medio resulte ser el mínimo cada vez. ¿Cómo hacer que QuickSort sea resistente a tal "sabotaje"? Es muy simple: elegir un elemento aleatorio de la matriz de entrada como referencia. Entonces se neutralizarán los patrones desagradables en el flujo de entrada. Otra opción es reorganizar los elementos de la matriz aleatoriamente antes de ordenarlos.
  4. Quicksort también se puede utilizar para listas doblemente enlazadas. El único problema con esto es la falta de acceso directo al elemento aleatorio. Por lo tanto, debe elegir el primer elemento como referencia y esperar buenos datos iniciales o reorganizar aleatoriamente los elementos antes de clasificarlos.

Consideremos el peor de los casos, cuando los elementos de referencia seleccionados al azar resultaron ser muy malos (cerca de los valores extremos). La probabilidad de esto es extremadamente pequeña, ya en n = 1024 es menor que 2 -50, por lo que el interés es más teórico que práctico. Sin embargo, el comportamiento de "clasificación rápida" es el "punto de referencia" para algoritmos de divide y vencerás implementados de manera similar. No en todas partes se puede reducir la probabilidad del peor de los casos a casi cero, por lo que esta situación merece ser estudiada.

Deje, por definición, elegir cada vez el elemento más pequeño a min. Luego, el procedimiento de división moverá este elemento al comienzo de la matriz y dos partes pasarán al siguiente nivel de recursividad: una del único elemento a min , la otra contiene los n-1 elementos restantes de la matriz. Luego se repetirá el proceso para una parte de (n-1) elementos.. Y así sucesivamente..
Cuando se usa un código recursivo como el anterior, esto significaría n llamadas recursivas anidadas de QuickSort.
Cada llamada recursiva significa almacenar información sobre el estado actual de las cosas. Por lo tanto, la clasificación requiere O (n) memoria adicional ... Y no solo en cualquier lugar sino en la pila. Para n lo suficientemente grande, tal requisito puede tener consecuencias impredecibles.

Para evitar esta situación, puede reemplazar la recursividad con la iteración implementando una pila basada en arreglos. El procedimiento de separación se realizará como un bucle.
Cada vez que la matriz se divide en dos partes, se enviará una solicitud a la pila para clasificar la más grande y la más pequeña se procesará en la siguiente iteración. Las solicitudes se eliminarán de la pila a medida que el procedimiento de partición se libere de las tareas actuales. La clasificación termina su trabajo cuando finalizan las solicitudes.

Pseudocódigo.
QuickSort iterativo (matriz a, tamaño tamaño) ( Empuje una consulta para ordenar la matriz de 0 a tamaño 1 en la pila. do ( Quite los límites lb y ub de la matriz actual de la pila. do ( 1. Realice una división operación en el arreglo actual a. 2. Empuje los límites de la parte más grande en la pila 3. Mueva los límites de ub, lb para señalar la parte más pequeña ) siempre que la parte más pequeña consista en dos o más elementos ) siempre que ya que hay solicitudes en la pila) Implementación en C.
#define MAXSTACK 2048 // tamaño máximo de pila modelo void qSortI(T a, long size) ( long i, j; // punteros involucrados en la división libras largas, ub; // bordes del fragmento ordenado en el bucle pila larga, pila inferior; // pila de solicitudes // cada solicitud recibe un par de valores, // a saber: izquierda (lbstack) y derecha (ubstack) // límites de la brecha pila larga pos = 1; // posición actual de la pila ppos largo; // medio de la matriz pivote en T; // elemento de referencia Ttemp; pila de libras = 0; ubstack = tamaño-1; hacer( // Extraiga los límites lb y ub de la matriz actual de la pila. lb = lbpila[pilapila]; ub = ubstack[stackpos]; stackpos--; hacer( // Paso 1. Dividir por pivote ppos = (lb + ub) >> 1; yo = lb; j = ub; pivote = un; hacer (mientras que (a[i]< pivot) i++; while (pivot < a[j]) j--; if (i <= j) { temp = a[i]; a[i] = a[j]; a[j] = temp; i++; j--; } } while (i <= j); // Ahora el puntero i apunta al comienzo del subarreglo derecho, // j - hasta el final de la izquierda (ver ilustración arriba), lb ? j? ¿i? ub. // Es posible que el puntero i o j esté fuera de los límites de la matriz // Pasos 2, 3. Empuje la mayor parte sobre la pila y mueva lb,ub si yo< ppos) { // el lado derecho es mas grande si yo< ub) { // si tiene más de 1 elemento, necesita pilapos++; // ordenar, solicitar a la pila lbstack[stackpos] = i; ubstack[stackpos] = ub; )ub = j; // próxima iteración dividida // trabajará en el lado izquierdo) más ( // el lado izquierdo es mas grande if (j > lb) ( stackpos++; lbstack[ stackpos ] = lb; ubstack[ stackpos ] = j; ) lb = i; ) ) mientras (lb< ub); // mientras que la parte más pequeña tiene más de 1 elemento) while (pilapos != 0); // siempre que haya solicitudes en la pila}

El tamaño de la pila con esta implementación siempre es O(log n), por lo que el valor especificado en MAXSTACK es más que suficiente.

¡Hola a todos! Hablaré sobre el algoritmo de clasificación rápida y mostraré cómo se puede implementar mediante programación.

Entonces, quicksort o, como el nombre de la función C, Qsort, es un algoritmo de clasificación cuya complejidad es promedio es O(nlog(n)). Su esencia es extremadamente simple: se selecciona el llamado elemento de referencia y la matriz se divide en 3 subarreglos: referencia más pequeña, igual a la referencia y referencia grande. Luego, este algoritmo se aplica recursivamente a los subarreglos.

Algoritmo

  1. Selección de un elemento base
  2. Divide la matriz en 3 partes
    • Creamos las variables l y r - índices, respectivamente, del comienzo y el final del subarreglo considerado
    • Incremente l siempre que el l-ésimo elemento sea menor que el pivote
    • Disminuye r mientras el elemento r-ésimo es mayor que el pivote
    • Si l sigue siendo menor que r, entonces intercambie los elementos lth y rth, incremente l y disminuya r
    • Si l de repente se vuelve mayor que r, entonces interrumpimos el ciclo
  3. Repetir recursivamente hasta llegar a una matriz de 1 elemento
Bueno, no parece tan difícil. Implementando en C? ¡No hay problema!
void qsort (int b, int e)
{
int l = b, r = e;
int piv = arr[(l + r) / 2]; // Por ejemplo, toma el elemento del medio como el elemento de referencia
mientras (l<= r)
{
mientras (arriba[l]< piv)
l++;
mientras (arr[r] > piv)
r--;
si (l<= r)
intercambiar (arr, arr);
}
si (b< r)
qordenar(b, r);
si (e > l)
qclasificar(l, e);
} /* ----- fin de la función qsort ----- */

// qsort(0, n-1);


* Este código fuente se resaltó con el Resaltador de código fuente .

Esta implementación tiene una serie de desventajas, como posibles desbordamientos de pila debido a una gran cantidad de recursividad anidada y el hecho de que el elemento central siempre se toma como pivote. Por ejemplo, esto puede ser normal, pero al resolver, por ejemplo, problemas de Olimpiadas, un jurado astuto puede seleccionar específicamente dichas pruebas para que esta solución les funcione durante demasiado tiempo y no pase el límite. En principio, puedes tomar cualquiera como elemento de referencia, pero es mejor que esté lo más cerca posible de la mediana, por lo que puedes elegirlo al azar o tomar el valor promedio del primero, medio y último. La dependencia del rendimiento del elemento de referencia es uno de los inconvenientes del algoritmo, no se puede hacer nada al respecto, pero rara vez ocurre una degradación grave del rendimiento, generalmente si se ordena un conjunto de números especialmente seleccionado. Si aún necesita una ordenación que garantice ser rápida, puede usar, por ejemplo, la ordenación en montón, que siempre funciona estrictamente en O(n log n). Por lo general, Qsort aún gana en rendimiento sobre otros tipos, no requiere mucha memoria adicional y es lo suficientemente simple de implementar, por lo tanto, es merecidamente popular.

Lo escribí yo mismo, ocasionalmente mirando Wikipedia. Aprovechando esta oportunidad, me gustaría agradecer a los maravillosos profesores y estudiantes de PetrSU, que me enseñaron muchas cosas útiles, ¡incluido este algoritmo!

Etiquetas: Qsort, quicksort, algoritmos de clasificación, algoritmos, C

Publicaciones relacionadas