Пирамидальная сортировка. Алгоритм и его сложность

Гоша: Тимофей, ты говорил, что с использованием бинарной кучи можно реализовать алгоритм сортировки. Расскажешь как?
Тимофей: А ты хорошо разобрался с тем, как работает пирамида?
Гоша: Ну да!
Какая сложность вставки нового элемента в пирамиду в худшем случае?
Какая сложность извлечения самого приоритетного элемента из пирамиды в худшем случае?
С использованием бинарной кучи можно реализовать алгоритм сортировки, который в худшем случае работает за O(nlog⁡n)O(n \log n)O(nlogn).
Разберёмся, как он работает.
Дан массив:
Скопировать кодPYTHON
arr = [3, 5, 1, 6, 9, 2]
Нужно отсортировать его за O(nlog⁡n)O(n \log n)O(nlogn) в худшем случае.
Отсортирован ли массив, который представляет бинарную кучу?
Массив, который является представлением бинарной кучи, не обязан быть отсортированным. Выполнение того свойства, что значение в родителе более приоритетно, чем значения в дочерних узлах, не гарантирует отсортированность.
image
Например, на схеме неотсортированный массив представляет бинарную кучу.
Разберёмся, как используют эту структуру данных для задачи сортировки. Общий алгоритм такой:
  1. Создадим бинарную кучу.
  2. Вставим в неё элементы массива.
  3. Будем извлекать из неё наиболее приоритетные элементы, удаляя их из кучи.
Рассмотрим алгоритм на примере, приведённом выше. Так выглядит вставка элементов из массива в бинарную кучу:
image
На этом шаге извлечём наиболее приоритетный элемент из кучи. По свойству пирамиды самый приоритетный элемент находится в вершине. Поэтому извлечём элемент из вершины, удаляя его из кучи. Для задачи сортировки по возрастанию приоритетный — минимальный элемент.
Таким образом, мы получили отсортированный массив.
Какая сложность у рассмотренного алгоритма в лучшем случае?
Какая сложность у рассмотренного алгоритма в худшем случае?
Первый шаг — создание бинарной кучи. Сложность этой операции — O(n)O(n)O(n). Нам просто нужно создать массив из n элементов.
Далее вставим nnn элементов в бинарную кучу.
Сложность этого этапа:
O(log⁡1)+O(log⁡2)+...+O(log⁡n)<O(log⁡n)+O(log⁡n)+......+O(log⁡n)=O(nlog⁡n)O(\log 1)+O(\log 2)+...+O(\log n) < O(\log n)+O(\log n)+...\\ ...+O(\log n) = O(n \log n)O(log1)+O(log2)+...+O(logn)<O(logn)+O(logn)+......+O(logn)=O(nlogn)
Последним шагом извлекаем nnn элементов. Сложность этой операции также O(nlog⁡n)O(n \log n)O(nlogn).
O(log⁡n)+...+O(log⁡2)+O(log⁡1)<O(log⁡n)+......+O(log⁡n)+O(log⁡n)=O(nlog⁡n)O(\log n)+...+O(\log 2)+O(\log 1) < O(\log n)+...\\...+O(\log n)+O(\log n) = O(n \log n)O(logn)+...+O(log2)+O(log1)<O(logn)+......+O(logn)+O(logn)=O(nlogn)
Получим:
T=O(n)+O(nlog⁡n)+O(nlog⁡n)=O(nlog⁡n)T = O(n) + O(n \log n) + O(n \log n) = O(n \log n)T=O(n)+O(nlogn)+O(nlogn)=O(nlogn)
Это алгоритм сортировки, который в худшем случае работает за O(nlog⁡n)O(n \log n)O(nlogn).
Требуется ли для такой реализации дополнительная память?
Гоша: Здорово! Какой интересный алгоритм. Только я не понимаю, чем он лучше сортировки слиянием. Она тоже в худшем случае работает за O(nlog⁡n)O(n \log n)O(nlogn) и требует O(n)O(n)O(n) дополнительной памяти.
Тимофей: Можно модифицировать алгоритм пирамидальной сортировки так, что не придётся выделять память под новый массив. Рассказать как?
Гоша: Нет, я хочу сам подумать, как это сделать. Если можно реализовать пирамидальную сортировку без использования дополнительной памяти, то почему в стандартных библиотеках многих языков программирования применяют быструю сортировку? Она ведь в худшем случае работает за квадратичное время. Не понимаю.
Тимофей: Быструю сортировку применяют чаще по двум причинам:
  1. В алгоритме быстрой сортировки используется меньше операций обмена с памятью, чем в пирамидальной сортировке. Желательно избегать лишней работы с памятью.
  2. nnn обращений к последовательным ячейкам памяти исполняются быстрее, чем к случайным. Это связано с ограниченным количеством аппаратной кэш-памяти у процессоров. Некоторые алгоритмы могут быть дружелюбны к кэшу. Их называют cache-friendly. А некоторые — нет.
Пирамидальная сортировка — самая медленная из тех, которые работают за O(nlog⁡n)O(n \log n)O(nlogn).
image
Гоша: Ясно. Тогда я всегда буду выбирать быструю сортировку из этих трёх алгоритмов.
Тимофей: Мне кажется, это неправильное решение. Подбирать алгоритм нужно специально под задачу. Выбирай пирамидальную сортировку, если хочешь быть уверен, что алгоритм всегда будет работать за O(nlog⁡n)O(n \log n)O(nlogn), и в наличии нет O(n)O(n)O(n) дополнительной памяти. Кстати этот алгоритм сортировки был основным на компьютерах, выпускаемых в 1960–1970 годах.
Решите задачи G, H: https://contest.yandex.ru/contest/18996/problems/G/.