{{TOC float}}

{{Sources| Tohle je poněkud obšírnější výcuc ke z , z různých zápisků k předmětu Datové struktury I, a výtahů k -- 00:13, 18 Aug 2010 (CEST)

}}

Binární vyhledávací stromy

Definice

Binární vyhledávací strom <math> T\,\!</math> reprezentující množinu prvků <math>S\,\;</math> z (uspořádaného) univerza <math>U\,\;</math> je úplný strom (tj. všechny vnitřní vrcholy mají 2 syny), ve kterém existuje bijekce mezi množinou <math> S\,\!</math> a vnitřními vrcholy taková, že pro <math> v\,\!</math> vnitřní vrchol stromu platí:

  • všechny vrcholy podstromů levého syna jsou <math> \leq v\,\!</math>

  • všechny vrcholy podstromů pravého syna jsou <math> >v\,\!</math>.

Listy reprezentují jednotlivé intervaly mezi vnitřními vrcholy. Můžeme je vynechat, ale s nimi je to (jak pro koho) logičtější.

Základní operace na stromech

  • MEMBER -- test, zda prvek <math>x\,\;</math> je obsažen ve stromě (vyhledání, zpravidla s využitím invariantu )

  • INSERT -- vložení prvku <math>x\,\;</math> do stromu

  • DELETE -- odebrání prvku <math>x\,\;</math> ze stromu

  • MIN, MAX, ORD -- nalezení prvního, posledního, <math> k\,\!</math>-tého největšího prvku

  • SPLIT -- rozdělení stromu podle <math> x\,\!</math>, které vyhodí, je-li ve stromě

  • JOIN -- spojení dvou stromů (jsou dvě verze, s přidáním prvku navíc, nebo bez něho)

Obecná (nevyvážená) implementace

  • INSERT: najít list reprezentující interval, kam vkládám, udělat z něj normální uzel se vkládanou hodnotou a dát mu dva listy s podintervaly.

  • DELETE: najdu vrchol, má-li jednoho syna-lista, pak druhý syn ho nahradí na jeho místě, jinak najdeme a dáme na jeho místo nejmenší větší vnitřní vrchol, jehož levý syn je list, a pravého syna tohoto vrcholu dáme na jeho místo.

  • SPLIT: procházím stromem a hledám <math> x\,\!</math>, ost. prvky házím cestou do dvou stromů <math> T_1,T_2\,\!</math>, ve kterých si vždy uchovávám ukazatel na list, místo kterého vkládám (odkrojím syna, ve kterém hledám dál, místo něj vložím list, na který si pamatuju ukazatel).

  • JOIN: s prvkem navíc, triviální -- spojím stromy jako 2 syny nového prvku.

Tato struktura sama nepodporuje efektivní ORD, je nutné přidat navíc položky, které určují počet listů v podstromu každého vrcholu. ORD je pak jen jde do pravých synů a přičítá levé podstromy když může, jinak jde do levého syna (a nepřičte nic).

Analýza algoritmů

Definujme si pomocné hodnoty <math> \lambda,\pi\,\!</math> jako hodnoty nejbližšího menšího (levějšího), resp. většího (pravějšího) prvku na vyšší úrovni, nebo <math> -\infty\,\!</math>, resp. <math> +\infty\,\!</math>, pokud tyto prvky neexistují.

Korektnost vyhledávání: Je-li <math> T'\,\!</math> podstrom <math> t\in T\,\!</math>, pak <math> T'\,\!</math> reprezentuje <math> S\cap(\lambda(t),\pi(t))\,\!</math> (a je to největší interval nezastoupený mimo <math> T'\,\!</math>). Pak pro vyhledání vrcholu <math> x\,\!</math> platí <math> \lambda(t)<x<\pi(t)\,\!</math>, vyšetřuji-li vrchol <math> t\,\!</math>.

Díky tomu je korektní MEMBER a INSERT. U DELETE musím dokázat korektnost případu s přehazováním vrcholů (dostávám bin. strom reprezentující <math> S\setminus\{x\}\,\!</math>).

Korektnost MIN, MAX, JOIN je zřejmá, u SPLIT plyne z korektnosti hledání a toho, že moje označené listy jsou nejlevější, resp. nejpravější.

Korektnost ORD plyne z toho, že v každém kroku je <math> k\,\!</math>-tý prvek představován tolikátým v pořadí vrcholů akt. vyšetřovaného podstromu, kolik mi zbývá přičíst.

Složitost: Zpracování 1 vrcholu je vždy <math> O(1)\,\!</math> a alg. se pohybuje po nějaké cestě od kořene k listu, která má <math> O(h)\,\!</math>, kde <math> h\,\!</math> je výška stromu.

Vyvažování

Chceme-li pro zachování efektivity operací zajistit, že výška bude <math> O(\log|S|)\,\!</math>, přidáme pro strom další podmínky, které bude muset splňovat a operace je zachovávat.

Pro vyvažovací operace, které se snaží zachovat logaritmickou výšku, se používá pomocný algoritmus ROTACE<math> (u,v)\,\!</math>:

  1. Vezmu <math> v\,\!</math>, jeho pravého syna <math> u\,\!</math> a podstromy (zleva) <math> A,B,C\,\!</math>.

  2. Přehodím <math> v\,\!</math> pod <math> u\,\!</math>, upravím ukazatel v otci a přeházím podstromy.

Existuje i symetrický případ, kdy se postupuje přesně opačným směrem. Někdy se této dvojici operací říká LL-ROTACE a RR-ROTACE.

Další potřebný algoritmus je DVOJROTACE<math> (u,v,w)\,\!</math>:

  1. Vezmu <math> u\,\!</math>, jeho levého syna <math> v\,\!</math> a pravého syna <math>v\,\;</math> -- <math> w\,\!</math>

  2. Seřadím je tak, že <math> w\,\!</math> je otec obou, <math> u\,\!</math> vpravo a <math> v\,\!</math> vlevo.

  3. Přitom opět upravím ukazatele v nadřízeném uzlu a přepojím podstromy.

Taky existuje symetrický případ. Jiné označení je LR-ROTACE a RL-ROTACE.

U obou operací lze aktualizovat i počty listů v podstromě a obě pracují v <math> O(1)\,\!</math>.

Alternativy k vyvažování

Je velká pravděpodobnost, že i bez vyvažování strom zůstane <math> O(\log|S|)\,\!</math> vysoký a operace na něm můžou tak (bez vyvažování) běhat i rychleji. Proto existují i pravděpodobnostní postupy, nahrazující vyvažování znáhodněním posloupností operací. Další možnost jsou samoopravující struktury -- operace samy bez dalších uchovávaných dat obstarávají vyvažování, existuje strategie, která zajistí dobré chování bez ohledu na data. Nebo se sleduje chování struktury, a když začne být příliš pomalá, vytvoří se nová -- vyvážená. Poslední možnost je upravit dat. strukturu podle známého pravděpodobnostního rozdělení dat.

AVL-stromy

AVL-stromy (Adel'son-Velskii, Landis) jsou nejstarší vyvážené stromy, dodnes oblíbené, jednoduše definované, ale detailně technicky složité.

Podmínka AVL pro vyvažování: Výška pravého a levého podstromu lib. vrcholu se liší max. o 1.

Definice: <math> \eta(v)\,\!</math> -- výška vrcholu (délka nejdelší cesty z vrcholu do listů), <math> \omega(v)\,\!</math> -- rozdíl výšek levého a pravého podstromu (<math> \in\{-1,0,1\}\,\!</math>). Uchovávat potřebuji jenom <math> \omega\,\!</math>.

Logaritmická výška

Výška celého stromu (<math> \eta(\,\!</math>kořen<math> )\,\!</math>) vychází z toho, že podstrom AVL stromu je vždy AVL strom. Vezmeme rekurzivní vztahy pro největší a nejmenší množinu uzlů v AVL stromu výšky <math> i\,\!</math>:

:Nejmenší: <math>mn(i)=mn(i-1)+mn(i-2)+1\,\!</math> :Největší: <math>mx(i)=2mx(i-1)+1\,\!</math>

Indukcí dokážeme, že <math> mx(i)=2^i-1\,\!</math> a <math> mn(i)=F_{i+2}-1\,\!</math>, kde <math> F_i\,\!</math> je <math> i\,\!</math>-té Fibonacciho číslo (pro ty platí vzorec <math> F_{i+2}=F_{i+1}+F_{i}\,\!</math>). Víme, že <math> \lim_{i\to\infty}F_i= \sqrt{5}(\frac{1+\sqrt{5}}{2})^{-i}\,\!</math> a z toho zlogaritmováním plyne pro AVL-strom o výšce <math> i\,\!</math> s <math> n\,\!</math> prvky:

:<math>\log\left(\frac{c_1}{\sqrt{5}}\right)+(i+2)\log\left(\frac{1+\sqrt{5}}{2}\right)<\log(n+1)<i\,\!</math>

A tedy <math> 0.69i<\log(n+1)<i\,\!</math>, takže <math> i=\Theta(\log n)\,\!</math>.

Operace na AVL stromech

Operace MEMBER je stejná jako pro nevyvážené.

INSERT se musí po běžném vložení zabývat vyvažováním. Jde zpět ke kořeni a hledá, který nejnižší vrchol <math> x\,\!</math> nemá po vložení vyvážené <math> \omega\,\!</math>, přičemž cestou upravuje <math> \omega\,\!</math>. Na vrcholu <math>x\,\!</math> se provede vhodná ROTACE nebo DVOJROTACE, což zajistí vyváženost (existuje několik podpřípadů).

Operace DELETE odstraní vrchol a pak vyvažuje podobně jako INSERT, ale potřebuje víc operací (až <math> O(\log|S|)\,\!</math> rotací). Asymptotická složitost je ale stejná -- logaritmická.

Červeno-černé stromy

Červeno-černý strom má tyto čtyři povinné vlastnosti:

  1. Každý uzel má definovanou barvu, a to černou nebo červenou.

  2. Každý list je černý.

  3. Každý červený vrchol musí mít oba syny černé.

  4. Každá cesta od libovolného vrcholu k listům v jeho podstromě musí obsahovat stejný počet černých uzlů. Pro červeno-černé stromy se definuje černá výška uzlu (<math> \mathbf{bh}(x)\,\!</math>) jako počet černých uzlů na nejdelší cestě od uzlu k listu.

Garantování výšky

Podstrom libovolného uzlu <math> x\,\!</math> obsahuje alespoň <math> 2^{\mathbf{bh}(x)}-1\,\!</math> interních uzlů. Díky tomu má červeno-černý strom výšku vždy nejvýše <math> 2\log(n+1)\,\!</math> (kde <math> n\,\!</math> je počet uzlů). (Důkaz prvního tvrzení indukcí podle <math> \mathbf{h}(x)\,\!</math>, druhého z prvního a třetí vlastnosti červeno-černých stromů)

Algoritmy

U algoritmů INSERT a DELETE jde také o vložení a následné vyvažování. Bez porušení vlastností červeno-černých stromů lze kořen vždy přebarvit načerno, můžeme pro ně předpokládat, že kořen stromu je vždy černý.

{{TODO|zkontrolovat operace, přepsat do formálních volání rotací}}

INSERT vypadá následovně:

  • Vložený prvek se přebarví načerveno.

  • Pokud je jeho otec černý, můžeme skončit -- vlastnosti stromů jsou splněné. Pokud je červený, musíme strom upravovat (předpokládejme, že otec přidávaného uzlu je levým synem, opačný připad je symetrický):

    • Je-li i strýc červený, přebarvit otce a strýce načerno a přenést chybu o patro výš (je-li děd černý, končím, jinak můžu pokračovat až do kořene, který už lze přebarvovat beztrestně).

    • Je-li strýc černý a přidaný uzel je levým synem, udělat pravou rotaci na dědovi a přebarvit uzly tak, aby odpovídaly vlastnostem stromů.

    • Je-li strýc černý a přidaný uzel je pravým synem, udělat levou rotaci na otci a převést tak na předchozí případ.

DELETE se provádí takto:

  • Skutečně odstraněný uzel (z přepojování -- viz ) má max. jednoho syna. Pokud odstraňovaný uzel byl červený, neporuším vlastnosti stromů, stejně tak pokud jeho syn byl červený -- to řeším přebarvením toho syna načerno.

  • V opačném případě (tj. syn odebíraného -- <math> x\,\!</math> -- je černý) musím udělat násl. úpravy (předp., že <math> x\,\!</math> je levým synem svého nového otce, v opačném případě postupuji symetricky):

    • <math> x\,\!</math> prohlásím za "dvojitě černý" ("porucha") a této vlastnosti se pokouším zbavit.

    • Pokud je (nový) bratr <math> x\,\!</math> (buď <math> w\,\!</math>) červený, pak má 2 černé syny -- provedu levou rotaci na rodiči <math> x\,\!</math>, prohodím barvy rodiče <math> x\,\!</math> a uzlu <math> w\,\!</math> a převedu tak situaci na jeden z násl. případů:

      • Je-li <math> w\,\!</math> černý a má-li 2 černé syny, prohlásím <math> x\,\!</math> za černý a přebarvím <math> w\,\!</math> načerveno, rodiče přebarvím buď na černo (a končím) nebo na "dvojitě černou" a propaguji chybu (mohu dojít až do kořene, který lze přebarovat beztrestně).

      • Je-li <math> w\,\!</math> černý, jeho levý syn červený a pravý černý, vyměním barvy <math> w\,\!</math> s jeho levým synem a na <math> w\,\!</math> použiji pravou rotaci, čímž dostanu poslední případ:

      • Je-li <math> w\,\!</math> černý a jeho pravý syn červený, přebarvím pravého syna načerno, odstraním dvojitě černou z <math> x\,\!</math>, provedu levou rotaci na <math> w\,\!</math> a pokud měl původně <math> w\,\!</math> (a <math> x\,\!</math>) červeného otce, přebarvím <math> w\,\!</math> načerveno a tohoto (teď už levého syna <math> w\,\!</math>) přebarvím načerno.

MIN a MAX jsou stejné jako pro nevyvážené.

JOIN (s prvkem navíc): mám-li černou výšku u obou stejnou, není co řešit, pokud ne, projdu po tom s větší <math> \mathbf{bh}(x)\,\!</math> do patra, kde se výšky rovnají, půjčím si přísl. podstrom a slepím s ním, vrátím celek zpátky a aplikuji vyvažování, jako kdybych vložil 1 prvek (poruším výšku max. o 1).

SPLIT: rozhazuji podstromy do zásobníků, odkud je pak slepuji operací JOIN.

Každý algoritmus pracuje jen s vrcholy na jedné cestě od kořene k listům a s každým dělá konstantně činností, takže všechny algoritmy mají logaritmickou složitost. DELETE volá max. 2 rotace nebo 1 rotaci a 1 dvojrotaci, INSERT zase max. 1 rotaci nebo dvojrotaci (i když přebarvovat můžou rekurzivně až do kořene).

Váhově vyvážené stromy (BB-&alpha;)

Dnes jsou už na ústupu, ale občas se ještě používají. Mějme <math> 1/4<\alpha<1-\sqrt{2}/2\,\!</math>, označme <math> p(T)\,\!</math> počet listů ve stromě <math> T\,\!</math>. Pak strom je BB-<math> \alpha\,\!</math>, když

:<math>\alpha\leq\frac{p(T_{\mbox{levý}(v)})}{p(T_v)}\leq 1-\alpha\,\!</math>

pro <math> T_v\,\!</math> jako podstrom určený (každým) vrcholem <math> v\,\!</math>. O BB-<math> \alpha\,\!</math> stromech platí, že:

:výška<math>(T)\leq 1+\frac{\log(n+1)-1}{\log{\frac{1}{1-\alpha}}}\,\!</math>

Takže jsou také vyvážené a operace mají zaručenou logaritmickou hloubku, vyvažuje se na nich také rotacemi a dvojrotacemi. Vždy totiž existuje <math> \alpha\leq d\leq1-\alpha\,\!</math> takové, že když mám strom, jehož oba podstromy splňují vlastnosti a navíc <math> p(T_l)/p(T)\leq \alpha\,\!</math> a <math> p(T_l)/p(T)-1\,\!</math> nebo <math> p(T_l)+1/p(T)+1\,\!</math> vyhovuje, vezmu <math> \rho=\frac{p(T')}{p(T_r)}\,\!</math>, kde <math> T'\,\!</math> je určen levým synem <math> T_r\,\!</math>, a pro <math> \rho\leq d\,\!</math> provedu ROTACE(<math> T\,\!</math>, <math> T_r\,\!</math>), jinak DVOJROTACE(<math> T\,\!</math>, <math> T_r\,\!</math>, <math> T'\,\!</math>) a dostanu BB-<math> \alpha\,\!</math> strom (bez důkazu). Opačný případ je popsaný symetricky.

Mají pěknou vlastnost, kvůli které se používaly: pro <math> \forall \alpha\,\!</math> existuje <math> c>0\,\!</math> takové, že každá posloupnost <math> k\,\!</math> operací INSERT a DELETE volá max. <math> c\cdot k\,\!</math> rotací a dvojrotací.

B-Stromy a jejich varianty

(a,b)-stromy

<math>(a,b)\,\!</math>-strom pro <math> a\leq b\,\!</math> přirozená je strom <math>T\,\;</math>, který splňuje následující podmínky:

  • každý vnitřní vrchol <math> v\,\!</math> stromu <math> T\,\!</math> různý od kořene <math> t\,\!</math> má alespoň <math> a\,\!</math> a nejvíc <math> b\,\!</math> synů

  • všechny cesty od kořene k listům mají stejnou délku

Tato definice je ale pro praktické účely příliš obecná -- budeme chtít navíc podmínky:

  • <math> a\geq 2\,\!</math> a <math> b\geq 2a-1\,\!</math>

  • kořen je buď list nebo má alespoň 2 a nejvíc <math> b\,\!</math> synů

Takový <math>(a,b)\,\!</math>-strom existuje pro každý přirozený počet listů, jeho výška je mezi <math> \log_b n\,\!</math> a <math> 1+\log_a(\frac{n}{2})\,\!</math>, tedy <math> O(\log n)\,\!</math>. Indukcí: strom o výšce <math> h\,\!</math> má <math> 2a^{h-1}\leq n\leq b^h\,\!</math> listů (přidáním <math> h\,\!</math>-té hladiny do stromu s <math> k\,\!</math> listy dostaneme strom s <math> ka\leq n\leq kb\,\!</math> listy.

Reprezentace množiny

Strom reprezentuje nějakou množinu <math> S\,\!</math> (prvků z univerza <math>U\,\!</math>), když mám bijekci mezi uspořádáním <math> S\,\!</math> a lexikografickým uspořádáním listů.

Každý vnitřní vrchol <math> v\,\!</math> obsahuje informaci o počtu synů <math> \rho(v)\,\!</math>, pole ukazatelů na syny <math> S_v\,\!</math> a pole <math> H_v\,\!</math> prvků z <math> U\,\!</math> takových, že <math> i\,\!</math>-tý je největší v <math> S\,\!</math> reprezentovaný v podstromě <math> i\,\!</math>-tého syna. Listy mají jen svůj prvek.

Pro každý prvek kromě největšího existuje vnitřní vrchol, který obsahuje jeho klíč, proto lze listy i vynechat a ukládat data ve vnitřních vrcholech (což ale není moc přehledné). Vrcholy můžou mít i odkaz na otce, nebo si otce můžu pamatovat při průchodech dolů (na vrcholech, ke kterým jsem nedošel od kořene, otce nepotřebuju).

Algoritmy

Máme pomocnou funkci VYHLEDEJ, který do hloubky projde stromem a vrátí nejbližší větší k nějakému prvku (nebo prvek sám, je-li ve stromě).

Základní operace:

  • MEMBER: přímo použije onu pomocnou operaci a pak zjistí, jestli našel, co hledal

  • INSERT: vyhledám místo kam, pokud tam prvek není, vytvořím nový list, připojím na správné místo do <math> S_v\,\!</math> a postupně nahoru štěpím, je-li potřeba, extrémně rozštěpím kořen.

  • DELETE: najdu prvek, najdu, kam je pověšený a to jedno políčko v <math> S_v\,\!</math> a <math> H_v\,\!</math> zruším, opravím <math> \rho\,\!</math>, pokud dostanu méně než <math> a\,\!</math> synů v uzlu, spojím s bezprostředním bratrem (má-li ten právě <math> a\,\!</math> synů), nebo přesunu nějaký list z bratra do mého uzlu.

Oba algoritmy pracují v <math> O(\log |S|)\,\!</math> v nejhorším případě.

JOIN (bez prvku navíc): Přepokládá <math> \max S_1<\min S_2\,\!</math>. Je-li <math> h(T_1)\geq h(T_2)\,\!</math>, najde v <math> T_1\,\!</math> hladinu o 1 nad připojení a v ní největší prvek / vytvoří nadkořen <math> T_1\,\!</math> v případě rovnosti, slije do něj prvky obou kořenů a případně provede štěpení. Jinak hledá a připojuje v <math> T_2\,\!</math>. Potřebuje čas <math> O(|h(T_1)-h(T_2)|)\,\!</math>.

SPLIT: Prochází postupně dolů, rozděluje uzly (podstromy s prvky <math> <x\,\!</math>, resp. <math> \geq x\,\!</math>) a hází výsledky do 2 zásobníků. Pokud oddělí více než 1 krajní prvek, hodí na zásobník strom, jehož kořen je právě oddělená část uzlu, jinak na zásobník dává podstrom onoho krajního prvku. Tak pokračuje až k listům, pokud tam najde přímo <math> x\,\!</math>, tak ho vyhodí. Stromy ze zásobníků spojí postupným voláním JOIN -- v 1 zásobníku jsou max. 2 stromy stejné výšky, celkem jich je <math> k\leq 2\log_a |S|\,\!</math> a jejich zpracování trvá <math> O(\sum_{i=1}^{k-1}(h(T_i)-h(T_{i+1})+1))=O(h(T_1)+k)\,\!</math>.

ORD: S takovouto reprezentací efektivně nejde, proto musíme navíc <math> \forall\,\!</math> uzel udržovat pole <math> P_v\,\!</math> s počty vrcholů v jeho jednotlivých podstromech a při vkládání a odebírání ho průběžně aktualizovat. Pak v ORD procházím do hloubky a postupně přičítám velikosti přeskočených podstromů (pokud bych se přičtením dalšího dostal k <math> 0\,\!</math>, jdu na nižší hladinu).

Implementace

  • Pro vnitřní paměť se doporučuje <math> a=2-3\,\!</math> a <math> b=2a\,\!</math>, pro vnější <math> a\approx 100\,\!</math> a <math> b=2a\,\!</math> (tj. v obou případech vlastně ).

  • Při přístupu více uživatelů ke struktuře je problém s aktualizačními operacemi -- zamykání celého stromu není efektivní: použije se vyvažování shora dolů: algoritmus INSERT zamkne uzel, jeho otce a syny. Pak pokud je počet synů <math> =b\,\!</math>, rozštěpí ho (předem), nebude se pak už štěpit zpátky.

    • Aby tohle fungovalo (abych měl <math> \geq a\,\!</math> synů všude), je nutné <math> b\geq 2a\,\!</math>.

    • Podobně funguje DELETE -- najde-li uzel s <math> a\,\!</math> syny, provede "preventivně" slití nebo přesun.

    • Provádí se tak víc slití a štěpení než v původní variantě, ale asymptoticky je to furt stejné.

    • Pro takovéto struktury na externí paměti se doporučuje <math> a\approx 100\,\!</math> a <math> b=2a+2\,\!</math>.

{{TODO|přesunout do externích dat. struktur ?}}

B-Stromy

B-strom řádu <math>m\,\!</math> je vlastně <math>(a,b)\,\!</math> strom pro <math>a = \frac{m}{2}\,\!</math> a <math>b = m\,\;</math>. V určitých implementacích se ovšem data nacházejí už ve vnitřních vrcholech, potom má každý uzel vždy o 1 méně datových záznamů než potomků. Pokud jsou data uložena až v listech, jedná se o Redundantní B-strom.

Implementační detaily:

  • Někdy jdou z uzlů na data jen pointery, listy můžou mít jinou (jednodušší) dat. strukturu než vnitřní uzly.

  • Pro implementaci je vhodné pamatovat si celou aktuálně procházenou větev v nějakém bufferu.

  • V redundantních stromech nemusím při odstranění dat odstraňovat klíč ve vnitrnich uzlech (lze podle toho hledat i kdyz to tam neni).

  • Vylepšení -- vyvažování stránek-- při přetečení stránky se nejdřív dívám, jestli není volno v sousedních. Pokud ano, přerozdělím a upravím klíče -- zaručuje lepší zaplnění, ale je pomalejší. Podobně je možné vyvažovat počty se sousedy v případě vyhazování (i když nemerguju).

Další varianty

B* stromy -- Na základě vyvažování stránek zpřísníme podmínky na počet uzlů: Kořen ma min. 2 potomky, ost. uzly minimálně <math> \lceil(2m-1)/3\rceil\,\!</math> potomků, všechny větve jsou stejné dlouhé. Štěpení se odkládá, dokud nejsou sourozenci plní, potom se štěpí buď 2 do 3 (jen s jedním sourozencem), nebo 3 do 4 (s oběma) uzlů. Při odebírání se slévají 3 uzly do 2 (nebo 4 do 3). Štěpení a slévání jde zesložitit ještě na víc stránek.

Odložené štěpení -- používá stránku přetečení, vkládá znova, až když se naplní. Stránka přetečení může být jedna pro jeden listový uzel, nebo ji může sdílet nějaká skupina listů -- štěpí se, až když jsou všechny listy i přetečení zaplněné. Tedy pokud má strom víc než 1 úroveň, má všechny listy zaplněné (za předpokladu nepoužití DELETE). S odebíráním musím i slévat a štěpit skupiny -- jejich velikost není pevná.

Prefixové stromy -- pro redundantní B-stromy; klíče jsou co nejkratší řetězce nutné k odlišení listů, nikoliv celé hodnoty, které se nacházejí až v listech. Při vkládání a štěpení stránek se nějakou heuristikou hledá nejkratší prefix, který by dvě vznikající stránky oddělil. Mazaní a slévání -- žádná změna. Další zkrácení -- u potomků se neopakuje předpona klíče, kterou ma rodič -- to ale hodně zvýší nároky na CPU.

B+ stromy -- pro intervalové dotazy: zrychlení tím, že zřetězíme vždy uzly v jedné hladině (a nebo jenom listy), tj. přidáme do uzlů ukazatele na levého a pravého souseda.

Hladinově propojené (a,b)-stromy s prstem (Finger trees) -- pro vyhledávání navíc ještě přidáme odkaz na otce do každého uzlu a pro celou strukturu jeden "prst" -- odkaz na nějaký list. Vyhledávání začíná od prstu a postupuje nahoru, dokud nenajde podstrom, v němž by měl být hledaný prvek; potom se spustí dolů. Pokud je prvek poblíž prstu, je to rychlejší než klasická varianta. Typicky máme funkci nastevní prstu na nějaký prvek a pokud se motáme v jeho okolí, vyjde to lépe.

Proměnná délka zaznamů -- modifikace pro záznamy různé délky: neštěpit podle počtu záznamů, ale na zhruba 1/2 podle velikosti. Podmínka existence uzlu: součet délek záznamů v něm je <math> \geq B/2\,\!</math> kde <math> B\,\!</math> je délka uzlu(stránky) (pro B* stromy <math> 2B/3\,\!</math>). Problémy: dlouhé klíče mají tendenci propadávat ke kořeni, tím se zmenšuje arita stromu; může se 1 stránka štěpit i na 3 (pokud vkládám záznam delší nez 1/2 stránky); vložením záznamu může dojít ke zmenšení stromu (jak, to se ve skriptech nepíše :( ) Nejde vyrobit nezávislé INSERT a DELETE, řešení: univerzální alg. nahrazování řetězce řetězcem, INSERT a DELETE jsou jeho spec. příp. Řešeni snižování arity stromu: minimalizace délky klíčů (nalezeni klíče min. délky, která navíc splňuje min. naplněni) - pro B* stromy docela složité.

Amortizované odhady počtů štěpení a slití a vyvážení

{{Sources|Důkaz je podle knihy K. Mehlhorn: Algorithms and Data Structures - The Basic Toolbox (Springer 2008). -- 20:35, 1 Sep 2010 (CEST)}}

Obecně může INSERT volat až <math> \log(|S|)\,\!</math>-krát štěpení a DELETE <math> \log(|S|)\,\!</math>-krát slití a jedno vyvážení (přesun). Začínáme-li s prázdným stromem a měříme na nějaké posloupnosti <math>n</math> operací, zjistíme, že jde amortizovaně o <math> O(1)\,\!</math>.

Důkaz pro <math>(2,4)</math>-stromy:

  • Použijeme bankovní metodu, kdy INSERT bude stát dvě jednotky a DELETE jednu.

  • Za štěpení a slití pak budeme platit vždy jednu jednotku, vyvážení nebude stát nic, protože je v každém DELETE voláno max. jednou a asymptoticky nic nezkazí.

  • V jednotlivých uzlech stromu budeme udržovat následující počty jednotek podle stupně uzlů (přidáváme i stupně 1 a 5, které mají uzly těsně před štěpením a sléváním):

  • Potom INSERT a DELETE bez štěpení díky své ceně udržují (občas i přeplácí) správné stavy jednotek v uzlech -- snížení stupně stojí max. 1 a zvýšení max. 2 jednotky.

  • Dojde-li ke štěpení uzlu stupně 5 do uzlů stupňů 3 a 2, čtyři jednotky se zaplatí: jedna za rozdělení, jedna na účet nového uzlu stupně 2 a (max.) dvě za zvýšení stupně rodiče.

  • Dojde-li k vyvažování (přesunu), máme 2 jednotky, což nám stačí k vytvoření dvou uzlů stupně 2 ze stupňů 1 a 3 a přebývá nám při vyvážení uzlů stupňů 1 a 4.

  • Sléváme-li uzly stupně 1 a 2, máme 3 jednotky celkem: jednou zaplatíme vlastní slití, jednou snížení stupně rodiče a jedna zbyde.

Protože všechny možnosti zachovávají invariant, je vidět, že celkem bude max. <math>2n</math> operací slití a štěpení. Důkaz (prý) jde rozšířit i na libovolné <math>a</math> a <math>b\geq 2a</math> (podle mě by mělo stačit zachovat ceny za operace a počty jednotek v krajních případech).

Pro <math> b=2a-1\,\!</math> lze bohužel jednoduše nalézt takové posloupnosti operací, kde počet slití a štěpení je <math> O(n\log n)\,\!</math>, to samé, máme-li paralelní operace při <math> b=2a+1\,\!</math>. Proto se doporučuje <math> 2a\,\!</math>, resp. <math> 2a+2\,\!</math>.

V hladinově propojeném stromě platí, že posloupnost <math> n\,\!</math> operací MEMBER, INSERT, DELETE a PRST vyžaduje <math> O(\log n+\,\!</math> čas na vyhledání prvků <math>)\,\!</math>.

Trie

{{Sources|

Sekce o Trie je popsaná podle Koubkových skript, a knihy K. Mehlhorna -- 10:43, 22 Aug 2010 (CEST) }}

Trie je vlastně stromová reprezentace slovníku. Její označení zřejmě pochází od slova "retrieval". Jejím úkolem je reprezentovat množinu <math>S\subseteq U\,\!</math>, kde <math>U\,\!</math> je tvořeno všemy slovy nad abecedou <math>\Sigma, |\Sigma|=k\,\!</math> o délce <math>l\,\!</math>. Na této množině budeme provádět operace MEMBER, INSERT a DELETE.

Požadavek na délku se použije pouze u odhadů na složitost algoritmů. Ve skutečnosti ale není omezující -- slova můžeme vždycky doplnit nějakými znaky mezery nebo lehce algoritmy upravit, aby s kratšími slovy počítaly.

Základní varianta

Definice

Trie je strom takový, že každý vnitřní vrchol má <math>k\,\!</math> synů, odpovídajících všem znakům abecedy. Každému vrcholu lze rekurzivně přiřadit slovo nad abecedou <math>\Sigma\,\!</math> následujícím způsobem:

  • Kořeni patří prázdné slovo <math>\lambda\,\!</math>.

  • <math>a\,\!</math>-tému synu patří slovo otce doplněné o <math>a\,\!</math> (kde <math>a\,\!</math> je libovolné písmeno).

Pro každý vnitřní vrchol trie musí platit, že tento vrchol je prefixem nějakého slova z reprezentované množiny <math>S\,\!</math>. Každý list obsahuje jeden bit, který udává přítomnost nebo nepřítomnost slova, které představuje, v množině <math>S\,\!</math>.

Je vidět, že taková struktura je dost paměťově náročná -- každý vrchol potřebuje paměť <math>O(k)\,\!</math> a celkem máme aspoň tolik vrcholů, co bodů množiny, násobeno délkou cesty k nim, tedy <math>O(kl|S|)\,\!</math>.

Operace

MEMBER je velice jednoduchý -- postupně sestupuje stromem podél syna, který odpovídá <math>i\,\!</math>-tému písmenu hledaného slova v <math>i\,\!</math>-tém kroku. Pokud se dostane do listu dřív než dojde na konec slova, skončí neúspěchem. Jinak vrátí informaci o přítomnosti slova z listu, do kterého se dostal.

INSERT dojde do listu podobně jako MEMBER. Potom (je-li to potřeba) mění listy na vnitřní vrcholy a vkládá pokračování cesty až do dosažení délky slova. V posledním kroku upraví indikaci v listu.

DELETE vyhledá prvek a nastaví indikaci v jeho listu na FALSE. Pak se postupně vrací a dokud nalézá jen samé listy s FALSE, zruší celý vrchol a změní ho na list s FALSE.

Algoritmus MEMBER projde až <math>l\,\!</math> vrcholů a každý v konstantním čase (vrcholy se indexují přímo písmeny abecedy), tedy je <math>O(l)\,\!</math>. Algoritmy INSERT a DELETE vyžadují čas <math>O(kl)\,\!</math>, protože úprava jednoho vrcholu vyžaduje až <math>k\,\!</math> operací.

Komprimované Trie

Trie upravíme tak, že vyházíme vrcholy, jejichž slovo je prefixem stejné množiny slov jako slovo jejich otců (tj. vrcholy, kde nedochází k žádnému větvení a je jen jedna možnost pokračování). Ve vrcholech si teď ale místo toho musíme udržovat informaci o aktuální hloubce <math>\kappa(v)\,\!</math> a navíc v listech musíme držet celé slovo, které reprezentují (abychom neztratili písmena z vrcholů, které jsme vyházeli).

Operace pak bude třeba upravit, ale protože takto upravené trie má jen <math>S-1\,\!</math> vnitřních vrcholů, paměťová náročnost klesla na <math>O(k|S|)\,\!</math>

Operace

MEMBER pracuje podobně, ale ve vrcholu <math>v\,\!</math> vždy testuje <math>\kappa(v) + 1\,\!</math>-ní písmeno hledaného slova. Nakonec (protože neotestoval všechna písmena a mohlo by tedy dojít ke kolizi) navíc porovná slovo uložené ve vrcholu se slovem, které jsme hledali.

INSERT pro nějaké slovo <math>x\,\!</math> opět vyhledá místo pro vložení a dojde do listu, kde najde jiné slovo <math>y\,\!</math>. Pak vezme největší společný prefix slov <math>x,y\,\!</math> a v jeho místě strom rozdělí -- pokud ve správné hloubce už je vnitřní vrchol, pokračuje z něj, jinak nový dělící vrchol přidá. Potom upraví hodnoty v listech.

DELETE zruší informaci o mazaném slově stejně jako předtím. Navíc ale pokud zjistí, že otec "vyčištěného" listu v hierarchii stromu má jen jednoho dalšího potomka -- vnitřní uzel, nacpe tohoto potomka na jeho místo.

Je vidět, že INSERT a DELETE změní maximálně jeden vnitřní vrchol a pracují tak v <math>O(k+l)\,\!</math>.

Očekávaná hloubka

Odhady časů pro operace MEMBER, INSERT a DELETE závisí na (maximální) délce slov <math>l\,\!</math>, ale takové hloubky komprimované trie dosáhne jen v nejhorším případě. Chceme proto odhad očekávané hloubky, za předpokladu, že reprezentovaná množina <math>S\,\!</math> je vzorkem dat z rovnoměrného rozdělení (což bývá často přibližně splněno).

Označíme <math>q_d\,\!</math> pravděpodobnost, že komprimovaný trie nad množinou <math>S, |S| = n\,\!</math> má hloubku aspoň <math>d_n = d\,\!</math>. Pak je naše očekávaná (střední) hodnota hloubky: :<math>E(d_n) = \sum_{i=1}^{\infty} i(q_i - q_{i+1}) = \sum_{i=1}^{\infty} q_i\,\!</math>

Odhadneme proto velikost <math>q_d\,\!</math>. Víme, že hloubka trie je menší než <math>d\,\!</math>, když prefixy o délce <math>d\,\!</math> slov z naší množiny rozlišují tato slova jednoznačně. Pravděpodobnost, že toto nastane, je (počet jednoznačných prefixů a k nim počet libovolných doplnění do délky <math>l\,\!</math>, děleno počtem jednoznačných slov délky <math>l\,\!</math>, vše nad abecedou o velikosti <math>k\,\!</math>):

<math>P(</math>jednoznačné rozlišení o délce <math>d\,\!</math>) <math> = \frac{\mathbf{}\binom{k^d}{n}k^{n(l-d)}}{\binom{k^l}{n}}\,\!</math>

Z toho:

<math>q_d \leq 1 - P(</math>jednoznačné rozlišení o délce <math>d-1) = </math> <math>1 - \frac{\mathbf{}\binom{k^{(d-1)}}{n}k^{n(l-d+1)}}{\binom{k^l}{n}} \leq 1 - \frac{\left(\prod_{i=0}^{n-1}(k^{d-1}-i)\right)k^{n(l-d+1)}}{k^{nl}} = 1 - \prod_{i=0}^{n-1}\left(1 - \frac{i}{k^{d-1}}\right) \leq 1 - \exp\left(\frac{-n^2}{k^{d-1}}\right) \leq \frac{n^2}{k^{d-1}}</math>

Poslední kroky jsme mohli udělat, protože platí (integrál s nerovností můžeme použít, protože daný logaritmus je klesající, při výpočtu integrálu použijeme substituci za <math>1-\frac{x}{k^{d-1}}\,\!</math>): :<math>\prod_{i=0}^{n-1}\left(1 - \frac{i}{k^{d-1}}\right) = \exp\left(\sum_{i=0}^{n-1}\ln\left(1-\frac{i}{k^{d-1}}\right)\right) \geq</math> <math>\geq \exp\left(\int_{0}^{n}\ln\left(1-\frac{x}{k^{d-1}}\right) \mathrm{d} x\right) = \exp\left((n-k^{d-1})\ln\left(1-\frac{n}{k^{d-i}}\right) - n\right) \leq \exp\left(\frac{-n^2}{k^{d-1}}\right)\,\!</math>

Očekávaná výška stromu pak vyjde, položíme-li <math>c = 2\lceil \log_k n\rceil\,\!</math>:

:<math>\sum_{i=1}^{\infty} q_i = \sum_{i=1}^c q_i + \sum_{i=c+1}^{\infty} q_i \leq \sum_{i=1}^{c} 1 + \sum_{i=c+1}^{\infty} \frac{n^2}{k^{i-1}} = c + \frac{n^2}{k^c}\left(\sum_{i=0}^{\infty}\frac{1}{k^i}\right) \leq 2\lceil \log_k n \rceil + \frac{1}{1-\frac{1}{k}}\,\!</math>

Trie v tabulce

Pokud se vzdáme operací INSERT a DELETE, můžeme trie reprezentovat na extrémně zcvrklém prostoru. Nejdříve si ho představme jako matici <math>M\,\!</math> dimenze <math>r\times s\,\!</math>, kde každý vnitřní vrchol odpovídá jednomu řádku a sloupce jsou písmena abecedy. Potom na pozici <math>M(v,a)\,\!</math> je <math>a\,\!</math>-tý syn vrcholu <math>v\,\!</math>. Pole matice může obsahovat buď odkazy na další vrcholy (identifikátor řádku), nebo přímo slova, která jsou obsažena v reprezentované množině, nebo prázdnou hodnotu null. Ve vedlejším poli si musíme uchovat i hloubky vrcholů odpovídající nekomprimovanému trie -- <math>\kappa(v)\,\!</math>.

Komprese matic -- uložení do pole

Hodnoty null ale nepřinášejí novou informaci -- stačí při průchodu maticí na další vrcholy testovat, zda nám stoupá hodnota <math>\kappa(v)\,\!</math> a nakonec provést test shody s nalezeným prvkem. Díky tomu můžeme na místa, kde je null, v klidu ukládat něco jiného.

Matici <math>M\,\!</math> tak můžeme reprezentovat dvěma poli

  • <math>VAL\,\!</math> -- v něm budou hodnoty z různých řádků matice

  • <math>RD\,\!</math> ("row displacement", "posunutí řádku") -- bude udávat, kde začíná který řádek původní matice <math>M\,\!</math> ve <math>VAL\,\!</math>

Jednotlivé datové řádky původní matice se v poli <math>VAL\,\!</math> v klidu můžou překrývat, pokud překryté hodnoty jsou jen null. Formálně musíme zachovat, že když <math>M(i,j)\,\!</math> je definováno, pak <math>M(i,j) = VAL(RD(i) + j)\,\!</math> a že když <math>M(i,j)\,\!</math> a <math>M(i',j')\,\!</math> jsou definovány pro <math>(i,j)\neq (i',j')\,\!</math> , pak <math>RD(i) + j \neq RD(i') + j'\,\!</math>.

Pro nalezení "dobrého" rozložení řádků původní matice do pole <math>VAL\,\!</math> se používá algoritmus First-Fit Decreasing:

  • Pro každý řádek původní matice <math>M\,\!</math> spočteme, kolik míst je non-null a setřídíme řádky matice podle této hodnoty v klesajícím pořadí

  • Bereme řádky podle setřídění a vkládáme je na první místo od začátku pole <math>VAL\,\!</math> tak, že neporušují výše uvedené podmínky.

Označíme počet všech non-null hodnot jako <math>m\,\!</math> a počet non-null hodnot v řádcích s alespoň <math>l\,\!</math> non-null hodnotami jako <math>m_l\,\!</math>. Pokud řádky matice <math>M\,\!</math> splňují pravidlo harmonického rozpadu, tj. <math>\forall l: m_l\leq \frac{m}{l+1}\,\!</math> (tj. např. více než polovina řádků obsahuje jen jednu skutečnou hodnotu), pak pro každý řádek <math>i\,\!</math> platí <math>RD(i)<m\,\!</math> a algoritmus stavby polí potřebuje <math>O(rs+m^2)\,\!</math> času (důkaz je hnusný).

Posouvání sloupců

Protože podmínku harmonického rozpadu splňuje jen málo matic, upravíme si obecné matice tak, aby ji splňovaly taky. Využijeme toho, že matici trochu "natáhneme" do počtu řádků (tím se, pravda, zvětší pole <math>RD\,\!</math>) a jednotlivé sloupce v ní rozstrkáme tak, aby v jednom řádku nevyšlo moc zaplněných míst najednou. Kde začíná který sloupec si zapamatujeme v dalším pomocném poli <math>CD\,\!</math> ("column displacement", "posunutí sloupce").

"Dobré" posunutí sloupců nalezneme obyčejným přístupem First-Fit, když pro každý sloupec <math>j\,\!</math> nalezneme nejmenší číslo <math>CD(j)\,\!</math> splňující:

:<math>m(j+1)_l\leq \frac{m}{f(l,m(j+1))}\ \forall l = 0,1,\dots\,\!</math> Hodnota <math>m(j)\,\!</math> je počet všech zaplněných míst v prvních <math>j\,\!</math> sloupcích právě konstruované matice a <math>m(j)_l\,\!</math> je počet zaplněných míst v řádcích s alespoň <math>l\,\!</math> zaplněnými místy.

Pozorování:

  • Je vidět, že každá funkce <math>f\,\!</math> musí splňovat <math>f(0,m(j))\leq \frac{m}{m(j)}\ \forall j\,\!</math>, protože jinak by v algoritmu nemohla být splněna testovaná podmínka pro <math>l=0\,\!</math> (protože <math>m_j = m(j)_0\,\!</math>).

  • Dále musí funkce <math>f\,\!</math> splňovat nerovnost <math>f(l,m)\leq l+1\ \forall l\,\!</math>, aby výsledná matice splňovala podmínku harmonického rozpadu.

Dá se ukázat (a je to hnusný důkaz), že vhodná funkce je třeba <math>f(x,y) = 2^{x(2-\frac{y}{m})}\,\!</math>, protože splňuje obě podmínky a navíc výsledný vektor <math>CD\,\!</math> má délku <math>s\,\!</math>, vektor <math>RD\,\!</math> má délku menší než <math>4m\log\log m + 15.3m + r\,\!</math> a vektor <math>VAL\,\!</math> má délku menší než <math>m+s\,\!</math>. Protože hodnoty <math>CD\,\!</math> indexují <math>RD\,\!</math> a hodnoty <math>RD\,\!</math> indexují <math>VAL\,\!</math>, plynou z toho omezení i na hodnoty v nich uložené.

Čas celého algoritmu vytváření matic je <math>O(s(r+m\log\log m)^2)\,\!</math>.

Další komprese vektoru RD

Protože <math>M\,\!</math> má jen <math>m\,\!</math> definovaných mít, z algoritmu pro výpočet <math>RD\,\!</math> plyne, že jen max. <math>m\,\!</math> míst v tomto vektoru bude různých od nuly. Proto můžeme použít následující kompresi (řekněme, že nenulových míst je <math>t\,\!</math>):

  1. Vektor <math>RD\,\!</math> rozdělíme na <math>n\,\!</math> bloků o délce <math>d\,\!</math>.

  2. Vytvoříme nový vektor <math>CRD\,\!</math> o délce <math>t\,\!</math>, který obsahuje jen nenulové prvky původního vektoru. Označme jejich původní pozice <math>i_j, j = 0\dots t-1\,\!</math> a jejich pozice ve vektoru <math>CRD\,\!</math> jako <math>v(i_j)\,\!</math>.

  3. Vytvoříme vektor <math>BASE\,\!</math> o délce <math>n\,\!</math>, kde <math>BASE(x) = \begin{cases}-1 & i_j \div d \neq x\ \forall j = 0,\dots t-1 \\ \min\{l; i_l\div d = x\} & \mbox{jinak}\end{cases}\,\!</math>{{ref|1}}

  4. Vytvoříme matici <math>OFFSET\,\!</math> typu <math>n\times d\,\!</math>, kde <math>OFFSET(x,y) = \begin{cases} -1 & x\cdot d + y \neq i_j\ \forall j \\ j - BASE(x) & x\cdot d + y = i_j \end{cases}\,\!</math>

  5. Uložíme matici <math>OFFSET\,\!</math> do vektoru <math>OFF\,\!</math> dimenze <math>n\,\!</math> tak, že z každého řádku vytvoříme číslo v soustavě o základu <math>d+1\,\!</math>: <math>OFF(x) = \sum_{k=0}^{d-1}(OFFSET(x,k)+1)(d+1)^{k}\,\!</math>.

Potom platí, že:

  • <math>v(h) = 0 \Leftrightarrow OFFSET(h\div d, h\mod d) = -1\,\!</math>

  • <math>v(h) = 1 \Rightarrow h = BASE(h\div d) + OFFSET(h\div d, h\mod d)\,\!</math>

  • <math>OFFSET(i,j) = ((OFF(i)\div (d+1)^j)\mod (d+1)) -1\,\!</math>

Celá tahle legrace má smysl, jen pokud <math>d\ll n,\;</math> a <math>t<n\,\!</math>. Když <math>d\leq \lceil \log\log n\rceil\,\!</math>, pak lze celé trie uložit pomocí pěti vektorů dimenze <math>n\,\!</math> s hodnotami menšími než <math>4n\log\log n\,\!</math>.


{{note|1|Funkce <math>\div\,\!</math> tu označuje celočíselné dělení (podobné operaci div z Pascalu).}}

Haldy

Haldy se používají pro měnící se uspořádané množiny. Nevyžaduje se efektivní operace MEMBER (často se předpokládá s argumentem operace informace o uložení prvku). Požadují se malé nároky na paměť a rychlost ostatních operací.

Definice, operace

Halda je stromová struktura nad množinou (dat) <math>S\,\!</math>, jejíž uspořádání je dáno funkcí <math> f:S\to \mathbb{R}\,\!</math>, splňující lokální podmínku haldy:

:<math> \forall v\in S: f(\,\!</math>otec<math> (v))\leq f(v)\,\!</math>, případně v duální podobě. Množina je reprezentovaná haldou, když přiřazení prvků vrcholům haldy je bijekce, splňující podmínku haldy. Různé druhy hald se liší podle dalších podmínek, které musí splňovat stromové struktury.

  • Krom běžných operací můžu měnit uspořádání: operace INCREASE a DECREASE změní velikost <math> f\,\!</math> na nějakém daném prvku <math> s\,\!</math> se známým uložením o <math> +a\,\!</math>, <math> -a\,\!</math>.

  • Další operace: DELETEMIN -- smazání prvku s nejmenší hodnotou <math> f\,\!</math>.

  • Pro operaci DELETE budeme požadovat přímé zadání uložení prvku.

  • Navíc definujeme operaci MAKEHEAP -- vytvoření haldy při známé množině a <math> f\,\!</math>,

  • a MERGE -- slití dvou hald do jedné, reprezentující <math> S_1 \cup S_2\,\!</math> a <math> f_1\cup f_2\,\!</math>, aniž by se ověřovala disjunktnost.

Regulární haldy

Pro <math> d\,\!</math>-regulární strom (<math> d\in\mathbb{N}\,\!</math>) s kořenem <math> r\,\!</math> platí, že existuje pořadí synů vnitřních vrcholů takové, že očíslování prohledáváním z <math> r\,\!</math> do šířky splňuje:

  1. každý vrchol má nejvýše <math> d\,\!</math> synů

  2. když vrchol není list, tak všechny vrcholy s menším číslem mají právě <math> d\,\!</math> synů

  3. má-li vrchol méně než <math> d\,\!</math> synů, pak všechny vrcholy s většími čísly jsou listy

Potom takový strom s <math> n\,\!</math> vrcholy má max. jeden ne-list, který nemá právě <math> d\,\!</math> synů, jeho výška je <math> \lceil\log_d(n(d-1)+1)\rceil\,\!</math>. Čísla synů vrcholu s číslem <math> k\,\!</math> jsou <math> (k-1)d+2,\dots,kd+1\,\!</math>, číslo otce je <math> 1+\lfloor\frac{k-2}{d}\rfloor\,\!</math>. Takto vytvořená halda umožňuje i efektivní reprezentaci v poli.

Operace na regulárních haldách

  • Není známa efektivní operace MERGE.

  • Máme pomocné operace UP, DOWN, posunující prvek níž/výš ve struktuře, dokud není splněna podmínka haldy ("probublávání").

  • INSERT jen vloží nový prvek za poslední a spustí UP

  • DELETE nahradí odstraněný prvek posledním listem a volá UP nebo DOWN podle potřeby

  • DELETEMIN odstraní kořen, nahradí ho posl. listem a volá DOWN

  • MIN jen vrátí kořen

  • INCREASE a DECREASE změní hodnotu <math> f\,\!</math> nějakého prvku a zavolají DOWN, resp. UP (pozor, je to naopak, než názvy napovídají).

  • Operace MAKEHEAP vytvoří libovolný strom a pak postupně od posledního ne-listu ke kořeni volá na všechno DOWN.

U všech operací je korektnost zajištěna podmínkou haldy (a tím, že UP a DOWN zaručí její splnění).

Složitost operací

Běh DOWN vyžaduje <math> O(d)\,\!</math> a UP <math> O(1)\,\!</math> v každém cyklu, takže celkem jde o <math> O(d\log|S|)\,\!</math> a <math> O(\log|S|)\,\!</math>.

Haldu lze vytvořit opakovaným INSERTem v čase <math> |S|\log|S|\,\!</math>, ale pro větší množiny je rychlejší MAKEHEAP -- uvažujeme-li, že operace DOWN vyžaduje čas odpovídající výšce vrcholu. Ve výšce <math>k-i\,\;</math> je <math>d^i\,\;</math> vrcholů. Tím dostávám celkový čas <math> O(\sum_{i=0}^{k-1}d^i(k-i)d)\,\!</math>, což se dá odhadnout jako <math> O(d^2|S|)\,\!</math>.

Aplikace

Heapsort -- vytvoření haldy a postupné volání MIN a DELETEMIN. Lze ukázat, že pro <math> d=3,d=4\,\!</math> je výhodnější než <math> d=2\,\!</math>, empiricky je do cca <math> 1 000 000\,\!</math> prvků <math> d=6\,\!</math> nebo <math> d=7\,\!</math> nejlepší. Pro delší posloupnosti je možné <math> d\,\!</math> zmenšit.

Dijkstra -- normální Dijkstrův algoritmus, jen vrcholy grafu uchovávám v haldě, tříděné podle aktuálního <math> d\,\!</math> (horního odhadu vzdálenosti). Složitost <math> O((m+n)\log n)\,\!</math>, pro <math> d=\max\{2,\frac{m}{n}\}\,\!</math> je to <math> O(m\log_d n)\,\!</math> a pro husté grafy (<math> m>n^{1+\varepsilon}\,\!</math>) je lineární v <math> m\,\!</math>.

Leftist haldy

Leftist halda je binární strom (<math> T,r\,\!</math>). Označme npl(<math> v\,\!</math>) délku nejkratší cesty z <math> v\,\!</math> do vrcholu s max. 1 synem. Leftist halda musí splňovat následující podmínky:

  1. má-li vrchol 1 syna, pak je vždy levý

  2. má-li 2 syny <math> l,p\,\!</math> , pak npl(<math> p\,\!</math>)<math> \leq\,\!</math>npl(<math> l\,\!</math>)

  3. podmínka haldy na klíče prvků (ex. přiřazení prvků vrcholům stromu)

Pro leftist haldu se definuje pravá cesta (posl. pravých synů) a pokud máme takovou cestu délky <math> k\,\!</math> z vrcholu <math> v\,\!</math>, víme, že podstrom <math> v\,\!</math> do hloubky <math> k\,\!</math> je úplný binární strom. Délka pravé cesty z každého vrcholu je tedy logaritmická ve velikosti podstromu.

Operace jsou založeny na algoritmech MERGE a DECREASE.

  • MERGE testuje prázdnost jednoho ze stromů (a pokud je jeden prázdný, vrátí ten druhý jako výsledek). Pokud ne, volá se rekurzivně na podstrom pravého syna kořene s menším klíčem dohromady s celým druhým a výsledek připojí místo onoho pravého syna. Pokud neplatí podmínka na npl, syny vymění.

  • INSERT je to samé co vytvoření jednoprvkové haldy a zavolání MERGE.

  • DELETEMIN je zMERGEování synů kořene (a jeho zahození).

  • MAKEHEAP je vytvoření hald z jednotl. prvků. Nacpu je do fronty a potom v cyklu vyberu dva první, zmerguju a hodím výsledek na konec, dokud mám ve frontě víc než 1 haldu.

INCREASE a DECREASE se dělají jinak.

  • Mám pomocnou operaci OPRAV, která odtrhne podstrom a dopočítá všem vrcholům správné npl. Po odtržení vrcholu a příp. přehození pravého syna doleva jde nahoru, dokud provádí změny npl (možno až do kořene), vztahuje npl odspoda a příp. prohazuje syny.

  • DECREASE se pak udělá snížením hodnoty ve vrcholu, zavoláním OPRAV, tj. jeho odříznutím od zbytku haldy, a MERGE podstromu a zbytku.

INCREASE: zapamatuju si levý a pravý podstrom vrcholu s mým prvkem a provedu na něj OPRAV (vyhodím ho), potom vyrobím nový vrchol s mým prvkem se zvednutou hodnotou a jako samostatnou haldu ho zMERGEuju s levým podstromem. Pravý podstrom zMERGEuju se zbytkem haldy a nakonec s tím zMERGEuju výsledek MERGE levého podstromu a zvednutého prvku.

Složitost

1 běh MERGE bez rekurze je <math> O(1)\,\!</math>, hloubka rekurze je omezena pravými cestami, takže je to <math> O(\log(|S_1|+|S_2|))\,\!</math>. Z toho plyne logaritmovost INSERT a DELETEMIN.

Pro MAKEHEAP se uvažuje, kolikrát projdou haldy frontou: po <math> k\,\!</math> projití frontou mají velikost <math> 2^{k-1}\,\!</math> a tedy fronta obsahuje <math> \lceil\frac{|S|}{2^{k-1}}\rceil\,\!</math> hald. Jeden MERGE je <math>O(k)</math> a jedno projití frontou pro všechny haldy tedy trvá <math> O(k\lceil\frac{|S|}{2^{k-1}}\rceil)\,\!</math>. Celkem dostávám <math> O(|S|\sum_{k=1}^{\infty}\frac{k}{2^{k-1}})=O(|S|)\,\!</math> (součet řady je <math> 4\,\!</math>).

OPRAV chodí jen po pravé cestě, takže má logaritmickou složitost. INSERT, INCREASE a DECREASE se díky ní dostanou taky na <math> O(\log|S|)\,\!</math>, protože jejich části kromě MERGE a OPRAV mají konstantní složitost.

Binomiální haldy

Binomiální stromy se definují rekurentně jako <math> H_i\,\!</math>, kde <math> H_0\,\!</math> je jednoprvkový a <math> H_{i+1}\,\!</math> vznikne z dvou <math> H_i\,\!</math>, kdy se kořen jednoho stane dalším (krajním) synem kořenu druhého. Pak strom <math> H_i\,\!</math> má <math> 2^i\,\!</math> prvků, jeho kořen má <math> i\,\!</math> synů, jeho výška je <math> i\,\!</math> a podstromy určené syny kořene jsou právě <math> H_{i-1},\dots,H_0\,\!</math>.

Binomiální halda reprezentující <math> S\,\!</math> je seznam stromů <math> T_i\,\!</math> takový, že celkový počet vrcholů v těchto stromech je <math> |S|\,\!</math> a je dáno jednoznačné přiřazení prvků vrcholům, respektující podmínku haldy. Každý strom je přitom izomorfní s nějakým <math> H_i\,\!</math> a dva <math> T_i,T_j, i\neq j\,\!</math> nejsou izomorfní.

Existence binomiální haldy pro každé přirozené <math> |S|\,\!</math> plyne z existence dvojkového zápisu čísla.

Operace

Operace na binomiálních haldách jsou založené na MERGE.

  • MERGE pracuje stejně jako binární sčítání -- za pomoci operace SPOJ (slepení dvou stromů, přilepím jako syna toho, který má v kořeni vyšší klíč) slepí stromy stejného řádu, přenáší výsledky do dalšího spojování (přenos + obě haldy mající strom daného řádu = vyplivnutí 1 stromu na výsledek a spojení zbývajících dvou).

  • INSERT je MERGE s jednoprvkovou haldou.

  • MIN je projití kořenů a vypsání nejmenšího.

  • DELETEMIN je MIN, odebrání stromu s nejmenším prvkem v kořeni a přidání (MERGE) podstromů jeho kořene do haldy.

  • INCREASE a DECREASE se dělají úplně stejně jako u regulárních hald.

  • Přímo není podporováno DELETE, jen jako DECREASE + DELETEMIN.

  • MAKEHEAP se provádí opakováním INSERT.

Složitost MERGE je <math> O(\log|S_1|+\log|S_2|)\,\!</math>, protože 1 krok SPOJ je konstantní. Halda má nejvýše <math> \log|S|\,\!</math> stromů, takže MIN a DELETEMIN mají tuto složitost. Výška všech stromů je <math> \leq\log|S|\,\!</math>, což dává složitost INCREASE <math> O(\log^2|S|)\,\!</math> a DECREASE <math> O(\log|S|)\,\!</math>. Pro odhad složitosti MAKEHEAP se použije amortizovaná složitost přičítání jedničky k binárnímu číslu, což je <math> O(1)\,\!</math>, tedy celkem <math> O(|S|)\,\!</math>.

Líná implementace

Vynecháme předpoklad neexistence dvou izomorfních stromů v haldě a budeme "vyvažování" provádět jen u operací MIN a DELETEMIN, kdy se stejně musí projít všechny stromy. MERGE je pak prosté slepení seznamů hald. Vyvažování se provádí operací VYVAZ, která sloučí izomorfní stromy (podobně jako MERGE z pilné implementace).

Složitost INSERT a MERGE je <math> O(1)\,\!</math>, ale DELETEMIN a MIN v nejhorším případě <math> O(|S|)\,\!</math>.

Amortizovaná složitost vychází ale líp: použijeme , když za hodnocení konfigurace <math>w(H)\,\;</math> zvolíme počet stromů v haldě <math>|\mathcal{H}|\,\!</math>. INSERT a MERGE ho nemění, resp. mění o <math> 1\,\!</math>, takže jsou stále <math> O(1)\,\!</math>.

Operace VYVAZ potřebuje <math> O(|H|)\,\!</math>, protože slití dvou stromů trvá konstantně dlouho a nelze slévat víc stromů, než kolik jich je v haldě. Kromě operace VYVAZ potřebuje MIN <math> O(|\mathcal{H}|)\,\!</math> a DELETEMIN <math> O(|\mathcal{H}|+\log|S|)\,\!</math> (max. stupeň stromu je logaritmický).

Dohromady vychází amortizovaná složitost pro MIN: <math>am(o) = t(o) - w(H) + w(H') = O(|\mathcal{H}|-|\mathcal{H}|+\log|S|)\,\!</math>, protože výsledný počet stromů <math>|H'|\,\;</math> odpovídá pilné implementaci. Pro DELETEMIN podobně dostanu <math> O(|\mathcal{H}|+\log|S|-|\mathcal{H}|+\log|S|)=O(\log|S|)\,\!</math>.

Fibonacciho haldy

Definují se jako množiny stromů, které splňují podmínku haldy a musely vzniknout posloupností operací z prázdné haldy. Všechny operace zachovávají podmínku, že jednomu vrcholu lze odříznout max. dva syny. Strom má rank <math> i\,\!</math>, má-li jeho kořen <math> i\,\!</math> synů (podobné jako izomorfismus s <math> H_i\,\!</math> u binomiálních hald).

Podmínka odříznutí max. dvou synů se zachovává pomocnou operací VYVAZ2. Když vrchol není kořen a byl mu předtím někdy odříznut syn, je speciálně označený. VYVAZ2 prochází od daného vrcholu ke kořeni a dokud nalézá označené vrcholy, odtrhává je i s jejich podstromy, ruší jejich označení a vkládá do haldy jako zvláštní stromy. Když se vrchol stane kořenem, označení se zapomene.

Operace

MERGE, INSERT, MIN a DELETEMIN jsou stejné jako v líné implementaci binomiálních hald, jen požadavek na isomorfismus s <math> H_i\,\!</math> je nahrazen požadavkem na daný rank. Pomocné operace z binomiálních hald VYVAZ a SPOJ jsou také stejné.

DECREASE, INCREASE a DELETE vycházejí z leftist hald. Používají pomocnou operaci VYVAZ2

  • DECREASE odtrhne podstrom určený snižovaným vrcholem (není-li to už kořen), zruší u něj případné označení a vloží ho zvlášť do haldy, na odtržené místo zavolá VYVAZ2.

  • INCREASE provede to samé, jen ještě roztrhá podstrom zvedaného vrcholu (odtrhne všechny syny, zruší jejich příp. označení a vloží jako samostatné stromy do haldy) a vloží zvednutý vrchol do haldy zvlášť.

  • DELETE je to samé co INCREASE, bez přidání vrcholu zpět do haldy.

Korektnost a složitost

Operace SPOJ podobně jako u binomiálních hald vyrobí ze dvou stromů ranku <math> i\,\!</math> jeden strom ranku <math> i+1\,\!</math>. Operace VYVAZ2 zajistí, že od každého vrcholu kromě kořenů byl odtržen max. 1 syn -- když odtrhnu dalšího, odtrhu i tento vrchol a propaguju operaci nahoru.

Složitost operací:

  • MERGE a INSERT je <math> O(1)\,\!</math> (stejně jako u binomiálních hald)

  • MIN má <math> O(|\mathcal{H}|)\,\!</math> (nemění označení vrcholů)

  • DELETEMIN <math> O(|\mathcal{H}|+\mathrm{maxrank}(\mathcal{H}))\,\!</math>, kde <math> maxrank\,\!</math> udává maximální rank stromu v haldě (může navíc odznačit některé vrcholy)

  • DECREASE je <math> O(1+c)\,\!</math>, kde <math> c\,\!</math> je počet odznačených vrcholů (navíc označí max. 1 vrchol)

  • INCREASE a DELETE jsou <math> O(1+c+d)\,\!</math>, kde navíc <math> d\,\!</math> je počet synů zvedaného nebo odstraňovaného vrcholu (také označí navíc max. 1 vrchol).

Pro výpočet amortizované složitosti použijeme potenciálovou metodu a zvolíme hodnotící funkci <math> w\,\!</math> jako počet stromů v haldě + <math> 2\times\,\!</math> počet označených vrcholů. Můžeme říct, že amortizovaná složitost MERGE, INSERT a DECREASE je <math> O(1)\,\!</math>.

Označme max. rank stromů v lib. haldě reprezentující <math> n\,\!</math>-prvkovou množinu jako <math> \rho(n)\,\!</math>. Amortizovaná složitost MIN, DELETEMIN, INCREASE a DELETE pak je <math> O(\rho(n))\,\!</math> (pro MIN a DELETEMIN je vzorec amortizované složitosti podobný jako u binomiálních hald, pro INCREASE a DELETE je to vidět přímo ze vzorců).

Pro odhad <math>\rho(n)\,\;</math> je potřeba znát fakt, že <math> i\,\!</math>-tý nejstarší syn libovolného vrcholu má aspoň <math> i-2\,\!</math> synů (plyne z toho, že se slévají jen stromy stejného řádu a odtrhnout lze max. jednoho syna).

Vezmeme tedy nejmenší strom <math>T_j\,\;</math> ranku <math>j\,\;</math>, který toto splňuje. Ten musí být složením <math>T_{j-1}\,\;</math> a <math>T_{j-2}\,\;</math> (vzniká tak, že se slijí dva <math>T_{j-1}\,\;</math> a potom se na tom, který je pověšený jako syn nového kořenu, provede DECREASE a tím se z něj stane <math>T_{j-2}\,\;</math>). Z minimálního počtu synů se dá odvodit i rekurence <math>|T_k| \geq 1 + 1 + |T_0| + \dots + |T_{k-2}|</math>, která dá indukcí to samé.

Potom <math>|T_{k+1}| = F_k</math>, kde <math>F_k</math> je <math>k</math>-té Fibonacciho číslo. Pro Fibonacciho čísla platí, že <math>\lim_{k\to\infty} F_k = \frac{1}{\sqrt{5}}\left(\frac{1 + \sqrt{5}}{2}\right)^k</math>. Proto je <math>\rho(n) = O(\log(n))\,\;</math>, což dává logaritmickou amortizovanou složitost pro MIN, DELETEMIN, INCREASE a DELETE. Z toho pochází i název Fibonacciho haldy.

Aplikace

Fibonacciho haldy se díky své rychlosti INSERT, DECREASE a DELETEMIN často používají v grafových algoritmech. Praktické porovnání rychlosti s jinými haldami však není dosud přesně prostudováno.

Motivací pro vývoj Fibonacciho hald byla možnost aplikace v Dijkstrově algoritmu. Dává totiž složitost celého algoritmu <math> O(m+ n\log n)\,\!</math>, což by mělo být lepší pro velké, ale řídké grafy proti <math> d\,\!</math>-regulárním haldám. O prakticky zjištěném "zlomu" ale nevíme.

{{Statnice I3}}