Quick Sort (Быстрая сортировка)
Quick sort — эффективный алгоритм сортировки «разделяй и властвуй», который выбирает опорный элемент (pivot), разделяет массив на две части (меньше и больше pivot) и рекурсивно сортирует каждую.
Зачем нужно
Quick sort — один из самых быстрых алгоритмов сортировки на практике. Средняя сложность O(n log n), in-place (не требует дополнительной памяти O(n)), отличная производительность кэша. Часто используется в реализациях Array.sort().
Где используется
- Стандартная библиотека многих языков
- Сортировка больших массивов данных
- Базы данных (ORDER BY)
- Основа для IntroSort (гибридный алгоритм)
Предпосылки
Алгоритм
1. Выбрать опорный элемент (pivot)
2. Разделить массив на 3 части:
- Элементы < pivot
- Элементы === pivot
- Элементы > pivot
3. Рекурсивно отсортировать левую и правую части
4. Объединить: [отсортированные < pivot] + [pivot] + [отсортированные > pivot]
Визуализация
Массив: [3, 6, 8, 10, 1, 2, 1]
Pivot: 3
Partition:
left = [1, 2, 1] (< 3)
pivot = [3]
right = [6, 8, 10] (> 3)
Рекурсия left [1, 2, 1], pivot=1:
left =
pivot = [1, 1]
right = [2]
→ [1, 1, 2]
Рекурсия right [6, 8, 10], pivot=8:
left = [6]
pivot = [8]
right = [10]
→ [6, 8, 10]
Результат: [1, 1, 2] + [3] + [6, 8, 10] = [1, 1, 2, 3, 6, 8, 10]
Реализация в JavaScript
Простая версия (не in-place, но понятная)
function quickSort(array) {
if (array.length <= 1) return array;
const pivot = array[Math.floor(array.length / 2)];
const left = array.filter(x => x < pivot);
const middle = array.filter(x => x === pivot);
const right = array.filter(x => x > pivot);
return [...quickSort(left), ...middle, ...quickSort(right)];
}
console.log(quickSort([3, 6, 8, 10, 1, 2, 1]));
// [1, 1, 2, 3, 6, 8, 10]
In-place версия (Lomuto partition)
function quickSortInPlace(arr, low = 0, high = arr.length - 1) {
if (low < high) {
const pivotIndex = partition(arr, low, high);
quickSortInPlace(arr, low, pivotIndex - 1);
quickSortInPlace(arr, pivotIndex + 1, high);
}
return arr;
}
function partition(arr, low, high) {
const pivot = arr[high]; // последний элемент как pivot
let i = low - 1;
for (let j = low; j < high; j++) {
if (arr[j] <= pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
[arr[i + 1], arr[high]] = [arr[high], arr[i + 1]];
return i + 1;
}
const data = [10, 7, 8, 9, 1, 5];
console.log(quickSortInPlace([...data]));
// [1, 5, 7, 8, 9, 10]
Hoare partition (более эффективная)
function quickSortHoare(arr, low = 0, high = arr.length - 1) {
if (low < high) {
const p = hoarePartition(arr, low, high);
quickSortHoare(arr, low, p);
quickSortHoare(arr, p + 1, high);
}
return arr;
}
function hoarePartition(arr, low, high) {
const pivot = arr[Math.floor((low + high) / 2)];
let i = low - 1;
let j = high + 1;
while (true) {
do { i++; } while (arr[i] < pivot);
do { j--; } while (arr[j] > pivot);
if (i >= j) return j;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
Выбор pivot
Выбор pivot критически влияет на производительность:
| Стратегия | Описание | Проблема |
|---|---|---|
| Первый элемент | arr[0] |
O(n^2) на отсортированных данных |
| Последний элемент | arr[n-1] |
O(n^2) на отсортированных данных |
| Средний элемент | arr[n/2] |
Хороший выбор для большинства случаев |
| Случайный | arr[random] |
Статистически O(n log n) |
| Медиана трёх | median(first, mid, last) |
Надёжный выбор |
// Медиана трёх — надёжный выбор pivot
function medianOfThree(arr, low, high) {
const mid = Math.floor((low + high) / 2);
// Сортируем три элемента
if (arr[low] > arr[mid]) [arr[low], arr[mid]] = [arr[mid], arr[low]];
if (arr[low] > arr[high]) [arr[low], arr[high]] = [arr[high], arr[low]];
if (arr[mid] > arr[high]) [arr[mid], arr[high]] = [arr[high], arr[mid]];
return mid; // средний из трёх = pivot
}
Сложность
| Случай | Время | Память | Когда |
|---|---|---|---|
| Лучший | O(n log n) | O(log n) | Pivot всегда делит пополам |
| Средний | O(n log n) | O(log n) | Случайные данные |
| Худший | O(n^2) | O(n) | Pivot всегда min/max (уже отсортировано) |
Стабильная: нет (простая версия с filter — да, in-place — нет).
In-place: да (Lomuto/Hoare версии).
Почему O(n^2) в худшем случае
Отсортированный массив [1, 2, 3, 4, 5], pivot = последний:
Шаг 1: pivot=5, left=[1,2,3,4], right= → n-1 сравнений
Шаг 2: pivot=4, left=[1,2,3], right= → n-2 сравнений
Шаг 3: pivot=3, left=[1,2], right= → n-3 сравнений
...
Итого: (n-1) + (n-2) + ... + 1 = n(n-1)/2 = O(n²)
Решение: рандомизированный pivot или median-of-three.
Сравнение с другими сортировками
| Параметр | Quick sort | Merge sort | Bubble sort |
|---|---|---|---|
| Средний | O(n log n) | O(n log n) | O(n^2) |
| Худший | O(n^2) | O(n log n) | O(n^2) |
| Память | O(log n) | O(n) | O(1) |
| Стабильная | Нет | Да | Да |
| На практике | Быстрейший | Гарантированный | Самый медленный |
Частые ошибки
- Выбирать первый/последний элемент как pivot для отсортированных данных. Используйте рандомизацию или median-of-three
- Забывать базовый случай рекурсии.
if (array.length <= 1)— обязательно - Не учитывать нестабильность. Если порядок равных элементов важен — используйте Merge sort
- Простая версия с filter — O(n) памяти. Для больших данных используйте in-place версию
Практика
- Реализуйте простую версию quick sort с
filter - Реализуйте in-place версию с Lomuto partition
- Сравните производительность: quick sort vs
Array.sort()на 100 000 элементов - Проверьте: что происходит на уже отсортированном массиве?
Связанные темы
Ресурсы
- visualgo.net/en/sorting — визуализация
- Grokking Algorithms, глава 4 (Aditya Bhargava)
- Introduction to Algorithms (CLRS), глава 7