Рассмотрите этот фрагмент быстрой сортировки на 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), какие реальные наборы данных это вызывает, как это исправить (изменения в выборе опорного элемента, итеративные варианты, три-пивот и т.д.) и какие компромиссы вы бы выбрали для общей библиотеки сортировки
В вашем коде опорный элемент — первый элемент. Если он всегда оказывается минимальным или максимальным (или равны все элементы), разбиение будет крайне несбалансированным: один из подмассивов имеет размер (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.
Коротко и по делу.
Почему худший случай (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.