Binární minimová halda je datová struktura tvaru binárního stromu, v jehož každém vrcholu $x$ je uložen jeden klíč $k(x)$ a která splňuje tyto dvě vlastnosti:
Tvar haldy: Strom má všechny hladiny kromě poslední plně obsazené. Poslední hladina je zaplněna od levého okraje směrem k pravému.
Haldové uspořádání: Je-li $v$ vrchol a $s$ jeho syn, platí
Pokud nebude řečeno jinak, binární minimovou haldu budeme zkráceně označovat halda.
Binární maximová halda je definována analogicky.
Tvar haldy: Strom má všechny hladiny kromě poslední plně obsazené. Poslední je zaplněna zleva doprava.
Haldové uspořádání: Je-li $v$ vrchol a $s$ jeho syn, platí
Na cestě vedoucí z libovolného vrcholu do kořene tvoří klíče nerostoucí posloupnost. V kořeni je tedy globální minimum ze všech klíčů.
Binární halda s $n$ prvky má $⌊\log{n}⌋ + 1$ hladin.
Binární strom s $h ≥ 1$ úplnými hladinami má $n = 2^0 + 2^1 + 2^2 + · · · + 2^h−1 = 2^h − 1$ vrcholů.
Vzhledem ke tvaru haldy přibude nová hladina jen tehdy, když počet prvků $n$ vzroste z $2^h − 1$ na $2^h$.
Z toho plyne vzorec, neboť právě funkce $⌊\log{n}⌋$ se právě při této změně $n$ zvětší o jedna a pro $n = 2^h − 1$ je počet hladin
$\square$
Binární halda s $n ≥ 3$ prvky má $⌊n/2⌋$ vnitřních vrcholů a $⌈n/2⌉$ listů.
Indukcí podle počtu vrcholů binárního stromu.
Lemma platí triviálně pro $n = 3$ (2 listy a kořen − jediný vnitřní vrchol).
Nechť $n > 3$ je liché. Odebráním posledního listu, který je druhým (tj. pravým) synem posledního vnitřního vrcholu, vznikne halda o sudé velikosti $n − 1$, pro kterou z ind. předp. lemma platí.
Vrácením posledního listu se počet vnitřních vrcholů nezmění a původní strom jich tedy má $⌊(n − 1)/2⌋ = ⌊n/2⌋$.
A počet listů se zvětší o jedna a původní strom jich tedy má $⌈(n − 1)/2⌉ + 1 = ⌈n/2⌉$.
Nechť $n ≥ 4$ je sudé. Pak poslední list je jediný levý syn svého otce.
Jeho odebráním vznikne halda liché velikosti $n − 1$, která má podle ind. předpokladu $⌈(n − 1)/2⌉$ listů a $⌊(n − 1)/2⌋$ vnitřních vrcholů.
Vrácením posledního listu jeden vnitřní vrchol přibude a původní strom jich tedy má $⌊(n − 1)/2⌋ + 1 = ⌊n/2⌋$.
A počet listů se nezmění a původní strom jich tedy má $⌈(n − 1)/2⌉ = ⌈n/2⌉$.
$\square$
HeapInsert (H, $k$) | $O(\log n)$ | Vloží nový klíč $k$ do haldy H. |
HeapFindMin (H) | $O(1)$ | Najde a vrátí minimum ze všech prvků v haldě H. |
HeapExtractMin (H) | $O(\log n)$ | Odstraní minimum z haldy H a vrátí ho. |
Binární halda $H$ má atributy
HeapFindMin
($H$) je triviální: vrať klíč kořene haldy v $O(1)$ čase.
HeapExtractMin
($H$) je komplikovanější: Odstranění kořene $r = H.root$ stromu haldy by porušilo vlastnost tvar haldy.
Nicméně je možné triviálně odstranit poslední list $l$, čili list nejvíc vpravo v poslední hladině.
Z toho plyne idea algoritmu odstranění minima: 1. Prohoď $k(r)$ a $k(l)$. 2. Odstraň vrchol $l$ a zmenši haldu o 1 prvek. 3. „Probublávej dolů“ z kořene klíč $k(l)$ na jeho správné místo, aby opět platilo haldové uspořádání.
Binární halda $H$ má atributy
$H.\clr{green}{root}$, který je aktuální kořen
$H.\clr{green}{n}$, který udržuje aktuální počet prvků
$H.\clr{green}{last}$, který udržuje ukazatel na aktuální poslední list (nejpravější list v poslední hladině), pokud existuje
Prvek x haldy má
klíč $k(x)$
ukazatel na otce
ukazatele na své syny (jsou-li v haldě obsažené)
Na každé hladině strávíme $O(1)$ operací a procházených hladin je nejvýše logaritmicky mnoho, celkem tedy čas $O(\log{n})$.
Buď $H$ minimová halda a $k$ klíč.
Potom struktura vzniklá voláním HeapInsert
$(H, k)$ je opět minimová halda.
Pokud $H$ je prázdná, pak je výsledkem jednoprvková halda – hotovo.
Jinak označme $\hat{H}$ výsledek volání HeapInsert
$(H, k)$. $\hat{H}$ má dle (2) tvar haldy.
Zbývá ověřit, že splňuje podmínku haldového uspořádání.
Po přidání hodnoty k do nového listu $l$ může existovat jen jedna hrana s porušenou podmínkou.
Tento problém pomocí funkce BubbleUp posouváme směrem ke kořeni.
Je třeba ověřit, že nemůžeme porušit podmínku haldového uspořádání mezi nějakým otcem o a jeho (případným) druhým synem na cestě z $l$ do kořene.
Pozorujeme, že hodnota k(o) v o se aplikováním BubbleUp nemohla zvýšit (může klesnout, případně zůstat stejná, pokud otec vrcholu o měl stejnou hodnotu).
$\square$
Na každé hladině provedeme $O(1)$ operací a procházených hladin je nejvýše logaritmicky mnoho, tedy celkový čas je $O(\log n)$.
Buď $H$ minimová halda a $k$ klíč.
Potom struktura vzniklá voláním HeapExtractMin
($H$) je opět minimová halda.
Tvar haldy je opět zachován triviálně.
Pokud je výsledkem prázdná či jednoprvková halda, máme hotovo.
Prohození z (2) mohlo porušit haldové uspořádání (mezi hladinami 0 a 1).
Opět prohazujeme s menším ze synů, čímž posunujeme porušení na dvojici následujících hladin.
Toto může pokračovat jen do poslední hladiny.
$\square$
Co se změní, když místo minimové haldy budeme pracovat
s maximovou haldou a místo operací HeapExtractMin
a HeapFindMin
budeme tedy podporovat operace HeapExtractMax
a HeapFindMax
?
Rozmyslete algoritmus operace HeapChangeKey
,
která dostane ukazatel na prvek uložený v haldě,
ve kterém se má klíč změnit na zadanou hodnotu tak,
aby výsledkem byla opět halda.
Využitím operace HeapChangeKey
lze definovat operaci,
která umožňuje z haldy odstranit libovolný prvek zadaný ukazatelem.
Popište její algoritmus.
Jak haldu reprezentovat v paměti?
Očíslujme (oindexujeme) vrcholy po hladinách shora dolů a na nich zleva doprava (číslujeme od 1 do $n$):
TODO: Obrázek: Halda a Array ( Přednáška 4, slide 24)
Má-li vrchol $v$ index $i$, pak
jeho levý syn má index $2i$
jeho pravý syn index $2i + 1$
jeho otec má index $⌊i/2⌋$
výraz $i$ mod 2 udává, zda je $v$ připojen k otci levou či pravou hranou, tj. zda je jeho levý či pravý syn.
Haldu lze tedy jednoduše reprezentovat v poli a pohyb po stromu realizovat výše popsanými operacemi s indexy.
Poku indexujeme od $0$, potom má-li vrchol $v$ index $i$, pak
jeho levý syn má index $2i + 1$
jeho pravý syn index $2i + 2$
jeho otec má index $\lfloor(i-1)/2\rfloor$
Operace HeapBuild
má časovou složitost $O(n)$.
Nechť strom haldy má $h$ hladin (od $0$ do $h - 1$).
Platí tedy $2^{h−1} \le n \le 2^h − 1$.
Pro účely analýzy si představíme, že i poslední hladina je zcela plná, například nějakými „dummy“ prvky většími než vše ostatní. Tím jistě nesnížíme potřebný počet operací.
Procedura BubbleDown
od $j$-té hladiny trvá čas $O(h − 1 − j)$.
Pokud to sečteme přes všech $2^j$ vrcholů hladiny $j$ a poté přes všechny hladiny, dostaneme (až na konstantu)
Časovou složitost lze shora odhadnout
Pro $a_k = \frac{k}{2^k}$ máme:
$\square$
Na každý vrchol na začátku položíme $p$ penízků. Co dělá každý prvek?
Porovná se s menším ze synů (2 instrukce), může se s menším prohodit (1 instrukce) a pak se případně probublává dolů (směrem k listu).
Ukážeme, že za toto zvládne „zaplatit“ jeden z podstromů.
Když tedy nakonec přesuneme všechny zbylé penízky do kořene aktuálního stromečku, zbyde nám alespoň $p \cdot (k + 1)$ penízků, kde $k$ je vzdálenost kořenu uvažovaného stromečku od listů.
Důkaz proběhne indukcí podle $k$ – vzdálenosti od listu.
(Pro jednoduchost předpokládáme, že poslední hladina je zcela zaplněná – tím formálně ukazujeme $2pn$, ale není těžké důkaz poupravit.)
ZI: $k = 0$. Triviálně platí, neboť neprovedeme žádné porovnání a na kontě zůstane $p = p(k + 1)$.
IK: $k - 1 \rightarrow k$. Máme „nový kořen“ ve stromě $T$ a předpokládáme, že podstromy $T_\ell$ a $T_r$ jsou korektně vytvořeny a že (IP) každý z jejich kořenů má na kontě $pk$. Hloubka stromu $T$ je $k + 1$. Všimli jsem si, že v nejhorším případě uděláme s novým prvkem na každé hladině (kromě poslední) 3 operace. Celkově tedy za „opravení“ stromu $T$ zaplatíme $3k$. Na konci nám tedy zbyde $z = p + 2pk − 3k$. Pokud zvolíme $p \geq 3$, bude jistě $z \geq p(k + 1)$.
$\square$
Předchozí popis algoritmu HeapSort
předpokládal, že se odebíraná minima postupně zapisují do výstupního pole.
Jedná se tedy o efektivnější realizaci myšlenky algoritmu SelectSort
Algoritmus HeapSort
lze realizovat in-place, tedy bez použití extra paměti na uložení zkonstruované haldy.
Navrhněte, jak to provést, když vstupní posloupnost je uložena v poli $P$ a kromě tohoto pole smíte použít již jen $O(1)$ pomocných proměnných.