Быстрая сортировка (quicksort) выбирает опорный элемент (pivot), делит массив на «не больше опорного» и «больше опорного» и рекурсивно сортирует части. В среднем , в худшем , без буфера.

Зачем нужно

Сортировка слиянием гарантирует , но требует буфер на и копирует все элементы туда и обратно. Быстрая сортировка переставляет элементы на месте. Асимптотика в среднем та же, константа обычно меньше — разные сортировки, разные константы.

Как работает

Вся работа — в Partition (разбиении). Слияния после рекурсии нет: когда части отсортированы, весь отрезок уже отсортирован.

Схема с Partition Ломуто

Partition(arr, left, right)       // [left, right)
    p = arr[right − 1]
    i = left
    for j = left ... right − 2
        если arr[j] ≤ p
            swap(arr[i], arr[j])
            i++
    swap(arr[right − 1], arr[i])
    return i
 
Qsort(arr, left, right)
    если left ≥ right
        return
    id_p = Partition(arr, left, right)
    Qsort(arr, left, id_p)
    Qsort(arr, id_p + 1, right)

Инвариант Ломуто — четыре зоны: [left, i) , [i, j) , [j, right−1) не обработаны, arr[right−1] = p. После цикла ставится между зонами и больше не двигается — поэтому в рекурсию он не идёт, и обе части строго короче отрезка. Доказательство — в конспекте.

Partition Хоара

Два указателя идут навстречу: L пропускает элементы , R — элементы , остановившиеся меняются местами. Результат: [left, R] , [R+1, right) , но опорный не обязательно на позиции R. Поэтому рекурсия другая:

Partition(arr, left, right)
    p = arr[(left + right − 1) / 2]
    L = left;  R = right − 1
    while (true)
        пока arr[L] < p: L++
        пока arr[R] > p: R--
        если L ≥ R: return R
        swap(arr[L], arr[R]);  L++;  R--
 
Qsort(arr, left, right)
    если right − left ≤ 1: return
    R = Partition(arr, left, right)
    Qsort(arr, left, R + 1)
    Qsort(arr, R + 1, right)

На лекции Хоар записан с ошибками

На доске Хоар подставлен в схему Qsort для Ломуто (опорный выкидывается из рекурсии), а середина взята как (left + right) / 2. Первое даёт неотсортированный результат, второе после исправления рекурсии — бесконечную рекурсию на [1, 2]. Версия выше — исправленная и проверенная. Разбор — в конспекте.

Выбор опорного

С лекции: первый, последний, средний элемент, медиана трёх (первого, среднего, последнего), случайный. Для подотрезка — arr[left], а не arr[0].

Асимптотика

СлучайПамять на стек
разбиение примерно пополам
опорный — всегда крайний по величине
случайный опорный, в среднем

Partition — на уровень рекурсии. Уровней , если делится ровно, и , если одна часть каждый раз пустая.

Не из лекции: оценки

  • Худший случай: . Ломуто с последним элементом на отсортированном массиве — ровно так: на 7 998 000 сравнений.
  • Случайный опорный: и сравниваются с вероятностью — когда одним из них выбран первый опорный в группе . Сумма по парам — не больше . Подробно — в конспекте, раздел 6.
  • Равные элементы: Ломуто с ≤ на массиве одинаковых чисел вырождается в , Хоар делит пополам (55 454 сравнения на тех же 4000 элементах).

Тонкости

  • В таблице лекции у быстрой сортировки — это без стека рекурсии. Partition работает на месте, но глубина рекурсии — память.
  • Быстрая сортировка неустойчива: обмены перебрасывают элементы через весь отрезок.
  • Это сортировка сравнением, и нижняя оценка из Сортировка к ней относится.

Не из лекции

Неустойчивость: [2a, 2b, 1] у Ломуто с опорным 1. Оба элемента , цикл ничего не меняет, а финальный swap(arr[right − 1], arr[i]) при меняет 1 с 2a: получается [1, 2b, 2a].

Частые ошибки

  • Использовать рекурсию Ломуто (без опорного) с Partition Хоара.
  • Брать опорным крайний элемент — и получать на отсортированном входе.
  • Писать arr[0] вместо arr[left] в подотрезке.
  • Считать, что быстрая сортировка гарантирует .
  • Забывать, что у Ломуто на одинаковых элементах тот же худший случай.