1. Vyledávací stromy V této kapitole budeme postaveni před následující problém. Je dáno nějaké univerzum prvků U a naším úkolem bude navrnout datovou strukturu, která udržuje konečnou množinu prvků X U. Pod univerzem si můžeme představit například přirozená čísla, tedy univerzum může být nekonečné na rozdíl od X. Na našic datec budeme ctít provádět následující operace: Insert vložit novou položku Delete smazat položku Find najít položku Způsob realizace jednotlivýc operací a jejic časová složitost se bude zjevně odvíjet od vlastností prvků univerza. V našem případě budeme ctít udržovat slovník, tedy množinu dvojic (k, v) kde k U se nazývá klíč a v odnota. Dále předpokládejme, že univerzum U je lineárně uspořádané (důležitá vlastnost!) a s klíči i odnotami pracujeme v konstantním čase. Po slovníku budeme ctít: Insert(k, v) vložit novou odnotu spolu s klíčem (předpokládáme, že ve struktuře dosud tento klíč není přítomen) Delete(k) smazat položku podle klíče Find(k) najít položku podle klíče Tyto operace je možné realizovat nad množstvím různýc datovýc struktur, od jednoducýc po značně složité. Věříme, že čtenáři nedělá větší problém vymyslet, jak by požadované operace realizoval například nad setříděnýc čí nesetříděným polem, setříděným či nesetříděným spojovým seznamem. Hlavním cílem této kapitoly je popsat datovou strukturu vyledávacío stromu, nicméně než se pustíme do bližšío vysvětlování, uvedeme ještě srovnání časovýc složitostí operací nad vyledávacími stromy a nad jednoducými datovými strukturami. Insert Delete Find pole Θ(1) Θ(n) Θ(n) setříděné pole Θ(n) Θ(n) Θ(log n) spojový seznam Θ(1) Θ(n) Θ(n) setříděný seznam Θ(n) Θ(n) Θ(n) vyledávací stromy Θ(log n) Θ(log n) Θ(log n) 1.1. Denice binárnío vyledávacío stromu Zavzpomínáme nyní, jak funguje algoritmus vyledávání prvku půlením intervalů nad setříděným polem. Zvolíme prvek uprostřed pole, porovnáme s ledaným a podle výsledku porovnání se zaměříme buďto na levý nebo na pravý interval, kde opakujeme stejný algoritmus. 1 2014-03-27
Stejnou myšlenku je možné zopakovat s jiným uložením dat než je pole. Představme si, že máme binární strom s prvky uloženými ve vrcolec, jeož jednotlivé podstromy ztotožníme s intervaly, nad kterými pracuje půlící vyledávání, a vrcoly stromu ztotožníme se zvolenými prvky pole, se kterými půlící algoritmus porovnává ledaný prvek. Jeden průbě půlícío vyledávání potom odpovídá průcodu tímto stromem z kořene dolů do nějakéo listu. Dále si povšimněme, že pro správnou funkci vyledávacío algoritmu nebylo potřeba volit vždy prostředky intervalů, stačí volit libovolný vnitřní prvek intervalu. Tato volba se projeví pouze na čase, v jakém bude vyledávání probíat. ni příslušný ztotožněný strom tedy nemusí být stejné pravidelný jako Tyto postřey nyní přetavíme do přesné definice binárníc vyledávacíc stromů. Definice: Strom T nazveme binární, pokud je zakořeněný a každý vrcol v V (T ) má nejvýše dva syny, u nicž rozlišujeme, který je levý a který pravý. Definice: Pro vrcol v binárnío stromu T značíme: l(v) a r(v) levý a pravý syn vrcolu v L(v) a R(v) levý a pravý podstrom vrcolu v p(v) otec vrcolu v (pokud eistuje) T (v) příslušný podstrom s kořenem v d(v) loubka stromu T (v) délka nejdelší cesty z kořene do listu Definice: Binární strom T nazveme binární vyledávací strom (BVS), pokud v každém vrcolu T je uložena dvojice (klíč, odnota) [ztotožníme vrcol s klíčem] a pro každý vrcol v V (T ) platí: ( u L(v) : u < v) & ( u R(v) : u > v). Poznamenejme ještě, že právě zavedená podmínka na uspořádání prvků uvnitř binárnío vyledávacío stromu je velmi silná, neboť dává kompletní informaci o uspořádání prvků reprezentované množiny. Pro lepší ilustraci definice BVS uveďme na obr. 1.1 několik příkladů. 7 4 9 2 4 5 7 8 9 2 5 8 Obr. 1.1: Příklady binárníc vyledávacíc stromů 2 2014-03-27
1.2. Operace nad BVS Nyní popíšeme operace nad binárním vyledávacím stromem. Operaci vyledávání prvku jsme už nastínili v neformálním úvodu. Začneme v kořeni, klíč v něm uložený porovnáme s ledaným prvek. Buďto jsme měli štěstí a klíč našli, nebo víme, že se klíč může být jen v levém nebo naopak pravém podstromu, kde celý postup zopakujeme. lgoritmus Find Vstup: kořen BVS v, vyledávaný klíč Výstup: ukazatel na vrcol obsaující (nebo ) 1. Pokud v =, vrať. 2. Pokud v =, vrať v. 3. Pokud v <, vrať Find(r(v), ). 4. Pokud v >, vrať Find(l(v), ). Vkládání novéo prvku funguje velmi podobně jako vyledávání, s tím rozdílem, že v okamžiku, kdy by vyledávací algoritmus měl přejít do neeistujícío vrcolu, tento nový vrcol vytvoříme a vložíme do něj vkládaný prvek. lgoritmus Insert Vstup: kořen BVS v, vkládaný klíč 1. Pokud v =, skonči. (Případně nějak vyřeš duplicitu prvků.) 2. Pokud v <, s r(v), jinak s l(v). 3. Jestliže s =, vytvoř nový vrcol s klíčem, napoj o pod p(s) a skonči. 4. Jinak zavolej Insert(s, ). lgoritmus Delete Vstup: ukazatel na mazaný vrcol v 1. Pokud v je list, jednoduše list utrni. 2. Pokud v má jednoo syna s, napojíme s pod p(v) namísto v a vrcol v zrušíme. 3. Jinak má v oba syny, potom do vrcolu v vložíme minimum z R(v) nacázející se ve vrcolu m. Vrcol m umíme smazat protože je to buď list nebo má jen pravéo syna. Poznámka: Pokud má vrcol v při operaci Delete oba syny, je vložení minima z R(v) ekvivalentní s vložením maima z L(v). Příklady operací Insert a Delete aplikovanýc na BVS ilustrujeme na obr. 1.2. Čtenáři ponecáváme ve cvičeni 1.2.1 k rozmyšlení, jak všecny výše uvedené operace realizovat i bez použití rekurze. To je ve většině imperativníc programovacíc jazyků i podstatně ryclejší a jednodušší implementace. Zároveň si však povšimněme, že rekurzivní povaa BVS i operací nad ním z něj činí přirozenou datovou strukturu k použití v čistě funkcionálníc programovacíc jazycíc. 3 2014-03-27
5 5 2 Insert 1 8 2 Delete 4 8 1 4 7 9 7 9 5 5 4 2 8 4 Delete 2 8 2 Delete 5 8 4 7 9 7 9 7 9 Obr. 1.2: Příklad operací Insert a Delete aplikovanýc na strom vlevo V průběu všec tří operací se kráčí od kořene v nejorším případě až do nejlubšío listu. Hloubka N-prvkovéo stromu může být Θ(N), když strom bude (téměř) lineární spojový seznam. Takovéto degenerované stromy vzniknou snadno, například přidáváním setříděné posloupnosti. Naopak když bude strom pěkně vyváženě vystavěný, dostaneme složitost Θ(log N). Vidíme tedy, že složitost operací stojí a padá s loubkou stromu. Srneme tedy složitost v následujícím tvrzení. Důsledek: Pro N-prvkový binární vyledávací strom T je časová složitost operací Find, Insert a Delete Θ(loubka T ). Uvedeme nyní podmínku, která zajišťuje dobrou loubku stromu. Definice: Binární vyledávací strom T nazveme dokonale vyvážený, pokud pro každý jeo vrcol v platí L(v) P (v) 1. Dokonale vyvážený binární strom bude mít určitě logaritmickou loubku. Jak lze takový strom konstruovat nebo dokonce průběžně udržovat při mazání a vkládání prvků? Odpověď není snadná; buďto strom zkonstruujeme naráz staticky, anebo se smíříme s tím, že operace na něm budou pomalejší než Θ(log N). lgoritmus pro postavení dokonale vyváženéo BVS ze setříděnéo pole ponecáme čtenáři jako cvičení. Lemma: Buď operace vkládání nebo operace mazání v dokonale vyváženém BVS trvá Ω(N). Důkaz: Necť N = 2 k 1 je počet vrcolů BVS T. Pak má dokonale vyvážený BVS T určený tvar a je jednoznačné, která odnota je ve kterém vrcolu. Necť nejmenší číslo v T je a největší je + N 1. Proveďme posloupnost operací Insert( + N), Delete(), Insert( + N + 1), Delete( + 1),... 4 2014-03-27
Za každou dvojicí operací se aspoň polovina listů posune o patro výš. Víme, že listů ve stromě T je (N + 1)/2. Tedy aspoň jedna z operací Insert nebo Delete trvá Ω(N). Poznamenejme ještě, že ve skutečnosti obě operace Insert i Delete současně musí trvat Ω(N), ale důkaz je o trocu obtížnější. vičení: 1. Operace Find, Insert a Delete jsme popsali rekurzivně. Navrněte, jak je dělat bez použití rekurze. 2. Navrněte algoritmus, který ze setříděnéo pole vyrobí v lineárním čase dokonale vyvážený BVS. 3. Navrněte algoritmus, který ze setříděnéo pole vyrobí v lineárním čase dokonale vyvážený BVS. 4. Navrněte algoritmus, který dostane dva BVS T 1 a T 2 sloučí jejic obsa do jedinéo BVS. lgoritmus by měl pracovat v čase O( T 1 + T 2 ). 5. Navrněte operaci Split, která dostane BVS T a odnotu s, a rozdělí strom na dva BVS T 1 a T 2 takové, že odnoty v T 1 jsou menší než s a odnoty v T 2 jsou větší než s. 1.3. Zavedení VL stromù Vidíme tedy, že dokonale vyvážené stromy nejsou vodné. Potřebovali bycom totiž, aby se strom dal také efektivně udržovat. Zavedeme proto slabší podmínku. Definice: Binární vyledávací strom T nazveme loubkově vyvážený, pokud pro každý jeo vrcol v V (T ) platí (L(v)) (P (v)) 1. Stromům s loubkovým vyvážením se říká VL stromy 1. Důležitou vlastností VL stromů je jejic logaritmická loubka. Tvrzení: VL strom na N vrcolec má loubku Θ(log N). Důkaz: Definujme čísla k = minimální počet vrcolů VL stromu loubky k. Stačí nyní ukázat, že posloupnost k roste eponenciálně. Jak bude vypadat minimální VL strom o k ladinác? S počtem ladin určitě poroste počet vrcolů, proto pro každý vrcol budeme ctít, aby se loubky jeo synů lišily. Tedy kořen v bude mít syna a loubky k 1 a syna b loubky k 2 1 Navrli je ruští matematikové G. M. děľson-veľskij a E. M. Landis, podle nicž jsou také pojmenovány. 5 2014-03-27
takové, že stromy T (a) a T (b) jsou minimální VL stromy o počtu ladin k 1 a k 2. Pro malá k dokážeme určit k ručně, pro větší k jsme právě sestrojili rekurentní vzta. 0 = 1 1 = 2 2 = 4 3 = 7. k = 1 + k 1 + k 2. Rekurentní vzorec jsme dostali rekurzivním stavěním stromu loubky k: nový kořen a 2 podstromy o loubkác k 1 a k 2. Lze poměrně snadno dokázat, že n = F n+2 1 (kde F n je n-té Fibonaccio číslo). My se však bez analýzy Fibonaccio posloupnosti obejdeme, protože nám stačí dokázat, že k rostou eponenciálně a nepotřebujeme přesný vzorec pro odnotu k. Tento fakt přenecáme čtenáři do cvičení 1.4.1. Indukcí dokážeme, že k 2 k 2. První indukční krok jsme již ukázali. Pro k 2 platí k = 1 + k 1 + k 2 > 2 k 1 2 + 2 k 2 2 = 2 k 2 (2 1 2 + 2 1 ) = 2 k 2 1.21 > 2 k 2. Tímto jsme dokázali, že na každé ladině je minimálně eponenciálně vrcolů, což nám zaručuje loubku O(log N). Druou nerovnost nalédneme zkoumáním B k maimálnío počtu vrcolů VL stromu loubky k. Ta je však sora omezena počtem vrcolů v dokonale vyváženém stromě, který je Θ(log N), z čeož vyplývá výsledná loubka VL stromů Θ(log N). 1.4. Operace nad VL stromy Operace vyledání prvku je naprosto stejná jako Find v binárníc stromec. Důraz klademe na operace Insert a Delete, protože při nic musíme ošetřit udržení struktury VL stromů. První nutnou podmínkou je pro návr těcto operací je, že v každém vrcolu VL stromu budeme udržovat informaci o vyvážení loubky jeo podstromů. Protože definice VL stromů povoluje v rozdíly loubek pouze o 1, vystačíme s třemi symboly. Dostaneme tedy tři typy vrcolů VL stromu, pro které budeme používat následující značení: Vrcol typu, pokud je pravý podstrom lubší Vrcol typu, pokud je levý podstrom lubší a Vrcol typu (nula), který má oba podstromy sodné loubky. 6 2014-03-27
Stromové rotace V průběu algoritmů vkládání a mazání prvku bude nějak třeba průběžně udržovat vyváženost VL stromů. Dosáneme too tzv. rotacemi. Jde v zásadě o převrácení rany mezi původním otcem (kořenem podstromu) a nevyváženým vrcolem tak, aby byli i po přeskupení synové vzledem k otcům správně uspořádáni. My budeme používat dva typy rotací: jednoducou a dvojitou. Namísto slovnío popisu tentokrát čtenáře odkážeme na popis obrázkem. Jednoducá rotace je na obr. 1.3 a dvojitá na obr. 1.4. y y B B Obr. 1.3: Jednoducá rotace z y D y z B D B Obr. 1.4: Dvojitá rotace Vkládání do VL stromu Nový prvek vložíme jako list. Nový list má vždy znaménko nula. Předpokládáme, že patří nalevo od poslednío otce. Podíváme se na znaménko jeo otce: měl (neměl syna) teď má, po struktuře stromu naoru posíláme informaci, že se podstrom proloubil o 1, což může mít samozřejmě vliv na znaménka vrcolů na cestě ke kořeni. měl (měl pravéo syna, který je listem) teď má, loubka podstromu se nemění měl nenastane, protože v binární struktuře nemoou být dva leví synové Připadne-li přidaný list napravo, řešíme zrcadlově. 7 2014-03-27
+ y 0 z 0 y 0 z 0 + Proloubil-li se strom vložením novéo listu, musíme pracovat s vyvážením: Informace o proloubení přišla zleva do vrcolu typu mění jej na vrcol se znaménkem a informace o proloubení je třeba poslat o úroveň výš. Informace o proloubení přišla zleva do vrcolu typu mění jej na vrcol se znaménkem, loubka je vyrovnána, dál nic neposíláme. Informace o proloubení přišla zleva do vrcolu s rozebereme na tři případy podle znaménka vrcolu, ze kteréo přišla informace o proloubení: Informace přišla z vrcolu typu provedeme rotaci doprava tak, že novým kořenem se stane vrcol y, ze kteréo přišla informace o proloubení. [ + 1] [] + 1 y 1 B 1 + 1 y 0 B 1 1 Pozorování 1: znaménko vrcolů y a je Pozorování 2: loubka před vkládáním byla + 1 a nyní je také + 1, tedy nemusíme dále posílat informaci o proloubení a můžeme skončit Informace přišla z vrcolu typu uvažme ješte vrcol z jako pravéo syna vrcolu y, ze kteréo prišla informace o proloubení, a jeo podstromy B a vrcoly B a mají loubku nebo 1 označme ji tedy (to zřejmě protože vrcol y má znaménko, tedy jeo pravý podstrom s kořenem z má loubku + 1 ) 8 2014-03-27
provedeme dvojrotaci tak, že novým kořenem se stane vrcol z [ + 1] + 2 [ + 2] y + B z z 0 D + 1 + 2 y + 1 B D + 1 Pozorování 1: znaménko vrcolu z bude Pozorování 2: znaménka vrcolu a y se dopočítají v závislosti na loubce B a Pozorování 3: rozdíl loubky pravéo a levéo podstromu bude u těcto vrcolů 0 nebo 1 Pozorování 4: loubka před vkládáním byla + 2 a nyní je také + 2, tedy nemusíme dále posílat informaci o proloubení a můžeme skončit informace přišla z vrcolu typu to nemůže nastat, protože v tom případě by nešlo o proloubení Delete odebrání vrcolu z VL stromu který měl nějaké syny. Buď mažeme list nebo mažeme vrcol, pokud mažeme list, podíváme se na typ otce. Předpokládáme mazání levéo syna. byl typu (neměl pravéo syna) změní se na (vrcol teď nemá žádné syny) byl typu (měl oba syny) změní se na (mažeme-li pravý list, řešíme zrcadlově) mažeme vrcol s jedním (levým nebo pravým) synem syn nastupuje na místo otce a získává typ V obou případec posílame informaci o změně loubky stromu... mazaný vrcol měl oba syny (listy) vybereme jednoo ze synů na místo smazanéo otce. Hloubka se nemění. mazaný vrcol měl syny podstromy na jeo místo vezmeme největší prvek levéo podstromu (nebo nejmenší prvek pravéo podstromu) a od odebranéo (narazujícío) listu kontrolujeme vyvážení podstromu. Úprava vyvážení stromu po odebrání listu z podstromu 9 2014-03-27
informace o změně loubky přišla z levéo podstromu do vrcolu typu vrcol se změní na a dál se loubka nemění informace přišla zleva do vrcolu s mění se na a posíláme informaci o změně loubky. y y 0 + problémová situace nastává, když informace o změně přišla zleva do vrcolu se znaménkem Rozebereme na tři případy podle znaménka pravéo syna nevyváženéo vrcolu pravý syn je typu provedeme rotaci vlevo, novým kořenem se stává y (pravý syn), oba vrcoly změní typ na a posíláme informaci o změně loubky. [ + 3] + + 2 [ + 1] + 2 y 0 y + + 1 + 1 B B + 1 pravý syn je typu provedeme opět rotaci vlevo, kořenem se stává y, následně se u y změní typ na, u vrcolu se typ nemění. Hloubka stromu se nemění, tudíž není třeba posílat informaci. [ + 3] + + 3 [ + 1] + 2 y y 0 + 2 + + 1 B B + 1 + 1 + 1 pravý syn je typu v tomto případě uvažujeme ještě vrcol z jako levéo syna vrcolu y, s podstromy B a, podstromy B a 10 2014-03-27
vičení: mají loubku nebo 1. Provedeme dvojrotaci, napřed vpravo rotujeme vrcoly z a y, potom vlevo vrcoly a z tak, že se z stane novým kořenem, typ vecolu bude potom nebo, typ y nebo (podle too, jaké znaménko měl původně vrcol z), typ z bude a opět posíláme informaci o změně loubky stromu. [ + 3] + 2 + z [ + 1] 0 + 2 y + 1 y + 1 + 1 P z Q P Q 1. Dokažte, že pro minimální velikost k VL stromu loubky k platí vzta k = F k+2 1 (kde F n je n-té Fibonaccio číslo). 11 2014-03-27