Ordenación rápida bidireccional
El algoritmo de ordenación rápida bidireccional es una versión mejorada de la ordenación rápida aleatorizada. El proceso de partición utiliza dos índices (i, j) para recorrer el array, y coloca los<velementos a la izquierda de la posición apuntada por el índice i, y coloca los>velementos a la derecha de la posición apuntada por el índice j,vrepresenta el valor pivote.
Instrucciones de aplicabilidad
La complejidad temporal y espacial es la misma que la de la ordenación rápida aleatorizada. Para arrays con muchos elementos repetidos, usar la ordenación rápida aleatorizada de la sección anterior es muy ineficiente; después de la partición, las longitudes de los subarrays de datos mayores o menores que el pivote pueden quedar extremadamente desequilibradas, e incluso puede degenerar enO(n*2)un algoritmo de complejidad temporal O(n^2). Para este caso, se puede utilizar el algoritmo de ordenación rápida bidireccional.
Diagrama del proceso
Usando dos índices (i, j) para recorrer nuestra secuencia, colocamos los<=velementos a la izquierda de la posición apuntada por el índice i, y colocamos los>=velementos a la derecha de la posición apuntada por el índice j, equilibrando los subarrays de ambos lados.

Código de ejemplo en Java
Descarga del paquete de código fuente:Download
Código del archivo QuickSort2Ways.java:
/**
* Ordenación rápida bidireccional
*/
public class QuickSort2Ways {
// Código central --- inicio
private static int partition(Comparable[] arr, int l, int r){
// Elegir aleatoriamente un valor como pivote en el rango arr[l...r]
swap( arr, l , (int)(Math.random()*(r-l+1))+l );
Comparable v = arr[l];
// arr[l+1...i) <= v; arr(j...r] >= v
int i = l+1, j = r;
while( true ){
while( i <= r && arr[i].compareTo(v) < 0 )
i ++;
while( j >= l+1 && arr[j].compareTo(v) > 0 )
j --;
if( i > j )
break;
swap( arr, i, j );
i ++;
j --;
}
swap(arr, l, j);
return j;
}
// Código central --- fin
// Ordenar recursivamente el rango arr[l...r] usando ordenación rápida
private static void sort(Comparable[] arr, int l, int r){
if (l >= r) {
return;
}
int p = partition(arr, l, r);
sort(arr, l, p-1 );
sort(arr, p+1, r);
}
public static void sort(Comparable[] arr){
int n = arr.length;
sort(arr, 0, n-1);
}
private static void swap(Object[] arr, int i, int j) {
Object t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
// Probar QuickSort
public static void main(String[] args) {
// El algoritmo de ordenación rápida bidireccional también es un algoritmo de complejidad O(n log n)
// Puede procesar fácilmente datos del orden de 1 millón en menos de 1 segundo
// Quick Sort también es un algoritmo de complejidad O(n log n)
// Puede procesar fácilmente datos del orden de 1 millón en menos de 1 segundo
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
sort(arr);
SortTestHelper.printArray(arr);
}
}
Explicación detallada del principio del algoritmo
El núcleo de la ordenación rápida bidireccional reside en sumétodo de partición de dos punteros. A diferencia de la ordenación rápida de una vía (que solo escanea desde un extremo), usa dos punteros que escanean desde el principio y el final del subarray a ordenar hacia el centro, asegurando que los elementos iguales no sean empujados todos hacia un solo lado.
Núcleo: proceso de partición bidireccional
Supongamos que queremos ordenar el arrayarren el que los índices desdeleftllegarrightconstituyen la parte a ordenar.
Selección del valor pivote: desdearr[left...right]elegir aleatoriamente un elemento como valor pivote.pivot. La aleatorización tiene como objetivo evitar el peor caso en arrays ordenados.
Inicialización de los punteros:
i = left + 1: el puntero que escanea de izquierda a derecha, buscando el primermayor o igual apivotelemento.j = right: el puntero que escanea de derecha a izquierda, buscando el primermenor o igual apivotelemento.
Bucle de escaneo e intercambio:
whileEl bucle, con la condicióni <= j。- el bucle interno
whilewhile: haga queise mueva continuamente hacia la derecha, hasta quearr[i] >= pivot。 - el bucle interno
whilewhile: haga quejse mueva continuamente hacia la izquierda, hasta quearr[j] <= pivot。 - En este punto,
arr[i]es un elemento "grande" que no debería estar en la mitad izquierda,arr[j]es un elemento "pequeño" que no debería estar en la mitad derecha. - Si en este momento
i <= j, entonces intercambiearr[i]yarr[j], luegoi++,j--, continúe con el bucle externo.
Colocación del pivote y retorno del punto de partición:
- Cuando el bucle termina,
iyjse han cruzado (j < i). En este momentoarr[left](es decir, el pivote) necesita ser colocado en la posición correcta. - va a
arr[left]yarr[j]Intercambie. Porquejla posición final donde se detiene, cuyo elemento apuntado esel último elemento menor o igual que el pivote. - Devolver
jcomo nuevo punto de partición. En este momento,arr[left...j-1] <= pivot,arr[j+1...right] >= pivot, yarr[j] == pivot。
Ordenación recursiva
Una vez obtenido el punto de particiónp, para el subarray izquierdoarr[left...p-1]y para el subarray derechoarr[p+1...right]se ejecuta recursivamente el proceso anterior, hasta que la longitud del subarray sea 1.
Implementación del código
Entendamos concretamente la ordenación rápida bidireccional mediante una implementación completa en Java.
1. Función principal de ordenación
Ejemplo
// Interfaz pública de ordenación
public static void sort(int[] arr) {
if (arr == null || arr.length < 2) {
return; // Manejo de casos límite: si el array está vacío o tiene un solo elemento, no necesita ordenación
}
quickSort(arr, 0, arr.length - 1); // Llamar a la función recursiva de ordenación rápida
}
// Función recursiva de ordenación rápida
private static void quickSort(int[] arr, int left, int right) {
// Condición de terminación recursiva: cuando el límite izquierdo es mayor o igual que el límite derecho, el subarray ya está ordenado o vacío
if (left >= right) {
return;
}
// Paso clave: realizar la partición bidireccional y devolver el índice del punto de partición
int p = partition(arr, left, right);
// Ordenar recursivamente la mitad izquierda (left, p-1)
quickSort(arr, left, p - 1);
// Ordenar recursivamente la mitad derecha (p+1, right)
quickSort(arr, p + 1, right);
}
}
2. Núcleo: función de partición bidireccional
Esta es la parte central del algoritmo; por favor, estúdiala detenidamente junto con el diagrama de flujo y los comentarios anteriores.
Ejemplo
private static int partition(int[] arr, int left, int right) {
// 1. Seleccionar aleatoriamente el pivote e intercambiarlo con la posición left, para evitar el peor caso en arrays ordenados
int randomIndex = left + (int)(Math.random() * (right - left + 1));
swap(arr, left, randomIndex);
int pivot = arr[left]; // Valor pivote
// 2. Inicializar los dos punteros
// i: escanea de izquierda a derecha, buscando elementos >= pivot
// j: escanea de derecha a izquierda, buscando elementos <= pivot
int i = left + 1;
int j = right;
// 3. Bucle principal: mientras i y j no se hayan cruzado
while (i <= j) {
// 3.1 Mover el puntero izquierdo i: encontrar el primer elemento >= pivot
// Nota: límite i <= right, para evitar salir del array
while (i <= right && arr[i] < pivot) {
i++;
}
// 3.2 Mover el puntero derecho j: encontrar el primer elemento <= pivot
// Nota: límite j >= left+1, porque la posición left es el propio pivot
while (j >= left + 1 && arr[j] > pivot) {
j--;
}
// 3.3 Comprobar el estado de los punteros
// Si i > j, el escaneo ha terminado y las particiones izquierda y derecha están listas, no es necesario intercambiar
if (i > j) {
break;
}
// 3.4 Intercambiar arr[i] y arr[j]
// En este momento arr[i] >= pivot, arr[j] <= pivot; intercambiarlos hace que los elementos grandes y pequeños vuelvan a su lugar
swap(arr, i, j);
// Después del intercambio, mover los punteros para continuar escaneando
i++;
j--;
}
// 4. Colocar el pivote en su posición final correcta j
// Después del bucle, j apunta al último elemento <= pivot
swap(arr, left, j);
// 5. Devolver el índice j del punto de partición
return j;
}
// Función auxiliar: intercambia la posición de dos elementos en el array
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
3. Pruebas y verificación
Probemos nuestra implementación con un array que contiene elementos repetidos.
Ejemplo
public static void main(String[] args) {
// Caso de prueba 1: array con muchos elementos repetidos
int[] arr1 = {4, 2, 2, 8, 3, 3, 1, 5, 3, 2};
System.out.println("Antes de ordenar: " + Arrays.toString(arr1));
TwoWayQuickSort.sort(arr1);
System.out.println("Después de ordenar: " + Arrays.toString(arr1));
// Caso de prueba 2: array grande generado aleatoriamente
int[] arr2 = new int[20];
Random rand = new Random();
for (int i = 0; i < arr2.length; i++) {
arr2[i] = rand.nextInt(50); // Generar números aleatorios entre 0 y 49, es muy probable que haya repetidos
}
System.out.println("\n"Antes de ordenar el array aleatorio: " + Arrays.toString(arr2));
TwoWayQuickSort.sort(arr2);
System.out.println("Después de ordenar el array aleatorio: " + Arrays.toString(arr2));
// Verificar si el resultado de la ordenación es correcto
for (int i = 1; i < arr2.length; i++) {
if (arr2[i] < arr2[i-1]) {
System.out.println("¡Error de ordenación!");
return;
}
}
System.out.println("¡Verificación de ordenación superada!");
}
}
Ejemplo de salida de datos de prueba:
排序前: [4, 2, 2, 8, 3, 3, 1, 5, 3, 2] 排序后: [1, 2, 2, 2, 3, 3, 3, 4, 5, 8] 随机数组排序前: [17, 33, 12, 48, 8, 2, 41, ...] 随机数组排序后: [2, 8, 12, 17, 33, 33, 41, 48, ...] 排序结果验证通过!
Análisis y comparación de algoritmos
Complejidad temporal
- Caso promedio: $O(n \log n)$. La ordenación rápida bidireccional distribuye los elementos repetidos de manera equilibrada, haciendo que el árbol de recursión esté relativamente balanceado.
- Peor caso: $O(n^2)$. Aunque la selección aleatoria del pivote reduce en gran medida la probabilidad, cuando cada partición está extremadamente desequilibrada (por ejemplo, cada vez el pivote es el valor mínimo o máximo actual), aún puede aparecer el peor caso.
- Mejor caso:$O(n \log n)$. Cada partición puede dividir el arreglo de manera uniforme.
Complejidad espacial
- Principalmente el espacio ocupado por la pila de llamadas recursivas.
- La profundidad media es $O(\log n)$, y en el peor caso la profundidad es $O(n)$.
- Por lo tanto,La complejidad espacial promedio es $O(\log n)$, y la peor es $O(n)$.。
Estabilidad
Quicksort (incluido el quicksort de doble vía) no es un algoritmo de ordenamiento estable. Porque durante el proceso de partición, el intercambio de elementos no adyacentes altera el orden relativo original de los elementos iguales.
Comparación entre ordenación rápida de una vía, bidireccional y de tres vías
| Características | Quicksort de una vía (Lomuto) | Quicksort de doble vía | Quicksort de triple vía |
|---|---|---|---|
| Método de partición | Un solo puntero escanea de izquierda a derecha | Dos punteros escanean desde ambos extremos hacia el centro | Tres punteros, dividen el arreglo en<pivot, =pivot, >pivottres partes |
| Manejo de elementos repetidos | Malo, puede provocar particiones desequilibradas | Bueno, puede distribuir de manera equilibrada los elementos repetidos | Óptimo, puede manejar de una vez todos los elementos iguales al valor pivote |
| Complejidad del código | Simple | Medio | Ligeramente complejo |
| Escenarios de aplicación | Enseñanza, sin/pocos elementos repetidos | Uso general, especialmente adecuado para escenarios con posibles elementos repetidos | Escenarios con gran cantidad de elementos repetidos |