ADT Strom
Typy stromů a jejich implementace
 Vytisknout studijní materiál

                       Typy stromů a jejich implementace

Implementuje se obvykle jako seznam zřetězených prvků, kde každý uzel stromu obsahuje pole ukazatelů na následníky (syny).

mít nejvýše dva následníky (syny). Implementuje se jako seznam zřetězených prvků, někdy jako pole.

Pozice vrcholů binárního stromu lze očíslovat následujícím způsobem:

Začneme kořenem, kterému přiřadíme 1, dále jeho levému následníku 2, pravému následníku 3, a v číslování pokračujeme na každé úrovni zleva až do konce, kde přejdeme na další úroveň.

Tyto čísla jsou pak indexy uvedených pozic v binárním stromu. Pokud se na pozici skutečně nachází vrchol, prvek pole obsahuje klíč. V opačném případě je prvek pole označen jako nepoužit, například jsou-li hodnoty vrcholů nezáporná celá čísla, může být jeho hodnota -1.

Má-li vrchol hodnotu indexu i, potom

levý následník má index 2i

pravý následník má index 2i + 1

předchůdce, pokud existuje, má index i/2 (celočíselně).

Obecně, tato implementace není efektivní, protože nejenom musíme vytvořit pole pro předpokládanou maximální velikost stromu, ale navíc musí obsahovat i prvky pro pozice neobsazené vrcholy.

Poznamenejme, že uvedené vztahy platí, má-li kořen stromu index 1. Pokud by jsme ho umístnili do prvku pole s indexem 0, tyto vztahy nutno upravit.

Vrchol binárního stromu můžeme implementovat obdobně jako prvek seznamu, jenomže namísto položky dalsi budou v implementaci vrcholu polozky levy a pravy

class Vrchol {

int klic;

Vrchol levy;

Vrchol pravy;

...

void tiskVrcholu() {

    System.out.print(klic+” “);

}

}

V případě seznamu, když jsme potřebovali projít (navštívit) všechny jeho prvky, například pro jejich vytištění, postupovali jsme od prvního k dalšímu pomocí ukazatele dalsi, atd.

V případě stromu začneme kořenem, ale pro další systematický postup máme tři možnosti

Přímý průchod (preorder) navštívíme vrchol, potom levý a pravý podstrom

Vnitřní průchod (inorder) navštívíme levý podstrom, vrchol a pravý podstrom

Zpětný průchod (postorder) navštívíme levý a pravý podstrom a potom vrchol

Binární strom je definován rekurzivně a následně můžeme rekurzivně vyjádřit jeho průchody.

Rekurzivní průchod stromem

void pruchodR(Vrchol v) {

if (v == null)

    return;

v.tiskVrcholu();

pruchodR(v.levy);

pruchodR(v.pravy);

}

Vrchol koren;

pruchodR (koren);

Uvedená metoda implementuje průchod preorder, posunutím řádku s tiskem mezi rekurzivní volání získáme implementaci průchodu inorder a jeho posunutím za obě rekurzivní získáme implementaci průchodu postorder.

Čas průchodu stromem je pro prázdný strom dán vyhodnocením podmínky

T(0) = cpor

Trvá-li tisk ctisk a má-li levý strom m vrcholů a prvý strom n-m vrcholů, potom

T(n) = T(m) + T(n-m) + ctisk              pro n > 0

Její řešení je T(n) = (cpor + ctisk)n + cpor čas průchodu stromem je O(n).

Použitím abstraktního zásobníku, do kterého můžeme ukládat klíče anebo stromy můžeme vytvořit nerekurzivní implementace průchodů.

1. Do zásobníku vložíme procházený strom

2. Dokud zásobník není prázdný, v cyklu vybereme prvek ze zásobníku a je-li ním klíč vytiskneme ho, jinak do zásobníku vložíme

pro preorder: pravý podstrom, levý podstrom, klíč vrcholu

pro inorder: pravý podstrom, klíč vrcholu, levý podstrom

pro postorder: klíč vrcholu, pravý podstrom, levý podstrom

Pro preorder průchod je při vkládání jako poslední vložen klíč a ten je tedy vybrán na začátku uvedeného cyklu a vytisknut.

Nerekurzivní preorder průchod pak je

void pruchod(Vrchol v) {

VZasobnik z = new VZasovnik();

z.push(v);

while (!z.jePrazdny()) {

    v = z.pop();

    v.tiskVrcholu();

    if (v.pravy != null) z.push(v.pravy);

    if (v.levy != null) z.push(v.levy);

}

}

Binární vyhledávací strom je takový strom pro který platí (kromě toho, že je binární):

Nechť x je vrchol stromu.

Je-li y vrchol v levém podstromu, potom hodnota vrcholu y (klíč) < hodnota vrcholu x.

Je-li y vrchol v pravém podstromu, potom hodnota vrcholu y (klíč) > hodnota vrcholu x.

Od toho se odvíjí i vkládání prvku do stromu .

    Příklad BVS stromu zobrazuje obrázek

Nevýhody BVS

1.složitost vyhledávání závisí na pořadí vkládání prvků do stromu

2.složité rušení prvku

Rušení prvku

1.v listu - není problém

2.ve vnitřním uzlu - prvek nahradíme symetrickým předchůdcem ("nejpravějším" uzlem z levého podstromu) nebo symetrickým následovníkem ("nejlevějším" uzlem z pravého podstromu). Problém jsme tak převedli na rušení prvku v listu. Obdobně se postupuje i u AVL - stromů. Ale to už trochu předbíhám.

Vkládání a rušení prvku v BVS ukazuje obrázek .

Symetrický následovník a předchůdce jsou zakresleny na obrázku

Možnosti (volby mezi sym. následovníkem a předchůdcem):

(a) zvolíme "napevno" předchůdce, nebo následovníka ->usíme zajistit jeho existenci ->problém

(b) ten samý přístup, ale pokud předchůdce, případně následovník neexistuje, zvolíme druhou alternativu

(c) zjistíme, který strom je větší, a z té strany vezmeme "náhradní uzel" (zaručuje existenci předchůdce, příp. následovníka uzlu, který chceme odebrat)

U Binárních vyhledávacích stromů rozlišujeme stejně jako u Binárních stromů tři strategie prohledávání: PreOrder (v pořadí: kořen, levý syn-následovník, pravý syn-následovník), InOrder (v pořadí levý syn, kořen, pravý syn) a PostOrder (pravý, kořen, levý). Implementace jsou popsány výše.

Viz.tabulka .

Zkusme si nyní implementaci BVS:

Základní operací nad BVS, je hledání hodnoty uložené ve vrcholu podle klíče. Třída Vrchol tedy bude obsahovat navíc člen data, například typu String.

class Vrchol {

int klic;

String data;

Vrchol levy;

Vrchol pravy;

Vrchol (int klic, String data) {

    this.klic = klic;

    this.data = data;

}

void tiskVrcholu() {

    System.out.print(data+” “);

}

}

BVS strom je reprezentován svým kořenem

private Vrchol koren;

Pro prázdný strom je

koren == null

Signatura metody hledej je

String hledej(int)

Z definice BVS přímo plyne její rekurzivní implementace

private String hledejR(Vrchol v, int klic) {

if (v == null)

    return null;

if (klic == v.klic)

    return v.data;

if (klic < v.klic)

    return hledejR(v.levy, klic);

else

    return hledejR(v.pravy, klic);

}

String hledej(int klic) {

return hledejR(koren, klic);

}

Hledání v BVS je stejně efektivní jako binární hledání v uspořádané množině. Pro BVS je významné, že stejně efektivně můžeme implementovat i vkládání.

Signatura metody vloz je

void vloz(int, String)

Její rekurzivní implementaci můžeme opsat následovně.

Je-li strom prázdný, nový prvek se stane jeho kořenem. Jinak, je-li klíč nového prvku menší než klíč kořene, prvek vložíme do levého podstromu a do pravého podstromu, je-li klíč nového prvku větší než klíč kořene.

private Vrchol vlozR(Vrchol v, int klic,

                        String data) {

if (v == null)

    return new Vrchol(klic, data);

    if (klic < v.klic)

      v.levy = vlozR(v.levy, klic, data);

    else

      v.pravy = vlozR(v.pravy, klic, data);

    return v;

}

void vloz(int klic, String data) {

koren = vlozR(koren, klic, data);

}

Obě metody při každém rekurzivním volání sestoupí o jednu úroveň níž a tedy složitost uvedených algoritmů je O(h), kde h je výška stromu.

Průchod inorder BVS, sestrojeným pro posloupnost prvků jejich postupným vkládáním metodou vloz(), vytiskne tyto prvky uspořádané podle klíče.

Nerekurzivní verze uvedených metod se v cyklu rozhodují, budou-li pokračovat v levém nebo v pravém podstromu. V následující metodě vloz se nejprve najde pozice, kam nový vrchol vložit a potom se vloží. Protože proměnná x, pomocí které stromem sestupujeme má v tom okamžiku hodnotu null, udržujeme na proměnné predch (obdobně jako u seznamu) ukazatel na vrchol, ke kterému nový vrchol připojíme.

void vloz(int klic, String data) {

if (koren == null) {

    koren = new Vrchol(klic, data);

    return;

}

Vrchol x = koren, predch = null;

while (x != null)

    if (klic < x.klic) {

      predch = x;

      x = x.levy;

    }

    else {

      predch = x;

      x = x.pravy;

    }

if (klic < predch.klic)

    predch.levy = new Vrchol(klic, data);

else

    predch.pravy = new Vrchol(klic, data);

}

hashovací - vznikly "zkřížením" hash-tabulek se stromovými grafy.

Princip:

hodnoty hash-funkce se interpretují jako bitové řetězce, jimiž se kódují přístupové cesty v patřičném stromu (0 = levý syn, 1 = pravý syn).

Viz obr.

bitové - vzniknou z hashovacích, položíme-li h(x) = x , prvky umístíme do listů (na konec cesty) odpovídajícím bitovým řetězcům.

Příklad: Adresní strom reprezentující množinu {01001, 01010, 01011, 11010} je zobrazen na obrázku .