Двоична купчина срещу двоично дърво C++

Имам известно объркване относно времената на изпълнение на операцията find_min на двоично дърво за търсене и двоична купчина. Разбирам, че връщането на min в двоична купчина е O(1) операция. Също така разбирам защо на теория връщането на минималния елемент в двоично дърво за търсене е операция O(log(N)). За моя голяма изненада, когато прочетох структурата на данните в C++ STL, документацията гласи, че връщането на итератор към първия елемент в картата (което е същото като връщането на минималния елемент) се случва в постоянно време! Това не трябва ли да се връща в логаритмично време? Имам нужда от някой, който да ми помогне да разбера какво прави C++ под капака, за да върне това в постоянно време. Тъй като тогава няма смисъл наистина да се използва двоична купчина в C++, структурата на данните на картата тогава ще поддържа извличане на min и max в постоянно време, изтриване и търсене в O(log(N)) и ще поддържа всичко сортирано. Това означава, че структурата на данните има предимствата както на BST, така и на Binary Heap, всички свързани в едно!

Имах спор за това с интервюиращ (всъщност спор), но се опитвах да му обясня, че в C++ връщането на min и max от map в C++ (което е самобалансиращо се двоично дърво за търсене) се случва в постоянно време. Той беше объркан и продължаваше да казва, че греша и че бинарният хийп е правилният начин. Разяснението ще бъде много оценено


person AyBayBay    schedule 07.01.2014    source източник
comment
Двоичната купчина ми се струва, че би била полезна само ако консумирате/премахвате елементи, докато итерирате. Избухването на следващия елемент донякъде зависи от дупката, причинена от това отстраняване.   -  person cHao    schedule 07.01.2014
comment
Интервюиращият явно е объркан. Надявам се, че не сте постъпили на работа в неговата компания :-)   -  person Sergey Kalinichenko    schedule 07.01.2014
comment
добре, поради спора той се ядоса и интервюто не мина добре, така че предполагам, че няма да получа обратно обаждане. Което е едновременно добро и лошо, предполагам :/ Не бях много конкретен в обяснението си с него, поради което публикувах този въпрос, за да се уверя, че съм прав и защо това е така   -  person AyBayBay    schedule 07.01.2014


Отговори (3)


Търсенето в постоянно време на минимума и максимума се постига чрез съхраняване на препратки към най-левия и най-десния възли на RB-дървото в структурата на заглавката на картата. Ето коментар fот изходния код на RB- дърво, шаблон, от който се извлича изпълнението на std::set, std::map и std::multimap:

заглавната клетка се поддържа с връзки не само към корена, но също и към най-левия възел на дървото, за да се даде възможност за постоянно време begin(), и към най-десния възел на дървото, за да се даде възможност за линейно представяне на времето, когато се използва с алгоритмите за общ набор ( set_union и т.н.)

Компромисът тук е, че тези указатели трябва да се поддържат, така че операциите за вмъкване и изтриване ще трябва да извършат друга „операция за поддръжка“. Вмъкванията и изтриванията обаче вече се извършват в логаритмично време, така че няма допълнителни асимптотични разходи за поддържане на тези указатели.

person Sergey Kalinichenko    schedule 07.01.2014
comment
Така че, ако бях загрижен само за асимптотичното време на изпълнение, структурата на данните на картата на C++ има предимствата както на двоична купчина, така и на двоично дърво за търсене, що се отнася до времето на изпълнение на следните операции (findmax, findmin, изтриване, вмъкване, търсене) поради начина, по който се прилага и счетоводството/състоянието, което проследява в изпълнението на заглавката - person AyBayBay; 07.01.2014
comment
@AyBayBay Това е правилно: асимптотичната сложност ще бъде същата, въпреки че купчината може да бъде внедрена по по-икономичен начин (т.е. постоянният множител, факторизиран при изчисляването на голямото O, може да бъде по-нисък за купчината, отколкото за дървото). - person Sergey Kalinichenko; 07.01.2014
comment
уау, тогава бях прав, трябва да се науча да бъда по-уверен, когато съм прав, и да го обясня ясно, всичко, което ми казахте, е точно това, което подозирах. Благодаря! - person AyBayBay; 07.01.2014

Поне в типична реализация std::setstd::map) ще бъдат имплементирани като резбовано двоично дърво1. С други думи, всеки възел съдържа не само указател към неговите (до) две деца, но също така към предишния и следващия възел по ред. След това самият клас set има указатели не само към корена на дървото, но и към началото и края на списъка с нишки от възли.

За търсене на възел по ключ се използват нормалните двоични указатели. За преминаване на дървото в ред се използват указателите на резби.

Това наистина има редица недостатъци в сравнение с двоичната купчина. Най-очевидният е, че той съхранява четири указателя за всеки елемент от данни, където двоичната купчина може да съхранява само данни, без указатели (връзките между възлите са имплицитни в позициите на данните). В екстремен случай (напр. std::set<char>) това може да доведе до използване на много повече място за съхранение за указателите, отколкото за данните, които всъщност ви интересуват (напр. на 64-битова система, можете да получите 4 указателя от 64 бита всеки, за съхраняване на всеки 8-битов знак). Това може да доведе до лошо използване на кеша, което (от своя страна) намалява скоростта.

В допълнение, всеки възел обикновено ще бъде разпределен индивидуално, което може да намали локалността на препратката доста зле, като отново навреди на използването на кеша и допълнително намали скоростта.

Като такова, въпреки че резбованото дърво може да намери минимума или максимума, или да премине към следващия или предишния възел в O(1) и да търси всеки даден елемент в O(log N), константите могат да бъдат значително по-високи от това, същото с купчина. В зависимост от размера на елементите, които се съхраняват, общото използвано хранилище може да бъде значително по-голямо, отколкото при купчина (най-лошият случай е очевидно, когато във всеки възел се съхраняват само малко данни).


1. С прилагане на някакъв алгоритъм за балансиране - най-често червено-черно, но понякога AVL дървета или B-дървета. Могат да се използват произволен брой други балансирани дървета (напр. алфа-балансирани дървета, k-съседни дървета, двоични b-дървета, общи балансирани дървета).

person Jerry Coffin    schedule 07.01.2014
comment
Това беше много задълбочено, благодаря за отговора. Ще го имам предвид занапред. - person AyBayBay; 07.01.2014

Не съм експерт по картите, но връщането на първия елемент от карта ще се счита за нещо като „корен“. винаги има указател към него, така че времето за търсене ще бъде моментално. Същото ще важи и за BSTree, тъй като очевидно има основен възел, след това 2 възела извън него и т.н. ).

O(log(N)) обикновено се използва само за получаване на оценка на най-лошия сценарий. Така че, ако имате напълно небалансирано BSTree, всъщност ще имате O(N), така че ако търсите последния възел, трябва да направите сравнение с всеки възел.

Не съм много сигурен в последното ви твърдение, въпреки че картата е различна от самобалансиращо се дърво, те се наричат ​​AVL дървета (или така са ме учили). Картата използва „ключове“, за да организира обектите по определен начин. Ключът се намира чрез сериализиране на данните в число и числото в по-голямата си част се поставя в списък.

person user3164339    schedule 07.01.2014
comment
map в C++ е червеночерно дърво - person AyBayBay; 07.01.2014
comment
Предполагам, че това е терминологичен проблем от моя страна, извинявам се. Въпреки това, когато бях в училище (беше преди известно време), така внедрихме карта cplusplus. com/reference/map/map и доколкото знам, това не е RBTree, освен ако наистина не съм се объркал. Но ако си спомням правилно, RBTree беше дърво, където всеки възел имаше 3 подвъзела (отново паметта ми може да греши, в който случай ще изтрия този коментар) - person user3164339; 07.01.2014