Tietorakenteet, laskuharjoitus, 23.-2.1 1. (a) Kuvassa 1 on esitetty eräät pienimmistä AVL-puista, joiden korkeus on 3 ja 4. Pienin h:n korkuinen AVL-puu ei ole yksikäsitteinen juuren alipuiden keskinäisen järjestyksen tai avaimien suhteen. Kuvasta 1a kuitenkin havaitaan, että pienimmässä kolmen korkuisessa AVL-puussa on juuren alipuina pienin yhden ja kahden korkuinen AVL-puu. 2 Vastaava 13 pätee 22 myös pienimmälle neljän korkuiselle AVL-puulle, sen juuren alipuut ovat pienimmät kahden ja kolmen korkuiset AVL-puut. 1 3 10 17 21 33 55 77 2 22 7 101 1 150 17 22 2 22 2 13 2 13 22 22 107 2 22 33 55 77 77 1 3 1 10 17 21 33 55 77 7 101 (a) Pienin AVL puu, jonka korkeus on 3 150 (b) Pienin AVL puu, 101 jonka korkeus on 4 Kuva 1: Pienimpiä mahdollisia AVL-puita 150 107 1 1 Kuvassa 17 2 on esitetty suurimmat mahdolliset AVL-puut, joiden korkeus on 3 ja 4. Täydellinen puu on erityisen 2 tasapainoinen 13 22 puuja se täyttää 107 myös 2 AVLehdon, joten suurin h:n korkuinen AVL-puu on h:n korkuinen täydellinen puu. 10 17 11 1 3 10 17 21 33 55 77 102 10 11 133 154 12 177 221 33 55 77 101 107 1 2 2 1 3 150 10 13 10 17 22 11 17 21 33 55 77 102 10 11 133 154 12 177 221 (a) Suurin AVL puu, jonka korkeus on 3 101 101 150 1 17 2 22 2 13 22 107 2 10 11 1 3 10 17 21 33 55 77 102 77 10 11 133 154 12 177 221 (b) Suurin AVL puu, jonka korkeus on 4 Kuva 2: Suurimpia mahdollisia 150 AVL-puita (b) Kuvassa 3 on esitetty puu, jonka 1 täytyi osoittaa 17 toteuttavan AVL-ominaisuus. Kuvan yhteyteen on merkitty alipuiden korkeudet kunkin solmun vasemmalle 2 13 22 107 1 1 101 150
puolelle. Puu on AVL-puu jos kaikille solmuille n pätee: Height(n.left) Height(n.right) 1 Siis jokaisen solmun vasemman ja oikean alipuun korkeuksien erotus on itseisarvoltaan -1, 0 tai 1. Laskemalla jokaisen solmun alipuiden korkeuden erotuksen havaitaan, että kuvassa 3 esiintyvä puu on AVL-puu. 21 10 30 13 24 55 Kuva 3: Puu, jonka solmujen yhteyteen on merkitty niistä lähtevien alipuiden korkeus 2. (a) Kuvassa 4 on esitetty annettujen avainten lisäys AVL-puuhun. Ensin lisätään avaimet, ja (kuvat 4a, 4b ja 4c). Viimeisen lisäyksen jälkeen puu on epätasapainossa. Se saadaan jälleen tasapainoon kiertämällä avaimen solmua oikealle (kuva 4d). Lisätään avaimet ja (kuvat 4e ja 4f). Jälkimmäinen lisäys saa puun epätasapainoon, joka korjataan suorittamalla avaimen solmuun vasen-oikea kaksoiskierto. Ensin kierretään avaimen solmu vasemmalle (kuva 4g), jonka jälkeen kierretään avaimen solmu oikealle (kuva 4h). Tämän jälkeen lisätään puuhun (kuva 4i), joka saa puun juuren epätasapainoon. Puun juurta käännetään oikealle (kuva 4j). Viimeisenä puuhun lisätään ja. Kumpikaan lisäyksistä ei vaikuta puun tasapainoon, joten kiertoja ei tarvita enempää. (b) Kuvassa 5 on esitetty edellisen tehtävän avainten lisääminen AVL-puuhun käänteisessä järjestyksessä. Alussa puuhun lisätään avaimet, ja (kuvat 5a, 5b ja 5c). Viimeisen lisäyksen jälkeen puun juuri on epätasapainossa. Puu saadaan tasapainoiseksi kiertämällä juurta oikealle (kuva 5d). Tämän jälkeen puuhun lisätään avaimet ja (kuvat 5e ja 5f). Nyt avaimen solmu on epätasapainossa. Se saadaan korjattua oikea-vasen kaksoiskierrolla. Ensin kierretään avaimen solmua oikealle (kuva 5g) jonka jälkeen avaimen solmu kierretään vasemmalle (kuva 5h). Tämän jälkeen lisätään avaimet ja (kuvat 5i ja 5j), jonka jälkeen avaimen solmu on epätasapainossa. Tilanne korjataan tekemällä vasen-oikea kaksoiskierto solmuun (kuvat 5k ja 5l). Viimeiseksi lisätään (kuva 5m). 2
3 4 (a) (b) (c) (d) (e) 1 1 2 1 2 (f) (g) (h) 1 (i) (j) (k) (l) Kuva 4: Avaimien,,,,,,, lisäys AVL-puuhun 3
9 (a) (b) (c) (d) (e) 7 1 (f) (g) (h) (i) (j) (k) 9 (l) (m) Kuva 5: Avaimien,,,,,,, lisäys AVL-puuhun 4
3. (a) Kuvassa aloitetaan poistojen sarja kuvan 4l tilanteesta. Ensin poistetaan, jonka yhteydessä solmu asetetaan juuren vasemmaksi alipuuksi (kuva a). Tämän seurauksena juuri tulee epätasapainoon, mikä korjataan kiertämällä juurta vasemmalle (kuva b). Seuraavaksi poistetaan (kuva c), jonka seurauksena juuri on jälleen epätasapainossa. Tilanteesta selvitään suorittamalla vasen-oikea kaksoiskierto juureen. Ensin kierretään solmu vasemmalle (kuva d), jonka jälkeen juuri kierretään oikealle (kuva e). Tämän jälkeen poistetaan tavalliseen binäärihakupuun poiston tapaan (kuva f). Siinä korvataan ensin avaimen solmun avain oikean alipuun pienimmällä avaimella. Varsinainen poisto kohdistuu korvaavan avaimen () solmuun. Lopuksi poistetaan (kuva g), jonka poisto ei horjuta AVL-puun tasapainoehtoa. (b) Kuvassa 7 aloitetaan poistojen sarja kuvan 5m tilanteesta. Ensin poistetaan ja (kuvat 7a ja 7a). Jälkimmäinen poisto saa juuren epätasapainoon. Tilanne korjataan kiertämällä juurta vasemmalle (kuva 7c). Tämän jälkeen poistetaan, joka ei eroa tavallisesta binäärihakupuun poistooperaatiosta lainkaan (kuva 7d). Vimeiseksi poistetaan (kuva 7e), joka saa jälleen juuren epätasapainoon. Tilanne korjataan kiertämällä juurta oikealle (kuva 7f). 5
(a) (b) (c) (d) (e) (f) (g) Kuva : Avaimien,, ja poisto kuvan 4l tilanteesta aloitettuna
(a) (b) (c) (d) (e) (f) Kuva 7: Avaimien,, ja poisto kuvan 5m tilanteesta aloitettuna 7
4. Ohessa on binääripuu toteutettuna Javalla. Luokan main-metodissa on puun toiminnan testailua. class BTree { private long key; private BTree left; private BTree right; public BTree(long value) { this.key = value; this.right = null; this.left = null; public void setright(btree right) { this.right = right; public BTree getright() { return this.right; public void setleft(btree left) { this.left = left; public BTree getleft() { return this.left; /* Insert-operaatio olisi luonnollisinta kirjoittaa rekursiivisesti, * mutta säästääksemme muistia teemme siitä iteratiivisen version. */ public void insert(long key) { boolean inserted = false; BTree node = new BTree(key); BTree curr = this; while(!inserted) { if (key <= curr.key) { if (curr.left == null) { curr.left = node; inserted = true; else { curr = curr.left;
else { if (curr.right == null) { curr.right = node; inserted = true; else { curr = curr.right; /* Tämäkin olisi tehokkaampi iteratiivisena versiona, mutta näytetään * miten tämä (ja edellinen) voidaan toteuttaa rekursiivisena. */ public boolean search(long key) { if (key == this.key) return true; else if (key < this.key && this.left!= null ) return this.left.search(key); else if ( this.right!= null ) return this.right.search(key); else return false; public long height() { long left = (this.left == null)? 0 : this.left.height(); long right = (this.right == null)? 0 : this.right.height(); return Math.max(left, right) + 1; public void print() { if ( this.left!= null ) this.left.print(); System.out.print(this.key + " "); if ( this.right!= null ) this.right.print(); public static void main(string[] args) { int kertaa = 100; if (args.length > 0) kertaa = Integer.parseInt(args[0]); 9
long eka = (long) (Long.MAX_VALUE * Math.random()); BTree puu = new BTree(eka); for (int i = 0; i < kertaa; i++) puu.insert((long) (Long.MAX_VALUE * Math.random()) ); puu.print(); System.out.println(); long num = (long) (Long.MAX_VALUE * Math.random()); if (puu.search(num)) System.out.println(num + " löytyi puusta."); else System.out.println(num + " ei löytynyt puusta."); if (puu.search(eka)) System.out.println(eka + " löytyi puusta."); else System.out.println(eka + " ei löytynyt puusta."); System.out.println("Puun korkeus on " + puu.height()); System.out.println("Vertailun vuoksi: kaksikantainen logaritmi luvusta " + kertaa + " on "+ Math.log(kertaa)/Math.log(2)); 100 Binääripuun korkeus lisättäessä n avainta korkeus log 2 0 0 40 20 0 5e+0 1e+07 1.5e+07 2e+07 2.5e+07 3e+07 3.5e+07 4e+07 4.5e+07 5e+07 n Kuva : Puun korkeus lisättäessä n satunnaista avainta 5. (a) Kuvassa on kaksikantaisen logaritmin ja puun korkeuden kuvaajat kun puuhun on lisätty n satunnaista avainta. Kuvaajasta havaitaan, että kun puuhun 10
lisätään satunnaisia avaimia ei sen korkeus eroa kovinkaan paljon logaritmifunktion arvoista. Itse asiassa näyttääkin, puun korkeudelle h pätisi n:n satunnaisen avaimen lisäyksessä h 3 log 2 n = O(log n). Tietenkään tämä ei ole vielä validi perustelu, mutta voidaan osoittaa, että tämänlaisessa tilanteessa puun korkeuden odotusarvo on logaritminen puun avainten lukumäärän suhteen. (b) Kuvissa 9, 10 ja 11 on esitetty oman puumme vertailuja Javan kirjastoista löytyvään TreeSettiin. Yllättävästi tasapainoittamaton puumme oli nopeampi sekä satunnaisissa lisäyksissä, että hauissa. Tämä selittyy osittain siitä, ettei tasapainoittamatonkaan puu kasva kovinkaan korkeaksi jos avaimet lisätään satunnaisesti. Lisäysten nopeus selittyy sillä, ettei omassa puutoteutuksessamme tarvittu tasapainotusoperaatioita. Hakujen nopeutta selittää se, että puumme on tarpeeksi matala. Luultavasti TreeSettiä hidastaa hiukan sen geneerinen toteutus, joka pakottaa sen käyttämään Long-olioita primitiivisen long-tietotyypin sijasta. Tasapainotetun puun suurin etu nähdään etsittäessä avaimia puusta, johon avaimet on lisätty järjestyksessä. Tässä tapauksessa oma tasapainoittamaton puumme surkastuu tavalliseksi listaksi, jossa haut ovat hitaita. Kesto lisättäessä n satunnaista avainta 00 oma puu Javan TreeSet 1000 00 00 400 200 0 100000 200000 300000 400000 500000 00000 700000 00000 900000 1e+0 n Kuva 9: Kesto lisättäessä n satunnaista avainta. Vakioaikaiset min- ja max-operaatiot voidaan toteuttaa säilyttämällä puun yhteydessä osoittimia sen pienimpään ja suurimpaan avaimeen. Näiden tallettaminen lisää puun operaatioita ainoastaan vakioajalla, koska ehdot voidaan tarkistaa yksinkertaisella ehtolausekkeella. Vakioaikaiset seuraaja- ja edeltäjä-operaatiot saadaan tallettamalla jokaisen solmun yhteyteen viite sen avaimen seuraajan ja edeltäjän sisältäviin solmuihin. Binääripuu- 11
500 n hakua 1000000:n satunnaisen avaimen puuhun oma puu Javan TreeSet 400 300 200 100 0 50000 100000 150000 200000 250000 300000 350000 400000 450000 500000 n Kuva 10: Kesto haettaessa n satunnaista avainta 1000000:n satunnaisessa järjestyksessä lisätyn avaimen puusta 1000 n hakua 20000:n järjestyksessä lisätyn avaimen puuhun oma puu Javan TreeSet 00 00 400 200 0 1000 2000 3000 4000 5000 000 7000 000 9000 10000 n Kuva 11: Kesto haettaessa n avainta 20000:n järjestyksessä lisätyn avaimen puusta
ta, jossa on tavallisten left- ja right-viitteiden lisäksi viitteet solmun seuraajaan ja edeltäjään kutsutaan langoitetuksi (engl. threaded). Langoitus ei lisää operaatioiden aikavaativuutta kuin vakiokertoimella. Lisättäessä binäärihakupuuhun avainta, sen seuraaja ja edeltäjä tulevat vastaan hakupolulla, joten langoituksen toteuttamiseksi ei tarvitse toteuttaa edes ylimääräisiä hakuja puuhun. Näiden muutoksien huonona puolena on lisääntynyt muistin tarve ja aavistuksen hidastuvat lisäys- ja poisto-operaatiot. Jos puun yksi viite vie tilaa 4 tavua ja avain vie tilaa c tavua, niin ilman edeltäjä- ja seuraaja-osoittimia n:n alkion puu vie tilaa (+c)n+ tavua. Lausekkeen tulee viitteestä juureen sekä min- ja max-viitteistä. Jos puuhun talletetaan lisäksi viitteet edeltäjään ja seuraajaan, niin n:n alkion puu vie tilaa (1 + c)n +. Siis itse rakenteen koko kaksinkertaistuu! Huomaa, että tässä tarkastelussa käytetyt arvot eivät kuvasta Javan todellisuutta, koska Javassa olioiden yhteyteen talletetaan paljon muutakin tavaraa. Ohessa on vielä langoitetun binääripuun toteuts Javana. class ThreadedBTree { private class BTnode { public BTnode left; public BTnode right; public BTnode succ; public BTnode pred; public long key; public BTnode(long key) { this.key = key; this.succ = null; this.pred = null; this.left = null; this.right = null; public BTnode(long key, BTnode succ, BTnode pred) { this.key = key; this.succ = succ; this.pred = pred; this.left = null; this.right = null; private BTnode min; private BTnode max; 13
private BTnode root; public ThreadedBTree(long key) { BTnode node = new BTnode(key); this.min = node; this.max = node; this.root = node; public void insert(long key) { BTnode succ, pred, curr; BTnode node = new BTnode(key); boolean inserted = false; succ = pred = null; curr = this.root; /* Väistämättä avaimen seuraaja ja edeltäjä * tulevat vastaan matkan varrella kun etsitään * kohtaa johon lisätä. */ while(!inserted) { /* Huomaa ettemme hyväksy tässä avainten duplikaatteja. */ if ( (succ == null curr.key < succ.key) && key < curr.key) succ = curr; if ( (pred == null curr.key > pred.key) && key > curr.key) pred = curr; if( key < curr.key ) { if (curr.left == null) { curr.left = node; inserted = true; else { curr = curr.left; else if ( key > curr.key ) { if (curr.right == null) { curr.right = node; inserted = true; else { curr = curr.right; else return; if (pred!= null) { 14
node.pred = pred; pred.succ = node; if (succ!= null) { node.succ = succ; succ.pred = node; if (this.min.key > key) this.min = node; if (this.max.key < key) this.max = node; /* Koska jouduimme ottamaan mukaan sisäluokan joudumme * hoitamaan tavallisen puun rekursion käsin käyttäen pinoa, * kirjoittamaan metodit sisäluokalle tai käyttämällä * staattisia metodeja. Rekursion käyttäminen on puiden * tapauksessa luonnollista, joten tässä on päätetty * ohittaa hankaluudet sen avulla. */ public void print_default() { print_helper(this.root); private static void print_helper(btnode solmu) { if (solmu == null) return; print_helper(solmu.left); System.out.print(solmu.key + " "); print_helper(solmu.right); public void print_threaded_asc() { BTnode curr = this.min; while(curr!= null) { System.out.print(curr.key + " "); curr = curr.succ; public static void main(string[] args) { 15
ThreadedBTree puu = new ThreadedBTree(0); for (int i = 0; i < 20; i++) puu.insert((long) (100*Math.random())); puu.print_default(); System.out.println(); puu.print_threaded_asc(); 1