Изменения

Перейти к: навигация, поиск

Быстрая сортировка

12 791 байт добавлено, 19:25, 4 сентября 2022
м
rollbackEdits.php mass rollback
==Алгоритм==
Быстрый метод сортировки функционирует по принципу "разделяй и властвуй". * Массив <tex> Aa[l \ldots r]</tex> типа <tex> T </tex> разбивается на два (возможно пустых) подмассива <tex> Aa[l \ldots q-1]</tex> и <tex> Aa[q+1 \ldots r]</tex>, таких, что каждый элемент <tex> Aa[l \ldots q-1]</tex> меньше или равен <tex> Aa[q]</tex>, который в свою очередь, не превышает любой элемент подмассива <tex> Aa[q+1 \ldots r]</tex>. Индекс вычисляется в ходе процедуры разбиения.* Подмассивы <tex> Aa[l \ldots q-1]</tex> и <tex> Aa[q+1 \ldots r]</tex> сортируются с помощью рекурсивного вызова процедуры быстрой сортировки.* Поскольку подмассивы сортируются на месте, для их объединения не требуются никакие действия: весь массив <tex> Aa[l \ldots r]</tex> оказывается отсортированным.
==Псевдокод==
'''functionvoid''' quicksort(Aa: '''intT'''[n], '''int''' l, '''int''' r):
'''if''' l < r
'''int''' q = partition(Aa, l, r) quicksort(Aa, l, q - 1) quicksort(Aa, q + 1, r)Для сортировки всего массива необходимо выполнить процедуру <tex>\mathrm{quicksort(Aa, 0, length[Aa] - 1)}</tex>.
===Разбиение массива===
Основной шаг алгоритма сортировки {{---}} процедура <tex>\mathrm{partition}</tex>, которая переставляет элементы массива <tex>Aa[p l \ldots r]</tex> типа <tex> T </tex> нужным образом:.Разбиение осуществляется с использованием следующей стратегии. Прежде всего, в качестве разделяющего элемента произвольно выбирается элемент <tex> a[(l + r) / 2] </tex>. Далее начинается просмотр с левого конца массива, который продолжается до тех пор, пока не будет найден элемент, превосходящий по значению разделяющий элемент, затем выполняется просмотр, начиная с правого конца массива, который продолжается до тех пор, пока не отыскивается элемент, который по значению меньше разделяющего. Оба элемента, на которых просмотр был прерван, очевидно, находятся не на своих местах в разделенном массиве, и потому они меняются местами. Так продолжаем дальше, пока не убедимся в том, что слева от левого указателя не осталось ни одного элемента, который был бы больше по значению разделяющего, и ни одного элемента справа от правого указателя, которые были бы меньше по значению разделяющего элемента. Переменная <tex> v </tex> сохраняет значение разделяющего элемента <tex> a[(l + r) / 2] </tex>, a <tex> i </tex> и <tex> j </tex> представляет собой, соответственно, указатели левого и правого просмотра. Цикл разделения увеличивает значение <tex> i </tex> и уменьшает значение <tex> j </tex> на <tex> 1 </tex>, причем условие, что ни один элемент слева от <tex> i </tex> не больше <tex> v </tex> и ни один элемент справа от <tex> j </tex> не меньше <tex> v </tex>, не нарушается. Как только значения указателей пересекаются, процедура разбиения завершается.  '''int''' partition(Aa: '''intT'''[n], '''int''' l, '''int''' r): x '''T''' v = Aa[(l+ r) / 2] '''int''' i = l '''int''' j = r '''while''' ''true'' (i <tex> \leqslant </tex> j) '''while''' A(a[i] < x v) i = i + 1+ '''while''' A(a[j] > x v) j = j - 1- '''if''' (i < tex> \geqslant </tex> j ) '''break''' swap(Aa[i++], Aa[j--]) '''else''' '''return''' j
==Асимптотика==
В некоторых алгоритмах быстрой сортировки в качестве опорного выбирается элемент, который стоит в середине рассматриваемого массива. Рассмотрим массив, на котором быстрая сортировка с выбором среднего элемента в качестве опорного сделает <tex>\Theta(n^2)</tex> сравнений. Очевидно, что это будет достигаться при худшем случае (когда при каждом разбиении в одном массиве будет оказываться <tex>1</tex>, а в другом <tex> n - 1 </tex> элемент).
Заполним сначала массив <tex>Aa</tex> длины <tex>n</tex> элементами от <tex>1</tex> до <tex> n </tex>, затем применим следующий алгоритм (нумерация с нуля):
'''functionvoid''' antiQsort(Aa: '''intT'''[n]):
'''for''' i = 0 '''to''' n - 1
swap(Aa[i], Aa[i / 2])
Тогда на каждом шаге в качестве среднего элемента будет ставиться самый крупный элемент.
! Шаг 1.0 || Шаг 1.1 || Шаг 1.2 || Шаг 2.0 || Шаг 2.1 || Шаг 2.2 || Шаг 3.0
|-align="right"
|style="text-align: center;"|1 2 3 4 <br\> 0 '''0''' 0 0| style="text-align: center;"|1 2 3 4 <br\> 0 '''4''' 0 0|style="text-align: center;"|1 4 3 2 <br\> 0 0 0 '''4'''|style="text-align: center;"|1 4 3 2 <br\> 0 '''0''' 0 4|style="text-align: center;"|1 4 3 2 <br\> 0 '''3''' 0 4|style="text-align: center;"|1 3 4 2 <br\> 0 0 '''3''' 4|style="text-align: center;"|1 3 4 2 <br\> '''0''' 0 3 4
|-
! Шаг 3.1 || Шаг 3.2 || Шаг 4.0 || Шаг 4.1 || Шаг 4.2 || colspan="2" style="vertical-align: middle;"| Результат
|-align="right"
|style="text-align: center;"|1 3 4 2 <br\> '''2''' 0 3 4|style="text-align: center;"|3 1 4 2 <br\> 0 '''2''' 3 4|style="text-align: center;"|3 1 4 2 <br\> '''0''' 2 3 4|style="text-align: center;"|3 1 4 2 <br\> '''1''' 2 3 4|style="text-align: center;"|3 1 4 2 <br\> '''1''' 2 3 4| colspan="2" style="text-align: center;vertical-align: middle;" |'''1 2 3 4''' <br\> '''2 4 1 3'''
|-
</center>
Покажем, почему на данном массиве будет достигаться максимальное время работы быстрой сортировки. На этапе построения мы каждый раз присваивали опорному элементу минимальное максимальное значение. Следовательно, при выполнении <tex>\mathrm{quicksort}</tex> алгоритм в качестве опорного всегда будет выбирать наибольший элемент массива (выборка будет производится в том же порядке ввиду детерминированности определения опорного элемента).
Таким образом, так как каждый раз массив разбивается на две части {{---}} большие или равные опорному элементы и меньшие его {{---}} на каждом шаге имеем разбиение на массивы длины <tex>1</tex> и <tex>n-1</tex>, чего мы, собственно, и добивались. При таком выполнении алгоритма происходит <tex>\Theta(n^2)</tex> разделений на два подмассива, и на каждом разделении выполняется <tex>\Theta(n^2)</tex> сравнений.
Следовательно, на данном массиве быстрая сортировка работает за <tex>\Theta(n^2)</tex>.
Лемма
|statement=Время работы алгоритма быстрой сортировки равно <tex>O(n \log n)</tex>.
|proof=Пусть Х <tex>X</tex> {{---}} полное количество сравнений элементов с опорным за время работы сортировки. Нам необходимо вычислить полное количество сравнений.Переименуем элементы массива как <tex>z_1...\ldots z_n</tex>, где <tex>z_i</tex> наименьший по порядку элемент.Также введем множество <tex>Z_{ij} = \{z_i, z_{i+1}...\ldots z_j\}</tex>.
Заметим, что сравнение каждой пары элементов происходит не больше одного раза, так как элемент сравнивается с опорным, а опорный элемент после разбиения больше не будет участвовать в сравнении.
<tex>X = \sum\limits_{i=1}^{n-1}\sum\limits_{j=i+1}^{n} X_{ij}</tex>, где <tex>X_{ij} = 1</tex> если произошло сравнение <tex>z_i</tex> и <tex>z_j</tex> и <tex>X_{ij} = 0</tex>, если сравнения не произошло.
Применим к обоим обеим частям равенства операцию вычисления матожидания и воспользовавшись ее линейностью получим
<tex>E[X] = E\left[\sum\limits_{i=1}^{n-1}\sum\limits_{j=i+1}^{n} X_{ij}\right] = \sum\limits_{i=1}^{n-1}\sum\limits_{j=i+1}^{n} E[X_{ij}] = \sum\limits_{i=1}^{n-1}\sum\limits_{j=i+1}^{n} Pr\{z_i</tex> сравнивается с <tex>z_j\}</tex>
Pr\{</tex>первым опорным элементом был <tex>z_i</tex> или <tex>z_j\} =
Pr\{</tex>первым опорным элементом был <tex>z_i\} +
Pr\{</tex>первым опорным элементом был <tex>z_j\} =</tex><tex> =\frac dfrac {1}{j-i+1} + \frac dfrac {1}{j-i+1} = \frac dfrac {2}{j-i+1} </tex>
<tex> E[X] = \sum\limits_{i=1}^{n-1}\sum\limits_{j=i+1}^{n} \frac dfrac {2}{j-i+1} = \sum\limits_{i=1}^{n-1}\sum\limits_{k=1}^{n-i} \frac dfrac 2{k+1} < \sum\limits_{i=1}^{n-1}\sum\limits_{k=1}^{n-i} \frac dfrac 2{k} </tex> <tex>= \sum\limits_{i=1}^{n-1}O(\log n) = O(n \log n)</tex>
}}
Mатожидание времени работы быстрой сортировки будет <tex>O(n \log n)</tex>.
==УлучшенияМодификации==В случае повторяющихся неудачных разбиений опорным элементом===Нерекурсивная реализация быстрой сортировки===Для выполнения быстрой сортировки можно воспользоваться [[Стек | стеком]], в котором в виде сортируемых подмассивов содержится перечень действий, которые предстоит выполнить. Каждый раз когда возникает необходимость в обработке подмассива, он выталкивается из стека. После разделения массива получаются два подмассива, требующих дальнейшей обработки, которые и заталкиваются в стек.Представленная ниже нерекурсивная реализация использует стек, заменяя рекурсивные вызовы помещением в стек параметров функции, а вызовы процедур и выходы из них — циклом, который осуществляет выборку параметров из стека и их обработку, пока стек не пуст. Мы помещаем больший из двух подмассивов в стек первым с тем, чтобы максимальная глубина рекурсии может достичь стека при сортировке <tex>N</tex> элементов не превосходила величины <tex>\log n<Tex/tex>O. '''void''' quicksort(a: '''T'''[n], '''int''' l, '''int''' r) '''stack'''< '''pair'''</Tex'''int''','''int'''> >s s.push(l, а время работы алгоритма r) '''while''' (s.isNotEmpty) (l, r) = s.pop() '''if''' (r <tex>O(n^2)\leqslant </tex>l) '''continue''' '''int''' i = partition(a, l, r) '''if''' (i - l > r - i) s.push(l, i - 1) s.push(i + 1, r) '''else''' s.push(i + 1, r) s. Существуют различные способы разбиения push(l, i - 1) В качестве альтернативного варианта можно использовать обычную рекурсивную версию, в которой вместо того, чтобы после разделения массивавызывать рекурсивно процедуру разделения для обоих найденных подмассивов, направленные против худшего случаярекурсивный вызов делается только для меньшего подмассива, а больший обрабатывается в цикле в пределах этого же вызова процедуры. С точки зрения эффективности в среднем случае разницы практически нет:* При выборе опорного элемента из данного диапазона случайным образом худший случай становится очень маловероятным накладные расходы на дополнительный рекурсивный вызов и на организацию сравнения длин подмассивов и ожидаемое время выполнения алгоритма сортировки — цикла — примерно одного порядка. Зато глубина рекурсии ни при каких обстоятельствах не превысит <tex>O(n \log n)</tex>, а в худшем случае вырожденного разделения она вообще будет не более <tex>1</tex> — вся обработка пройдёт в цикле первого уровня рекурсии.* Выбирать опорным элементом средний ===Улучшенная быстрая сортировка=== Выбор медианы из трех (первого, среднего и последнего элементовв качестве разделяющего элемента и отсечение рекурсии меньших подмассивов может привести к существенному повышению эффективности быстрой сортировки. Функция <tex>\mathrm{median}</tex> возвращает индекс элемента, являющегося медианой трех элементов. После этого он и средний элемент массива меняются местами, при этом медиана становится разделяющим элементом. Массивы небольшого размера (длиной <tex> M = 11</tex> и меньше).* Разбивать массив не на двев процессе разделения игнорируются, а на три частизатем для окончания сортировки используется [[Сортировка вставками | сортировка вставками]].
'''const int''' M =10 '''void''' quicksort(a: '''T'''[n], '''int''' l, '''int''' r) '''if''' (r - l <tex> \leqslant </tex> M) insertion(a, l, r) '''return''' '''int''' med =Оптимизация глубины рекурсии до Omedian(a[l], a[(l + r) / 2], a[r]) swap(a[med], a[(l + r) / 2]) '''int''' i = partition(a, l, r) quicksort(logna, l, i) quicksort(a, i + 1, r) Вообще, можно применять любые эвристики по выбору опорного элемента. Например, в худшем случаестандартной реализации в Java в качестве разделяющего выбирается средний из 7 элементов, равномерно распределённых по массиву. ===Быстрая сортировка с разделением на три части===Во избежание достижения опасной глубины рекурсии Когда в сортируемом массиве имеется множество повторяющихся ключей предыдущие реализации быстрой сортировки можно существенно улучшить. Например массив, который состоит из равных ключей, вовсе не нуждается в худшем случае (или при приближении к нему) возможна модификация алгоритмадальнейшей сортировке, однако предыдущие реализации продолжают процесс разделения, подвергая обработке все более мелкие подмассивы, независимо от того, насколько большим является исходный файл.  В основу программы положено разделение массива на три части: на элементы,меньшие разделяющего элемента <tex> a[l] \ldots a[i]</tex>, элементы, равные разделяющему элементу <tex>a[i+1] \ldots a[j-1]</tex>, устраняющая одну ветвь рекурсиии элементы большие разделяющего элемента <tex>a[j] \ldots a[r]</tex>. После этого сортировка завершается двумя рекурсивными вызовами. [[Файл: вместо тогоG3.png |400px|thumb|center| Разделение массива <tex> a </tex>]] Элементы массива равные разделяющему элементу находятся между <tex> l </tex> и <tex> p </tex> и между <tex> q </tex> и <tex> r </tex>. В разделяющем цикле, когда указатели просмотра перестают изменяться и выполняется обмен значениями <tex> i </tex> и <tex> j </tex>, каждый из этих элементов проверяется на предмет равенства разделяющему элементу. Если элемент, который сейчас находится слева, равен разделяющему элементу, чтобы после разделения то при помощи операции обмена он помещается в левую часть массива вызывать рекурсивно процедуру разделения для обоих найденных подмассивов, рекурсивный вызов делается только для меньшего подмассиваесли элемент, который сейчас находится справа, равен разделяющему элементу, а больший обрабатывается то в результате операции обмена он помещается в цикле правую часть массива. После того как указатели пересекутся, элементы, равные разделяющему элементу и находящиеся на разных концах массива, после операции обмена попадают в пределах свои окончательные позиции. После этого же вызова процедурыуказанные ключи могут быть исключены из подмассивов, для которых выполняются последующие рекурсивные вызовы. С точки зрения эффективности в среднем случае разницы практически нет  '''void''' quicksort(a: накладные расходы '''T'''[n], '''int''' l, '''int''' r) '''T''' v = a[r] '''if''' (r <tex> \leqslant </tex> l) '''return''' '''int''' i = l '''int''' j = r - 1 '''int''' p = l - 1 '''int''' q = r '''while''' ''(i <tex> \leqslant </tex> j)'' '''while''' (a[i] < v) i++ '''while''' (a[j] > v) j-- '''if''' (i <tex> \geqslant </tex> j) '''break''' swap(a[i], a[j]) '''if''' (a[i] == v) p++ swap(a[p], a[i]) i++ '''if''' (a[j] == v) q-- swap(a[q], a[j]) j-- swap(a[i], a[r]) j = i - 1 i++ '''for''' ('''int''' k = l; k <tex> \leqslant </tex> p; k++, j--) swap(a[k], a[j]) '''for''' ('''int''' k = r - 1; k <tex> \geqslant </tex> q; k--, i++) swap(a[k], a[i]) quicksort(a, l, j) quicksort(a, i, r) ===Параллельная сортировка=== Еще одной оптимизацией является [[PSRS-сортировка|параллельная сортировка]] на основе быстрой. Пусть, исходный набор данных расположен на первом процессоре, с него начинается работа алгоритма. Затем исходный массив окажется разделенным на две части, меньшая из которых передастся другому свободному процессору, большая останется на дополнительный рекурсивный вызов исходном для дальнейшей обработки. Далее обе части опять будут разделены и опять на организацию сравнения длин подмассивов двух исходных останутся большие части, а меньшие отправятся другим процессорам. В этом заключается ускорение алгоритма. При задействовании всех процессоров, все части параллельно будут сортироваться последовательным алгоритмом. ===Introsort=== Для предотвращения ухудшения времени работы быстрой сортировки до <tex>O(n^2)</tex> при неудачных входных данных, также можно использовать алгоритм сортировки Introsort.Он использует быструю сортировку и цикла — примерно одного порядка. Зато переключается на [[Сортировка кучей|пирамидальную сортировку]], когда глубина рекурсии ни при каких обстоятельствах не превысит некоторый заранее установленный уровень (например, логарифм от числа сортируемых элементов). Так как после нескольких итераций быстрой сортировки с применением разных эвристик массив с большей вероятностью окажется «почти отсортированным», то пирамидальная сортировка может довольно быстро закончить дело. Также, пирамидальная сортировка хороша тем, что требует <tex>\log nO(1)</tex>дополнительной памяти, а в худшем случае вырожденного разделения она вообще будет не более 2 — вся обработка пройдёт в цикле первого уровня рекурсииотличие от, например, сортировки слиянием, где потребуется <tex>O(n)</tex> дополнительной памяти. ==См.также==* [[Сортировка Шелла]]* [[Сортировка кучей]]* [[Сортировка слиянием]]* [[Timsort]]* [[Smoothsort]]* [[PSRS-сортировка]]
== Источники информации ==
* [[wikipedia:ru:Быстрая сортировка|Википедия {{---}} быстрая Быстрая сортировка]]* [[wikipedia:en:Quicksort|Wikipedia {{---}} Quicksort]]* [[wikipedia:en:Introsort|Wikipedia {{---}} Introsort]]
* ''Т. Кормен, Ч. Лейзерсон, Р. Ривест'': Алгоритмы: построение и анализ глава 7
* ''Р. Седжвик'': Фундаментальные алгоритмы на С++ части 1 - 4
 
[[Категория: Дискретная математика и алгоритмы]]
[[Категория: Сортировка]]
[[Категория: Сортировки на сравнениях]]
1632
правки

Навигация