Smoothsort — различия между версиями
Novik (обсуждение | вклад) м (→Восстановление свойств последовательности) |
м (rollbackEdits.php mass rollback) |
||
(не показаны 54 промежуточные версии 5 участников) | |||
Строка 1: | Строка 1: | ||
− | '''Плавная сортировка''' (англ. Smooth sort) {{---}} алгоритм сортировки, модификация [[Сортировка кучей|сортировки кучей]], разработанный Э. Дейкстрой. Как и пирамидальная сортировка, | + | '''Плавная сортировка''' (англ. Smooth sort) {{---}} алгоритм сортировки, модификация [[Сортировка кучей|сортировки кучей]], разработанный Э. Дейкстрой. Как и пирамидальная сортировка, в худшем случае работает за время <tex> \Theta(N\log{N}) </tex>. Преимущество плавной сортировки в том, что её время работы приближается к <tex dpi = 120> O(N) </tex>, если входные данные частично отсортированы, в то время как у сортировки кучей время работы не зависит от состояния входных данных. |
==Основная идея== | ==Основная идея== | ||
− | + | Разовьём идею пирамидальной сортировки. Для этого используем не [[Двоичная куча|двоичную кучу]], а специальную, полученную с помощью чисел Леонардо<ref>[https://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D0%BB%D0%B0_%D0%9B%D0%B5%D0%BE%D0%BD%D0%B0%D1%80%D0%B4%D0%BE Википедия {{---}} Числа Леонардо]</ref>, которые задаются следующим образом: | |
<tex> | <tex> | ||
− | L(n) | + | L(n) = |
\begin{cases} | \begin{cases} | ||
− | 1 & | + | 1 & \mathrm{if}\ n = 0, \\ |
− | 1 & | + | 1 & \mathrm{if}\ n = 1, \\ |
− | L(n-1)+L(n-2)+1 & | + | L(n-1)+L(n-2)+1 & \mathrm{if}\ n > 1. \\ |
\end{cases} | \end{cases} | ||
</tex> | </tex> | ||
Строка 17: | Строка 17: | ||
{{Утверждение | {{Утверждение | ||
− | |statement= Любое натуральное число | + | |statement= Любое натуральное число представимо в виде суммы <tex dpi = 120> O(\log{N}) </tex> различных чисел Леонардо. |
+ | }} | ||
+ | |||
+ | {{Утверждение | ||
+ | |statement= <tex dpi = 120> L(n) = 2 \cdot F(n + 1) - 1 </tex>, где <tex dpi = 120> F(n + 1) </tex> {{---}} <tex dpi 120> (n + 1) </tex>-ое число Фибоначчи. | ||
+ | |proof= Это утверждение доказывается по индукции. База: <tex dpi =120> L(0) = 2 \cdot F(1) - 1 = 1 </tex>. Пусть для <tex dpi = 120> n </tex> первых чисел это равенство выполняется. Делаем индуктивный переход: <tex dpi =120> L(n + 1) = L(n) + L(n - 1) + 1 = 2 \cdot F(n + 1) - 1 + 2 \cdot F(n) - 1 + 1 = 2 \cdot F(n + 2) - 1 </tex>. Утверждение доказано. | ||
}} | }} | ||
Строка 23: | Строка 28: | ||
|definition = | |definition = | ||
'''''K-ая куча Леонардо''''' — это двоичное дерево с количеством вершин <tex dpi = 120> L(k) </tex>, удовлетворяющее следующим условиям: | '''''K-ая куча Леонардо''''' — это двоичное дерево с количеством вершин <tex dpi = 120> L(k) </tex>, удовлетворяющее следующим условиям: | ||
− | * число, записанное в корне не меньше чисел в поддеревьях, | + | * число, записанное в корне, не меньше чисел в поддеревьях, |
* левым поддеревом является <tex dpi = 120> (k-1) </tex>-я куча Леонардо, | * левым поддеревом является <tex dpi = 120> (k-1) </tex>-я куча Леонардо, | ||
* правым — <tex dpi = 120> (k-2) </tex>-я куча Леонардо. | * правым — <tex dpi = 120> (k-2) </tex>-я куча Леонардо. | ||
}} | }} | ||
− | Можно заметить, что куча Леонардо очень похожа на | + | Можно заметить, что куча Леонардо очень похожа на [[Биномиальная куча|биномиальную]]. |
[[Файл:leonardo-heap.png|600px|thumb|right|Пример последовательности куч (список хранит номера чисел Леонардо, соответствующих размерам куч)]] | [[Файл:leonardo-heap.png|600px|thumb|right|Пример последовательности куч (список хранит номера чисел Леонардо, соответствующих размерам куч)]] | ||
− | + | Будем поддерживать следующий инвариант: | |
+ | * сортируемый массив делится на группу подмассивов, | ||
+ | * каждый подмассив представляет собой структуру данных куча, | ||
+ | * каждая куча имеет размер равный одному из чисел Леонардо, | ||
+ | * размеры куч строго убывают слева направо, | ||
+ | * при этом не существует двух куч, имеющих одинаковый размер, | ||
+ | * значения ключей в корнях деревьев идут в порядке возрастания слева направо, | ||
+ | * в самих кучах значение в детях меньше либо равно значению в родителе. | ||
+ | В дальнейшем эту группу подмассивов будем называть последовательностью куч. | ||
===Алгоритм:=== | ===Алгоритм:=== | ||
Строка 37: | Строка 50: | ||
'''Шаг 1:''' Превращение массива в последовательность куч. | '''Шаг 1:''' Превращение массива в последовательность куч. | ||
− | '''Шаг 2:''' Пока последовательность куч не пустая достаем максимальный элемент (это всегда корень самой правой кучи) и восстанавливаем порядок куч, который мог измениться. | + | '''Шаг 2:''' Пока последовательность куч не пустая, достаем максимальный элемент (это всегда корень самой правой кучи) и восстанавливаем порядок куч, который мог измениться. |
==Операции над последовательностью куч== | ==Операции над последовательностью куч== | ||
− | При конструировании последовательности куч будем | + | При конструировании последовательности куч будем по очереди вставлять в конец новые элементы, а при получении отсортированного массива {{---}} удалять максимальный элемент из последовательности. Следовательно, нам необходимы две операции: увеличение последовательности куч путём добавления элемента справа (будем считать, что в начале последовательности располагаются кучи максимального размера) и уменьшение путём удаления крайнего правого элемента (корня последней кучи), с сохранением состояния кучи и последовательности. |
− | Чтобы быстро обращаться к кучам, будем хранить список их длин. | + | |
+ | Чтобы быстро обращаться к кучам, будем хранить список их длин. Зная индекс корня некоторой кучи и её длину, можно найти индекс корня кучи слева от неё. Чтобы искать индексы детей вершины, надо воспользоваться свойством кучи Леонардо, что левым поддеревом является <tex dpi = 120> (n - 1) </tex>-ая, а правым является <tex dpi = 120> (n - 2) </tex>-ая куча Леонардо. Для хранения списка длин куч придется выделить <tex dpi = 120> O(\log{N}) </tex> дополнительной памяти. | ||
===Вставка элемента=== | ===Вставка элемента=== | ||
[[Файл:add-example.png|470px|thumb|right|Пример вставки элемента (без просеивания вниз)]] | [[Файл:add-example.png|470px|thumb|right|Пример вставки элемента (без просеивания вниз)]] | ||
+ | [[Файл:Leonardo-heap-2.png|470px|thumb|right|Вставка в последовательность куч, показанную выше, числа 13. Далее будет сразу происходить просеивание внутри "зеленого" дерева Леонардо, так как корень соседнего дерева меньше, чем дети корня "зелёного" дерева.]] | ||
При добавлении в последовательность нового элемента возможны две ситуации: | При добавлении в последовательность нового элемента возможны две ситуации: | ||
* Если две последние кучи имеют размеры <tex dpi = 120> L(x + 1) </tex> и <tex dpi = 120> L(x) </tex> (двух последовательных чисел Леонардо), новый элемент становится корнем кучи большего размера, равного <tex dpi = 120> L(x+2) </tex>. Для неё свойство кучи необязательно. | * Если две последние кучи имеют размеры <tex dpi = 120> L(x + 1) </tex> и <tex dpi = 120> L(x) </tex> (двух последовательных чисел Леонардо), новый элемент становится корнем кучи большего размера, равного <tex dpi = 120> L(x+2) </tex>. Для неё свойство кучи необязательно. | ||
* Если размеры двух последних куч не равны двум последовательным числам Леонардо, новый элемент образует новую кучу размером <tex dpi = 120> 1 </tex>. Этот размер полагается равным <tex dpi = 120> L(1) </tex>, кроме случая, когда крайняя правая куча уже имеет размер <tex dpi = 120> L(1) </tex>, тогда размер новой одноэлементной кучи полагают равным <tex dpi = 120> L(0) </tex>. | * Если размеры двух последних куч не равны двум последовательным числам Леонардо, новый элемент образует новую кучу размером <tex dpi = 120> 1 </tex>. Этот размер полагается равным <tex dpi = 120> L(1) </tex>, кроме случая, когда крайняя правая куча уже имеет размер <tex dpi = 120> L(1) </tex>, тогда размер новой одноэлементной кучи полагают равным <tex dpi = 120> L(0) </tex>. | ||
− | После этого | + | После этого необходимо восстановить свойства кучи и последовательности куч, что, как правило, достигается при помощи разновидности [[Сортировка вставками|сортировки вставками]] (см. ниже псевдокод): |
# Крайняя правая куча (сформированная последней) считается «текущей» кучей. | # Крайняя правая куча (сформированная последней) считается «текущей» кучей. | ||
# Пока слева от неё есть куча, и значение её корня больше значения текущего корня и обоих корней куч-потомков: | # Пока слева от неё есть куча, и значение её корня больше значения текущего корня и обоих корней куч-потомков: | ||
− | #* Меняются местами новый корень и корень кучи слева ( | + | #* Меняются местами новый корень и корень кучи слева (это гарантирует выполнение инварианта для текущей кучи). И куча, с которой произошел обмен, становится текущей. |
− | # | + | # Потом выполняется «просеивание» кучи, на которой остановилась сортировка корней, чтобы гарантировать выполнение инварианта кучи: |
#* Пока размер текущей кучи больше <tex dpi = 120> 1 </tex>, и значение корня любой из куч-потомков больше значения корня текущей кучи: | #* Пока размер текущей кучи больше <tex dpi = 120> 1 </tex>, и значение корня любой из куч-потомков больше значения корня текущей кучи: | ||
#** Меняются местами наибольший по значению корень кучи-потомка и текущий корень. Куча-потомок становится текущей кучей. | #** Меняются местами наибольший по значению корень кучи-потомка и текущий корень. Куча-потомок становится текущей кучей. | ||
− | + | Просеивание в куче Леонардо сильно упрощено, так как каждая куча будет иметь либо двух потомков, либо ноль. Нет нужды беспокоиться об отсутствии одной из куч-потомков. | |
− | Так как в последовательности <tex dpi = 120> O(\log{N}) </tex> куч | + | Так как в последовательности <tex dpi = 120> O(\log{N}) </tex> куч, то модификация сортировки вставками будет работать за <tex dpi = 120> O(\log{N}) </tex>. Просеивание тоже выполняется за <tex dpi = 120> O(\log{N}) </tex>, тогда в итоге операция вставки выполняется за: |
<tex dpi = 120> O(\log{N}) + O(\log{N}) = O(\log{N}) </tex>. | <tex dpi = 120> O(\log{N}) + O(\log{N}) = O(\log{N}) </tex>. | ||
− | ==== | + | === Уменьшение последовательности куч путём удаления элемента справа === |
+ | Если размер крайней правой кучи равен <tex dpi = 120> 1 </tex> (то есть <tex dpi = 120> L(1) </tex> или <tex dpi = 120> L(0) </tex>), эта куча просто удаляется. | ||
+ | В противном случае корень этой кучи удаляется, кучи-потомки считаются элементами последовательности куч, после чего проверяется выполнение свойства последовательности куч (т.е. корни деревьев идут в порядке возрастания слева направо), сначала для левой кучи, затем — для правой. | ||
+ | |||
+ | Так как в последовательности <tex dpi = 120> O(\log{N}) </tex> куч, то восстановление свойства последовательности выполняется за <tex dpi = 120> O(\log{N}) </tex>. | ||
− | Пусть нам надо восстановить инвариант последовательности куч. Будем считать, что функции | + | ===Восстановление свойств последовательности=== |
+ | |||
+ | Пусть нам надо восстановить инвариант последовательности куч. Будем считать, что функции <tex>\mathrm{prev}</tex> (возвращает индекс корня ближайшей слева кучи), <tex>\mathrm{left}</tex> (возвращает индекс левого сына), <tex>\mathrm{right}</tex> (возвращает индекс правого сына) уже реализованы. В функцию <tex>\mathrm{ensureSequence}</tex> передается индекс корня кучи, с которой начинаем восстановление. | ||
<code> | <code> | ||
'''function''' ensureSequence(i: '''int'''): | '''function''' ensureSequence(i: '''int'''): | ||
Строка 74: | Строка 95: | ||
</code> | </code> | ||
− | === | + | ==Сложность== |
− | |||
− | |||
− | + | ===Построение последовательности=== | |
+ | Последовательность куч получается при вставке элементов массива по очереди в эту самую последовательность. Получаем время работы <tex dpi = 120> O(N \log{N}) </tex>. | ||
− | == | + | ===Получение отсортированного массива=== |
− | + | Так как удаление максимального элемента из последовательности выполняется за <tex dpi = 120> O(\log{N}) </tex>, то время работы сортировки составляет <tex dpi = 120> O(N\log{N}) </tex>. | |
− | Однако если подать на вход плавной сортировке уже отсортированный массив, асимптотика будет составлять <tex dpi = 120> O(N) </tex>. Дело в том, что | + | ===Лучший случай=== |
+ | Однако если подать на вход плавной сортировке уже отсортированный массив, асимптотика будет составлять <tex dpi = 120> O(N) </tex>. Дело в том, что: | ||
+ | *Операция добавления элемента последовательности на таком примере будет выполняться за <tex dpi = 120> O(1) </tex>, из-за того, что в конец будет добавляться максимальный элемент и просеивание будет сразу останавливаться. | ||
+ | *Операция получения и удаления максимального элемента будет также выполняться за <tex dpi = 120> O(1) </tex>, потому что в силу построения в корнях куч-детей будут новые максимальные элементы и следовательно восстановление свойства последовательности закончится на просмотре корня соседней кучи. | ||
+ | В итоге на таком примере получается асимптотика <tex dpi = 120> O(N) </tex>. | ||
===Достоинства=== | ===Достоинства=== | ||
− | * худшее время работы -- <tex dpi = 120> O(N\log{N}) </tex>, | + | * худшее время работы {{---}} <tex dpi = 120> O(N\log{N}) </tex>, |
− | * время работы в случае, когда подается отсортированный массив -- <tex dpi = 120> O(N) </tex>. | + | * время работы в случае, когда подается отсортированный массив {{---}} <tex dpi = 120> O(N) </tex>. |
===Недостатки=== | ===Недостатки=== | ||
* не является устойчивой, | * не является устойчивой, | ||
− | * требует <tex dpi = 120> O(\log{N}) </tex> дополнительной памяти для хранения длин куч в последовательности. | + | * требует <tex dpi = 120> O(\log{N}) </tex> дополнительной памяти для хранения длин куч в последовательности. Однако с помощью некоторых модификаций расходы на дополнительную память можно сократить до <tex dpi 120> O(1) </tex>. |
− | |||
− | |||
− | |||
− | == | + | ===Связь с быстрой сортировкой=== |
− | + | На практике, когда реализуют алгоритм быстрой сортировки, пытаются улучшить асимптотику в худшем случае. Для этого заводится некоторый лимит глубины рекурсии, при превышении которого запускают другую сортировку. Так реализована сортировка в стандартной библиотеке языка С++. Часто при превышении порога глубины рекурсии используют сортировку кучей. Замена неё на плавную сортировку могла бы улучшить время работы на некоторых тестах, так как после нескольких итераций быстрой сортировки массив окажется почти отсортированным, а на таких массивах время работы плавной сортировки приближается к линейному. Хотя итоговой линейной асимптотики достичь всё равно не получится по [[Теорема о нижней оценке для сортировки сравнениями | теореме о нижней оценке]]. | |
==См. также== | ==См. также== | ||
* [[Сортировка кучей|Сортировка кучей]] | * [[Сортировка кучей|Сортировка кучей]] | ||
* [[Быстрая сортировка|Быстрая сортировка]] | * [[Быстрая сортировка|Быстрая сортировка]] | ||
+ | |||
+ | ==Примечания== | ||
+ | |||
+ | <references /> | ||
==Источники информации== | ==Источники информации== | ||
− | * [https://en.wikipedia.org/wiki/Smoothsort Wikipedia -- Smoothsort] | + | * [https://en.wikipedia.org/wiki/Smoothsort Wikipedia {{---}} Smoothsort] |
* [http://www.keithschwarz.com/smoothsort/ Smoothsort Demystified] | * [http://www.keithschwarz.com/smoothsort/ Smoothsort Demystified] | ||
− | * [http://habrahabr.ru/post/133996/ Хабрахабр -- И снова про сортировки] | + | * [http://habrahabr.ru/post/133996/ Хабрахабр {{---}} И снова про сортировки] |
[[Категория: Дискретная математика и алгоритмы]] | [[Категория: Дискретная математика и алгоритмы]] | ||
− | [[Категория: Сортировки]] | + | [[Категория: Сортировка]] |
+ | [[Категория: Сортировки на сравнениях]] |
Текущая версия на 19:26, 4 сентября 2022
Плавная сортировка (англ. Smooth sort) — алгоритм сортировки, модификация сортировки кучей, разработанный Э. Дейкстрой. Как и пирамидальная сортировка, в худшем случае работает за время . Преимущество плавной сортировки в том, что её время работы приближается к , если входные данные частично отсортированы, в то время как у сортировки кучей время работы не зависит от состояния входных данных.
Содержание
Основная идея
Разовьём идею пирамидальной сортировки. Для этого используем не двоичную кучу, а специальную, полученную с помощью чисел Леонардо[1], которые задаются следующим образом:
Вот первые несколько членов этой последовательности:
Утверждение: |
Любое натуральное число представимо в виде суммы различных чисел Леонардо. |
Утверждение: |
, где — -ое число Фибоначчи. |
Это утверждение доказывается по индукции. База: | . Пусть для первых чисел это равенство выполняется. Делаем индуктивный переход: . Утверждение доказано.
Определение: |
K-ая куча Леонардо — это двоичное дерево с количеством вершин
| , удовлетворяющее следующим условиям:
Можно заметить, что куча Леонардо очень похожа на биномиальную.
Будем поддерживать следующий инвариант:
- сортируемый массив делится на группу подмассивов,
- каждый подмассив представляет собой структуру данных куча,
- каждая куча имеет размер равный одному из чисел Леонардо,
- размеры куч строго убывают слева направо,
- при этом не существует двух куч, имеющих одинаковый размер,
- значения ключей в корнях деревьев идут в порядке возрастания слева направо,
- в самих кучах значение в детях меньше либо равно значению в родителе.
В дальнейшем эту группу подмассивов будем называть последовательностью куч.
Алгоритм:
Шаг 0: В массиве записаны элементы, которые надо отсортировать.
Шаг 1: Превращение массива в последовательность куч.
Шаг 2: Пока последовательность куч не пустая, достаем максимальный элемент (это всегда корень самой правой кучи) и восстанавливаем порядок куч, который мог измениться.
Операции над последовательностью куч
При конструировании последовательности куч будем по очереди вставлять в конец новые элементы, а при получении отсортированного массива — удалять максимальный элемент из последовательности. Следовательно, нам необходимы две операции: увеличение последовательности куч путём добавления элемента справа (будем считать, что в начале последовательности располагаются кучи максимального размера) и уменьшение путём удаления крайнего правого элемента (корня последней кучи), с сохранением состояния кучи и последовательности.
Чтобы быстро обращаться к кучам, будем хранить список их длин. Зная индекс корня некоторой кучи и её длину, можно найти индекс корня кучи слева от неё. Чтобы искать индексы детей вершины, надо воспользоваться свойством кучи Леонардо, что левым поддеревом является
-ая, а правым является -ая куча Леонардо. Для хранения списка длин куч придется выделить дополнительной памяти.Вставка элемента
При добавлении в последовательность нового элемента возможны две ситуации:
- Если две последние кучи имеют размеры и (двух последовательных чисел Леонардо), новый элемент становится корнем кучи большего размера, равного . Для неё свойство кучи необязательно.
- Если размеры двух последних куч не равны двум последовательным числам Леонардо, новый элемент образует новую кучу размером . Этот размер полагается равным , кроме случая, когда крайняя правая куча уже имеет размер , тогда размер новой одноэлементной кучи полагают равным .
После этого необходимо восстановить свойства кучи и последовательности куч, что, как правило, достигается при помощи разновидности сортировки вставками (см. ниже псевдокод):
- Крайняя правая куча (сформированная последней) считается «текущей» кучей.
- Пока слева от неё есть куча, и значение её корня больше значения текущего корня и обоих корней куч-потомков:
- Меняются местами новый корень и корень кучи слева (это гарантирует выполнение инварианта для текущей кучи). И куча, с которой произошел обмен, становится текущей.
- Потом выполняется «просеивание» кучи, на которой остановилась сортировка корней, чтобы гарантировать выполнение инварианта кучи:
- Пока размер текущей кучи больше
- Меняются местами наибольший по значению корень кучи-потомка и текущий корень. Куча-потомок становится текущей кучей.
, и значение корня любой из куч-потомков больше значения корня текущей кучи:
- Пока размер текущей кучи больше
Просеивание в куче Леонардо сильно упрощено, так как каждая куча будет иметь либо двух потомков, либо ноль. Нет нужды беспокоиться об отсутствии одной из куч-потомков.
Так как в последовательности
куч, то модификация сортировки вставками будет работать за . Просеивание тоже выполняется за , тогда в итоге операция вставки выполняется за: .Уменьшение последовательности куч путём удаления элемента справа
Если размер крайней правой кучи равен
(то есть или ), эта куча просто удаляется. В противном случае корень этой кучи удаляется, кучи-потомки считаются элементами последовательности куч, после чего проверяется выполнение свойства последовательности куч (т.е. корни деревьев идут в порядке возрастания слева направо), сначала для левой кучи, затем — для правой.Так как в последовательности
куч, то восстановление свойства последовательности выполняется за .Восстановление свойств последовательности
Пусть нам надо восстановить инвариант последовательности куч. Будем считать, что функции
function ensureSequence(i: int): j = prev(i) // j - индекс корня соседней кучи while A[j] > A[i] and A[j] > A[left(i)] and A[j] > A[right(i)] swap(A[j], A[i]) i = j j = prev(i) siftDown(i)
Сложность
Построение последовательности
Последовательность куч получается при вставке элементов массива по очереди в эту самую последовательность. Получаем время работы
.Получение отсортированного массива
Так как удаление максимального элемента из последовательности выполняется за
, то время работы сортировки составляет .Лучший случай
Однако если подать на вход плавной сортировке уже отсортированный массив, асимптотика будет составлять
. Дело в том, что:- Операция добавления элемента последовательности на таком примере будет выполняться за , из-за того, что в конец будет добавляться максимальный элемент и просеивание будет сразу останавливаться.
- Операция получения и удаления максимального элемента будет также выполняться за , потому что в силу построения в корнях куч-детей будут новые максимальные элементы и следовательно восстановление свойства последовательности закончится на просмотре корня соседней кучи.
В итоге на таком примере получается асимптотика
.Достоинства
- худшее время работы — ,
- время работы в случае, когда подается отсортированный массив — .
Недостатки
- не является устойчивой,
- требует дополнительной памяти для хранения длин куч в последовательности. Однако с помощью некоторых модификаций расходы на дополнительную память можно сократить до .
Связь с быстрой сортировкой
На практике, когда реализуют алгоритм быстрой сортировки, пытаются улучшить асимптотику в худшем случае. Для этого заводится некоторый лимит глубины рекурсии, при превышении которого запускают другую сортировку. Так реализована сортировка в стандартной библиотеке языка С++. Часто при превышении порога глубины рекурсии используют сортировку кучей. Замена неё на плавную сортировку могла бы улучшить время работы на некоторых тестах, так как после нескольких итераций быстрой сортировки массив окажется почти отсортированным, а на таких массивах время работы плавной сортировки приближается к линейному. Хотя итоговой линейной асимптотики достичь всё равно не получится по теореме о нижней оценке.