Рассмотрите алгоритм быстрой сортировки (псевдокод partition/quickSort) — укажите возможные причины деградации до O(n^2) на конкретных входах и предложите модификации (выбор опорного элемента, случайная перестановка, три-way partition) для стабилизации времени работы.
Причины деградации до O(n2)O(n^2)O(n2) - Плохой выбор опорного элемента: если опора каждый раз оказывается минимумом или максимумом, разбиения имеют размеры 000 и n−1n-1n−1. Тогда рекуррентное соотношение T(n)=T(n−1)+Θ(n)T(n)=T(n-1)+\Theta(n)T(n)=T(n−1)+Θ(n)
даёт T(n)=Θ(n2)T(n)=\Theta(n^2)T(n)=Θ(n2). Конкретные входы: уже отсортированный или обратно отсортированный массив при выборе опоры как первого/последнего элемента. - Много одинаковых ключей: при стандартном двухсекторном partition элементы, равные опоре, могут уходить в одну сторону, и количество элементов уменьшается лишь на единицу за шаг — тоже даёт O(n2)O(n^2)O(n2) на входах с множественными дубликатами (например, все элементы равны). - Адвёрсарные входы для детерминированных эвристик (напр., специально составленные последовательности, которые ломают median-of-three). - Глубокая рекурсия/стек при плохом балансе (риск переполнения стека). Модификации для стабилизации времени работы 1) Случайный выбор опоры (рандомизированный quicksort) - В каждом partition выбирать опору случайно (или обменять случайный элемент с первым перед partition). - Гарантии: ожидаемое время O(nlogn)O(n\log n)O(nlogn) и высокие шансы на близкие к средним разбиения. Простая, эффективная в практике защита от худших входов. 2) Предварительная случайная перестановка (Fisher–Yates shuffle) - Перемешать массив один раз за O(n)O(n)O(n), затем применять обычный quicksort (даже если опора — первый/последний элемент). - Гарантия: порядок ввода больше не влияет, ожидаемое время O(nlogn)O(n\log n)O(nlogn). 3) Эвристики выбора опоры - Median-of-three: выбрать медиану из первого, среднего и последнего элементов как опору. Уменьшает вероятность крайних разбиений на частично отсортированных данных; простой и дешёвый. - Median-of-medians (детерминированный выбор квазимедианы): даёт гарантированные приближённо сбалансированные разбиения (например, гарантирует, что одна часть не больше некоторой доли <1<1<1 от nnn), что приводит к детерминированному времени O(nlogn)O(n\log n)O(nlogn) (с большими константами). Используется редко в практических реализациях из-за накладных расходов. 4) 333-way partition (Dutch national flag) для множества дублей - Разбить на три части: \ опора. Псевдокод основной идеи: - инициализировать lt=lo, i=lo, gt=hi, pivot=A[lo]lt=lo,\; i=lo,\; gt=hi,\; pivot=A[lo]lt=lo,i=lo,gt=hi,pivot=A[lo]
- пока i≤gti\le gti≤gt: - если A[i]<pivotA[i]<pivotA[i]<pivot — swap(A[lt],A[i]A[lt],A[i]A[lt],A[i]); lt++; i++lt++;\; i++lt++;i++
- иначе если A[i]>pivotA[i]>pivotA[i]>pivot — swap(A[i],A[gt]A[i],A[gt]A[i],A[gt]); gt−−gt--gt−−
- иначе i++i++i++
- рекурсивно сортировать диапазоны [lo,lt−1][lo,lt-1][lo,lt−1] и [gt+1,hi][gt+1,hi][gt+1,hi]
- Эффект: при большом количестве одинаковых элементов алгоримт работает значительно быстрее (в пределе — линейно O(n)O(n)O(n) если все элементы равны). 5) Introsort (гибридный подход) - Запускают quicksort, отслеживают глубину рекурсии; при превышении порога (например, clognc\log nclogn) переключаются на heapsort. Это даёт гарантированное худшее время O(nlogn)O(n\log n)O(nlogn) и практическую скорость quicksort. Дополнительные практические рекомендации - Для конкурентной реализации — итеративный код/элиминация хвостовой рекурсии, сортировать сначала меньшую часть, рекурсировать в меньшей глубине, это уменьшает стек. - В большинстве реализаций комбинируют: сначала shuffle или случайный опорный элемент, использовать median-of-three как эвристику, применять 333-way partition при ожидании дубликатов, и introsort для гарантии худшего случая. Кратко: основные причины — систематически неудачный выбор опоры и много дубликатов. Простые и эффективные меры: выбор опоры случайно или предварительная перестановка, 333-way partition для дублей, median-of-three/median-of-medians или introsort для жёстких гарантий.
- Плохой выбор опорного элемента: если опора каждый раз оказывается минимумом или максимумом, разбиения имеют размеры 000 и n−1n-1n−1. Тогда рекуррентное соотношение
T(n)=T(n−1)+Θ(n)T(n)=T(n-1)+\Theta(n)T(n)=T(n−1)+Θ(n) даёт T(n)=Θ(n2)T(n)=\Theta(n^2)T(n)=Θ(n2). Конкретные входы: уже отсортированный или обратно отсортированный массив при выборе опоры как первого/последнего элемента.
- Много одинаковых ключей: при стандартном двухсекторном partition элементы, равные опоре, могут уходить в одну сторону, и количество элементов уменьшается лишь на единицу за шаг — тоже даёт O(n2)O(n^2)O(n2) на входах с множественными дубликатами (например, все элементы равны).
- Адвёрсарные входы для детерминированных эвристик (напр., специально составленные последовательности, которые ломают median-of-three).
- Глубокая рекурсия/стек при плохом балансе (риск переполнения стека).
Модификации для стабилизации времени работы
1) Случайный выбор опоры (рандомизированный quicksort)
- В каждом partition выбирать опору случайно (или обменять случайный элемент с первым перед partition).
- Гарантии: ожидаемое время O(nlogn)O(n\log n)O(nlogn) и высокие шансы на близкие к средним разбиения. Простая, эффективная в практике защита от худших входов.
2) Предварительная случайная перестановка (Fisher–Yates shuffle)
- Перемешать массив один раз за O(n)O(n)O(n), затем применять обычный quicksort (даже если опора — первый/последний элемент).
- Гарантия: порядок ввода больше не влияет, ожидаемое время O(nlogn)O(n\log n)O(nlogn).
3) Эвристики выбора опоры
- Median-of-three: выбрать медиану из первого, среднего и последнего элементов как опору. Уменьшает вероятность крайних разбиений на частично отсортированных данных; простой и дешёвый.
- Median-of-medians (детерминированный выбор квазимедианы): даёт гарантированные приближённо сбалансированные разбиения (например, гарантирует, что одна часть не больше некоторой доли <1<1<1 от nnn), что приводит к детерминированному времени O(nlogn)O(n\log n)O(nlogn) (с большими константами). Используется редко в практических реализациях из-за накладных расходов.
4) 333-way partition (Dutch national flag) для множества дублей
- Разбить на три части: \ опора. Псевдокод основной идеи:
- инициализировать lt=lo, i=lo, gt=hi, pivot=A[lo]lt=lo,\; i=lo,\; gt=hi,\; pivot=A[lo]lt=lo,i=lo,gt=hi,pivot=A[lo] - пока i≤gti\le gti≤gt:
- если A[i]<pivotA[i]<pivotA[i]<pivot — swap(A[lt],A[i]A[lt],A[i]A[lt],A[i]); lt++; i++lt++;\; i++lt++;i++ - иначе если A[i]>pivotA[i]>pivotA[i]>pivot — swap(A[i],A[gt]A[i],A[gt]A[i],A[gt]); gt−−gt--gt−− - иначе i++i++i++ - рекурсивно сортировать диапазоны [lo,lt−1][lo,lt-1][lo,lt−1] и [gt+1,hi][gt+1,hi][gt+1,hi] - Эффект: при большом количестве одинаковых элементов алгоримт работает значительно быстрее (в пределе — линейно O(n)O(n)O(n) если все элементы равны).
5) Introsort (гибридный подход)
- Запускают quicksort, отслеживают глубину рекурсии; при превышении порога (например, clognc\log nclogn) переключаются на heapsort. Это даёт гарантированное худшее время O(nlogn)O(n\log n)O(nlogn) и практическую скорость quicksort.
Дополнительные практические рекомендации
- Для конкурентной реализации — итеративный код/элиминация хвостовой рекурсии, сортировать сначала меньшую часть, рекурсировать в меньшей глубине, это уменьшает стек.
- В большинстве реализаций комбинируют: сначала shuffle или случайный опорный элемент, использовать median-of-three как эвристику, применять 333-way partition при ожидании дубликатов, и introsort для гарантии худшего случая.
Кратко: основные причины — систематически неудачный выбор опоры и много дубликатов. Простые и эффективные меры: выбор опоры случайно или предварительная перестановка, 333-way partition для дублей, median-of-three/median-of-medians или introsort для жёстких гарантий.