Сортировка Хоара (Quick Sort): быстрая сортировка

Модуль "Сортировки" • урок 8 из 17 •

Также упоминается как: quicksort, qsort (функция стандартной библиотеки C), алгоритм Хоара, метод Хоара

Суть урока. Сортировка Хоара - рекурсивный алгоритм "разделяй и властвуй". Выбирается опорный элемент (pivot). Массив разбивается так: элементы меньше опорного - влево, элементы больше - вправо. Сам опорный встает на окончательное место. Обе части рекурсивно сортируются. Среднее время O(N log N), худшее O(N²).
Историческая справка: Тони Хоар и машинный перевод

Алгоритм быстрой сортировки был придуман в 1959 году британским ученым Тони Хоаром во время его стажировки в СССР - в Московском государственном университете!

Читать дальше

Хоар занимался алгоритмами машинного перевода с русского языка на английский. Ему требовалось искать слова в словаре, который был записан на магнитной ленте. Чтобы ускорить поиск, он решил сначала отсортировать слова по алфавиту в оперативной памяти компьютера. Попробовав сортировку вставкой, Хоар понял, что на больших объемах данных она работает слишком медленно, и придумал изящный метод разбиения массива по опорному элементу.

Вернувшись в Великобританию, он реализовал свой алгоритм на языке ALGOL 60 (кстати, одним из создателей которого был Хайнц Рутисхаузер) и опубликовал его в 1961 году. Сегодня сортировка Хоара - один из самых применяемых алгоритмов сортировки, а сэр Тони Хоар удостоен престижнейшей премии Тьюринга.

Идея метода сортировки

Быстрая сортировка Хоара является одним из самых популярных алгоритмов сортировки благодаря очень низкой константе (числу операций процессора) при среднем времени работы O(N log N).

Сортировка Хоара: опорный элемент делит массив, два указателя идут навстречу и меняют местами чужие пары
Опорный элемент делит участок на меньшие и большие, затем каждая часть сортируется тем же способом.

Интерактивный пример наглядно иллюстрирует, как сортировка Хоара выбирает опорный элемент и раскидывает числа на две группы.

Тренажер: сортировка Хоара двумя указателями

Отсортируем массив 4 2 6 1 5 3 двумя указателями навстречу.

текущая часть опорный указатели i, j на месте другие части
Опорный: - Обмены: 0 Шаг: 0 / 0
<|>
Quick Sort
Быстрая сортировка
Разделяй и властвуй: опорный элемент делит массив на меньшие и большие. Две схемы разбиения - Хоара и Ломуто.
Разобрать по шагам в большой версии →

Уязвимость (Худший случай O(N²))

Несмотря на название, "Быстрая" сортировка скрывает в себе серьезную уязвимость. Если в качестве опорного элемента всегда брать крайний элемент, а на вход программе подать уже отсортированный массив (например, [1, 2, 3, 4, 5, 6, 7]), то алгоритм сломается:

Вместо красивого деления массива пополам (что дает глубину рекурсии log N), мы будем откусывать от массива по 1 элементу. Алгоритм деградирует до O(N²), а глубина рекурсии достигнет N.

Python и лимит рекурсии
Интерпретатор Python имеет жесткий лимит глубины рекурсии (по умолчанию 1000). Если применить чистую сортировку Хоара к неудачно отсортированному массиву из 2000 элементов, программа упадет с ошибкой RecursionError! Поэтому в Python встроенный sort() использует Timsort, который гарантирует время O(N log N) в абсолютно любых случаях, а сортировку Хоара часто применяют в C/C++ с оптимизациями выбора опорного элемента (например, медиана из трех или случайный элемент).

Две схемы разбиения: Ломуто и Хоара

В быстрой сортировке этап Partition (Разбиение) можно реализовать двумя классическими способами. Они отличаются тем, как выбирается опорный элемент и как элементы переносятся между частями.

Две схемы разбиения: Ломуто и Хоара
Ломуто ведет один указатель слева направо, Хоар пускает два указателя навстречу друг другу.

Реализация на языке C (Схема разбиения Ломуто)

Сортировка Хоара (разбиение Ломуто): код целиком C, 30 строк
void Swap(int* a, int* b) { int t = *a; *a = *b; *b = t; }

// Функция разбиения: берет последний элемент как опорный
int Partition(int arr[], int low, int high) {
    int pivot = arr[high]; // Опорный элемент
    int i = (low - 1);     // Индекс границы элементов, меньших pivot

    for (int j = low; j <= high - 1; j++) {
        // Если текущий элемент меньше или равен опорному
        if (arr[j] <= pivot) {
            i++; 
            Swap(&arr[i], &arr[j]);
        }
    }
    // Ставим опорный элемент на его окончательное место
    Swap(&arr[i + 1], &arr[high]);
    return (i + 1); // Возвращаем индекс опорного элемента
}

// Сама рекурсивная функция
void QuickSort(int arr[], int low, int high) {
    if (low < high) {
        // Получаем индекс разделения
        int pi = Partition(arr, low, high);

        // Рекурсивно сортируем элементы до и после опорного
        QuickSort(arr, low, pi - 1);
        QuickSort(arr, pi + 1, high);
    }
}

Коротко: главное

  • Сортировка Хоара опирается на принцип "разделяй и властвуй".
  • На каждом этапе выбирается опорный элемент, относительно которого массив делится на две части.
  • Среднее время работы составляет O(N log N) с минимальным числом фактических операций.
  • В худшем случае (на неудачных данных) алгоритм деградирует до O(N²).
  • В отличие от сортировок слиянием и вставкой, классическая сортировка Хоара неустойчива.

Проверьте себя

В каком случае наивная (крайний опорный) сортировка Хоара сработает медленнее всего?

Как вам урок? Спасибо за отзыв!