Какая алгоритмическая сложность у быстрой сортировки (QuickSort)?

Ответ

Быстрая сортировка (QuickSort) имеет следующую сложность:

  • Средний и лучший случай: O(n log n). Достигается при хорошем (близком к медиане) выборе опорного элемента (pivot).
  • Худший случай: O(n²). Происходит при постоянном выборе наихудшего опорного элемента (например, минимального или максимального), что превращает алгоритм в подобие сортировки выбором.

Оптимизации для избегания худшего случая:

  1. Рандомизированный выбор pivot.
  2. Выбор медианы трех элементов (первого, среднего, последнего).

Пример реализации на Swift:

func quickSort<T: Comparable>(_ array: [T]) -> [T] {
    guard array.count > 1 else { return array }
    // Выбор опорного элемента (здесь — средний)
    let pivot = array[array.count / 2]
    // Разделение на три части
    let less = array.filter { $0 < pivot }
    let equal = array.filter { $0 == pivot }
    let greater = array.filter { $0 > pivot }
    // Рекурсивная сортировка и объединение
    return quickSort(less) + equal + quickSort(greater)
}

let sorted = quickSort([3, 1, 4, 1, 5, 9, 2, 6])
print(sorted) // [1, 1, 2, 3, 4, 5, 6, 9]

Примечание: Данная реализация для наглядности, но не оптимальна по памяти из-за создания множества новых массивов.

Ответ 18+ 🔞

Давай разжую эту тему про быструю сортировку, чтобы даже у мартышлюшки с одним полушарием всё встало на свои места. Ситуация, в общем-то, простая, но с подвохом, как всегда.

Вот смотри, QuickSort — это как если бы ты взял кучу носков, выбрал один наугад и сказал: «Все, что хуёвее этого — налево, все, что охуеннее — направо, а такие же — в серединку». Потом с каждой кучкой повторяешь тот же трюк. В лучшем и среднем раскладе, когда тебе везёт с выбором этого центрального носка (pivot), ты разбираешь эту кучу за O(n log n). Быстро, эффективно, все довольны.

Но есть же, блядь, и худший случай! O(n²), ёпта! Это когда тебе на каждом шагу невезуха, и ты постоянно хватаешь самый потрёпанный или самый новенький носок из кучи. Получается, что одна кучка у тебя — огромная, а вторая — пустая. И ты тупо перебираешь всё по одному элементу, как дебил. Это пиздец как медленно, почти как сортировка пузырьком в плохом настроении.

Как не наступить на эти грабли? Да элементарно, Ватсон!

  1. Рандомизация. Хватай pivot не по порядку, а как бог на душу положит. Случайность — наша подруга, она обычно не подводит.
  2. Медиана трёх. Ну или не будь лохом — посмотри на первый, средний и последний элемент, и выбери из них середнячка. Шансы налажать резко падают.

А вот тебе пример кода на Swift, смотри, не запороть бы:

func quickSort<T: Comparable>(_ array: [T]) -> [T] {
    guard array.count > 1 else { return array }
    // Выбор опорного элемента (здесь — средний)
    let pivot = array[array.count / 2]
    // Разделение на три части
    let less = array.filter { $0 < pivot }
    let equal = array.filter { $0 == pivot }
    let greater = array.filter { $0 > pivot }
    // Рекурсивная сортировка и объединение
    return quickSort(less) + equal + quickSort(greater)
}

let sorted = quickSort([3, 1, 4, 1, 5, 9, 2, 6])
print(sorted) // [1, 1, 2, 3, 4, 5, 6, 9]

Только не обосрись с восторга — эта реализация красивая для учебника, но по памяти она жрёт, как не в себя, потому что плодит кучу новых массивов на каждом шагу. Для продакшена надо крутить всё на месте, но это уже другая история, блядь. Главное — принцип уловил.