1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6

# теория · шаг 3 из 6

Пирамидальная сортировка: разобрать кучу на месте

Коротко

Пирамидальная сортировка сортирует на месте за гарантированные Θ(n·log n). Шаг первый: за Θ(n) строим из массива макс-кучу - максимум оказывается в корне (индекс 0). Шаг второй: n − 1 раз меняем корень (максимум) с ПОСЛЕДНИМ элементом кучи - максимум встаёт на своё финальное место в конце, - уменьшаем кучу на один и просеиваем новый корень вниз (O(log n)). Массив заполняется отсортированным с конца. Итог - Θ(n) на построение плюс n просеиваний по O(log n) = Θ(n·log n) В ЛЮБОМ случае, без лишней памяти. Плата: не устойчива.

Если ты умеешь сортировать пирамидально (построить макс-кучу, потом n−1 раз «корень в конец + просеять») и знаешь, почему это Θ(n·log n) на месте, но не устойчиво, листай до «Теперь сам».

Как думаешь, как превратить кучу в отсортированный массив

Куча даёт максимум за O(1) - он в корне. Идея сортировки напрашивается: доставай максимумы по одному. Достал наибольший - куда его деть, чтобы не заводить второй массив? В КОНЕЦ этого же массива, на место, которое куче больше не нужно: куча ужмётся на один, а освободившийся хвост будет копить отсортированное. Достаём максимумы - и они ложатся в конец по убыванию извлечения, то есть массив выстраивается по возрастанию. Всё на месте, без буфера. Это и есть пирамидальная сортировка.

На что это похоже

Турнир, из которого чемпиона после победы сажают в «зал славы» - крайнее правое кресло. Ушёл чемпион - на вершину поднимают сильнейшего из оставшихся (быстрая перестройка пирамиды), он становится новым чемпионом и садится в кресло левее прежнего. Зал славы заполняется справа налево - сильнейший дальше всех справа. Когда турнир опустеет, зал славы - это все участники по возрастанию силы слева направо. Ни одного лишнего стула рядом не понадобилось: чемпионы садятся в те же кресла, что освобождает турнир.

Механизм: построить кучу, потом снимать максимумы в конец

1. построить макс-кучу из всего массива          (Θ(n))
2. для end от n−1 до 1:
       поменять a[0] и a[end]   # максимум - в конец, на финальное место
       уменьшить кучу до end    # хвост [end..n−1] уже отсортирован
       просеять_вниз(0, end)    # новый корень - на место, O(log n)

После шага 1 максимум в a[0]. На каждой итерации шага 2 мы кладём текущий максимум в конец активной кучи и восстанавливаем свойство одним просеиванием нового корня. Активная куча тает слева, отсортированный хвост растёт справа - пока не останется один элемент, уже наименьший, на месте 0.

978 35 корень=макс конец кучи меняем a[0] и a[конец] → 578 39 куча ужалась, 5 вниз 9 на месте - хвост
Максимум уходит в конец на финальное место, куча ужимается, новый корень просеивается вниз. Отсортированное копится справа.

Правило

Пирамидальная сортировка: (1) за Θ(n) построить макс-кучу; (2) n − 1 раз поменять корень (максимум) с последним элементом кучи, ужать кучу на один и просеять новый корень вниз за O(log n). Отсортированное копится в конце массива. Итог - Θ(n·log n) в ЛЮБОМ случае, НА МЕСТЕ (память O(1)). Плата - неустойчивость. Это единственная из наших сортировок, дающая и гарантию n·log n, и работу на месте одновременно.

Разбор: почему это Θ(n·log n)

Складываем два шага. Построение кучи - Θ(n) (прошлый урок). Затем n − 1 извлечений максимума, каждое - это обмен (O(1)) плюс просеивание нового корня вниз, а просеивание стоит O(log n) (высота дерева). Значит второй шаг - (n−1) · O(log n) = O(n·log n). Всего Θ(n) + O(n·log n) = O(n·log n). Заметь: этот потолок верен ДЛЯ ЛЮБОГО входа - в отличие от быстрой сортировки, у пирамидальной нет худшего случая Θ(n²), потому что высота кучи всегда log n, как бы ни были расставлены данные. Именно поэтому её используют как «страховку»: промышленная быстрая сортировка (introsort - её разберём в последней главе раздела) переключается на пирамидальную, когда рекурсия уходит слишком глубоко, - чтобы гарантировать n·log n даже в худшем случае.

Разбор: почему на месте, но не устойчива

На месте - потому что и построение кучи, и извлечения переставляют элементы прямо внутри исходного массива: активная куча и отсортированный хвост делят один и тот же массив, граница между ними просто сдвигается. Дополнительной памяти нужно O(1) (пара переменных), не считая стека - а его нет, если просеивание написать циклом, а не рекурсией. Неустойчива она потому, что просеивание меняет местами далёкие элементы (корень с последним, узел с ребёнком), и равные легко перепрыгивают друг друга - их исходный порядок теряется, как и у быстрой сортировки. Так что пирамидальная - это «гарантия n·log n + на месте, но без устойчивости»; если нужна устойчивость, берут слияние.

Что ломается без этого

Первая ошибка - при извлечении просеивать вниз в ГРАНИЦАХ всей кучи, а не ужатой: просеивание должно знать новый (уменьшенный) размер, иначе оно затянет уже уложенный в конец максимум обратно в кучу, и сортировка сломается. Вторая - строить МИН-кучу и удивляться, что массив выходит отсортированным по УБЫВАНИЮ: для возрастания нужна макс-куча (максимум уходит в конец). Третья - ждать устойчивости: её нет. Четвёртая - писать просеивание рекурсией и потерять преимущество O(1) памяти (появится стек глубины log n); на практике его пишут циклом.

Зачем это в работе

Пирамидальная сортировка ценна одним редким сочетанием: гарантия n·log n при ЛЮБОМ входе И работа на месте (без Θ(n) буфера, как у слияния). Поэтому её берут как страховку от худшего случая быстрой сортировки - в introsort (стандартная быстрая сортировка в C++ и не только) она включается при слишком глубокой рекурсии, спасая от Θ(n²). Её же выбирают там, где память в дефиците, а гарантия важна (встроенные системы, ядра ОС). На практике она обычно чуть медленнее быстрой сортировки по КОНСТАНТЕ (прыжки по массиву при просеивании плохо ложатся на кеш - привет локальности памяти из раздела про массивы), но её n·log n непробиваем. А сама куча под ней - это очередь с приоритетом, рабочая лошадка Дейкстры, Хаффмана и планировщиков.

Теперь сам

После построения макс-кучи из пяти элементов первый шаг извлечения меняет a[0] и a[4]. Что теперь на месте 4, что стало корнем и что делают дальше? Почему всего таких шагов n − 1, а не n?

Ответ. На месте 4 (конец массива) оказался бывший корень - МАКСИМУМ всех пяти элементов, и это его финальная позиция (больше него нет). Корнем стал бывший последний элемент a[4] - скорее всего маленький, свойство кучи нарушено, поэтому его ПРОСЕИВАЮТ ВНИЗ в границах ужатой кучи (теперь размер 4), чтобы наверх снова поднялся максимум оставшихся. Дальше повторяют: меняют новый корень с a[3] и так далее. Шагов n − 1, а не n, потому что последний оставшийся элемент (на месте 0) - уже наименьший и заведомо на своём месте: когда куча ужалась до одного элемента, сортировать нечего.

Главное

Пирамидальная сортировка: (1) за Θ(n) построить макс-кучу (максимум в корне); (2) n−1 раз поменять корень с последним элементом кучи (максимум - на финальное место в конце), ужать кучу и просеять новый корень вниз за O(log n). Отсортированное копится справа. Итог - O(n·log n) в ЛЮБОМ случае (высота кучи всегда log n, худшего случая нет; на различных ключах это и есть Θ(n·log n), а на массиве из одинаковых просеивание сразу останавливается, и выходит линия) и НА МЕСТЕ (O(1) памяти). Плата - неустойчивость (просеивание перепрыгивает равные). Уникальное сочетание «гарантия n·log n + на месте» делает её страховкой в introsort и выбором при дефиците памяти.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.