J-сортировка

77247e7d9f1ee87a59f4c29ba35ae331.jpgПирамидальная сортировка (она же сортировка кучей) — классический алгоритм который, пожалуй, должен знать любой программист. Старая добрая «пирамидка» примечательна тем, что в независимости от набора данных у неё одна и та же сложность по времени (причём, очень пристойная) — O (n log n). Лучших и вырожденных случаев для неё нет.С момента изобретения метода (а в этом году алгоритм празднует свой полувековой юбилей) было немало охочих кардинально оптимизировать процесс накладывания сортирующих куч. Тернарная пирамидальная сортировка, плавная сортировка, сортировка декартовым деревом — вот неполный список инноваций. Перечисленные алгоритмы хотя при тестировании и опережают оригинал по абсолютной скорости кто на 12, а кто и на 25%, в оценке временной сложности всё равно крутятся вокруг O (n log n). При этом данные методы весьма изощрённо реализованы.

Своё видение пирамидальной сортировки предложил и скромный труженик Университета Манитобы Джейсон Моррисон. При этом способ в некоторых случаях по скорости приближается к O (n).

Для тех кто не в теме, кратенько обрисую как работает пирамидальная сортировка.fe24d05fd0dc07ff8fa39effefa84e2c.gifМассив можно отсортировать, если на его основе строить и перестраивать сортирующее дерево, известное как двоичная куча или просто пирамида.

Что есть сортирующее дерево? Это дерево, у которого любой родитель больше (или меньше, смотря в какую сторону оно сортирующее) чем его потомки.

Как из обычного дерева сделать сортирующее дерево? Очень просто — нужно двигаться от потомков вверх к родителям и если потомок больше родителя, то менять местами. Если такой обмен произошёл, опустившегося на один уровень родителя нужно сравнить с потомками ниже — может и там тоже будет повод для обмена.

Преобразовывая неотсортированную часть массива в сортирующее дерево, в итоге в корень «всплывёт» наибольший элемент. Обмениваем максимум в с последним ключом неотсортированного подмассива. Структура перестанет быть сортирующим деревом, но в качестве моральной компенсации его неотсортированная часть станет меньше на один узел. К этой неотсортированной части заново применим всю процедуру, то есть преобразуем её в сортирующее дерево с последующей перестановкой найденного максимума в конец. И так действуем до тех пор, пока неотсортированная часть не скукожится до одного-единственного элемента.

Подход, что и говорить, остроумный, но при этом специалисты по алгоритмам отмечают у сортировки кучей целую кучу недостатков, как-то: неустойчивость, хаотичность выборки, нечувствительность к почти упорядоченным массивам и пр. Смущает всех также неулучшаемая скорость O (n log n), демонстрируемая сортировкой абсолютно при любых наборах входящих данных.

fc3022f70adb9d429175b4d96ffa6417.jpg Выдающиеся умы computer science предлагают различные мозговыносящие идеи (тернарные пирамиды, числа Леонардо, декартовы деревья) с помощью которых можно улучшить алгоритм. Джейсон Моррисон (Jason Morrison, сортировка названа в честь автора) предложил нечто противоположное — для оптимизации надо не усложнять, а максимально упрощать.

Канадский учёный пришёл к выводу, что заново перестраивать кучу для каждого элемента — дорогое удовольствие. Так уж ли необходимо массив из n элементов радикально перелопачивать n раз? Если для массива построить всего пару куч (нисходящую и восходящую), то это значительно его упорядочит в обоих направлениях.

8d61acebcaaa36ad47fe0489f14b2a18.gifСначала нужно осуществить построение невозрастающей кучи. В результате меньшие элементы повсплывают в верхние узлы пирамиды (что будет соответствовать левой половине массива), наименьший элемент вообще окажется в корне. Чем выше элементы находятся в сортирующем дереве — тем более упорядоченной будет соответствующая часть массива. Элементы находящиеся ближе к листьям дерева (им будет соответствовать вторая половина массива) будут иметь менее упорядоченный вид, поскольку они не сравнивались, а просто были оттеснены на задворки в результате перемещения их родителей.

Для наведения относительного порядка в правой части массива следует построить кучу ещё раз, во всём противоположную первой. Во-первых, эта куча должна быть неубывающей (мы ведь теперь хотим разобраться с крупными по значению ключами). Во-вторых, она должна быть «зеркальной» к массиву, то есть её корень должен соответствовать не первому, а последнему элементу и выстраивать дерево следует, перебирая массив от конца к началу.

Выстроив такие себе близняшки-пирамидки, получаем во многом упорядоченный массив. Довершает дело сортировка вставками. Этот алгоритм имеет весьма скромную среднюю сложность по времени O (n2), но творит чудеса на массивах, которые уже почти отсортированы. Их сортировка вставками щёлкает как орешки.

Попробуем оценить при самом благоприятном раскладе. Однократное пирамидостроение — это O (n). Куч мы наложили две, так что получаем O (2n). Почти упорядоченный массив сортировка вставками может отсортировать за рекордные O (n). Общая сложность получается O (3n), что тоже самое что и O (n).Впрочем, на практике всё выглядит скромнее. Возьмём, например, такой неотсортированный массив, содержащий числа от 1 до 30:

b56488a309ecee6a2294f21131e2c7ca.jpg Построим обычную невозрастающую кучу: 486c4b113cb255850cbce269e1840646.jpg Первая треть массива приняла вполне благообразный вид, в середине — кто в лес кто по дрова, конец массива пока тоже не впечатляет.Построим теперь «зеркальную» неубывающую кучу:

d5e3741227937fc0663c39b2fb5da474.jpg Конец массива заметно похорошел. В середине есть некие зачатки упорядоченности, но выглядит не так гламурно, как левый и правый сектор. В принципе, такой массив вполне годится, чтобы скормить его сортировке вставками.Обратите внимание на ключ со значением 20.Почему этот элемент застрял в первой трети массива? Банально не повезло — перед построением «зеркальной» неубывающей кучи все родители вверх по ветке оказались больше по значению (в корне сейчас 17, но этот ключ утонет в левой половине дерева и уступит место 30). Поэтому в пирамиде ему не суждено возвыситься хотя бы на ступеньку.

ec1928c6634bc65db2932158ffd684cf.jpg Чем длиннее массив, тем чаще такие вырожденные узлы будет возникать. А значит, в средней полосе длинных массивов сортировке вставками придётся изрядно потрудиться. Подаваемый ей массив для обработки будет не то чтобы почти отсортированным, а скорее почти почти отсортированным. На очень длинных списках временная сложность у Insertion Sort будет, конечно, не её средние/худшие O (n2), но и до O (n) будет далеко. Есть ещё один алгоритм сортировки с очень похожим наименованием — J sort, которую разработал Джон Коен (John Cohen). Это тоже гибридный алгоритм, используется для обработки двусвязных списков. Сочетает в себе нитевидную сортировку (Strand sort) и сортировку перетасовкой (Shuffle sort). Но это уже другая история. Название Jsort (J-сортировка) Автор Джейсон Моррисон (Jason Morrison) Класс Гибридная сортировка (кучей + вставками) Устойчивость Устойчивая Сравнения Есть Временная сложность лучшая O (n) средняя ? худшая O (n2) Сложность по памяти всего O (n) дополнительные данные O (1) Heap sort на Java /** * Демонстрационный алгоритм для пирамидальной сортировки * Авторы алгоритма — J.W. J. Williams и R.W. Floyd * * SortAlgorithm.java, Thu Oct 27 10:32:35 1994 * * @author Jason Harrison@cs.ubc.ca * @version 1.0, 23 Jun 1995 */ class HeapSortAlgorithm extends SortAlgorithm { //Сортировка кучей void sort (int a[]) throws Exception { int N = a.length;

//Создаём из массива сортирующее дерево //Максимальный элемент окажется в корне. for (int k = N / 2; k > 0; k--) downheap (a, k, N); //Избавляемся от максимумов //и перетряхиваем сортирующее дерево do { //Меняем максимум с последним элементом… int T = a[0]; a[0] = a[N — 1]; a[N — 1] = T; //… и перестравиваем сортирующее дерево //для неотсортированной части массива N = N — 1; downheap (a, 1, N); } while (N > 1); //До последнего элемента } //Просматриваем ветку и в её корень перемещаем наибольший узел void downheap (int a[], int k, int N) throws Exception { //В корне поддерева //запоминаем родителя int T = a[k — 1]; //Смотрим потомков в пределах ветки while (k <= N / 2) { int j = k + k;//Левый потомок

//Если есть правый потомок, //то сравниваем его с левым //и из них выбираем наибольший if ((j < N) && (a[j - 1] < a[j])) j++;

//Если родитель больше (или равен) наибольшего потомка… if (T >= a[j — 1]) {

//… то значит всё в порядке в этой ветке break; } else { //Если родитель меньше наибольшего потомка… //… то потомок становится на место родителя a[k — 1] = a[j — 1]; k = j; } }

//Родитель в итоге меняется местами с наибольшим из потомков //(или остаётся на своём месте, если все потомки меньше его) a[k — 1] = T; } } YouTube-версия анимации Heap sort [embedded content]

Jsort на Java /** * Демонстраицонный алгоритм для J-сортировки (JSort). * Автор алгоритма — Джейсон Моррисон (Jason Morrison) * * * JSortAlgorithm.java * * Автор реализации — Патрик Морин * @author Patrick Morin */ class JSortAlgorithm extends SortAlgorithm { //С помошью неполной НЕУБЫВАЮЩЕЙ кучи //крупные элементы закидываем поближе к концу массива void reheap (int a[], int length, int i) throws Exception { //С этим родителем ещё не разобрались boolean done = false; //Запоминаем отдельно родителя //и смотрим на его потомка слева int T = a[i]; int parent = i; int child = 2 * (i + 1) — 1; //Просматриваем потомков, а также потомков потомков //и сравниваем их с родителем (если что — передвигаем потомков влево) //Цикл продолжается пока не выпадем за пределы массива //или пока не обменяем какого-нибудь потомка на родителя. while ((child < length) && (!done)) { //Если правый потомок в пределах массива if (child < length - 1) { //То из левого и правого потомка выбираем наименьшего if (a[child] >= a[child + 1]) { child += 1; } } //Родитель меньше потомков? if (T < a[child]) { //Тогда с этим родителем и его потомками разобрались done = true; //Родитель НЕ меньше чем наименьший из его потомков. //Перемещаем потомка на место родителя //и с родителем в цикле сравниваем уже потомков этого потомка } else {

a[parent] = a[child]; parent = child; child = 2 * (parent + 1) — 1; } } //Родитель, с которого всё начиналось //передвигается ближе к концу массива //(или остаётся на месте если не повезло) a[parent] = T; }

//С помошью неполной НЕВОЗРАСТАЮЩЕЙ кучи //мелкие элементы закидываем поближе к началу массива void invreheap (int a[], int length, int i) throws Exception { //С этим родителем ещё не разобрались boolean done = false;

//Запоминаем отдельно родителя //и смотрим на его потомка слева int T = a[length — 1 — i]; int parent = i; int child = 2 * (i + 1) — 1; //Просматриваем потомков, а также потомков потомков //и сравниваем их с родителем (если что — передвигаем потомков) //Цикл продолжается пока не выпадем за пределы массива //или пока не обменяем какого-нибудь потомка на родителя. while ((child < length) && (!done)) { //Если левый потомок в пределах массива if (child < length - 1) { //То из левого и правого потомка выбираем наибольшего if (a[length - 1 - child] <= a[length - 1 - (child + 1)]) { child += 1; } } //Родитель больше потомков? if (T > a[length — 1 — child]) { //Тогда с этим родителем и его потомками разобрались done = true; } else {

//Родитель НЕ больше чем наибольший из его потомков. //Перемещаем потомка на место родителя //и с родителем в цикле сравниваем уже потомков этого потомка a[length — 1 — parent] = a[length — 1 — child]; parent = child; child = 2 * (parent + 1) — 1; } } //Родитель, с которого всё начиналось //передвигается ближе к началу массива //(или остаётся на месте если не повезло) a[length — 1 — parent] = T; }

//Основная процедура сортировки void sort (int a[]) throws Exception {

//Строим неубывающую кучу //Большие элементы из начала массива //закидываем поближе к концу for (int i = a.length-1; i >= 0; i--) reheap (a, a.length, i); //Строим невозрастающую кучу //Меньшие элементы из конца массива //закидываем поближе к началу for (int i = a.length — 1; i >= 0; i--) invreheap (a, a.length, i); //Массив ПОЧТИ упорядочен //Досортировываем вставками for (int j = 1; j < a.length; j++) { int T = a[j]; int i = j - 1; while (i >= 0 && a[i] > T) { a[i + 1] = a[i]; i -= 1; } a[i + 1] = T; }

} } YouTube-версия анимации Jsort [embedded content]

Jsort в ВикипедииСтраница Джейсона Моррисона на сайте Университета МанитобыСтраница Джейсона Моррисона на LinkedIn

© Habrahabr.ru