Я думаю, що в цій темі є кілька питань:
- Як ви реалізуєте,
buildHeap
щоб він працював у O (n) час?
- Як ви показуєте, що
buildHeap
працює за O (n) час при правильній реалізації?
- Чому ця сама логіка не працює для того, щоб змусити сортувати групу в O (n) час, а не O (n log n) ?
Як ви реалізуєте, buildHeap
щоб він працював у O (n) час?
Часто відповіді на ці запитання зосереджуються на різниці між siftUp
та siftDown
. Роблячи правильний вибір між siftUp
і siftDown
має вирішальне значення для отримання (п) O продуктивність buildHeap
, але нічого не робить , щоб допомогти людині не зрозуміти різницю між buildHeap
і heapSort
в цілому. Дійсно, належні реалізації обох buildHeap
і heapSort
будуть використовуватись тількиsiftDown
. siftUp
Операція необхідна тільки для виконання вставки в існуючій купу, так що він буде використовуватися для реалізації черги з пріоритетами з допомогою бінарної купи, наприклад.
Я написав це, щоб описати, як працює максимальна купа. Це тип купи, який зазвичай використовується для сортування купи або для черги пріоритетів, де більш високі значення вказують на більший пріоритет. Мінна купа також корисна; наприклад, для отримання елементів з цілими ключами у порядку зростання або рядків в алфавітному порядку. Принципи точно однакові; просто переключіть порядок сортування.
У купі властивість визначає , що кожен вузол в двійковій купі має бути , по крайней мере , як великий , як і його дітей. Зокрема, це означає, що найбільший предмет у купі знаходиться в корені. Відсівання вниз і просіювання - це по суті та сама операція в протилежних напрямках: переміщуйте порушуючий вузол, поки він не задовольнить властивість купи:
siftDown
міняє занадто малий вузол зі своїм найбільшим дочірнім пристроєм (тим самим переміщуючи його вниз), поки він не буде настільки ж великим, як обидва вузли під ним.
siftUp
поміняє занадто великий вузол зі своїм батьком (тим самим переміщуючи його вгору), поки він не стане більшим, ніж вузол над ним.
Кількість операцій, необхідних для siftDown
та siftUp
пропорційна відстані, яку, можливо, доведеться перемістити. Бо siftDown
це відстань до дна дерева, тому siftDown
дороге для вузлів на верхівці дерева. З siftUp
, робота пропорційна відстані до верхівки дерева, тому siftUp
дорога для вузлів у нижній частині дерева. Хоча обидві операції є O (log n) у гіршому випадку, у купі, лише один вузол знаходиться вгорі, тоді як половина вузлів лежить у нижньому шарі. Тож не повинно бути занадто дивним, що якщо нам доведеться застосувати операцію до кожного вузла, ми віддамо перевагу siftDown
закінченню siftUp
.
buildHeap
Функція приймає масив невпорядкованих елементів і переміщує їх , поки вони все не задовольняють кучного власності, в результаті чого отримують дійсну купу. Можна використовувати два підходи для buildHeap
використання siftUp
та siftDown
описаних нами операцій.
Почніть з верхньої частини купи (початок масиву) і зателефонуйте siftUp
до кожного елемента. На кожному кроці попередньо просіяні елементи (елементи перед поточним елементом у масиві) утворюють дійсну купу, а просіювання наступного елемента вгору ставить його у дійсне місце у купі. Після просіювання кожного вузла всі елементи задовольняють властивості купи.
Або йдіть у зворотному напрямку: почніть з кінця масиву і рухайтеся назад вперед. При кожній ітерації ви просіюєте елемент донизу, поки він не знаходиться в потрібному місці.
Яка реалізація buildHeap
є більш ефективною?
Обидва ці рішення дадуть дійсну купу. Не дивно, що більш ефективною є друга операція, яка використовується siftDown
.
Нехай h = log n позначає висоту купи. Робота, необхідна для siftDown
підходу, задається сумою
(0 * n/2) + (1 * n/4) + (2 * n/8) + ... + (h * 1).
Кожен доданок у сумі має максимальну відстань, яку повинен буде перемістити вузол на заданій висоті (нуль для нижнього шару, h для кореня), помножений на кількість вузлів на цій висоті. На відміну від цього, сума для виклику siftUp
на кожному вузлі є
(h * n/2) + ((h-1) * n/4) + ((h-2)*n/8) + ... + (0 * 1).
Повинно бути зрозуміло, що друга сума більша. Перший додаток лише hn / 2 = 1/2 n log n , тому такий підхід має складність у кращому випадку O (n log n) .
Як ми доводимо, що сума за siftDown
підхід справді є O (n) ?
Один з методів (є й інші аналізи, які також працюють) - перетворити кінцеву суму в нескінченний ряд, а потім використовувати ряд Тейлора. Ми можемо ігнорувати перший термін, який дорівнює нулю:
Якщо ви не впевнені, чому працює кожен з цих кроків, ось виправдання процесу словами:
- Усі умови позитивні, тому кінцева сума повинна бути меншою, ніж нескінченна сума.
- Серія дорівнює ряду потужностей, оцінених за x = 1/2 .
- Цей силовий ряд дорівнює (постійному часу) похідній ряду Тейлора для f (x) = 1 / (1-x) .
- x = 1/2 знаходиться в інтервалі зближення цього ряду Тейлора.
- Тому ми можемо замінити ряд Тейлора на 1 / (1-x) , диференціювати та оцінити, щоб знайти значення нескінченного ряду.
Оскільки нескінченна сума рівно n , ми робимо висновок, що кінцева сума не більша, і тому O (n) .
Чому для сортування купи потрібен O (n log n) час?
Якщо можливо запустити buildHeap
в лінійний час, чому сортування купи вимагає часу O (n log n) ? Ну, купа купи складається з двох етапів. По-перше, ми закликаємо buildHeap
масив, який вимагає часу O (n), якщо його оптимально реалізувати. Наступним етапом є багаторазове видалення найбільшого елемента з купи та розміщення його в кінці масиву. Оскільки ми видаляємо предмет з купи, завжди є відкрите місце після закінчення купи, де ми можемо зберігати предмет. Таким чином, купа купівлі досягає відсортованого порядку, послідовно видаляючи наступний найбільший елемент і поміщаючи його в масив, починаючи з останньої позиції та рухаючись у напрямку спереду. Саме складність цієї останньої частини домінує в купі сортів. Петля виглядає так:
for (i = n - 1; i > 0; i--) {
arr[i] = deleteMax();
}
Зрозуміло, що цикл працює O (n) разів ( n - 1, якщо бути точним, останній елемент вже на місці). Складність deleteMax
для купи становить O (log n) . Зазвичай він реалізується, видаляючи корінь (найбільший елемент, що залишився в купі) і замінюючи його останнім елементом у купі, який є листом, а отже, одним із найменших елементів. Цей новий корінь майже напевно порушить властивість купи, тому вам доведеться дзвонити, siftDown
поки ви не перемістите його назад у прийнятну позицію. Це також призводить до переміщення наступного найбільшого елемента до кореня. Зауважте, що, на відміну від того, buildHeap
де для більшості вузлів ми дзвонимо siftDown
з низу дерева, ми зараз телефонуємо siftDown
зверху дерева на кожній ітерації!Хоча дерево скорочується, воно не скорочується досить швидко : висота дерева залишається незмінною, поки ви не видалите першу половину вузлів (коли повністю очистите нижній шар). Тоді для наступної чверті висота h - 1 . Тож загальна робота для цього другого етапу
h*n/2 + (h-1)*n/4 + ... + 0 * 1.
Помітьте перемикач: тепер нульовий робочий випадок відповідає одному вузлу, а h робочий випадок відповідає половині вузлів. Ця сума є O (n log n) так само, як неефективна версія buildHeap
, реалізована за допомогою siftUp. Але в цьому випадку у нас немає вибору, оскільки ми намагаємося сортувати, і нам потрібно видалити наступний найбільший елемент.
Підсумовуючи, робота для сортування купи - це сума двох етапів: O (n) час для buildHeap та O (n log n) для видалення кожного вузла в порядку , тому складність становить O (n log n) . Ви можете довести (використовуючи деякі ідеї з інформаційної теорії), що для сортування на основі порівняння O (n log n) - найкраще, на що ви могли сподіватися, так що немає жодних причин для цього розчаровуватись чи сподіватися на нагромадження купи, щоб досягти досягнення O (n) обмежений час, що buildHeap
робить.