Двухпутевая быстрая сортировка
Алгоритм двусторонней быстрой сортировки — улучшенная версия рандомизированной быстрой сортировки. В процессе разделения используются два значения индекса (i, j) для обхода массива, при этом размещая<vэлементы слева от позиции, на которую указывает индекс i, и размещая>vэлементы справа от позиции, на которую указывает индекс j,vОбозначает опорное значение.
Применимость
Временная и пространственная сложность такие же, как у рандомизированной быстрой сортировки. Для массивов с большим количеством повторяющихся элементов использование рандомизированной быстрой сортировки из предыдущего раздела очень неэффективно, потому что длины подмассивов после разделения для данных больше или меньше опорного элемента становятся крайне несбалансированными, и она может даже выродиться в алгоритм сO(n*2)временной сложностью. В этой ситуации можно использовать алгоритм двусторонней быстрой сортировки.
Схема процесса
Используйте два значения индекса (i, j) для обхода нашей последовательности, размещая<=vэлементы слева от позиции, на которую указывает индекс i, и размещая>=vэлементы справа от позиции, на которую указывает индекс j, балансируя левый и правый подмассивы.

Пример кода на Java
Загрузка пакета исходного кода:Скачать
Код файла QuickSort2Ways.java:
/**
* Двусторонняя быстрая сортировка
*/
public class QuickSort2Ways {
// Основной код --- начало
private static int partition(Comparable[] arr, int l, int r){
// Случайным образом выбрать значение в качестве опорного элемента в диапазоне 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;
}
// Основной код --- конец
// Рекурсивно применяйте быструю сортировку для сортировки диапазона arr[l...r]
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;
}
// Тестирование QuickSort
public static void main(String[] args) {
// Двухсторонняя быстрая сортировка — это также алгоритм сложности O(nlogn)
// Может легко обрабатывать данные порядка 1 миллиона за 1 секунду
// Быстрая сортировка также является алгоритмом со сложностью O(n log n)
// Может легко обрабатывать данные порядка 1 миллиона за 1 секунду
int N = 1000000;
Integer[] arr = SortTestHelper.generateRandomArray(N, 0, 100000);
sort(arr);
SortTestHelper.printArray(arr);
}
}
Подробное объяснение принципа алгоритма
Ядро двухсторонней быстрой сортировки заключается в еёметоде разбиения с двумя указателямиВ отличие от односторонней быстрой сортировки (которая сканирует только с одного конца), она использует два указателя, которые сканируют от начала и конца сортируемого подмассива к середине, гарантируя, что равные элементы не все сдвигаются в одну сторону.
Суть: процесс двухстороннего разбиения
Предположим, мы хотим отсортировать массивarrсредний индекс отleftвrightчасть.
Выберите опорное значение: изarr[left...right]Случайным образом выберите из него элемент в качестве опорного значенияpivotРандомизация используется, чтобы избежать худшего случая на отсортированных массивах.
Инициализируйте указатели:
i = left + 1Указатель, сканирующий слева направо, ищет первыйбольший или равныйpivotelements.j = rightУказатель, сканирующий справа налево, ищет первыйменьший или равныйpivotelements.
Цикл сканирования и обмена:
whileЦикл, условием которого являетсяi <= j。- внутренний
whileloop: letiПродолжайте двигаться вправо, покаarr[i] >= pivot。 - внутренний
whileloop: letjПродолжайте двигаться влево, покаarr[j] <= pivot。 - В этот момент,
arr[i]является «большим» элементом, который не должен находиться в левой половине,arr[j]является «маленьким» элементом, который не должен находиться в правой половине. - Если в этот момент
i <= j, то поменяйте местамиarr[i]иarr[j], затемi++,j--затем продолжайте внешний цикл.
Поместите значение опорного элемента и верните точку разделения:
- После завершения цикла,
iиjуже пересеклись (j < i). В этот моментarr[left](т. е. опорный элемент) нужно поместить в правильную позицию. - будет
arr[left]иarr[j]swap. Потому чтоjПозиция, на которой он в итоге останавливается, а элемент, на который он указывает, — этопоследний элемент, меньший или равный опорному значениюэлементы. - return
jкак новую точку разбиения. На этом этапе,arr[left...j-1] <= pivot,arr[j+1...right] >= pivot, иarr[j] == pivot。
рекурсивно сортировать
получаем точку разбиенияpПосле этого для левого подмассиваarr[left...p-1]и правого подмассиваarr[p+1...right]Повторяйте описанный процесс рекурсивно, пока длина подмассива не станет равной 1.
Реализация кода
Давайте конкретно разберём двустороннюю быструю сортировку на примере полной реализации на Java.
1. Основная функция сортировки
Пример
// Публичный интерфейс сортировки
public static void sort(int[] arr) {
if (arr == null || arr.length < 2) {
return; // Обработать граничное условие: массив пуст или содержит только один элемент, сортировка не требуется
}
quickSort(arr, 0, arr.length - 1); // Вызов рекурсивной функции быстрой сортировки
}
// Рекурсивная функция быстрой сортировки
private static void quickSort(int[] arr, int left, int right) {
// Условие завершения рекурсии: когда left >= right, подмассив уже отсортирован или пуст
if (left >= right) {
return;
}
// Ключевой шаг: выполнить двустороннее разбиение и вернуть индекс разбиения
int p = partition(arr, left, right);
// Рекурсивно сортируем левую половину (left, p-1)
quickSort(arr, left, p - 1);
// Рекурсивно сортируем правую половину (p+1, right)
quickSort(arr, p + 1, right);
}
}
2. Ядро: функция двустороннего разбиения
Это ядро алгоритма. Пожалуйста, внимательно разберитесь в нём вместе с приведённой выше блок-схемой и комментариями.
Пример
private static int partition(int[] arr, int left, int right) {
// 1. Случайно выбираем опорный элемент и переставляем его в левую позицию, избегая наихудшего случая на отсортированных массивах
int randomIndex = left + (int)(Math.random() * (right - left + 1));
swap(arr, left, randomIndex);
int pivot = arr[left]; // Значение опорного элемента
// 2. Инициализация двух указателей
// i: сканировать слева направо в поиске элементов >= pivot
// j: сканировать справа налево в поиске элементов <= pivot
int i = left + 1;
int j = right;
// 3. Основной цикл: пока i и j не пересеклись
while (i <= j) {
// 3.1 Переместить левый указатель i: найти первый элемент >= pivot
// Примечание: граница i <= right для предотвращения выхода за пределы массива
while (i <= right && arr[i] < pivot) {
i++;
}
// 3.2 Переместить правый указатель j: найти первый элемент <= pivot
// Примечание: граница j >= left+1, потому что позиция left — это сам pivot
while (j >= left + 1 && arr[j] > pivot) {
j--;
}
// 3.3 Проверка состояния указателей
// Если i > j, сканирование завершено, левая и правая части готовы, менять местами не нужно
if (i > j) {
break;
}
// 3.4 Поменять местами arr[i] и arr[j]
// Теперь arr[i] >= pivot, arr[j] <= pivot, поменяйте их местами, чтобы элементы с обеих сторон оказались на своих местах
swap(arr, i, j);
// После обмена переместите указатели, чтобы продолжить сканирование
i++;
j--;
}
// 4. Поместите pivot в его окончательную правильную позицию j
// После завершения цикла j указывает на последний элемент <= pivot
swap(arr, left, j);
// 5. Вернуть индекс j точки разделения
return j;
}
// Вспомогательная функция: поменять местами позиции двух элементов в массиве
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
3. Тестирование и проверка
Давайте протестируем нашу реализацию на массиве, содержащем дублирующиеся элементы.
Пример
public static void main(String[] args) {
// Тестовый случай 1: массив, содержащий много дублирующихся элементов
int[] arr1 = {4, 2, 2, 8, 3, 3, 1, 5, 3, 2};
System.out.println("Перед сортировкой: " + Arrays.toString(arr1));
TwoWayQuickSort.sort(arr1);
System.out.println("После сортировки: " + Arrays.toString(arr1));
// Тестовый случай 2: большой массив, сгенерированный случайным образом
int[] arr2 = new int[20];
Random rand = new Random();
for (int i = 0; i < arr2.length; i++) {
arr2[i] = rand.nextInt(50); // Генерируем случайные числа от 0 до 49, возможны дубликаты
}
System.out.println("\n"Перед сортировкой массива случайных чисел: " + Arrays.toString(arr2));
TwoWayQuickSort.sort(arr2);
System.out.println("После сортировки массива случайных чисел: " + Arrays.toString(arr2));
// Проверяем, правильно ли отсортирован результат
for (int i = 1; i < arr2.length; i++) {
if (arr2[i] < arr2[i-1]) {
System.out.println("Ошибка сортировки!");
return;
}
}
System.out.println("Проверка результата сортировки пройдена!");
}
}
Пример вывода тестовых данных:
排序前: [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, ...] 排序结果验证通过!
Анализ и сравнение алгоритмов
Временная сложность
- Средний случай:$O(n \log n)$. Двусторонняя быстрая сортировка поддерживает дерево рекурсии относительно сбалансированным, равномерно распределяя дублирующиеся элементы.
- Худший случай:$O(n^2)$. Хотя рандомизированный выбор опорного элемента значительно снижает вероятность, худший случай всё ещё может возникнуть, когда каждое разбиение крайне несбалансировано (например, когда опорный элемент всегда является текущим минимальным или максимальным значением).
- Лучший случай:$O(n \log n)$. Каждое разбиение делит массив равномерно.
Пространственная сложность
- В основном это пространство, занимаемое стеком рекурсивных вызовов.
- Средняя глубина — $O(\log n)$, а глубина в худшем случае — $O(n)$.
- Следовательно,Средняя пространственная сложность — $O(\log n)$, а в худшем случае — $O(n)$.。
Стабильность
Быстрая сортировка (включая двустороннюю быструю сортировку) не является устойчивым алгоритмом сортировки, потому что во время разбиения обмены несмежных элементов могут нарушить исходный относительный порядок равных элементов.
Сравнение односторонней, двусторонней и трёхсторонней быстрой сортировки
| Особенности | Односторонняя быстрая сортировка (Ломуто) | Двухпутевая быстрая сортировка | Быстрая сортировка с трёхсторонним разбиением |
|---|---|---|---|
| Метод разбиения | Один указатель сканирует слева направо | Два указателя сканируют с обоих концов к середине | Три указателя, разделяющие массив на<pivot, =pivot, >pivotТри части |
| Обработка повторяющихся элементов | Плохо, может привести к несбалансированному разбиению | Хорошо, может равномерно распределять повторяющиеся элементы | Оптимально, может обрабатывать все элементы, равные опорному элементу, за один раз |
| Сложность кода | Простая | Средняя | Немного сложный |
| Применимые сценарии | Обучение, нет/мало дублирующихся элементов | Универсальный, особенно подходит для сценариев, где могут присутствовать дублирующиеся элементы | Сценарии с большим количеством дублирующихся элементов |