Как работает быстрая сортировка и когда она работает медленно?
- Почему у быстрой сортировки худший случай O(n²)?
- Как выбирать опорный элемент?
- Что такое разбиение Ломуто и Хоара?
Выбираем опорный элемент, разбиваем массив: меньшие — влево, большие — вправо, опорный встаёт на своё место; затем рекурсивно сортируем части.
- В среднем O(n log n), на практике часто быстрее остальных: работает на месте, хорошо дружит с кэшем
- Худший O(n²) — когда опорный всё время крайний: например, последний элемент на отсортированных данных
А ещё Защита: случайный опорный или медиана трёх; при глубокой рекурсии — переход на пирамидальную (introsort)
Подробно
Схема «разделяй и властвуй»:
- выбрать опорный элемент (pivot);
- разбиение (partition): переставить элементы так, чтобы слева были не больше опорного, справа — больше; опорный оказывается на своей итоговой позиции;
- рекурсивно отсортировать левую и правую части.
Разбиение выполняется на месте за O(n). Две известные схемы: Ломуто — проще, опорный — последний элемент; Хоара — два указателя навстречу, меньше обменов.
Почему O(n log n) в среднем. Если опорный делит массив примерно пополам, глубина рекурсии — log n уровней, на каждом суммарно O(n) работы.
Почему O(n²) в худшем. Если опорный каждый раз минимальный или максимальный, одна часть пустая, а другая меньше всего на один элемент: n уровней по O(n). Классический случай — опорный «последний элемент» на уже отсортированных или почти отсортированных данных. Ещё один — много одинаковых значений при наивном разбиении.
Как защищаются:
- случайный опорный — худший случай становится крайне маловероятным;
- медиана трёх (первый, средний, последний);
- трёхпутевое разбиение для повторов (меньше, равно, больше);
- introsort — при слишком глубокой рекурсии переключение на пирамидальную сортировку, что гарантирует O(n log n).
Быстрая сортировка неустойчива, дополнительная память — O(log n) на стек рекурсии (если сначала рекурсивно обрабатывать меньшую часть).
Пример
def quicksort(a, lo=0, hi=None):
if hi is None:
hi = len(a) - 1
if lo >= hi:
return
pivot = a[hi] # Ломуто: опорный — последний
i = lo - 1
for j in range(lo, hi):
if a[j] <= pivot:
i += 1
a[i], a[j] = a[j], a[i]
a[i + 1], a[hi] = a[hi], a[i + 1] # опорный на своё место
quicksort(a, lo, i)
quicksort(a, i + 2, hi)Что хотят услышать
- Опорный, разбиение, рекурсия.
- Средняя сложность и почему.
- Причина худшего случая и пример данных.
- Способы защиты: случайный опорный, медиана трёх, introsort.
Частые ошибки
- Не знать про худший случай O(n²).
- Считать быструю сортировку устойчивой.
Проверьте себя
Один верный вариант. Подумайте, потом откройте ответ.
Показать варианты и ответ
- ✗Быстрая сортировка всегда работает за O(n log n), устойчива и требует O(n) дополнительной памяти; медленной она становится только на маленьких массивах
- ✗Быстрая сортировка устойчива
- ✓Разбиение вокруг опорного элемента; в среднем O(n log n), худший O(n²) при неудачном выборе опорного — защита случайным опорным
- ✗Быстрая сортировка требует O(n) дополнительной памяти
Что спросят дальше
Связанные вопросы
Разбор собеседования с sotto
После собеседования sotto разбирает разговор: оценка, каждый вопрос — хорошо, частично или слабо, чего не хватило в ответе и над чем поработать к следующему разу. Первые 30 минут бесплатно.