Курс 3.8 — Алгоритмы · Раздел 05 · Урок 5.10

Быстрая сортировка (QuickSort)

Познакомимся с опорным элементом, разбиением массива и быстрой сортировкой.

Познакомимся с опорным элементом, разбиением массива и быстрой сортировкой.
5.10 — Быстрая сортировка (QuickSort)

Материал урока

Сегодня мы познакомимся с одним из самых красивых и быстрых алгоритмов сортировки — быстрой сортировкой.

Она была придумана в 1960 году в МГУ британским учёным Энтони Хоаром. С тех пор этот алгоритм стал одним из самых популярных в мире и используется даже в стандартных библиотеках многих языков программирования.

Идея алгоритма

  1. Базовый случай: если массив пустой или в нём только один элемент, то он уже отсортирован.
  2. Выбираем опорный элемент (pivot) — это может быть любой элемент массива: первый, последний или даже случайный.
  3. Разделяем элементы так, чтобы:
  • слева оказались все числа меньше или равные pivot;
  • справа — все числа больше pivot.
  1. Рекурсивно повторяем сортировку для левой и правой части.

Таким образом, мы шаг за шагом делим задачу на всё меньшие и меньшие кусочки, пока массив полностью не отсортируется.

Важные детали

  • Опорный элемент можно выбирать разными способами (например, последний элемент, центральный или случайный). От этого зависит скорость работы.
  • Когда мы переносим элементы относительно опорного, то сам pivot тоже может двигаться: например, если он оказался самым большим, то он уходит вправо.
  • Алгоритм нестабилен, то есть одинаковые элементы могут поменяться местами.

Вариант 1. Реализация с буфером

Простейшая версия быстрой сортировки: мы выбираем pivot, создаём новый массив (буфер) и складываем туда элементы в правильном порядке — сначала меньшие pivot, потом сам pivot, затем остальные. После этого копируем результат обратно.

Этот способ легко понять, но он требует дополнительной памяти.

Вариант 2. Разбиение Ломуто (in-place)

Более интересный и практичный вариант называется разбиение Ломуто. В нём мы не используем буфер, а просто переставляем элементы прямо внутри массива.

Как это работает:

  • Берём pivot = последний элемент.
  • Создаём специальный маркер p_index, который сначала указывает на начало массива.
  • Перебираем все элементы:
  • если текущий элемент меньше или равен pivot, меняем его местами с элементом на позиции p_index, и двигаем маркер вправо;
  • если больше pivot, ничего не делаем.

В конце p_index указывает на то место, куда встанет pivot. Таким образом, массив делится на две части: слева всё меньше pivot, справа — всё больше.

Пример работы разбиения

Допустим, у нас есть массив:

[1, 10, 2, 5, 3, 6, 4, 9, 8, 7]

Опорный элемент — последний, то есть 7.

  1. Сначала p_index = 0, i = 0.
  2. 1 меньше 7, значит, делаем обмен (ничего не изменилось, потому что 1 и так на месте) и сдвигаем p_index.
  3. Теперь p_index = 1, i = 1.
  4. 10 больше 7 → i просто двигается дальше.
  5. На шаге i = 2 число 2 меньше 7, значит, обмениваем его с элементом на позиции p_index (1), и двигаем p_index.

И так продолжаем до конца массива. В итоге pivot оказывается на своём месте, а массив делится на две части:

[1, 2, 5, 3, 6, 4, 7, 9, 8, 10]

Сложность алгоритма

  • В среднем случае (если pivot делит массив примерно пополам):
  • мы делим массив на два и повторяем сортировку для половинок. Получаем:
  • O(n · log₂ n)
  • В худшем случае (если pivot всегда оказывается самым большим или самым маленьким):
  • массив не делится, и приходится сортировать всё снова и снова. Тогда сложность:
  • O(n²)
  • На практике быстрая сортировка работает очень быстро, поэтому её часто используют.

Итоги

  • Быстрая сортировка — рекурсивный алгоритм.
  • Работает в среднем за O(n log n), что делает её одной из самых быстрых.
  • Может работать прямо в массиве, не занимая лишнюю память.
  • Но алгоритм нестабилен и в худшем случае может работать за O(n²).

Пример 1. Быстрая сортировка для обычного списка Python

# Быстрая сортировка для списка Python
def quick_sort(arr):
    # Базовый случай: если список пустой или из одного элемента
    if len(arr) <= 1:
        return arr
    
    pivot = arr[-1]  # выбираем последний элемент как опорный
    left = [x for x in arr[:-1] if x <= pivot]   # элементы меньше или равны pivot
    right = [x for x in arr[:-1] if x > pivot]   # элементы больше pivot
    
    # Рекурсивно сортируем левую и правую части и объединяем
    return quick_sort(left) + [pivot] + quick_sort(right)

# Пример использования
data = [1, 10, 2, 5, 3, 6, 4, 9, 8, 7]
print("Оригинальный список:", data)
sorted_data = quick_sort(data)
print("Отсортированный список:", sorted_data)

Пример 2. Быстрая сортировка с NumPy

import numpy as np

def quick_sort_numpy(arr):
    # Если массив пустой или из одного элемента
    if arr.size <= 1:
        return arr
    
    pivot = arr[-1]  # опорный элемент (последний)
    
    # Разделяем массив на элементы меньше/больше pivot
    left = arr[arr[:-1] <= pivot]
    right = arr[arr[:-1] > pivot]
    
    # Рекурсивно сортируем левую и правую части
    return np.concatenate((quick_sort_numpy(left), [pivot], quick_sort_numpy(right)))

# Пример использования
data_np = np.array([1, 10, 2, 5, 3, 6, 4, 9, 8, 7])
print("Оригинальный массив:", data_np)
sorted_np = quick_sort_numpy(data_np)
print("Отсортированный массив:", sorted_np)

В чём разница:

  • В версии с Python-списком мы используем генераторы списков ([x for x in arr ...]).
  • В NumPy-версии мы используем булевы маски (arr[arr <= pivot]), которые делают выборку элементов.
  • Обе версии работают одинаково по смыслу, но NumPy-версия удобна для работы с числовыми массивами.