Ці два здаються дуже схожими і мають майже однакову структуру. Яка різниця? Які часові складності для різних операцій кожного?
Ці два здаються дуже схожими і мають майже однакову структуру. Яка різниця? Які часові складності для різних операцій кожного?
Відповіді:
Купа просто гарантує, що елементи на більш високих рівнях є більшими (для max-heap) або меншими (для min-heap), ніж елементи на нижчих рівнях, тоді як BST гарантує порядок (від "зліва" до "направо"). Якщо ви хочете відсортувати елементи, перейдіть з BST. Данте - це не витівка
Купа краще знайти findMin / findMax (O (1)), тоді як BST хороший у всіх знахідках (O (logN)). Для обох структур Insert - O (logN). Якщо ви дбаєте лише про findMin / findMax (наприклад, пов’язаний з пріоритетом), перейдіть до купи. Якщо ви хочете, щоб все було відсортовано, перейдіть з BST.
І двійкові дерева пошуку, і бінарні купи - це структури даних на основі дерев.
Купи вимагають, щоб вузли мали пріоритет над своїми дітьми. У максимальній купі діти кожного вузла повинні бути меншими за себе. Це щонайменше навпаки для хвилини купи:
Двійкові дерева пошуку (BST) дотримуються певного впорядкування (попереднє замовлення, замовлення, після замовлення) серед вузлів рідних братів. Дерево необхідно сортувати, на відміну від купи:
BST мають середнє значення для вставки, видалення та пошуку.
Бінарні Heaps мають середній для findMin / findMax та для вставки та видалення.O ( 1 ) O ( log n )
Підсумок
Type BST (*) Heap
Insert average log(n) 1
Insert worst log(n) log(n) or n (***)
Find any worst log(n) n
Find max worst 1 (**) 1
Create worst n log(n) n
Delete worst log(n) log(n)
Усі середні часи цієї таблиці такі ж, як і їх найгірші часи, за винятком Insert.
*
: скрізь у цій відповіді BST == Збалансований BST, оскільки незбалансований смокче асимптотично**
: використання тривіальної модифікації, поясненої у цій відповіді***
: log(n)
для купи вказівних дерев, n
для маси динамічного масивуПереваги бінарної купи над BST
середній час вставки в двійкову купу є O(1)
, для BST є O(log(n))
. Це вбивча особливість купи.
Існують також інші купи, які доходять до O(1)
амортизованого (сильнішого) рівня, як фібоначчанська купа , і навіть у гіршому випадку, як черга Бродала , хоча вони можуть бути не практичними через неасимптотичну продуктивність: https://stackoverflow.com/questions/30782636 / are -jedno-heaps-or-brodal-queues-used-in-практиці будь-де
бінарні купи можна ефективно реалізовувати поверх будь-яких динамічних масивів або дерев на основі вказівника, лише BST-дерева. Тож для купи ми можемо вибрати більш просторову реалізацію масиву, якщо ми можемо дозволити собі періодичні затримки для зміни розміру.
створення двійковій купи є O(n)
гіршим випадком , O(n log(n))
для BST.
Перевага BST над бінарною купою
пошук довільних елементів є O(log(n))
. Це вбивча особливість BST.
Для купи це O(n)
взагалі, за винятком найбільшого елемента, який є O(1)
.
"Хибна" перевага купи над BST
купи - O(1)
знайти макс., BST O(log(n))
.
Це поширена помилкова думка, тому що тривіально змінювати BST, щоб відслідковувати найбільший елемент, і оновлювати його, коли цей елемент можна було змінити: при вставці більшого заміну, при видаленні знайдіть другий за величиною. https://stackoverflow.com/questions/7878622/can-we-use-binary-search-tree-to-simulate-heap-operation (згаданий Єо ).
Власне, це обмеження купи в порівнянні з BST: єдиний ефективний пошук полягає в тому, що це найбільший елемент.
Середній розмір бінарної купи є O(1)
Джерела:
Інтуїтивний аргумент:
У бінарній купі збільшення значення при заданому індексі також відбувається O(1)
з тієї ж причини. Але якщо ви хочете це зробити, цілком ймовірно, що вам захочеться оновлювати додатковий індекс для операцій по купі https://stackoverflow.com/questions/17009056/how-to-implement-ologn-decrease- черга чергових операцій на основі клавіш для мінімуму, наприклад, для Dijkstra. Можливо без додаткових витрат часу.
Стандартна бібліотека вставки GCC C ++ для реального обладнання
Я встановив орієнтацію на C ++ std::set
( червоно-чорне дерево BST ) та std::priority_queue
( динамічний масив масиву ), щоб побачити, чи я маю рацію щодо часу вставки, і ось що я отримав:
Так чітко:
Час вставлення купи в основному постійний.
Ми можемо чітко бачити точки динамічного розміру масиву. Оскільки ми усереднюємо кожні 10-ти вкладишів, щоб мати змогу побачити що-небудь вище, ніж системний шум , ці піки насправді приблизно в 10 кб більше, ніж показано!
Масштабований графік виключає по суті лише точки розміру масиву і показує, що майже всі вставки потрапляють під 25 наносекунд.
BST - логарифмічний. Усі вставки набагато повільніше, ніж середні вставки.
Детальний аналіз BST vs hashmap за адресою: https://stackoverflow.com/questions/18414579/what-data-structure-is-inside-stdmap-in-c/51945119#51945119
Стандартний орієнтир вставки бібліотеки GCC C ++ на gem5
gem5 - це повноцінний системний симулятор, а тому забезпечує нескінченно точний годинник з m5 dumpstats
. Тому я спробував використати його для оцінки термінів для окремих вставок.
Інтерпретація:
купа все ще є постійною, але тепер ми більш детально бачимо, що є кілька ліній, і кожна вища лінія є більш рідкою.
Це повинно відповідати затримкам доступу до пам’яті для більш високих і вищих вставок.
TODO Я не можу реально інтерпретувати BST повністю, оскільки він не виглядає настільки логарифмічним і дещо більш постійним.
Однак, якщо ми розглядаємо детальніше, ми можемо також побачити декілька чітких ліній, але я не впевнений, що вони представляють: я б очікував, що нижня лінія буде тоншою, оскільки ми вставляємо верхню низ?
Цей параметр Buildroot відмічений на процесорі aarch64 HPI .
BST не може бути ефективно реалізований у масиві
Операції купи потребують лише міхура вгору або вниз по одній гілці дерева, тому O(log(n))
найгірший випадок, O(1)
середній.
Для утримання збалансованого BST потрібні повороти дерев, які можуть змінити верхній елемент на інший і потребуватимуть переміщення всього масиву навколо ( O(n)
).
Гропи можуть бути ефективно реалізовані на масиві
Індекси батьків і дітей можуть бути обчислені з поточного індексу, як показано тут .
Таких операцій з балансування, як BST, немає.
Видалити хв - це сама тривожна операція, оскільки вона має бути зверху вниз. Але це завжди можна зробити, "просочивши" одну гілку купи, як пояснено тут . Це призводить до гіршого випадку O (log (n)), оскільки купа завжди добре збалансована.
Якщо ви вставляєте один вузол для кожного видаленого, ви втрачаєте перевагу середньої асимптотичної вставки O (1), яку купують, оскільки видалення буде домінувати, і ви також можете використовувати BST. Однак Dijkstra оновлює вузли кілька разів для кожного видалення, тож ми добре.
Динамічні купи масивів проти купи дерев вказівників
Купи можуть бути ефективно реалізовані на вершині купи покажчиків: https://stackoverflow.com/questions/19720438/is-it-possible-to-make-efficient-pointer-based-binary-heap-implementations
Реалізація динамічного масиву є більш просторовою. Припустимо, що кожен елемент купи містить лише вказівник на struct
:
реалізація дерева повинна зберігати три покажчики на кожен елемент: батько, ліва дитина та права дитина. Тож використання пам'яті завжди 4n
(3 вказівника на дерево + 1 struct
покажчик).
Дерево BST також потребуватиме додаткової інформації про балансування, наприклад, чорно-червоного кольору.
реалізація динамічного масиву може мати розмір 2n
відразу після подвоєння. Так що в середньому це буде 1.5n
.
З іншого боку, в купі дерев є кращі вставки, тому що копіювання динамічного масиву резервного копіювання для подвоєння його розміру має O(n)
найгірший випадок, тоді як дерево дерев робить лише нові невеликі виділення для кожного вузла.
Проте подвоєння резервного масиву O(1)
амортизується, тож воно зводиться до максимальної затримки. Тут згадується .
Філософія
BSTs підтримують глобальну властивість між батьком та всіма нащадками (лівіші менші, праворуч більші).
Верхній вузол BST - це середній елемент, який потребує глобальних знань для підтримання (знаючи, скільки там є менших і великих елементів).
Ця глобальна властивість дорожче підтримувати (вставка n-вхід), але дає більш потужні пошуки (пошук n пошуку).
Купи підтримують місцеву властивість між батьками та прямими дітьми (батьки> діти).
Верхня нота купи - це великий елемент, який вимагає лише місцевих знань для підтримки (знаючи свого батька).
Двозначно пов'язаний список
Подвійно пов'язаний список можна розглядати як підмножину купи, де перший елемент має найбільший пріоритет, тому давайте порівняємо їх і тут:
O(1)
найгірший випадок, оскільки у нас є вказівники на елементи, і оновлення дійсно простеO(1)
середня, таким чином, гірша за пов'язаний список. Компроміс за більш загальне положення вставки.O(n)
для обохВипадком для цього є тоді, коли ключем купи є поточна мітка часу: у такому випадку нові записи завжди перейдуть на початок списку. Тож ми можемо взагалі забути точну позначку часу і просто зберегти позицію в списку як пріоритетну.
Це можна використовувати для реалізації кешу LRU . Як і для купи програм, таких як Dijkstra , ви захочете зберегти додаткову хеш-карту від ключа до відповідного вузла списку, щоб знайти, який вузол швидко оновити.
Порівняння різних збалансованих BST
Хоча асимптотична вставка та час пошуку для всіх структур даних, які зазвичай класифікуються як «Збалансовані BST», які я бачив досі, є однаковим, різні BBST мають різні вигоди. Я ще цього не вивчив повністю, але було б добре підсумувати ці компроміси тут:
Дивитися також
Аналогічне запитання щодо CS: Яка різниця між бінарним деревом пошуку та бінарною купою?
У структурі даних слід відрізняти стурбованість.
Ці абстрактні структури даних (об'єкти , що зберігаються, їх операція) в цьому питанні різні. Один реалізує чергу пріоритетів, інший - набір. Черга з пріоритетами не зацікавлена у пошуку довільного елемента, лише того, який має найбільший пріоритет.
Конкретна реалізація структур. Тут на перший погляд обидва (бінарні) дерева, але мають різні структурні властивості. Як відносне впорядкування ключів, так і можливі глобальні структури відрізняються. (Дещо неточно, в BST
клавішах упорядковано зліва направо, в купі вони впорядковані зверху вниз.) Оскільки IPlant правильно зауважує, купа також повинна бути "повною".
Існує остаточна різниця в реалізації низького рівня . (Незбалансоване) двійкове дерево пошуку має стандартну реалізацію за допомогою покажчиків. Бінарна купа, навпаки, має ефективну реалізацію з використанням масиву (саме через обмежену структуру).