Ordenación rápida: elegir el pivote

109

Al implementar Quicksort, una de las cosas que debe hacer es elegir un pivote. Pero cuando miro un pseudocódigo como el siguiente, no está claro cómo debo elegir el pivote. ¿Primer elemento de la lista? ¿Algo más?

 function quicksort(array)
     var list less, greater
     if length(array) ≤ 1  
         return array  
     select and remove a pivot value pivot from array
     for each x in array
         if x ≤ pivot then append x to less
         else append x to greater
     return concatenate(quicksort(less), pivot, quicksort(greater))

¿Puede alguien ayudarme a comprender el concepto de elegir un pivote y si diferentes escenarios requieren diferentes estrategias?

Jacob T. Nielsen
fuente

Respuestas:

87

La elección de un pivote aleatorio minimiza la posibilidad de que encuentre un rendimiento O (n 2 ) en el peor de los casos (si elige siempre el primero o el último, se produciría el peor rendimiento para los datos casi ordenados o casi al revés). La elección del elemento intermedio también sería aceptable en la mayoría de los casos.

Además, si está implementando esto usted mismo, hay versiones del algoritmo que funcionan en el lugar (es decir, sin crear dos listas nuevas y luego concatenarlas).

Dormir
fuente
10
Apoyaría la idea de que implementar una búsqueda usted mismo podría no valer la pena. Además, tenga cuidado al elegir números aleatorios, ya que los generadores de números aleatorios a veces son un poco lentos.
PeterAllenWebb
La respuesta de @Jonathan Leffler es mejor
Nathan
60

Depende de sus requisitos. La elección de un pivote al azar dificulta la creación de un conjunto de datos que genere un rendimiento O (N ^ 2). La 'mediana de tres' (primero, último, medio) también es una forma de evitar problemas. Sin embargo, tenga cuidado con el rendimiento relativo de las comparaciones; si sus comparaciones son costosas, entonces Mo3 hace más comparaciones que elegir (un solo valor pivote) al azar. Los registros de bases de datos pueden ser costosos de comparar.


Actualización: Conversión de comentarios en respuesta.

mdkess afirmó:

'Mediana de 3' NO es el primer último medio. Elija tres índices aleatorios y tome el valor medio de esto. El objetivo es asegurarse de que su elección de pivotes no sea determinista; si lo es, los datos del peor de los casos se pueden generar con bastante facilidad.

A lo que respondí:

  • El análisis del algoritmo de búsqueda de Hoare con una partición mediana de tres (1997) de P Kirschenhofer, H Prodinger, C Martínez respalda su afirmación (que la 'mediana de tres' son tres elementos aleatorios).

  • Hay un artículo descrito en portal.acm.org que trata sobre 'La permutación del peor caso para la clasificación rápida de mediana de tres' por Hannu Erkiö, publicado en The Computer Journal, Vol 27, No 3, 1984. [Actualización 2012-02- 26: Tengo el texto del artículo . La sección 2 'El algoritmo' comienza: ' Al usar la mediana del primer, medio y último elemento de A [L: R], se pueden lograr particiones eficientes en partes de tamaños bastante iguales en la mayoría de las situaciones prácticas. 'Por lo tanto, está discutiendo el enfoque de Mo3 primero-medio-último.]

  • Otro artículo breve que es interesante es el de MD McIlroy, "A Killer Adversary for Quicksort" , publicado en Software-Practice and Experience, vol. 29 (0), 1–4 (0 1999). Explica cómo hacer que casi cualquier Quicksort se comporte de forma cuadrática.

  • AT&T Bell Labs Tech Journal, Oct 1984 "Teoría y práctica en la construcción de una rutina de clasificación de trabajo" afirma "Hoare sugirió dividir alrededor de la mediana de varias líneas seleccionadas al azar. Sedgewick recomendó elegir la mediana de la primera [. ..] último [...] y medio ". Esto indica que ambas técnicas para 'mediana de tres' son conocidas en la literatura. (Actualización 2014-11-23: el artículo parece estar disponible en IEEE Xplore o en Wiley , si es miembro o está dispuesto a pagar una tarifa).

  • 'Engineering a Sort Function' de JL Bentley y MD McIlroy, publicado en Software Practice and Experience, Vol 23 (11), noviembre de 1993, entra en una discusión extensa de los problemas, y eligieron un algoritmo de partición adaptativo basado en parte en el tamaño del conjunto de datos. Hay mucha discusión sobre las compensaciones de varios enfoques.

  • Una búsqueda en Google de 'mediana de tres' funciona bastante bien para un mayor seguimiento.

Gracias por la información; Solo me había encontrado con la 'mediana de tres' determinista antes.

Jonathan Leffler
fuente
4
La mediana de 3 NO es el primero, el último, el medio. Elija tres índices aleatorios y tome el valor medio de esto. El objetivo es asegurarse de que su elección de pivotes no sea determinista; si lo es, los datos del peor de los casos se pueden generar con bastante facilidad.
Mindvirus
Estaba leyendo abt introsort, que combina buenas características tanto de quicksort como de heapsort. El enfoque para seleccionar el pivote utilizando una mediana de tres podría no ser siempre favorable.
Sumit Kumar Saha
4
El problema de elegir índices aleatorios es que los generadores de números aleatorios son bastante caros. Si bien no aumenta el costo de clasificación, probablemente hará las cosas más lentas que si acabara de elegir el primer, último y medio elemento. (En el mundo real, apuesto a que nadie está creando situaciones artificiales para ralentizar tu clasificación rápida.)
Kevin Chen
20

Je, acabo de dar esta clase.

Hay varias opciones.
Simple: elija el primer o último elemento del rango. (malo en la entrada parcialmente ordenada) Mejor: Elija el elemento en el medio del rango. (mejor en entradas parcialmente ordenadas)

Sin embargo, elegir cualquier elemento arbitrario corre el riesgo de dividir mal la matriz de tamaño n en dos matrices de tamaño 1 y n-1. Si lo hace con suficiente frecuencia, su clasificación rápida corre el riesgo de convertirse en O (n ^ 2).

Una mejora que he visto es elegir la mediana (primero, último, medio); En el peor de los casos, todavía puede ir a O (n ^ 2), pero probabilísticamente, este es un caso raro.

Para la mayoría de los datos, basta con elegir el primero o el último. Pero, si encuentra que se encuentra con los peores escenarios a menudo (entrada parcialmente ordenada), la primera opción sería elegir el valor central (que es un pivote estadísticamente bueno para datos parcialmente ordenados).

Si todavía tiene problemas, siga la ruta mediana.

Chris Cudmore
fuente
1
Hicimos un experimento en nuestra clase, obteniendo los k elementos más pequeños de una matriz en orden ordenado. Generamos matrices aleatorias, luego usamos un min-montón, o selección aleatoria y clasificación rápida de pivote fijo y contamos el número de comparaciones. En estos datos "aleatorios", la segunda solución se comportó peor en promedio que la primera. Cambiar a un pivote aleatorio resuelve el problema de rendimiento. Entonces, incluso para datos supuestamente aleatorios, el pivote fijo funciona significativamente peor que el pivote aleatorio.
Robert S. Barnes
¿Por qué dividir la matriz de tamaño n en dos matrices de tamaño 1 y n-1 correría el riesgo de convertirse en O (n ^ 2)?
Aaron Franke
Suponga una matriz de tamaño N. Divida en tamaños [1, N-1]. El siguiente paso es dividir la mitad derecha en [1, N-2]. y así sucesivamente, hasta que tengamos N particiones de tamaño 1. Pero, si tuviéramos que dividir por la mitad, estaríamos haciendo 2 particiones de N / 2 en cada paso, lo que lleva al término Log (n) de la complejidad;
Chris Cudmore
11

Nunca elija un pivote fijo; esto puede ser atacado para explotar el peor tiempo de ejecución O (n ^ 2) de su algoritmo, que solo está buscando problemas. El peor tiempo de ejecución de Quicksort ocurre cuando la partición da como resultado una matriz de 1 elemento y una matriz de n-1 elementos. Suponga que elige el primer elemento como partición. Si alguien alimenta una matriz a su algoritmo que está en orden decreciente, su primer pivote será el más grande, por lo que todo lo demás en la matriz se moverá a la izquierda. Luego, cuando recurras, el primer elemento volverá a ser el más grande, así que una vez más pones todo a la izquierda, y así sucesivamente.

Una mejor técnica es el método de la mediana de 3, en el que se seleccionan tres elementos al azar y se elige el medio. Sabes que el elemento que elijas no será el primero ni el último, pero también, según el teorema del límite central, la distribución del elemento medio será normal, lo que significa que tenderás hacia el medio (y por tanto , n lg n tiempo).

Si absolutamente desea garantizar el tiempo de ejecución O (nlgn) para el algoritmo, el método de columnas de 5 para encontrar la mediana de una matriz se ejecuta en el tiempo O (n), lo que significa que la ecuación de recurrencia para la ordenación rápida en el peor de los casos será sea ​​T (n) = O (n) (encuentre la mediana) + O (n) (partición) + 2T (n / 2) (recursiva izquierda y derecha). Según el Teorema principal, esto es O (n lg n) . Sin embargo, el factor constante será enorme, y si el rendimiento en el peor de los casos es su principal preocupación, utilice una ordenación combinada en su lugar, que es solo un poco más lenta que la ordenación rápida en promedio y garantiza el tiempo O (nlgn) (y será mucho más rápido que este rápido ordenamiento medio cojo).

Explicación del algoritmo de la mediana de medianas

Mindvirus
fuente
6

No intente ser demasiado inteligente y combine estrategias de pivote. Si combinó la mediana de 3 con un pivote aleatorio eligiendo la mediana del primero, el último y un índice aleatorio en el medio, seguirá siendo vulnerable a muchas de las distribuciones que envían una mediana de 3 cuadráticas (por lo que en realidad es peor que pivote aleatorio simple)

Por ejemplo, una distribución de órganos de tubos (1,2,3 ... N / 2..3,2,1) primero y último será 1 y el índice aleatorio será un número mayor que 1, tomando la mediana da 1 ( ya sea primero o último) y se obtiene una partición desequilibrada externamente.

caballo de papel
fuente
2

Es más fácil dividir la clasificación rápida en tres secciones haciendo esto

  1. Función de intercambio o intercambio de elementos de datos
  2. La función de partición
  3. Procesando las particiones

Es solo un poco más ineficaz que una función larga, pero es mucho más fácil de entender.

El código sigue:

/* This selects what the data type in the array to be sorted is */

#define DATATYPE long

/* This is the swap function .. your job is to swap data in x & y .. how depends on
data type .. the example works for normal numerical data types .. like long I chose
above */

void swap (DATATYPE *x, DATATYPE *y){  
  DATATYPE Temp;

  Temp = *x;        // Hold current x value
  *x = *y;          // Transfer y to x
  *y = Temp;        // Set y to the held old x value
};


/* This is the partition code */

int partition (DATATYPE list[], int l, int h){

  int i;
  int p;          // pivot element index
  int firsthigh;  // divider position for pivot element

  // Random pivot example shown for median   p = (l+h)/2 would be used
  p = l + (short)(rand() % (int)(h - l + 1)); // Random partition point

  swap(&list[p], &list[h]);                   // Swap the values
  firsthigh = l;                                  // Hold first high value
  for (i = l; i < h; i++)
    if(list[i] < list[h]) {                 // Value at i is less than h
      swap(&list[i], &list[firsthigh]);   // So swap the value
      firsthigh++;                        // Incement first high
    }
  swap(&list[h], &list[firsthigh]);           // Swap h and first high values
  return(firsthigh);                          // Return first high
};



/* Finally the body sort */

void quicksort(DATATYPE list[], int l, int h){

  int p;                                      // index of partition 
  if ((h - l) > 0) {
    p = partition(list, l, h);              // Partition list 
    quicksort(list, l, p - 1);        // Sort lower partion
    quicksort(list, p + 1, h);              // Sort upper partition
  };
};
Uglybb
fuente
1

Depende completamente de cómo se ordenen sus datos para empezar. Si cree que será pseudoaleatorio, su mejor opción es elegir una selección aleatoria o elegir el medio.

Joe Phillips
fuente
1

Si está ordenando una colección de acceso aleatorio (como una matriz), en general es mejor elegir el elemento físico del medio. Con esto, si la matriz está lista y ordenada (o casi ordenada), las dos particiones estarán casi uniformes y obtendrá la mejor velocidad.

Si está ordenando algo con acceso únicamente lineal (como una lista vinculada), entonces es mejor elegir el primer elemento, porque es el elemento de acceso más rápido. Aquí, sin embargo, si la lista ya está ordenada, está jodido: una partición siempre será nula y la otra lo tendrá todo, produciendo el peor momento.

Sin embargo, para una lista vinculada, elegir cualquier cosa que no sea la primera solo empeorará las cosas. Elija el elemento del medio en una lista, tendrá que recorrerlo en cada paso de la partición, agregando una operación O (N / 2) que se realiza logN veces, lo que hace que el tiempo total sea O (1.5 N * log N) y eso es si sabemos cuánto tiempo es la lista antes de comenzar; por lo general, no lo sabemos, por lo que tendríamos que recorrer todo el camino para contarlos, luego pasar a la mitad para encontrar el medio, luego pasar por un tercera vez para hacer la partición real: O (2.5N * log N)

James Curran
fuente
0

Idealmente, el pivote debería ser el valor medio de toda la matriz. Esto reducirá las posibilidades de obtener el peor rendimiento posible.

Faizan
fuente
1
carro delante del caballo aquí.
ncmathsadist
0

La complejidad de la clasificación rápida varía mucho con la selección del valor de pivote. por ejemplo, si siempre elige el primer elemento como pivote, la complejidad del algoritmo se vuelve tan peor como O (n ^ 2). Aquí hay un método inteligente para elegir el elemento pivote: 1. Elija el primer, medio y último elemento de la matriz. 2. compare estos tres números y encuentre el número que sea mayor que uno y menor que otro, es decir, la mediana. 3. Haga de este elemento un elemento pivote.

La elección del pivote mediante este método divide la matriz en casi dos mitades y, por lo tanto, la complejidad se reduce a O (nlog (n)).

vivek
fuente
0

En promedio, la mediana de 3 es buena para n pequeña. La mediana de 5 es un poco mejor para n más grandes. El ninther, que es la "mediana de tres medianas de tres" es incluso mejor para n muy grande.

Cuanto más alto vaya con el muestreo, mejor obtendrá a medida que aumenta n, pero la mejora se ralentiza drásticamente a medida que aumenta las muestras. Y se incurre en los gastos generales de muestreo y clasificación de muestras.

S0lo
fuente
0

Recomiendo usar el índice medio, ya que se puede calcular fácilmente.

Puede calcularlo redondeando (array.length / 2).

Milesman34
fuente
-1

En una implementación verdaderamente optimizada, el método para elegir el pivote debería depender del tamaño de la matriz; para una matriz grande, vale la pena dedicar más tiempo a elegir un buen pivote. Sin hacer un análisis completo, supongo que "la mitad de los elementos O (log (n))" es un buen comienzo, y esto tiene la ventaja adicional de no requerir memoria adicional: usar tail-call en la partición más grande y en colocando particiones, usamos la misma memoria adicional O (log (n)) en casi todas las etapas del algoritmo.

Morten Kloster
fuente
1
Encontrar el medio de 3 elementos se puede hacer en tiempo constante. Más, y esencialmente tenemos que ordenar la submatriz. A medida que n se vuelve grande, volvemos al problema de clasificación.
Chris Cudmore