Разберите алгоритм быстрой сортировки на примере массива [3, 7, 2, 9, 1, 5] с описанием выбора опорного элемента, анализом сложности в среднем и худшем случае и модификациями для устойчивой сортировки
Кратко: быстрая сортировка (Quicksort) — разделяй-и-властвуй: выбрать опорный элемент (pivot), переставить элементы так, чтобы слева были ≤ pivot, справа — > pivot (partition), рекурсивно отсортировать две части. Разбор на примере массива [3,7,2,9,1,5] [3,7,2,9,1,5] [3,7,2,9,1,5]. Возьмём простую схему Lomuto (pivot = последний элемент). 1) Исход: [3,7,2,9,1,5] [3,7,2,9,1,5] [3,7,2,9,1,5], pivot =5=5=5. Проход partition (переменная iii — индекс последнего элемента ≤ pivot, начинается i=−1i=-1i=−1): - j=0j=0j=0: 3≤5⇒i=03\le5 \Rightarrow i=03≤5⇒i=0, swap позиций 000 и 000 → [3,7,2,9,1,5] [3,7,2,9,1,5] [3,7,2,9,1,5] - j=1j=1j=1: 7>57>57>5 → ничего - j=2j=2j=2: 2≤5⇒i=12\le5 \Rightarrow i=12≤5⇒i=1, swap 111 и 222 → [3,2,7,9,1,5] [3,2,7,9,1,5] [3,2,7,9,1,5] - j=3j=3j=3: 9>59>59>5 → ничего - j=4j=4j=4: 1≤5⇒i=21\le5 \Rightarrow i=21≤5⇒i=2, swap 222 и 444 → [3,2,1,9,7,5] [3,2,1,9,7,5] [3,2,1,9,7,5] После цикла swap i+1=3i+1=3i+1=3 с pivot → [3,2,1,5,7,9] [3,2,1,5,7,9] [3,2,1,5,7,9]. Pivot (555) оказался на позиции 333. 2) Рекурсивные вызовы: - Левая часть [3,2,1] [3,2,1] [3,2,1] (индексы 0…20\ldots20…2), pivot =1=1=1: - partition даёт [1,2,3] [1,2,3] [1,2,3], pivot на позиции 000. Подмассивы пусты или длины 1, возвращаем. - Правая часть [7,9] [7,9] [7,9] (индексы 4…54\ldots54…5), pivot =9=9=9: остаётся [7,9] [7,9] [7,9]. Итог: [1,2,3,5,7,9] [1,2,3,5,7,9] [1,2,3,5,7,9]. Анализ сложности: - Среднее время: O(nlogn)O(n\log n)O(nlogn). Формально среднее число сравнений Θ(nlogn)\Theta(n\log n)Θ(nlogn). - Худший случай: O(n2)O(n^2)O(n2) (например, уже отсортированный массив + выбор крайнего pivot каждый раз). - Память (рекурсивный стек): в среднем O(logn)O(\log n)O(logn), в худшем O(n)O(n)O(n) при сильно несбалансированных разбиениях. Как уменьшить вероятность худшего случая: - Случайный pivot (randomized quicksort) — ожидаемая сложность O(nlogn)O(n\log n)O(nlogn). - Median-of-three (первый/средний/последний) или выбор медианы по отбору (median of medians) для более сбалансированных разбиений. Устойчивость (stable sort) и модификации: - Обычный in-place Quicksort неустойчив. Возможные подходы для устойчивой версии: 1. Stable partition с дополнительной памятью: при разделении формировать три списка (меньше, равно, больше), добавлять элементы в исходном порядке и затем конкатенировать — это даёт устойчивую сортировку, но требует O(n)O(n)O(n) дополнительной памяти. 2. «Декоративный» приём: сопоставить каждому элементу его исходный индекс и сортировать пары (ключ,исходный_индекс)(ключ, исходный\_индекс)(ключ,исходный_индекс) по лексикографическому сравнению; итоговый порядок по ключу будет устойчивым (за счёт вторичного ключа). Нужна модификация компаратора, память как правило не увеличивается заметно. 3. Существуют in-place стабильные варианты quicksort, но они сложны и медленнее на практике; обычно предпочитают mergesort (стабильный и O(nlogn)O(n\log n)O(nlogn) по времени, O(n)O(n)O(n) по памяти) если нужна устойчивость. Резюме: Quicksort на примере дал [1,2,3,5,7,9] [1,2,3,5,7,9] [1,2,3,5,7,9]. Средняя сложность Θ(nlogn)\Theta(n\log n)Θ(nlogn), худшая O(n2)O(n^2)O(n2). Для устойчивости проще использовать дополнительную память (stable partition) или применять стабильный алгоритм (mergesort) либо декорировать ключи индексами.
Разбор на примере массива [3,7,2,9,1,5] [3,7,2,9,1,5] [3,7,2,9,1,5]. Возьмём простую схему Lomuto (pivot = последний элемент).
1) Исход: [3,7,2,9,1,5] [3,7,2,9,1,5] [3,7,2,9,1,5], pivot =5=5=5.
Проход partition (переменная iii — индекс последнего элемента ≤ pivot, начинается i=−1i=-1i=−1):
- j=0j=0j=0: 3≤5⇒i=03\le5 \Rightarrow i=03≤5⇒i=0, swap позиций 000 и 000 → [3,7,2,9,1,5] [3,7,2,9,1,5] [3,7,2,9,1,5]
- j=1j=1j=1: 7>57>57>5 → ничего
- j=2j=2j=2: 2≤5⇒i=12\le5 \Rightarrow i=12≤5⇒i=1, swap 111 и 222 → [3,2,7,9,1,5] [3,2,7,9,1,5] [3,2,7,9,1,5]
- j=3j=3j=3: 9>59>59>5 → ничего
- j=4j=4j=4: 1≤5⇒i=21\le5 \Rightarrow i=21≤5⇒i=2, swap 222 и 444 → [3,2,1,9,7,5] [3,2,1,9,7,5] [3,2,1,9,7,5]
После цикла swap i+1=3i+1=3i+1=3 с pivot → [3,2,1,5,7,9] [3,2,1,5,7,9] [3,2,1,5,7,9]. Pivot (555) оказался на позиции 333.
2) Рекурсивные вызовы:
- Левая часть [3,2,1] [3,2,1] [3,2,1] (индексы 0…20\ldots20…2), pivot =1=1=1:
- partition даёт [1,2,3] [1,2,3] [1,2,3], pivot на позиции 000. Подмассивы пусты или длины 1, возвращаем.
- Правая часть [7,9] [7,9] [7,9] (индексы 4…54\ldots54…5), pivot =9=9=9: остаётся [7,9] [7,9] [7,9].
Итог: [1,2,3,5,7,9] [1,2,3,5,7,9] [1,2,3,5,7,9].
Анализ сложности:
- Среднее время: O(nlogn)O(n\log n)O(nlogn). Формально среднее число сравнений Θ(nlogn)\Theta(n\log n)Θ(nlogn).
- Худший случай: O(n2)O(n^2)O(n2) (например, уже отсортированный массив + выбор крайнего pivot каждый раз).
- Память (рекурсивный стек): в среднем O(logn)O(\log n)O(logn), в худшем O(n)O(n)O(n) при сильно несбалансированных разбиениях.
Как уменьшить вероятность худшего случая:
- Случайный pivot (randomized quicksort) — ожидаемая сложность O(nlogn)O(n\log n)O(nlogn).
- Median-of-three (первый/средний/последний) или выбор медианы по отбору (median of medians) для более сбалансированных разбиений.
Устойчивость (stable sort) и модификации:
- Обычный in-place Quicksort неустойчив. Возможные подходы для устойчивой версии:
1. Stable partition с дополнительной памятью: при разделении формировать три списка (меньше, равно, больше), добавлять элементы в исходном порядке и затем конкатенировать — это даёт устойчивую сортировку, но требует O(n)O(n)O(n) дополнительной памяти.
2. «Декоративный» приём: сопоставить каждому элементу его исходный индекс и сортировать пары (ключ,исходный_индекс)(ключ, исходный\_индекс)(ключ,исходный_индекс) по лексикографическому сравнению; итоговый порядок по ключу будет устойчивым (за счёт вторичного ключа). Нужна модификация компаратора, память как правило не увеличивается заметно.
3. Существуют in-place стабильные варианты quicksort, но они сложны и медленнее на практике; обычно предпочитают mergesort (стабильный и O(nlogn)O(n\log n)O(nlogn) по времени, O(n)O(n)O(n) по памяти) если нужна устойчивость.
Резюме: Quicksort на примере дал [1,2,3,5,7,9] [1,2,3,5,7,9] [1,2,3,5,7,9]. Средняя сложность Θ(nlogn)\Theta(n\log n)Θ(nlogn), худшая O(n2)O(n^2)O(n2). Для устойчивости проще использовать дополнительную память (stable partition) или применять стабильный алгоритм (mergesort) либо декорировать ключи индексами.