Tipo estable

La versión actual de la página aún no ha sido revisada por colaboradores experimentados y puede diferir significativamente de la versión revisada el 8 de enero de 2020; las comprobaciones requieren 2 ediciones .

Clasificación estable (estable)  : clasificación , que no cambia el orden relativo de los elementos ordenados que tienen las mismas claves, mediante el cual se produce la clasificación.

La estabilidad es una característica muy importante de un algoritmo de clasificación, pero, sin embargo, casi siempre se puede lograr alargando las claves originales con información adicional sobre su orden original. A pesar de la aparente necesidad implícita en el nombre, la estabilidad no es en absoluto necesaria para la clasificación correcta y la mayoría de las veces no se observa, ya que casi siempre se requiere memoria y tiempo adicionales para asegurarla.

Ejemplo

Al ordenar registros de la forma [apellido, nombre, patronímico] por apellido, los valores clave para Ivanov Sergey e Ivanov Ivan serán los mismos, por lo que la clasificación estable no intercambiará a Sergey e Ivan ( Python 3 , timsort sort [1] ):

registros = [ { 'apellido' : 'Ivanov' , 'nombre' : 'Sergey' , 'patronímico' : 'Mikhailovich' ,}, { 'apellido' : 'Ivanov' , 'nombre' : 'Ivan' , ' patronímico' : 'Borisovich' ,}, { 'apellido' : 'Abramov' , 'nombre' : 'Yuri' , 'patronímico' : 'Petrovich' ,}, ] registros . sort ( key = lambda x : x [ 'last name' ]) # sort by key "last name" for r in records : print ( ' %-12s %-12s %-12s ' % ( r [ 'last name' ] , r [ 'nombre de pila' ], r [ 'patronímico' ]))

Como resultado, los registros se ordenarán solo por apellido, manteniendo el orden original entre los registros con el mismo apellido:

Abramov Yuri Petrovich Ivanov Serguéi Mijáilovich Ivanov Iván Borisovich

Aplicación

La clasificación, que es el principal elemento de costo de la transformación de Burroughs-Wheeler , debe ser robusta.

Algoritmos Stablesort

A continuación, se encuentran descripciones de algoritmos de clasificación robustos con un tiempo de ejecución no peor que O( n log 2 n ) (excepto en los peores casos en el algoritmo que usa partición estable). En todo pseudocódigo, un par de barras // significa un comentario al final de la línea, como en C++.

Ordenar por fusión con memoria extra

Con este algoritmo de clasificación, la matriz inicial primero se divide recursivamente en partes hasta que cada parte de la matriz contenga uno o cero elementos (la reducción a la mitad se realiza en cada paso de recursividad). Después de eso, las partes resultantes en pares se "fusionan". La complejidad general del algoritmo es O( n log n ), y el algoritmo requiere O( n ) memoria adicional para almacenar las matrices fusionadas.

Clasificación usando partición estable

Este algoritmo es similar al algoritmo quicksort . Al igual que en el algoritmo de clasificación rápida, en este algoritmo la matriz inicial se divide en dos partes: en una, todos los elementos son menores o iguales que algún elemento pivote, y en la otra, son mayores o iguales. Después de eso, las partes separadas de la matriz se ordenan recursivamente de la misma manera. La diferencia entre este algoritmo y el algoritmo de clasificación rápida es que utiliza una partición estable en lugar de una inestable. A continuación se muestra la implementación de este algoritmo en pseudocódigo:

Ordenar(a[1..n]) si (n > 1) entonces N ← a[ 1 ]; m ← ParticiónEstable(a[1..n], N); Ordenar(a[1..m]); Ordenar(a[m+1..n]); ParticiónEstable(a[1..n], N) si ( n > 1 ) entonces metro ← ⌊ n/2 ⌋; m1 ← ParticiónEstable(a[1..m], N); m2 ← m+ParticiónEstable(a[m+1..n], N); Rotar(a[m1..m2], m); retorno(m1 + (m2-m)); más si(a[1] < N) entonces retorno(1); más volver(0)

Procedimiento de rotación:

Rotar(a[1..n], m) if( n > m > 1 ) //la matriz tiene al menos dos elementos y el cambio tiene sentido cambio ← min; //número de posiciones a cambiar mcd ← mcd(m-1, n); //MCD es el máximo común divisor para i ← 0 a mcd-1 hacer cabeza ← i+1; headVal ← a[cabeza]; cabeza actual ←; siguiente ← cabezal+cambio; mientras (siguiente ≠ cabeza) hacer a[actual] ← a[siguiente]; actual ← siguiente; siguiente ← siguiente+cambio; si (siguiente> n) siguiente ← siguiente-n; a[actual] ← headVal;

Se necesitan operaciones elementales O ( n log n ) para particionar la matriz de manera sostenible . Dado que la "profundidad" del descenso recursivo realizado es O(log n ) en promedio, la complejidad general del algoritmo es O( n log² n ). En este caso, el algoritmo requerirá memoria de pila O(log n ) para realizar la recursión, aunque en el peor de los casos, la complejidad total es O( n ² log n ) y se requiere memoria O( n ).

Combinar algoritmos de ordenación sin memoria extra

Todos los algoritmos que se describen a continuación se basan en fusionar matrices ya ordenadas sin usar memoria adicional más allá de la memoria de pila O(log²n ) (esta es la llamada condición de memoria mínima [2] ) y difieren solo en el algoritmo de fusión. El siguiente es el pseudocódigo de los algoritmos (los algoritmos de combinación, el procedimiento de combinación, se proporcionan por separado para cada método):

Ordenar(a[1..n]) si ( n > 1 ) entonces m ← n/2 ; Ordenar(a[1..m]); Ordenar(a[m+1..n]); Fusionar(a[1..n], m); Algoritmo de Pratt

En el algoritmo de Pratt, dos elementos α y β se encuentran en matrices ordenadas , que son las medianas de una matriz que consta de elementos de ambas matrices. Entonces todo el arreglo se puede representar como aαbxβy . Después de eso, se lleva a cabo un cambio cíclico de elementos, como resultado de lo cual obtenemos la siguiente secuencia: axβαby . Además, el algoritmo de combinación se repetirá recursivamente para las matrices ax y by.

Fusionar(a[1..n], m) if(m ≠ 1 and m ≠ n) //esta condición significa que el arreglo debe tener al menos 2 elementos if( n-1 > 2 ) //el caso en que hay exactamente dos elementos debe considerarse por separado si ( m-1 > nm ) Límite izquierdo←1; LímiteDerecha←m; while( leftBound < rightBound ) do //buscar medianas en este bucle m1 ← (Límite Izquierdo+Límite Derecho)/2; m2 ← BuscarPrimeraPosición(a[m..n], a[ m1 ]); //implementación del procedimiento FindFirstPosition ver siguiente. párrafo si ( m1 + (m2-m) > n/2 ) entonces límite derecho ← m1-1; más límite izquierdo ← m1+1; Rotar(a[m1..m2], m); Fusionar(a[1..m1+(m2-m)], m1); Fusionar(a[m1+(m2-m)+1..n], m2); más si(a[m] < a[1]) a[m]⟷a[1];

Cambiar elementos requiere operaciones de elementos O( n ) y memoria adicional O(1), mientras que encontrar medianas en dos matrices ya ordenadas requiere operaciones de elementos O(log² n ) y memoria adicional O(1). Dado que hay pasos de recursión O(log n ), la complejidad de dicho algoritmo de combinación es O( n log n ), y la complejidad general del algoritmo de clasificación es O( n log² n ). En este caso, el algoritmo requerirá memoria de pila O(log² n ).

Algoritmo sin búsqueda de medianas

Puede deshacerse de la búsqueda de medianas en el algoritmo anterior de la siguiente manera. En la mayor de las dos matrices, seleccione el elemento central α (el elemento pivote). A continuación, en la matriz más pequeña, busque la posición de la primera aparición de un elemento mayor o igual que α. Llamémoslo β. Después de eso, los elementos se desplazan cíclicamente, como en el algoritmo de Pratt ( aαbxβy → axβαby ), y las partes obtenidas se fusionan recursivamente. El pseudocódigo del algoritmo de fusión se proporciona a continuación.

Fusionar(a[1..n], m) if(m ≠ 1 and m ≠ n) //esta condición significa que el arreglo debe tener al menos 2 elementos if( n-1 > 2 ) //el caso cuando exactamente dos elementos tienen que ser considerados por separado si ( m-1 > nm ) m1 ← (m+1)/2 ; m2 ← BuscarPrimeraPosición(a[m..n], a[ m1 ]); más m2 ← (n+m)/2 ; m1 ← BuscarÚltimaPosición(a[1..m], a[ m2 ]); Rotar(a[m1..m2], m); Fusionar(a[1..m1+(m2-m)], m1); Fusionar(a[m1+(m2-m)+1..n], m2); más si(un[ m ] < un[ 1 ]) un[ m ]⟷a[ 1 ];

Los procedimientos FindFirstPosition y FindLastPosition son casi idénticos al procedimiento de búsqueda binaria:

BuscarPrimeraPosición(a[1..n], x) límite izquierdo ← 1; Límite derecho ← n; actual ← 1; while(LímiteIzquierdo <LímiteDerecho) hacer actual ← ⌊(Límite izquierda+Límite derecha)/2⌋; si (a [ actual ] < x) entonces límite izquierdo ← actual+1 más límite derecho ← actual; retorno (actual); BuscarÚltimaPosición(a[1..n], x) límite izquierdo ← 1; Límite derecho ← n; actual ← 1; while(LímiteIzquierdo <LímiteDerecho) hacer actual ← ⌊(Límite izquierda+Límite derecha)/2⌋; si ( x < a [ actual ] ) entonces límite derecho ← actual; más límite izquierdo ← actual+1 retorno (actual);

A diferencia del algoritmo de Pratt, en este algoritmo la partición puede ser sustancialmente desigual. Pero incluso en el peor de los casos, el algoritmo dividirá todo el rango [ a .. y ] en una proporción de 3:1 (si todos los elementos del segundo rango son mayores o menores que el de referencia y ambos rangos tienen el mismo número de elementos). Esto garantiza un número logarítmico de pasos de recurrencia al fusionar cualquier matriz. Por lo tanto, obtenemos que, al igual que en el algoritmo de Pratt, la complejidad del algoritmo de fusión es O( n log n ), la complejidad del algoritmo de clasificación es O( n log² n ) y la memoria requerida es O(log² n ).

Clasificación estable sin memoria adicional en tiempo O ( n log n )

Formas de mejorar los algoritmos

  • En todos los algoritmos anteriores, al dividir la matriz original en partes, la división recursiva se puede detener si el tamaño de la matriz de división se vuelve lo suficientemente pequeño. Luego, se puede aplicar cualquiera de los algoritmos de clasificación simples (por ejemplo , clasificación por inserción ), que se sabe que son más rápidos que los complejos si el tamaño de la matriz de entrada es pequeño. De hecho, esta técnica es aplicable no solo para los algoritmos que se presentan aquí, sino también para cualquier otro algoritmo que utilice la partición recursiva de la matriz original (por ejemplo, la ordenación rápida inestable habitual ). El número específico de elementos de entrada en los que es necesario cambiar a un algoritmo de clasificación simple depende de la computadora utilizada.
  • En el algoritmo de Pratt, el algoritmo no mediano y el algoritmo de partición robusto, en lugar de llamar a la combinación recursivamente cada vez, puede asignar dinámicamente una matriz de variables temporales. Entonces será posible continuar dividiendo el rango solo hasta que el número de elementos en el rango más grande sea menor o igual a la capacidad del arreglo temporal. De hecho, en algún paso de la recursividad, el algoritmo de Pratt y el algoritmo sin búsqueda de medianas se convierten en un simple algoritmo de fusión. Que. si el sistema tiene suficiente memoria dinámica, entonces el tiempo de ejecución asintótico puede llevarse a O( n log n ) en lugar de O( n log² n ).

Notas

  1. Ordenar Tim, c2.com . Consultado el 18 de noviembre de 2012. Archivado desde el original el 14 de junio de 2013.
  2. Knut D., El arte de la programación. v. 3, Clasificación y búsqueda, M., Williams, 2000