- Быстрая сортировка
- Содержание
- Алгоритм [ править ]
- Псевдокод [ править ]
- Разбиение массива [ править ]
- Асимптотика [ править ]
- Худшее время работы [ править ]
- Способ построить массив с максимальным количеством сравнений при выборе среднего элемента в качестве опорного [ править ]
- Способ построить массив с максимальным количеством сравнений при детерминированном выборе опорного элемента [ править ]
- Среднее время работы [ править ]
- Модификации [ править ]
- Нерекурсивная реализация быстрой сортировки [ править ]
- Улучшенная быстрая сортировка [ править ]
- Быстрая сортировка с разделением на три части [ править ]
- Параллельная сортировка [ править ]
- Introsort [ править ]
Быстрая сортировка
Быстрая сортировка (англ. quick sort, сортировка Хоара) — один из самых известных и широко используемых алгоритмов сортировки. Среднее время работы [math]O(n\log
Содержание
Алгоритм [ править ]
Быстрый метод сортировки функционирует по принципу «разделяй и властвуй».
- Массив [math] a[l \ldots r][/math] типа [math] T [/math] разбивается на два (возможно пустых) подмассива [math] a[l \ldots q][/math] и [math] a[q+1 \ldots r][/math] , таких, что каждый элемент [math] a[l \ldots q][/math] меньше или равен [math] a[q][/math] , который в свою очередь, не превышает любой элемент подмассива [math] a[q+1 \ldots r][/math] . Индекс вычисляется в ходе процедуры разбиения.
- Подмассивы [math] a[l \ldots q][/math] и [math] a[q+1 \ldots r][/math] сортируются с помощью рекурсивного вызова процедуры быстрой сортировки.
- Поскольку подмассивы сортируются на месте, для их объединения не требуются никакие действия: весь массив [math] a[l \ldots r][/math] оказывается отсортированным.
Псевдокод [ править ]
Для сортировки всего массива необходимо выполнить процедуру [math]\mathrm
Разбиение массива [ править ]
Основной шаг алгоритма сортировки — процедура [math]\mathrm
Переменная [math] v [/math] сохраняет значение разделяющего элемента [math] a[(l + r) / 2] [/math] , a [math] i [/math] и [math] j [/math] представляет собой, соответственно, указатели левого и правого просмотра. Цикл разделения увеличивает значение [math] i [/math] и уменьшает значение [math] j [/math] на [math] 1 [/math] , причем условие, что ни один элемент слева от [math] i [/math] не больше [math] v [/math] и ни один элемент справа от [math] j [/math] не меньше [math] v [/math] , не нарушается. Как только значения указателей пересекаются, процедура разбиения завершается.
Асимптотика [ править ]
Худшее время работы [ править ]
Предположим, что мы разбиваем массив так, что одна часть содержит [math]n — 1[/math] элементов, а вторая — [math]1[/math] . Поскольку процедура разбиения занимает время [math]\Theta(n)[/math] , для времени работы [math]T(n)[/math] получаем соотношение:
[math]T(n) = T(n — 1) + \Theta(n) = \sum\limits_
Мы видим, что при максимально несбалансированном разбиении время работы составляет [math]\Theta(n^2)[/math] . В частности, это происходит, если массив изначально отсортирован.
Способ построить массив с максимальным количеством сравнений при выборе среднего элемента в качестве опорного [ править ]
В некоторых алгоритмах быстрой сортировки в качестве опорного выбирается элемент, который стоит в середине рассматриваемого массива. Рассмотрим массив, на котором быстрая сортировка с выбором среднего элемента в качестве опорного сделает [math]\Theta(n^2)[/math] сравнений. Очевидно, что это будет достигаться при худшем случае (когда при каждом разбиении в одном массиве будет оказываться [math]1[/math] , а в другом [math] n — 1 [/math] элемент).
Заполним сначала массив [math]a[/math] длины [math]n[/math] элементами от [math]1[/math] до [math] n [/math] , затем применим следующий алгоритм (нумерация с нуля):
Тогда на каждом шаге в качестве среднего элемента будет ставиться самый крупный элемент.
При выполнении [math]\mathrm
Рассмотрим, какой элемент будет выбираться опорным на каждом шаге. [math]\mathrm
Разбиение массива будет произведено [math]\Theta(n)[/math] раз, потому что разбиение производится на массивы длины [math]1[/math] и [math] n — 1 [/math] из-за того, что на каждом шаге разбиения в качестве опорного будет выбираться самый крупный элемент (оценка на худшее время работы доказана выше). Следовательно, на массиве, который строится описанным выше способом, выполняется [math]\Theta(n)[/math] [math]\mathrm
Способ построить массив с максимальным количеством сравнений при детерминированном выборе опорного элемента [ править ]
Рассмотрим алгоритм построения массива, на котором быстрая сортировка с детерминированным выбором опорного элемента будет делать максимальное (в данном случае — [math]\Theta(n^2)[/math] ) количество сравнений. Такое число сравнений достигается при разбиении на массивы длиной [math]1[/math] и [math]n-1[/math] на каждой итерации. Создадим массив [math]a[/math] длины [math]n[/math] , заполненный элементами типа [math]pair[/math] . Такой элемент хранит пару значений [math](val, key)[/math] , где [math]val[/math] — элемент массива, а [math]key[/math] — индекс. Изначально [math]a[i][/math] элемент имеет вид [math](0, i)[/math] .
Далее, запустим для данного массива алгоритм быстрой сортировки. Сравниваем два элемента типа [math]pair[/math] по их значениям [math]val[/math] . На каждом шаге будем выполнять следующие действия: при обращении к [math]i[/math] -ому элементу в качестве опорного на шаге под номером [math]k[/math] , присвоим [math]val = n-k+1[/math] для элемента [math]a[i][/math] . Затем выполним шаг сортировки. После завершения работы алгоритма быстрой сортировки, дополнительно отсортируем получившиеся элементы [math]pair[/math] по значениям [math]key[/math] . Искомым будет являться массив элементов [math]val[/math] в соответствующей последовательности.
Пример для [math]n = 4[/math] , при последовательном выборе опорных элементов [math]2, 2, 1, 1[/math] .
Построение массива | |||||||
---|---|---|---|---|---|---|---|
Шаг 1.0 | Шаг 1.1 | Шаг 1.2 | Шаг 2.0 | Шаг 2.1 | Шаг 2.2 | Шаг 3.0 | |
1 2 3 4 0 0 0 0 | 1 2 3 4 0 4 0 0 | 1 4 3 2 0 0 0 4 | 1 4 3 2 0 0 0 4 | 1 4 3 2 0 3 0 4 | 1 3 4 2 0 0 3 4 | 1 3 4 2 0 0 3 4 | |
Шаг 3.1 | Шаг 3.2 | Шаг 4.0 | Шаг 4.1 | Шаг 4.2 | Результат | ||
1 3 4 2 2 0 3 4 | 3 1 4 2 0 2 3 4 | 3 1 4 2 0 2 3 4 | 3 1 4 2 1 2 3 4 | 3 1 4 2 1 2 3 4 | 1 2 3 4 2 4 1 3 | ||
Итоговый массив | |||||||
Доказательство: | |||||||
[math]\triangleright[/math] | |||||||
Пусть [math]X[/math] — полное количество сравнений элементов с опорным за время работы сортировки. Нам необходимо вычислить полное количество сравнений. Переименуем элементы массива как [math]z_1 \ldots z_n[/math] , где [math]z_i[/math] наименьший по порядку элемент. Также введем множество [math]Z_ Заметим, что сравнение каждой пары элементов происходит не больше одного раза, так как элемент сравнивается с опорным, а опорный элемент после разбиения больше не будет участвовать в сравнении. Поскольку каждая пара элементов сравнивается не более одного раза, полное количество сравнений выражается как [math]X = \sum\limits_^ Применим к обеим частям равенства операцию вычисления матожидания и воспользовавшись ее линейностью получим Осталось вычислить величину [math]Pr\ [math] E[X] = \sum\limits_^ | |||||||
[math]\triangleleft[/math] |
Mатожидание времени работы быстрой сортировки будет [math]O(n \log n)[/math] .
Модификации [ править ]
Нерекурсивная реализация быстрой сортировки [ править ]
Для выполнения быстрой сортировки можно воспользоваться стеком, в котором в виде сортируемых подмассивов содержится перечень действий, которые предстоит выполнить. Каждый раз когда возникает необходимость в обработке подмассива, он выталкивается из стека. После разделения массива получаются два подмассива, требующих дальнейшей обработки, которые и заталкиваются в стек. Представленная ниже нерекурсивная реализация использует стек, заменяя рекурсивные вызовы помещением в стек параметров функции, а вызовы процедур и выходы из них — циклом, который осуществляет выборку параметров из стека и их обработку, пока стек не пуст. Мы помещаем больший из двух подмассивов в стек первым с тем, чтобы максимальная глубина стека при сортировке [math]N[/math] элементов не превосходила величины [math]\log n[/math] .
В качестве альтернативного варианта можно использовать обычную рекурсивную версию, в которой вместо того, чтобы после разделения массива вызывать рекурсивно процедуру разделения для обоих найденных подмассивов, рекурсивный вызов делается только для меньшего подмассива, а больший обрабатывается в цикле в пределах этого же вызова процедуры. С точки зрения эффективности в среднем случае разницы практически нет: накладные расходы на дополнительный рекурсивный вызов и на организацию сравнения длин подмассивов и цикла — примерно одного порядка. Зато глубина рекурсии ни при каких обстоятельствах не превысит [math]\log n[/math] , а в худшем случае вырожденного разделения она вообще будет не более [math]1[/math] — вся обработка пройдёт в цикле первого уровня рекурсии.
Улучшенная быстрая сортировка [ править ]
Выбор медианы из первого, среднего и последнего элементов в качестве разделяющего элемента и отсечение рекурсии меньших подмассивов может привести к существенному повышению эффективности быстрой сортировки. Функция [math]\mathrm
Вообще, можно применять любые эвристики по выбору опорного элемента. Например, в стандартной реализации в Java в качестве разделяющего выбирается средний из 7 элементов, равномерно распределённых по массиву.
Быстрая сортировка с разделением на три части [ править ]
Когда в сортируемом массиве имеется множество повторяющихся ключей предыдущие реализации быстрой сортировки можно существенно улучшить. Например массив, который состоит из равных ключей, вовсе не нуждается в дальнейшей сортировке, однако предыдущие реализации продолжают процесс разделения, подвергая обработке все более мелкие подмассивы, независимо от того, насколько большим является исходный файл.
В основу программы положено разделение массива на три части: на элементы,меньшие разделяющего элемента [math] a[l] \ldots a[i][/math] , элементы, равные разделяющему элементу [math]a[i+1] \ldots a[j-1][/math] , и элементы большие разделяющего элемента [math]a[j] \ldots a[r][/math] . После этого сортировка завершается двумя рекурсивными вызовами.
Элементы массива равные разделяющему элементу находятся между [math] l [/math] и [math] p [/math] и между [math] q [/math] и [math] r [/math] . В разделяющем цикле, когда указатели просмотра перестают изменяться и выполняется обмен значениями [math] i [/math] и [math] j [/math] , каждый из этих элементов проверяется на предмет равенства разделяющему элементу. Если элемент, который сейчас находится слева, равен разделяющему элементу, то при помощи операции обмена он помещается в левую часть массива, если элемент, который сейчас находится справа, равен разделяющему элементу, то в результате операции обмена он помещается в правую часть массива. После того как указатели пересекутся, элементы, равные разделяющему элементу и находящиеся на разных концах массива, после операции обмена попадают в свои окончательные позиции. После этого указанные ключи могут быть исключены из подмассивов, для которых выполняются последующие рекурсивные вызовы.
Параллельная сортировка [ править ]
Еще одной оптимизацией является параллельная сортировка на основе быстрой. Пусть, исходный набор данных расположен на первом процессоре, с него начинается работа алгоритма. Затем исходный массив окажется разделенным на две части, меньшая из которых передастся другому свободному процессору, большая останется на исходном для дальнейшей обработки. Далее обе части опять будут разделены и опять на двух исходных останутся большие части, а меньшие отправятся другим процессорам. В этом заключается ускорение алгоритма. При задействовании всех процессоров, все части параллельно будут сортироваться последовательным алгоритмом.
Introsort [ править ]
Для предотвращения ухудшения времени работы быстрой сортировки до [math]O(n^2)[/math] при неудачных входных данных, также можно использовать алгоритм сортировки Introsort. Он использует быструю сортировку и переключается на пирамидальную сортировку, когда глубина рекурсии превысит некоторый заранее установленный уровень (например, логарифм от числа сортируемых элементов). Так как после нескольких итераций быстрой сортировки с применением разных эвристик массив с большей вероятностью окажется «почти отсортированным», то пирамидальная сортировка может довольно быстро закончить дело. Также, пирамидальная сортировка хороша тем, что требует [math]O(1)[/math] дополнительной памяти, в отличие от, например, сортировки слиянием, где потребуется [math]O(n)[/math] дополнительной памяти.
Источник