8.1 Binární halda

Definice 8.1 (Binární minimová halda)

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:

  1. 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.

  2. Haldové uspořádání: Je-li $v$ vrchol a $s$ jeho syn, platí

    \begin{equation*} k(v) \le k(s) \end{equation*}

Poznámka 8.1

Pokud nebude řečeno jinak, binární minimovou haldu budeme zkráceně označovat halda.

Binární maximová halda je definována analogicky.

Příklad 8.1 (Halda)

  • 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í

    \begin{equation*} k(v) \le k(s) \end{equation*}

Obrázek 8.1: Příklad haldy
Pozorování 8.1

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íčů.

Lemma 8.1 (O počtu hladin binární haldy)

Binární halda s $n$ prvky má $⌊\log{n}⌋ + 1$ hladin.

Zobrazit důkaz

  • 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

    \begin{equation*} h = (h − 1) + 1 = ⌊\log{(2^h − 1)}⌋ + 1 \end{equation*}

$\square$

Lemma 8.2

Binární halda s $n ≥ 3$ prvky má $⌊n/2⌋$ vnitřních vrcholů a $⌈n/2⌉$ listů.

Zobrazit důkaz

  • 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.

Tabulka 8.1: Složitosti operací na binární haldě

Poznámka 8.2 (Nalezení a odstranění minima)

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í.

Poznámka 8.3 (Binární minimová halda – Formalizace)

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é)

8.1.1 HeapInsert

\begin{algorithm}
\begin{algorithmic}
\FUNCTION {HeapInsert}{$H$, $k$}
    \STATE $H$.n := $H$.n + 1
    \STATE vlož na první volnou pozici v poslední
    hladině $H$ nový vrchol $p$ s klíčem $k$
    \STATE \CALL{BubbleUp}{$H$, $p$}
\ENDFUNCTION
\STATE
\FUNCTION {BubbleUp}{$H$, $p$}
    \WHILE {$p \neq $ $H$.root}
        \STATE $o$ := otec($p$)
        \IF {$k(o) \le k(p)$}
            \RETURN
        \ENDIF
        \STATE prohoď $k(o)$ a $k(p)$
        \STATE $p$ := $o$
    \ENDWHILE
\ENDFUNCTION\end{algorithmic}
\end{algorithm}

Algoritmus 8.1: Algoritmus HeapInsert($H$, $k$)

Poznámka 8.4 (Analýza složitosti)

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})$.

Lemma 8.3 (Korektnost HeapInsert)

Buď $H$ minimová halda a $k$ klíč. Potom struktura vzniklá voláním HeapInsert $(H, k)$ je opět minimová halda.

Zobrazit důkaz

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$

8.1.2 HeapExtractMin(H)

\begin{algorithm}
\begin{algorithmic}
\FUNCTION {HeapExtractMin}{$H$}
  \STATE $r$ = $H$.root, $l$ = $H$.last
  \STATE prohoď $k(r)$ a $k(l)$
  \STATE $H$.n := $H$.n − 1
  \STATE \CALL{BubbleDown}{$H$, $r$}
\ENDFUNCTION
\STATE
\FUNCTION {BubbleDown}{$H$, $v$}
  \WHILE { $v$ má nějaké syny }
    \STATE $s$ := takový syn $v$, který má nejmenší klíč
    \IF { $k(v) \le k(s)$ }
      \RETURN
    \ENDIF
    \STATE prohoď $k(v)$ a $k(s)$
    \STATE $v := s$
  \ENDWHILE
\ENDFUNCTION\end{algorithmic}
\end{algorithm}

Algoritmus 8.2: HeapExtractMin($H$)

Poznámka 8.5 (Analýza časové složitosti)

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)$.

Lemma 8.4 (Korektnost HeapExtractMin)

Buď $H$ minimová halda a $k$ klíč. Potom struktura vzniklá voláním HeapExtractMin ($H$) je opět minimová halda.

Zobrazit důkaz

  • 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$

Otázka 8.1

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?

Otázka 8.2

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.

Otázka 8.3

Využitím operace HeapChangeKey lze definovat operaci, která umožňuje z haldy odstranit libovolný prvek zadaný ukazatelem. Popište její algoritmus.

Postup 8.1

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)

Pozorování 8.2 (Vztahy indexů v haldě)

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.

Důsledek 8.1

Haldu lze tedy jednoduše reprezentovat v poli a pohyb po stromu realizovat výše popsanými operacemi s indexy.

Varování 8.1 (Vztahy indexů v haldě)

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$

8.1.3 HeapBuild

Věta 8.1 (Složitost HeapBuild)

Operace HeapBuild má časovou složitost $O(n)$.

Zobrazit důkaz

  • 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)

    \begin{equation*} \sum_{j=0}^{h-1}{2^j(h − 1 − j)} = \sum_{q=0}^{h-1}{2^{h−1−q}q} = \sum_{q=0}^{h−1}{\frac{ { 2^{h−1} } }{2^q}q } ≤ n \sum_{q=0}^{h−1}{ \frac{q}{2^q} } \end{equation*}

  • Časovou složitost lze shora odhadnout

    \begin{equation*} O(n\sum_{q=0}^{\infty}{\frac{q}{2^q}}) = O(n) \end{equation*}
    s použitím  d'Alembertova kritériua.

  • Pro $a_k = \frac{k}{2^k}$ máme:

    \begin{equation*} \lim_{k\to\infty} (\frac{k + 1}{2^k+1} : \frac{k}{2^k} ) = \lim_{k\to\infty} \frac{(k + 1) \cdot 2^k}{k \cdot 2^k+1} = \lim_{k\to\infty} \frac{k + 1}{k\cdot 2} = \frac{1}{2} \end{equation*}

$\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$

Poznámka 8.6

  • 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.