Smoothsort — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Основная идея)
Строка 1: Строка 1:
'''Плавная сортировка''' (англ. Smooth sort) -- алгоритм сортировки, разновидность [[Сортировка кучей|сортировки кучей]], разработанная Э. Дейкстрой в 1981 году. Как и пирамидальная сортировка, имеет сложность в худшем случае равную <tex dpi = 120> O(N log N) </tex>. Преимущество плавной сортировки в том, что её сложность приближается к <tex dpi = 120> O(N) </tex>, если входные данные частично отсортированы, в то время как у сортировки кучей сложность всегда одна, независимо от состояния входных данных.
+
'''Плавная сортировка''' (англ. Smooth sort) {{---}} алгоритм сортировки, модификация [[Сортировка кучей|сортировки кучей]], разработанный Э. Дейкстрой. Как и пирамидальная сортировка, имеет сложность в худшем случае равную <tex dpi = 120> O(N\log{N}) </tex>. Преимущество плавной сортировки в том, что её сложность приближается к <tex dpi = 120> O(N) </tex>, если входные данные частично отсортированы, в то время как у сортировки кучей сложность всегда одна, независимо от состояния входных данных.
  
 
==Основная идея==
 
==Основная идея==
Как и в пирамидальной сортировке, в массив накапливается куча из данных, которые затем сортируются путём непрерывного удаления максимума из кучи. В отличие от пирамидальной сортировки, здесь используется не двоичная куча, а специальная, полученная с помощью чисел Леонардо:
+
Будем развивать идею пирамидальной сортировки. Для этого будем использовать не двоичную кучу, а специальную, полученную с помощью чисел Леонардо, которые задаются следующим образом:
  
<tex dpi = 120> L(0) = 1, L(1) = 1, L(i) = L(i - 1) + L(i - 2) + 1 </tex>
+
<tex>  
 +
  L(n):=
 +
  \begin{cases}
 +
    1               &  :n = 0, \\
 +
    1               &  :n = 1, \\
 +
    L(n-1)+L(n-2)+1 &  :n > 1. \\
 +
  \end{cases}
 +
</tex>
  
 
Вот первые несколько членов этой последовательности:
 
Вот первые несколько членов этой последовательности:
Строка 10: Строка 17:
  
 
{{Утверждение
 
{{Утверждение
|statement= Любое натуральное число представить суммой из <tex dpi = 120> O(log N) </tex> различных чисел Леонардо.
+
|statement= Любое натуральное число можно представить суммой из <tex dpi = 120> O(\log{N}) </tex> различных чисел Леонардо.
 
}}
 
}}
  
Строка 16: Строка 23:
 
|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|Пример последовательности куч (список хранит номера чисел Леонардо, соответствующих размерам куч)]]
 +
Сортируемый массив делится на группу подмассивов. Каждый подмассив представляет собой структуру данных куча. Каждая куча имеет размер равный одному из чисел Леонардо. Размеры куч строго убывают слева направо. При этом не существует двух куч, имеющих одинаковый размер. В дальнейшем эту группу подмассивов будем называть последовательность куч. Будем поддерживать инвариант последовательности: корни деревьев идут в порядке возрастания слева направо, и инвариант куч: значение в детях меньше либо равно значению в родителе.  
  
При этом не существует двух куч, имеющих одинаковый размер. Так же никакие две кучи не будут иметь размер равным двум последовательным числам Леонардо. Исключением могут быть только две последние кучи.
+
===Алгоритм:===
Если мы будем использовать подряд две кучи размерностью <tex dpi = 120> L(x) </tex> и <tex dpi = 120> L(x+1) </tex>, то их можно будет заменить одной – размерностью <tex dpi = 120> L(x+2) </tex>.
+
'''Шаг 0:''' В массиве записаны элементы, которые надо отсортировать
  
'''Алгоритм состоит из двух стадий:'''
+
'''Шаг 1:''' Превращение массива в последовательность куч
* Конструирование последовательности куч
+
 
* Пока последовательность куч не пустая достаем максимальный элемент (это всегда корень самой правой кучи) и восстанавливаем порядок куч, который мог измениться.
+
'''Шаг 2:''' Пока последовательность куч не пустая достаем максимальный элемент (это всегда корень самой правой кучи) и восстанавливаем порядок куч, который мог измениться.
  
 
==Операции над последовательностью куч==
 
==Операции над последовательностью куч==
Необходимы две операции: увеличение последовательности куч путём добавления элемента справа и уменьшение путём удаления крайнего правого элемента (корня последней кучи), с сохранением состояния кучи и последовательности куч.
+
При конструировании последовательности куч будем последовательно выполнять вставку в конец последовательности куч новых элементов, а при операции получении отсортированного массива будем удалять максимальный элемент из последовательности. Следовательно нам необходимы две операции: увеличение последовательности куч путём добавления элемента справа (будем считать, что последовательность начинается кучами самого большого размера) и уменьшение путём удаления крайнего правого элемента (корня последней кучи), с сохранением состояния кучи и последовательности куч.
 +
Чтобы быстро обращаться к кучам будем хранить список их длин.
 +
 
 
===Вставка элемента===
 
===Вставка элемента===
 +
[[Файл:add-example.png|470px|thumb|right|Пример вставки элемента (без просеивания вниз)]]
 +
При добавлении в последовательность нового элемента возможны две ситуации:
 +
 
* Если две последние кучи имеют размеры <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>. Для неё свойство кучи необязательно.
* Если размеры двух последних куч не равны двум последовательным числам Леонардо, новый элемент образует новую кучу размером 1. Этот размер полагается равным <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>.
После этого должно быть восстановлено выполнение свойств кучи и последовательности куч, что, как правило, достигается при помощи разновидности [[Сортировка вставками|сортировки вставками]]:
+
После этого должно быть восстановлено выполнение свойств кучи и последовательности куч, что, как правило, достигается при помощи разновидности [[Сортировка вставками|сортировки вставками]] (см. ниже псевдокод):
 
# Крайняя правая куча (сформированная последней) считается «текущей» кучей.
 
# Крайняя правая куча (сформированная последней) считается «текущей» кучей.
 
# Пока слева от неё есть куча и значение её корня больше значения текущего корня и обоих корней куч-потомков:
 
# Пока слева от неё есть куча и значение её корня больше значения текущего корня и обоих корней куч-потомков:
 
#* Меняются местами новый корень и корень кучи слева (что не нарушит свойство для текущей кучи). Эта куча становится текущей.
 
#* Меняются местами новый корень и корень кучи слева (что не нарушит свойство для текущей кучи). Эта куча становится текущей.
 
# Выполняется «просеивание», чтобы выполнялось свойство кучи:
 
# Выполняется «просеивание», чтобы выполнялось свойство кучи:
#* Пока размер текущей кучи больше 1 и значение корня любой из куч-потомков больше значения корня текущей кучи:
+
#* Пока размер текущей кучи больше <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>.
 +
 
 +
====Восстановление свойств последовательности====
 +
 
 +
Пусть нам надо восстановить инвариант последовательности куч. Будем считать, что функции '''''prev''''' (возвращает индекс корня ближайшей слева кучи), '''''left''''' (возвращает индекс левого сына), '''''right''''' (возвращает индекс правого сына) уже реализованы. В функцию '''''ensureSequence''''' передается индекс корня кучи, с которой начинаем восстановление.
 +
<code>
 +
'''function''' ensureSequence(i: '''int'''):
 +
  j = prev(i) <font color = "green">// j - индекс корня соседней кучи</font>
 +
  '''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)
 +
</code>
  
 
=== Уменьшение последовательности куч путём удаления элемента справа ===
 
=== Уменьшение последовательности куч путём удаления элемента справа ===
Если размер крайней правой кучи равен 1 (то есть <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(0) </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(N) </tex> раз выполняется вставка элемента. И потом ещё <tex dpi = 120> O(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(N\log{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) </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) </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(N) </tex> дополнительной памяти для хранения последовательности куч
+
* требует <tex dpi = 120> O(\log{N}) </tex> дополнительной памяти для хранения длин куч в последовательности.
 +
 
 +
==Примечание==
 +
*[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 Википедия -- Числа Леонардо]
 +
 
 +
==См. также==
 +
* [[Сортировка кучей|Сортировка кучей]]
 +
* [[Быстрая сортировка|Быстрая сортировка]]
  
 
==Источники информации==
 
==Источники информации==

Версия 02:33, 29 марта 2015

Плавная сортировка (англ. Smooth sort) — алгоритм сортировки, модификация сортировки кучей, разработанный Э. Дейкстрой. Как и пирамидальная сортировка, имеет сложность в худшем случае равную [math] O(N\log{N}) [/math]. Преимущество плавной сортировки в том, что её сложность приближается к [math] O(N) [/math], если входные данные частично отсортированы, в то время как у сортировки кучей сложность всегда одна, независимо от состояния входных данных.

Основная идея

Будем развивать идею пирамидальной сортировки. Для этого будем использовать не двоичную кучу, а специальную, полученную с помощью чисел Леонардо, которые задаются следующим образом:

[math] L(n):= \begin{cases} 1 & :n = 0, \\ 1 & :n = 1, \\ L(n-1)+L(n-2)+1 & :n \gt 1. \\ \end{cases} [/math]

Вот первые несколько членов этой последовательности: [math] 1, 1, 3, 5, 9, 15, 25, 41, ... [/math]

Утверждение:
Любое натуральное число можно представить суммой из [math] O(\log{N}) [/math] различных чисел Леонардо.


Определение:
K-ая куча Леонардо — это двоичное дерево с количеством вершин [math] L(k) [/math], удовлетворяющее следующим условиям:
  • число, записанное в корне не меньше чисел в поддеревьях,
  • левым поддеревом является [math] (k-1) [/math]-я куча Леонардо,
  • правым — [math] (k-2) [/math]-я куча Леонардо.

Можно заметить, что куча Леонардо очень похожа на Биномиальную кучу. Куча Леонардо используется из-за свойства, что у каждой вершины либо два, либо ноль детей.

Пример последовательности куч (список хранит номера чисел Леонардо, соответствующих размерам куч)

Сортируемый массив делится на группу подмассивов. Каждый подмассив представляет собой структуру данных куча. Каждая куча имеет размер равный одному из чисел Леонардо. Размеры куч строго убывают слева направо. При этом не существует двух куч, имеющих одинаковый размер. В дальнейшем эту группу подмассивов будем называть последовательность куч. Будем поддерживать инвариант последовательности: корни деревьев идут в порядке возрастания слева направо, и инвариант куч: значение в детях меньше либо равно значению в родителе.

Алгоритм:

Шаг 0: В массиве записаны элементы, которые надо отсортировать

Шаг 1: Превращение массива в последовательность куч

Шаг 2: Пока последовательность куч не пустая достаем максимальный элемент (это всегда корень самой правой кучи) и восстанавливаем порядок куч, который мог измениться.

Операции над последовательностью куч

При конструировании последовательности куч будем последовательно выполнять вставку в конец последовательности куч новых элементов, а при операции получении отсортированного массива будем удалять максимальный элемент из последовательности. Следовательно нам необходимы две операции: увеличение последовательности куч путём добавления элемента справа (будем считать, что последовательность начинается кучами самого большого размера) и уменьшение путём удаления крайнего правого элемента (корня последней кучи), с сохранением состояния кучи и последовательности куч. Чтобы быстро обращаться к кучам будем хранить список их длин.

Вставка элемента

Пример вставки элемента (без просеивания вниз)

При добавлении в последовательность нового элемента возможны две ситуации:

  • Если две последние кучи имеют размеры [math] L(x + 1) [/math] и [math] L(x) [/math] (двух последовательных чисел Леонардо), новый элемент становится корнем кучи большего размера, равного [math] L(x+2) [/math]. Для неё свойство кучи необязательно.
  • Если размеры двух последних куч не равны двум последовательным числам Леонардо, новый элемент образует новую кучу размером [math] 1 [/math]. Этот размер полагается равным [math] L(1) [/math], кроме случая, когда крайняя правая куча уже имеет размер [math] L(1) [/math], тогда размер новой одноэлементной кучи полагают равным [math] L(0) [/math].

После этого должно быть восстановлено выполнение свойств кучи и последовательности куч, что, как правило, достигается при помощи разновидности сортировки вставками (см. ниже псевдокод):

  1. Крайняя правая куча (сформированная последней) считается «текущей» кучей.
  2. Пока слева от неё есть куча и значение её корня больше значения текущего корня и обоих корней куч-потомков:
    • Меняются местами новый корень и корень кучи слева (что не нарушит свойство для текущей кучи). Эта куча становится текущей.
  3. Выполняется «просеивание», чтобы выполнялось свойство кучи:
    • Пока размер текущей кучи больше [math] 1 [/math] и значение корня любой из куч-потомков больше значения корня текущей кучи:
      • Меняются местами наибольший по значению корень кучи-потомка и текущий корень. Куча-потомок становится текущей кучей.

Операция просеивания значительно упрощена благодаря использованию чисел Леонардо, так как каждая куча либо будет одноэлементной, либо будет иметь двух потомков. Нет нужды беспокоиться об отсутствии одной из куч-потомков.

Так как в последовательности [math] O(\log{N}) [/math] куч и просеивание можно выполнить за [math] O(\log{N}) [/math], то время вставки равно: [math] O(\log{N}) + O(\log{N}) = O(\log{N}) [/math].

Восстановление свойств последовательности

Пусть нам надо восстановить инвариант последовательности куч. Будем считать, что функции prev (возвращает индекс корня ближайшей слева кучи), left (возвращает индекс левого сына), right (возвращает индекс правого сына) уже реализованы. В функцию ensureSequence передается индекс корня кучи, с которой начинаем восстановление.

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)

Уменьшение последовательности куч путём удаления элемента справа

Если размер крайней правой кучи равен [math] 1 [/math] (то есть [math] L(1) [/math] или [math] L(0) [/math]), эта куча просто удаляется. В противном случае корень этой кучи удаляется, кучи-потомки считаются элементами последовательности куч, после чего проверяется выполнение свойства последовательности куч (т.е. что корни деревьев идут в порядке возрастания слева направо), сначала для левой кучи, затем — для правой.

Так как в последовательности [math] O(\log{N}) [/math] куч, то восстановление свойства последовательности выполняется за [math] O(\log{N}) [/math].

Сложность

Во время построения последовательности куч [math] O(N) [/math] раз выполняется вставка элемента. И потом ещё [math] O(N) [/math] раз выполняется удаление элемента при процедуре получения отсортированного массива. Таким образом сложность плавной сортировки составляет [math] O(N\log{N}) [/math].

Однако если подать на вход плавной сортировке уже отсортированный массив асимптотика будет составлять [math] O(N) [/math]. Дело в том, что во время процедуры получения последовательности куч мы всегда будем вставлять элемент, который больше остальных уже находящихся в последовательности. Поэтому восстановление свойства последовательности будет выполняться за [math] O(1) [/math] (так как алгоритм только посмотрит на корень соседней кучи, а просеивание закончится сразу потому что новый элемент будет больше своих детей). Операция получения и удаления максимального элемента будет тоже выполняться за [math] O(1) [/math], потому что в силу построения в корнях куч-детей будут новые максимальные элементы и следовательно восстановление свойства последовательности закончится на просмотре корня соседней кучи. В итоге, так как алгоритм [math] O(N) [/math] раз вставляет, а потом и удаляет элементы, получается асимптотика [math] O(N) [/math].

Достоинства

  • худшее время работы -- [math] O(N\log{N}) [/math],
  • время работы в случае, когда подается отсортированный массив -- [math] O(N) [/math].

Недостатки

  • не является устойчивой,
  • требует [math] O(\log{N}) [/math] дополнительной памяти для хранения длин куч в последовательности.

Примечание

См. также

Источники информации