Быстрая сортировка
Быстрая сортировка (англ. quick sort, сортировка Хоара) — один из самых известных и широко используемых алгоритмов сортировки. Среднее время работы
, что является асимптотически оптимальным временем работы для алгоритма, основанного на сравнении. Хотя время работы алгоритма для массива из элементов в худшем случае может составить , на практике этот алгоритм является одним из самых быстрых.Алгоритм
- из массива выбирается некоторый опорный элемент .
- запускается процедура разделения массива, которая перемещает все ключи, меньшие, либо равные , влево от него, а все ключи, большие, либо равные — вправо.
- для обоих подмассивов: если в подмассиве более двух элементов, рекурсивно запускаем для него ту же процедуру..
Псевдокод
function quicksort(A: int[n], int l, int r): if l < r q = partition(A, l, r) quicksort(A, l, q - 1) quicksort(A, q + 1, r)
Для сортировки всего массива необходимо выполнить процедуру
.Разбиение массива
Основной шаг алгоритма сортировки — процедура
, которая переставляет элементы массива нужным образом:int partition(A: int[n], int l, int r): x = A[l] i = l j = r while true while A[i] < x i = i + 1 while A[j] > x j = j - 1 if i < j swap(A[i++], A[j--]) else return j
Асимптотика
Худшее время работы
Предположим, что мы разбиваем массив так, что одна часть содержит
элементов, а вторая — . Поскольку процедура разбиения занимает время , для времени работы получаем соотношение:
.
Мы видим, что при максимально несбалансированном разбиении время работы составляет
. В частности, это происходит, если массив изначально отсортирован.Способ построить массив с максимальным количеством сравнений при выборе среднего элемента в качестве опорного
В некоторых алгоритмах быстрой сортировки в качестве опорного выбирается элемент, который стоит в середине рассматриваемого массива. Рассмотрим массив, на котором быстрая сортировка с выбором среднего элемента в качестве опорного сделает
сравнений. Очевидно, что это будет достигаться при худшем случае (когда при каждом разбиении в одном массиве будет оказываться , а в другом элемент).Заполним сначала массив
длины элементами от до , затем применим следующий алгоритм (нумерация с нуля):function antiQsort(A: int[n]): for i = 0 to n - 1 swap(A[i], A[i / 2])
Тогда на каждом шаге в качестве среднего элемента будет ставиться самый крупный элемент.
При выполнении
делается сравнений из-за того, что с помощью индексов и мы проходим в лучшем случае элементов (если функция прекращает свою работу, как только индексы встречаются), в худшем случае элементов (если оба индекса полностью проходят массив). При каждом изменении индекса делается сравнение, значит, процедура делает сравнений с точностью до константы.Рассмотрим, какой элемент будет выбираться опорным на каждом шаге.
на каждом шаге меняет местами последний и центральный элементы, поэтому в центре оказывается самый крупный элемент. А делает абсолютно симметричные этой процедуре операции, но в другую сторону: меняет местами центральный элемент с последним, так что самый крупный элемент становится последним, а затем выполняет на массиве длины на один меньшей ту же операцию. Получается, что опорным всегда будет выбираться самый крупный элемент, так как на массиве любой длины будет выполнять операции, обратные . Фактически, — это , запущенная в другую сторону. Также стоит отметить, что процедура разбиения будет делать на каждом шаге только одну смену элементов местами. Сначала дойдет до середины массива, до опорного элемента, останется равным индексу последнего элемента. Затем произойдет и снова начнет увеличиваться, пока не дойдет до последнего элемента, опять не изменит свою позицию. Потом произойдет выход из .Разбиение массива будет произведено
раз, потому что разбиение производится на массивы длины и из-за того, что на каждом шаге разбиения в качестве опорного будет выбираться самый крупный элемент (оценка на худшее время работы доказана выше). Следовательно, на массиве, который строится описанным выше способом, выполняется и сравнений для каждого выполнения . Тогда быстрая сортировка выполнит сравнений для массива, построенного таким способом.Среднее время работы
Лемма: |
Время работы алгоритма быстрой сортировки равно . |
Доказательство: |
Пусть Х — полное количество сравнений элементов с опорным за время работы сортировки. Нам необходимо вычислить полное количество сравнений. Переименуем элементы массива как , где наименьший по порядку элемент. Также введем множество .Заметим, что сравнение каждой пары элементов происходит не больше одного раза, так как элемент сравнивается с опорным, а опорный элемент после разбиения больше не будет участвовать в сравнении. Поскольку каждая пара элементов сравнивается не более одного раза, полное количество сравнений выражается как , где если произошло сравнение и и , если сравнения не произошло. Применим к обоим частям равенства операцию вычисления матожидания и воспользовавшись ее линейностью получим сравнивается с Осталось вычислить величину сравнивается с — вероятность того, что сравнивается с . Поскольку предполагается, что все элементы в массиве различны, то при выборе в качестве опорного элемента впоследствии не будут сравниваться никакие и для которых . С другой стороны, если выбран в качестве опорного, то он будет сравниваться с каждым элементом кроме себя самого. Таким образом элементы и сравниваются тогда и только тогда когда первым в множестве опорным элементом был выбран один из них.сравнивается с первым опорным элементом был или первым опорным элементом был первым опорным элементом был |
Mатожидание времени работы быстрой сортировки будет
.Улучшения
В случае повторяющихся неудачных разбиений опорным элементом, глубина рекурсии может достичь
, а время работы алгоритма . Существуют различные способы разбиения массива, направленные против худшего случая:- При выборе опорного элемента из данного диапазона случайным образом худший случай становится очень маловероятным и ожидаемое время выполнения алгоритма сортировки — .
- Выбирать опорным элементом средний из трех (первого, среднего и последнего элементов).
- Разбивать массив не на две, а на три части.
Оптимизация глубины рекурсии до O(logn) в худшем случае
Во избежание достижения опасной глубины рекурсии в худшем случае (или при приближении к нему) возможна модификация алгоритма, устраняющая одну ветвь рекурсии: вместо того, чтобы после разделения массива вызывать рекурсивно процедуру разделения для обоих найденных подмассивов, рекурсивный вызов делается только для меньшего подмассива, а больший обрабатывается в цикле в пределах этого же вызова процедуры. С точки зрения эффективности в среднем случае разницы практически нет: накладные расходы на дополнительный рекурсивный вызов и на организацию сравнения длин подмассивов и цикла — примерно одного порядка. Зато глубина рекурсии ни при каких обстоятельствах не превысит
, а в худшем случае вырожденного разделения она вообще будет не более 2 — вся обработка пройдёт в цикле первого уровня рекурсии.Источники информации
- Википедия — быстрая сортировка
- Wikipedia — Quicksort
- Т. Кормен, Ч. Лейзерсон, Р. Ривест: Алгоритмы: построение и анализ глава 7