Рассмотрите этот фрагмент быстрой сортировки на Python:
def quicksort(a):
if len(a) < 2: return a
pivot = a[0]
left = [x for x in a[1:] if x = pivot]
return quicksort(left) + [pivot] + quicksort(right)
Объясните, почему в худшем случае сложность O(n^2), какие реальные наборы данных это вызывает, как это исправить (изменения в выборе опорного элемента, итеративные варианты, три-пивот и т.д.) и какие компромиссы вы бы выбрали для общей библиотеки сортировки

20 Окт в 10:34
5 +2
0
Ответы
1

Коротко и по делу.

Почему худший случай (O(n^2))

В вашем коде опорный элемент — первый элемент. Если он всегда оказывается минимальным или максимальным (или равны все элементы), разбиение будет крайне несбалансированным: один из подмассивов имеет размер (n-1), другой — (0). Рекуррентное соотношение:
[
T(n)=T(n-1)+cn
]
где (cn) — затраты на одном уровне (проход по массиву). Решение даёт арифметическую прогрессию:
[
T(n)=\Theta!\left(\sum_{k=1}^{n} k\right)=\Theta(n^2),
]
значит худшая сложность (O(n^2)).

Какие реальные наборы данных вызывают это

Отсортированные массивы (по возрастанию или убыванию) при выборе первого/последнего элемента как опоры. Массивы с большим числом одинаковых элементов (в вашем коде, где право — (x\ge) pivot, всё равное идёт в правую часть → длина правой = (n-1)). Злонамеренно сконструированные входы, если выбор опоры детерминистичен.

Как исправить (варианты)

Случайный выбор опоры (randomized quicksort)

Перед началом выбирайте случайный индекс и меняйте его с первым. Это делает худший случай взлома входными данными маловероятным; средняя (ожидаемая) сложность (\,O(n\log n)).Компромисс: требуется генератор случайных чисел; немного проигрыш в константах.

Медиа́на из трёх / ни́нтер (median-of-three, Tukey ninther)

Берут медиану из ({a[0],a[n/2],a[n-1]}) или более сложный ninther для устойчивости против уже-частично-отсортированных данных.Хорошо уменьшает шанс очень несбалансированных разбиений на «типичных» данных.Компромисс: чуть больше сравнений при выборе опоры, но обычно выигрывает в практическом времени.

Трёхпутевое (three-way) разбиение (Dijkstra)

Разбивает на <, =, > pivot. Для данных с множеством одинаковых ключей устраняет деградацию — случай «все элементы равны» обрабатывается за (O(n)).Особенно полезно, если ожидаются повторяющиеся ключи.Компромисс: немного сложнее код, чуть больше движений при малом числе дубликатов.

Мультипивотные варианты (dual-pivot и т.д.)

Например dual-pivot quicksort (использует две опоры) в Java показал практическое ускорение для примитивных типов.Компромисс: более сложная логика, труднее доказать и оптимизировать под все аппаратные особенности.

Introsort (комбинация quicksort + heapsort)

Запускают quicksort, но если глубина рекурсии превышает порог (обычно (c\log n)), переключаются на heapsort, что даёт гарантию (\,O(n\log n)) в худшем случае.Это подход C++ std::sort.Компромисс: небольшие накладные расходы на отслеживание глубины и на переключение, но даёт жёсткую гарантию по худшему случаю.

Итеративные варианты / устранение хвостовой рекурсии

Реализовать с явным стеком или использовать оптимизацию хвостовой рекурсии: всегда рекурсивно обрабатывать меньшую часть, а в цикле — большую. Это ограничивает глубину стека в среднем до (O(\log n)).Компромисс: нужно аккуратно реализовать, но экономит стек.

Малые массивы — insertion sort

Для подмассивов размера ≤ порог (обычно 16–32) переключаться на insertion sort — лучше кэш и меньшие константы.Компромисс: порог подбирается эмпирически.

Какие компромиссы и что выбрать для общей библиотеки сортировки

Рекомендация для общей (быстрой, неблокирующей и надёжной) библиотеки:

Для нестабильной сортировки (по скорости и памяти): использовать introsort:
выбор опоры: median-of-three или ninther (для больших массивов) + случайный элемент/перемешивание по необходимости;три-way partitioning включать динамически: если много равных элементов (замечено по частому получению маленького/пустого одного из подмассивов), переключиться на 3-way;хвостовая рекурсия / явный стек для ограничения глубины;при глубине > (c\log n) — переключение на heapsort (гарантия (\,O(n\log n)));для малых подмассивов (например ≤ 16) — insertion sort.Для стабильной сортировки: использовать TimSort (как в Python) или стабильный mergesort; TimSort оптимизирован для уже-частично-отсортированных входов.Если целевая среда — примитивы и нужна максимум скоростей (как в Java 7), можно рассмотреть сильно оптимизированный dual-pivot quicksort, но это сложнее и хуже переносится на все типы/ситуации.Если важна непредсказуемость для противостояния злонамеренным входам, применять рандомизацию (или перестановку) перед сортировкой.

Коротко по приоритетам: для стандартной библиотеки — introsort (median-of-three/ninther + insertion cutoff + три-way когда нужно + heapsort fallback). Для стабильной API — TimSort.

Если нужно, могу привести компактный псевдокод для introsort или три-way partition.

20 Окт в 10:59
Не можешь разобраться в этой теме?
Обратись за помощью к экспертам
Гарантированные бесплатные доработки в течение 1 года
Быстрое выполнение от 2 часов
Проверка работы на плагиат
Поможем написать учебную работу
Прямой эфир