Hoofdstuk 6. Geordende binaire bomen



Vergelijkbare documenten
Java Programma structuur

Informatica 2e semester

Informatica 2e semester

Informatica. Deel II: les 9 OS & AVL. Jan Lemeire. Informatica deel II. februari mei Informatica II: les 9

Elementary Data Structures 3

Informatica. Deel II: les 10. Bomen. Jan Lemeire Informatica deel II februari mei Informatica II: les 9

Hoofdstuk 2. Week 4: Datastructuren. 2.1 Leesopdracht. 2.2 Bomen. 2.3 Definitie

Tree traversal. Bomen zijn overal. Ferd van Odenhoven. 15 november 2011

Informatica 2e semester

Datatypes Een datatype is de sort van van een waarde van een variabele, veel gebruikte datatypes zijn: String, int, Bool, char en double.

Informatica. Deel II: les 8. Software & binaire bomen. Jan Lemeire Informatica deel II&III februari mei Parallel Systems: Introduction

Datastructuren en algoritmen voor CKI

Datastructuren; (Zoek)bomen

Uitwerking tentamen Algoritmiek 9 juni :00 17:00

Datastructuren: stapels, rijen en binaire bomen

Tree traversal. Ferd van Odenhoven. 15 november Fontys Hogeschool voor Techniek en Logistiek Venlo Software Engineering. Doorlopen van bomen

Informatica. 2 e semester: les 9. OS & Sorteren. Jan Lemeire Informatica 2 e semester februari mei Informatica II: les 9

public boolean equaldates() post: returns true iff there if the list contains at least two BirthDay objects with the same daynumber

Programmeren in C++ Efficiënte zoekfunctie in een boek

Inleiding Programmeren 2

Stacks and queues. Introductie 45. Leerkern 45. Terugkoppeling 49. Uitwerking van de opgaven 49

Inleiding Programmeren 2

Tentamen Object Georiënteerd Programmeren TI oktober 2014, Afdeling SCT, Faculteit EWI, TU Delft

voegtoe: eerst methode bevat gebruiken, alleen toevoegen als bevat() false is

Hoofdstuk 9. Hashing

Objectgeorïenteerd werken is gebaseerd op de objecten die door het systeem gemanipuleerd worden.

IMP Uitwerking week 13

Dergelijke functionaliteit kunnen we zelf ook aan eigen code toevoegen.

Tentamen Programmeren in C (EE1400)

Opgaven Zoekbomen Datastructuren, 15 juni 2016, Werkgroep.

Een gelinkte lijst in C#

Uitwerking tentamen Algoritmiek 9 juli :00 13:00

Informatica. Deel II: les 1. Java versus Python. Jan Lemeire Informatica deel II februari mei Parallel Systems: Introduction

Tweede college algoritmiek. 12 februari Grafen en bomen

Modelleren en Programmeren

Addendum bij hoofdstuk 5 Generieke implementatie van de zoekalgoritmen

Elfde college algoritmiek. 18 mei Algoritme van Dijkstra, Heap, Heapify & Heapsort

Algoritmiek. 15 februari Grafen en bomen

Overerving & Polymorfisme

Kleine cursus PHP5. Auteur: Raymond Moesker

Modelleren en Programmeren

2 Recurrente betrekkingen

Lessen Java: Reeks pag. 1

recursie Hoofdstuk 5 Studeeraanwijzingen De studielast van deze leereenheid bedraagt circa 6 uur. Terminologie

Examen Datastructuren en Algoritmen II

Modelleren en Programmeren

Informatica. Deel II&III: les 7. AI linked lists - chips. Jan Lemeire Informatica deel II februari mei Parallel Systems: Introduction

Recursion. Introductie 37. Leerkern 37. Terugkoppeling 40. Uitwerking van de opgaven 40

Verslag Opdracht 4: Magische Vierkanten

Beginselen van programmeren Practicum 1 (Doolhof) : Oplossing

Opgaven Zoekbomen Datastructuren, 20 juni 2018, Werkgroep.

Zelftest Programmeren in Java

Informatica. Deel II: les 1. Java versus Python. Jan Lemeire Informatica deel II februari mei Parallel Systems: Introduction

Modelleren en Programmeren

Informatica. Deel II&III: les 8. Software & binaire bomen. Jan Lemeire Informatica deel II&III februari mei Parallel Systems: Introduction

Vakgroep CW KAHO Sint-Lieven

Tentamen Imperatief en Object-georiënteerd programmeren in Java voor CKI

Universiteit van Amsterdam FNWI. Voorbeeld van tussentoets Inleiding programmeren

Uitwerking tentamen Algoritmiek 10 juni :00 13:00

NAAM: Programmeren 1 Examen 21/01/2011

Informatica: C# WPO 11

Stacks and queues. Hoofdstuk 6

Abstracte klassen & Interfaces

HOGESCHOOL VAN AMSTERDAM Informatica Opleiding. CPP 1 van 10

Informatica. Objectgeörienteerd leren programmeren. Van de theorie met BlueJ tot een spelletje met Greenfoot... Bert Van den Abbeele

Lineaire data structuren. Doorlopen van een lijst

Design patterns Startbijeenkomst

Tentamen Programmeren in C (EE1400)

Informatica. 2 e semester: les 8. Software & binaire bomen. Jan Lemeire Informatica 2 e semester februari mei Parallel Systems: Introduction

Hoofdstuk 3. Week 5: Sorteren. 3.1 Inleiding

Programmeren (1) Examen NAAM:

In BlueJ. Doe onderstaande met muis/menu s:

Lab Webdesign: Javascript 3 maart 2008

10 Meer over functies

Design principes.

Zevende college Algoritmiek. 6 april Verdeel en Heers

Design principes.

Datastructuren en algoritmen voor CKI

Tentamen Imperatief Programmeren

Tentamen in2705 Software Engineering

NAAM: Programmeren 1 Examen 29/08/2012

Programmeren 1 23 januari 2013 Prof. T. Schrijvers

Zelftest Inleiding Programmeren

software constructie recursieve datastructuren college 15 5 stappen plan ontwerpen de software bestaat uiteindelijk uit datatypen functies

Programmeren in Java 3

Zevende college algoritmiek. 23/24 maart Verdeel en Heers

Tentamen Object Georiënteerd Programmeren TI januari 2013, Afdeling SCT, Faculteit EWI, TU Delft

Gegevens invullen in HOOFDLETTERS en LEESBAAR, aub. Belgische Olympiades in de Informatica (duur : maximum 1u15 )

5.4.2 a. Neen: dit lukt alléén met 1, 3, 7 enzovoort. b. Ja: dit lukt met elk aantal knopen! Bijvoorbeeld de volgende boom: 1

Hoofdstuk 1: Inleiding. Hoofdstuk 2: Klassen en objecten Datahiding: afschermen van implementatiedetails. Naar de buitenwereld toe enkel interfaces.

Inleiding tot Func.oneel Programmeren les 3

Programmeermethoden. Recursie. week 11: november kosterswa/pm/

Als een PSD selecties bevat, deelt de lijn van het programma zich op met de verschillende antwoorden op het vraagstuk.

Datastructuren en Algoritmen voor CKI

Een inleiding in de Unified Modeling Language 67

[15] Variabelen in functies (of: een woordje over scope)

Zevende college algoritmiek. 24 maart Verdeel en Heers

DIAGNOSTISCHE TOETS Softwaresystemen UITWERKING

Transcriptie:

Hoofdstuk 6 Geordende binaire bomen Eerder bespraken we hoe gelinkte lijsten een zeer flexibele structuur geven. Het zoeken in een gelinkte lijst was echter niet optimaal, aangezien je enkel de lijst van links naar rechts kunt doorlopen en alle elementen moet passeren. We zagen ook dat binair zoeken het snelste was in een geördende lijst: je kijkt halverwege of je element links of rechts zit en gaat verder met dat deel. Aldus kun je telkens de helft van de elementen elimineren. De zoektijd wordt dan log 2 n. Merk op dat hetzelfde idee van halveren terug te vinden is bij de snelle sorteeralgoritmen. Een binaire boom is gebaseerd op beide principes, het combineert de flexibiliteit van een gelinkte lijst met binaire zoeksnelheid. Een toepassing is dus een woordenboek of elke lijst die je gesorteerd wilt bijhouden. Boomstructuren zullen we nog tegenkomen in hoofdstuk 9. Daar zal het niet gaan om gesorteerde data, maar om de structuur van de gegevens zelf die zal vragen om een boom. 6.1 Definitie Left subtree ROOT Leaves Right subtree De basiselementen van een binaire boom zijn weergegeven in de figuur. Het centrale element langs waar men toegang krijgt tot de boom, noemt men de root (wortel). De twee bomen waarnaar de twee pointers van de wortel wijzen, noemt men respectievelijk "linker-subboom" en "rechtersubboom". De subbomen zijn uiteraard zelf ook gewone bomen met dezelfde eigenschappen als de boom waarvan ze deel uitmaken. Elementen helemaal onderaan in een subboom die geen subboom bevatten, noemt men "bladeren" (leaves). Deze plantkundige definities kunnen eigenaardig lijken; de verbeeldingrijke lezer mag eruit afleiden dat informatici ondersteboven leven. b d f h j l n Men spreekt van een geordende binaire boom wanneer alle elementen een "sleutelveld" bevatten voor welke een orderelatie kan worden gedefinieerd en wanneer elk element de wortel is van een boom met kleinere elementen in de linkersubboom en grotere elementen in de rechtersubboom. De figuur toont een a c e g i k m o 1

dergelijke boom waarin de sleutels letters van het alfabet zijn. Geordende binaire bomen worden veel aangewend voor het opbouwen en bijhouden van gegevens die men veelvuldig en snel wilt raadplegen (woordenboeken), want de zoektijd in een dergelijke boom is logaritmisch voor een gebalanceerde boom, zoals we later zullen zien. Dit is het gevolg van het halveren van de overblijvende zoekruimte bij het bezoeken van elk element. Het kan dus een traditioneel woordenboek zijn maar ook de lijst van de studenten van een universiteit of de catalogus van een winkel. 6.2 Het bouwen van een geordende binaire boom De basisbouwsteen is de node. class Node<T>{ T data; Node<T> left, right; Node(T data){ this.data = data; this.left = null; this.right = null; Met behulp van deze generieke node kunnen we een binaire boom bouwen die elementen van het type T opslaat. Het parameteriseren van een datatype (generics in Java) bespraken we reeds onder ArrayList en FifoQueue. De boom moet enkel de root kennen. Vanuit de root heeft ze toegang tot alle andere nodes. public class BinaryTree<T> { Node<T> root; Comparator<T> comparator; public BinaryTree(Comparator<T> comparator){ this.comparator = comparator; root = null; We verwachten van de gebruiker ook een Comparator. Deze zal ons vertellen hoe de elementen te ordenen. Het comparator-object zal gebruikt worden om twee objecten te vergelijken. Comparator is een interface met twee methodes gedefinieerd voor een type T: java.util Interface Comparator<T> 2

Method Summary int compare(t o1, T o2) Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second. boolean equals(object obj) Indicates whether some other object is "equal to" this comparator. De compare-methode vergelijkt twee objecten en geeft een negatieve waarde als het eerste object kleiner is dan het tweede, 0 als ze gelijk zijn en een positieve waarde als het tweede object kleiner is. De equals-methode mag je negeren. We komen hier nog op terug in het volgend hoofdstuk. Een object van de klasse construeren kunnen we als volgt doen: BinaryTree<Integer> tree = new BinaryTree<Integer>(new Comparator<Integer>() { @Override public int compare(integer val1, Integer val2) { return val1 - val2; ); We moeten met de constructor een comparator-object meegeven, een object van een klasse die de Comparator-klasse implementeert. Omdat we deze klasse nergens anders nodig hebben, kunnen we die klasse definiëren bij de creatie van het object. We creëren het object met new en de gevraagde interface en implementeren tezelfdertijd de gevraagde methodes van de interface. Dit wordt een instant class genoemd. Dit is bondig. Anders moesten we eerst een eigen Comparator-klasse definiëren, als volgt: class IntegerComparator implements Comparator<Integer> { @Override public int compare(integer val1, Integer val2) { return val1 - val2; Het aanmaken van de boom wordt dan: BinaryTree<Integer> tree = new BinaryTree<Integer>(new IntegerComparator()); Nog een opmerking: @Override geeft aan dat we de methode overschrijven of implementeren. Dit noemen we een annotatie en is een vorm van documenteren. Het toevoegen van een element in de boom, moet met de nodige zorg gebeuren: public void add(t object){ Node<T> newnode = new Node<T>(object); if (root == null) 3

else root = newnode; add(root, newnode); private void add(node<t> current, Node<T> newnode){ if (comparator.compare(newnode.data, current.data) < 0){ // moet links komen if (current.left == null) current.left = newnode; else add(current.left, newnode); else { // rechts if (current.right == null) current.right = newnode; else add(current.right, newnode); Initieel begint men met een lege boom, root staat op null. Het eerst-toe-te-voegen element moet aan de root toegekend worden. Verdere elementen gaan we via een recursief procédé toevoegen. We moeten immers de correcte positie zoeken. Tijdens het afdalen, vergelijken we het element met de node en bepalen we of het element in het linker- of rechterdeel moet komen. Merk op dat de recursieve add-methode private gedefinieerd is omdat die enkel binnen de klasse gebruikt zal worden. 6.3 Het zoeken van een element in een geordende binaire boom Het zoeken gebeurt ook recursief. We maken gebruik van de comparator om enerzijds tijdens het dalen langs de goede kant van de boom af te dalen, en anderzijds om te checken of het element gevonden werd (compare geeft dan 0). public boolean contains(t object){ return find(root, object); private boolean find(node<t> current, T object){ if (current == null) return false; else if (comparator.compare(current.data, object) == 0) return true; else if (comparator.compare(object, current.data) < 0) return find(current.left, object); else return find(current.right, object); De maximale zoektijd is gelijk aan de maximale diepte van de boom. Idealiter zal die log 2 n zijn, behalve wanneer de boom niet gebalanceerd is (zie verder). 6.4 Het verwijderen van een element uit een geordende binaire boom Het is soms nodig om uit een bestaande geordende boom een element te verwijderen, zonder de eigenschappen van de overblijvende boom aan te tasten. Dit is bijvoorbeeld het 4

geval wanneer men een woord uit een woordenboek wenst te schrappen. Afhankelijk van waar het element zich in de boom bevindt, zal het verwijderen min of meer moeilijk zijn. Onderstaande figuur toont een voorbeeld van een verwijderen, voorzien worden. 3 2 6 1 4 5 7 3 mogelijke situaties : a) 2 subbomen: 3, 6 b) 0 of 1 subboom: 1, 2, 4, 5, 7 c) niet aanwezig: 0, 8, geordende binaire boom waaruit elementen moeten worden verwijderd. Het verwijderen van een element met één of nul subbomen is eenvoudiger omdat dat neerkomt op het verwijderen van een element uit een lineaire lijst. Tenslotte, om de robuustheid van het algoritme te garanderen, moet ook het geval waarbij men een onbestaande sleutel probeert te Wanneer het te verwijderen element een linker- én een rechtersubboom bezit, gaat het om het verwijderen van de wortel van een boom. Dit is vele malen moeilijker. Er moet dan een nieuwe wortel gekozen worden tussen de overblijvende nodes. Omdat, per definitie, de wortel een sleutelwaarde heeft die kleiner is dan al de elementen van de rechter subboom en groter dan deze van de linker subboom, zijn het element met de grootste sleutel uit de linkersubboom of het element met de kleinste sleutel uit de rechtersubboom de beste kandidaten voor de rol van nieuwe wortel. Op deze manier blijven de herschikkingen aan de boom minimaal. De figuur toont de twee verschillende manieren waarop de vorige boom kan worden herschikt na het 2 6 2 6 verwijderen van element 3. 1 4 7 1 4 7 5 5 Meest linkse van rechtersubboom Meest rechtse van linkersubboom 5

Dit is het volledige algoritme: public boolean remove(t object){ if (comparator.compare(root.data, object)== 0){ root = createsubtree(root); return true; else return findnodeandremove(root, object); private boolean findnodeandremove(node<t> current, T object){ if (current == null) return false; if (current.left!= null && comparator.compare(current.left.data, object)== 0){ current.left = createsubtree(current.left); return true; else if (current.right!= null && comparator.compare(current.right.data, object)== 0){ current.left = createsubtree(current.right); return true; else if (comparator.compare(current.data, object) < 0) return findnodeandremove(current.left, object); else return findnodeandremove(current.right, object); /** * creates single tree from left and right nodes of current node */ private Node<T> createsubtree(node<t> current){ if (current.left == null){ // current.right is the only subtree that we should consider return current.right; else if (current.right == null){ // current.left is the only subtree that we should consider return current.left; else { // promote rightmost node of left subtree if (current.left.right == null){ current.left.right = current.right; return current.left; else { Node<T> rightmostnode = pickrightmostnode(current.left); // he is new root of subtree rightmostnode.right = current.right; rightmostnode.left = current.left; return rightmostnode; private Node<T> pickrightmostnode(node<t> p){ // we expect current.right not to be null if (p.right.right!= null){ return pickrightmostnode(p.right); else { 6

Node<T> rightmostnode = p.right; p.right = rightmostnode.left; rightmostnode.left = null; return rightmostnode; Eerst checken we of de root het gezochte element is. Zo ja, dan moet deze verwijderd worden en moeten we van de linker- en rechtersubboom een nieuwe boom maken. Dit doen we met createsubtree (). Op deze methode komen we dadelijk nog terug. Via findnodeandremove() dalen we in de boom tot we het element vinden of tot we merken dat het element niet aanwezig is. In dat laatste geval komen we op een null uit, net als bij het zoeken van een element dat we in het vorige deel hebben gezien. We keren onverrichterzake terug en geven false terug. Als we het element vinden als linker- of rechterelement, zullen we dit element vervangen door de subbomen van de elementen. Die we ook aanmaken met createsubtree (). Deze methode maakt één boom van beide subbomen. Als één van beide leeg is, is het gemakkelijk. Dan kan de andere teruggegeven worden. Indien geen van beide subbomen leeg is, moet een nieuwe wortel worden gekozen. Hier wordt er geopteerd voor het meest rechtse element van de linkersubboom. 6.5. Het doorlopen van binaire bomen Vaak willen we iets doen met alle elementen van de boom, zoals ze afprinten. De vraag is dan in welke volgorde we de elementen willen benaderen. Vaak worden drie specifieke volgordes aangewend die alle drie belangrijke toepassingen hebben. Ze worden respectievelijk in order, in preorder en in postorder genoemd. h d l b f j n a c e g i k m o PreOrder : root links - rechts : hdbacfegljiknmo InOrder : links root - rechts : abcdefghijklmno PostOrder : links rechts - root : acbegfdikjmonlh 7

Ze kunnen alle drie eenvoudig verwezenlijkt worden door middel van recursieve procedures. De figuur toont een boom en de uitgeschreven versies van dezelfde boom volgens de drie bovenvermelde methodes. Het is vanzelfsprekend ook nog mogelijk van rechts naar links eerder dan van links naar rechts te werken, dat vergt een kleine aanpassing aan de algoritmen. Hier de drie methodes die de boom volgens de drie bovenvermelde manieren doorlopen en de nodes printen public void preorder(node<t> node){ if (node!= null){ System.out.print(node.data+", "); preorder(node.left); preorder(node.right); public void inorder(node<t> node){ if (node!= null){ inorder(node.left); System.out.print(node.data+", "); inorder(node.right); public void postorder(node<t> node){ if (node!= null){ postorder(node.left); postorder(node.right); System.out.print(node.data+", "); Initieel worden deze procedures opgeroepen met als actuele parameter een pointer naar de wortel van de boom. De procedure verifieert dan eerst of de boom niet leeg is en indien nodig, begint hij aan de behandeling. De procedure PreOrder bv. zal eerst de wortel van de boom printen en daarna, recursief, zichzelf oproepen met eerst een pointer naar de linkersubboom en daarna een naar de rechtersubboom. Hier printen we de data van de node, maar dit kan vervangen worden door een andere actie die we met de data willen doen. Een tweede toepassing is om het aantal kinderen van alle nodes te berekenen. Hiervoor moeten we de boom in PostOrder doorlopen: het aantal nodes van linker- en rechtersubboom tellen. Hier de code: public int aantalkinderen(node<t> node){ if (node == null) return 0; int n = aantalkinderen(node.left); n += aantalkinderen(node.right); System.out.println("Node "+node.data+" heeft "+n+" kinderen"); return n + 1; 8

6.6 Perfect gebalanceerde bomen Het volstaat een beetje te experimenteren met de add van 6.2 om vast te stellen dat de structuur van de opgebouwde boom heel sterk afhangt van de volgorde waarin de elementen worden toegevoegd. Indien deze volgorde alfabetisch zou zijn, zou de boom zelfs degenereren in een lineaire lijst. De zoektijd in een lineaire lijst is evenredig met het aantal elementen in de lijst terwijl de zoektijd in een binaire boom evenredig is met het logaritme in basis 2 van dat aantal. Om de zoektijden te beperken moet men dus streven naar evenwichtige bomen, waarin alle bladeren ongeveer op het zelfde niveau te vinden zijn. Om het evenwicht van bomen te kwalificeren, zal men in elk element 12/11 van de boom de linker- en de 5/6 4/6 rechtersubboom vergelijken. Indien het verschil tussen het aantal elementen in deze twee subbomen 2/2 3/2 1/2 3/2 niet groter is dan één, spreekt men 1/0 0/1 1/1 1/0 0/0 1/0 1/1 1/0 van een perfect gebalanceerde boom. De figuur toont een boom die bijna perfect gebalanceerd is, in alle elementen behalve één is aan de voorwaarde voldaan. In de node hebben we telkens de grootte van linker- en rechterkant genoteerd. In de zwarte node merken we dus een verschil dat groter is dan 1. De relatieve waarde van het verschil tussen links en rechts geeft een maat voor de imbalans: imbalance # nodes # nodes links links # nodes # nodes rechts rechts Om te checken of een boom perfect gebalanceerd is moeten we dus het aantal nodes vergelijken van linker- en rechterkant. Dit zagen we in de methode aantalkinderen van vorige sectie. Deze methode kan omgebouwd worden tot een test van perfecte balans. We lopen de hele boom recursief af om de nodes te tellen, we geven het aantal nodes terug, maar als we een onbalans tegen komen moeten we die ook teruggeven. Met andere woorden: de recursieve functie zou twee dingen moeten teruggeven. Dit kan echter niet in Java. De oplossing die we hier toepassen is een -1 teruggeven als niet gebalanceerd. Deze onbalans moeten we verder doorgeven. Hier de code: // geeft -1 als niet perfect gebalanceerd, anders aantal nodes public int isperfectgebalanceerd(node<t> node){ if (node == null) return 0; int nlinks = aantalkinderen(node.left); Deel 2, Hoofdstuk 6, pag.9

int nrechts = aantalkinderen(node.right); if (nlinks < 0 && nrechts < 0) return -1; if (Math.abs(nLinks - nrechts) > 1) { System.out.println("Node "+node.data+" is niet perfect gebalanceerd: "+nlinks+" versus "+nrechts); return -1; else { return nlinks + nrechts + 1; 6.7 AVL-bomen Een perfect gebalanceerde boom opbouwen is echter vrij moeilijk want er bestaan geen eenvoudige manipulaties om een bijna perfect gebalanceerde boom om te toveren tot een perfect gebalanceerde. 6.7.1 Definitie. Twee Russische mathematici, Adelson-Velskii & Landis hebben een minder streng criterium voor het balanceren van bomen bedacht. In plaats van het aantal elementen in de linker- en de rechtersubbomen te vergelijken beschouwen zij enkel de hoogte van deze 3/4 subbomen. Bomen waarbij, in elk 2/2 3/3 element, de hoogte van de linkersubboom niet meer dan een 1/1 1/0 2/2 2/0 niveau verschilt van de hoogte van de rechtersubboom worden AVLbomen 0/0 0/0 0/0 1/0 1/0 1/1 genoemd. De figuur toont een boom waarvan alle elementen behalve één aan de AVLvoorwaarde voldoen. We tonen de diepte links en rechts in elke node. De zwarte node voldoet dus niet aan de voorwaarde. De volgende figuur toont dezelfde boom waarin, door middel van een eenvoudige lokale transformatie, de AVL voorwaarde hersteld werd. 1/1 0/0 0/0 0/0 1/0 3/4 2/2 3/3 2/2 1/0 1/0 0/0 1/1 0/0 Deel 2, Hoofdstuk 6, pag.10 Men dient op te merken dat de definitie van een AVL-boom geenszins uitsluit dat er tussen null-pointers (plaatsen zonder tak) niveauverschillen groter dan één kunnen bestaan. In deze AVL-

boom ziet men dat uiterst rechts van de linkersubboom een NULL-pointer te vinden is die twee niveaus hoger ligt dan de NULL-pointers onder de linkse subboom van de rechtersubboom. Dit komt omdat de hoogte van een boom, zoals gebruikt in de AVLdefinitie bepaald wordt door de hoogste subboom. De boom is ook niet perfect gebalanceerd. Ten opzichte van perfect gebalanceerde bomen hebben AVL-bomen het voordeel dat zij eenvoudig kunnen worden opgebouwd terwijl zij toch (redelijk) performant zoeken toelaten. 6.7.2 Hoogte van subboom. Om bij het aanmaken van de boom aan de AVL-voorwaarde te voldoen, zullen we de hoogte van de subboom van elke node bijhouden. De hoogte is recursief gedefinieerd als het maximum van de hoogte van de linkersubboom en hoogte van rechtersubboom plus 1. We breiden de definitie uit van Node zodat we die in elke node kunnen bijhouden (grijs): class Node<T>{ T data; Node<T> left, right; int hoogte=0; Node(T data){ this.data = data; left = null; right = null; public String tostring(){ return data.tostring()+"("+hoogte+")"; We veranderen ook de tostring-methode zodat de hoogte ook geprint wordt. Nu kunnen we de hoogte in een node berekenen met de volgende code: protected void berekenhoogte(node<t> node){ int nrechts = node.right == null? 0 : 1 + node.right.hoogte; int nlinks = node.left == null? 0 : 1 + node.left.hoogte; node.hoogte = Math.max(nRechts, nlinks); Hierbij gaan we er vanuit dat de hoogte in linker- en rechternode correct is. Oefening: verander de code zodat de AVL-voorwaarde getest wordt. Om deze hoogte up-to-date te houden, moeten we bij elke uitbreiding van de boom, de hoogtes aanpassen. Als we een node toevoegen, dalen we af in de boom tot we de juiste lege plek vinden waar we de node kunnen toevoegen. Het zijn enkel de nodes langs dit pad waarvan de hoogtes veranderd kan zijn. Dit doen we als we terugkeren uit de recursie, op het einde van de add. Dan roepen we de functie berekenhoogte op. Deze functie is enkel correct als de hoogte van de onderste nodes correct zijn. Aan deze voorwaarde is hier voldaan, we passen de hoogtes aan van onder naar boven. Deel 2, Hoofdstuk 6, pag.11

In grijs de aanpassingen van de add-methode. public void add(t object){ Node<T> newnode = new Node<T>(object); if (root == null) root = newnode; else add(root, newnode); berekenhoogte(root); private void add(node<t> current, Node<T> newnode){ if (comparator.compare(newnode.data, current.data) < 0){ // moet links komen if (current.left == null) current.left = newnode; else add(current.left, newnode); else { // rechts if (current.right == null) current.right = newnode; else add(current.right, newnode); // herbereken hoogte berekenhoogte(current); Bij toevoeging van een node worden de hoogtes waar nodig aangepast. Dit noemen we het consistent houden van de datastructuur: zorgen dat alle eigenschappen steeds correct blijven. De herberekening van de hoogtes moet dus gebeuren telkens we iets aan de boom aanpassen. Dus ook bij het verwijderen van een node! Oefening: pas de functie remove aan zodat het hoogte-atribuut herberekend wordt waar nodig. De hulpfuncties findnodeandremove, createtreefromsubs en pickrightmostnode zullen dus herbekeken moeten worden. Nu we de tostring van Node hebben aangepast en de hoogte ook geprint wordt, kunnen we onze aanpassingen testen en zien of de hoogte in elke node correct blijft bij het verwijderen van nodes. De encapsulatie-eigenschap van object-georiënteerd programmeren is in het leven geroepen om dataconsistentie te kunnen waarborgen. Zoals hier bij een boom. We houden de datastructuur verborgen voor de gebruiker en passen deze zelf aan in de publieke methodes. Deze publieke methodes zijn de enige die de gebruiker kan oproepen. We zorgen ervoor dat deze correct zijn door ze uitgebreid te testen. Eenmaal correct is dataconsistentie verzekerd, de gebruiker kan de datastructuur immers niet zelf aanpassen. En daarvoor is hij ons dankbaar, hij zou niet goed weten hoe! Deel 2, Hoofdstuk 6, pag.12

6.7.3 Het opbouwen van een AVL-boom. Het opbouwen van een AVL-boom gebeurt zoals het opbouwen van een gewone geordende boom. Met dat verschil dat telkens een nieuw element aan de boom wordt toegevoegd, er nagegaan wordt of de AVL-voorwaarden nog steeds voldaan zijn. Indien dit niet het geval is, wordt de boom herschikt. Eerst zullen hier deze herschikkingen besproken worden. A A De figuur toont schematisch twee verschillende situaties die een herschikking noodzakelijk maken na het toevoegen van één element aan een AVLboom. B Buitenwaarts onevenwicht B Binnenwaarts onevenwicht In de schema s worden subbomen gewoonweg voorgesteld door een rechthoek. Horizontale stippellijnen worden gebruikt om de onderste niveaus van deze subbomen aan te duiden. Het is evident dat door een symmetrie waarbij links en rechts worden omgewisseld er twee analoge configuraties ontstaan die ook een herschikking vergen. Hier worden enkel de gevallen getoond die in de figuur besproken worden. De behandeling van de twee anderen is identiek, mits het uitwisselen van links en rechts. Wanneer de hoogte van de linkersubboom één niveau groter is dan de hoogte van de rechtersubboom zal het toevoegen van een element aan de linkersubboom mogelijk aanleiding geven tot een overdreven hoogteverschil tussen linker- en rechtersubboom. Indien het toegevoegde element tot de linkersubboom van de linkersubboom behoort, spreekt men van een buitenwaarts onevenwicht. Indien het de rechtersubboom van de linkse subboom is die gegroeid is, heeft men te maken met een binnenwaarts onevenwicht. B A B A De herschikking die men kan toepassen in het geval van een buitenwaarts onevenwicht wordt beschreven in onderstaande figuur. Door een soort van rotatie wordt de wortel van de linkersubboom (B) de nieuwe wortel van het geheel terwijl de oorspronkelijke wortel (A) de Deel 2, Hoofdstuk 6, pag.13 Herschikking van een buitenwaarts onevenwicht

wortel van de rechtersubboom wordt. De rechtersubboom van B wordt de nieuwe linkersubboom van A. Hierbij dient opgemerkt te worden dat de nieuwe boom evenwichtig is en dat de hoogte ervan dezelfde is als deze van de oorspronkelijke boom, vóór het toevoegen van het element dat de herschikking noodzakelijk heeft gemaakt. De herschikking die moet toegepast worden bij een binnenwaarts onevenwicht is A C lichtjes complexer dan deze B B A voor een buitenwaarts onevenwicht. Onderstaande C figuur toont de toegepaste transformatie. Het is de rechtersubboom van de linkersubboom die te hoog geworden is. Dit kan het gevolg zijn van het toevoegen Herschikking van een binnenwaarts onevenwicht van een element aan de linkersubboom of van het toevoegen van een element aan de rechtersubboom. In het schema is dit element voorgesteld met een gestippeld blokje. We tonen twee gespikkelde blokjes voor beide gevallen. Er is er natuurlijk maar één toegevoegd. Beide gevallen kunnen echter op dezelfde manier behandeld worden. Als nieuwe wortel van het geheel kiest men de wortel (C) van de subboom waarvan de hoogte gegroeid is. Hier kan men eveneens vaststellen dat de resulterende boom evenwichtig is en dezelfde hoogte heeft als de oorspronkelijke boom. Het feit dat bij beide transformaties het resultaat dezelfde hoogte heeft als de oorspronkelijke boom is de sleutel tot de efficiëntie van AVL-bomen. Het heeft inderdaad als gevolg dat wanneer men ergens verplicht is geweest, na het toevoegen van een element, een herschikking door te voeren, men niet meer verder moet nagaan of de AVL-voorwaarde wel voldaan is, vermits de herschikte boom niet hoger is dan de oorspronkelijke boom, vóór het toevoegen van het element. 6.7.4 Programmatie van de add. In deze laatste paragraaf programmeren we de constructie van een boom waarvoor de AVL-voorwaarde bewaard blijft. We moeten hiervoor de add aanpassen. We kunnen deze van BinaryTree aanpassen. Een tweede oplossing is een subklasse aan te maken van BinaryTree. We laten de oorspronkelijke klasse ongemoeid, maar maken een subklasse om onze nieuwe ideeën in te ontwikkelen. We noemen deze AVLTree en definiëren ze als volgt: public class AVLTree<T> extends BinaryTree<T> { public AVLTree(Comparator<T> comparator) { super(comparator); Deel 2, Hoofdstuk 6, pag.14

Omdat de default constructor van BinaryTree niet meer van toepassing is, moeten we ook een constructor in AVLTree voorzien. We overschrijven nu de add van BinaryTree met een toevoeging van de herschikking besproken in de vorige paragraaf. Dit is een voorbeeld van overerving, de tweede pijler van object-georiënteerd programmeren. AVLTree zal automatisch alle attributen en methodes van BinaryTree overerven. We moeten deze niet opnieuw programmeren, maar kunnen wel uitbreidingen of overschrijvingen doen. Voor AVLTree gaan we enkel de add overschrijven. Zo zorgen we er voor dat de boom gebalanceerd blijft. De andere methodes van BinaryTree, zoals het opzoeken van een element, kunnen we gewoon blijven gebruiken! Dit illustreert mooi de kracht van object-georiënteerd programmeren. Nu de programmatie. We moeten echter een aanpassing doen van de hulpfunctie private void add(node<t> current, Node<T> newnode). We hebben immers gezien dat bij een herschikking we kiezen voor een nieuwe topnode van een subboom. Bij een buitenwaartse herschikking werd B de nieuwe topnode, bij een binnenwaartse A. Deze nieuwe topnode moet het nieuwe kind van de hogergelegen node worden (links of rechts). Dit lossen we op door de topnode terug te geven bij elke recursieve oproep. De hulpfunctie add heb ik hernoemd naar addavl en deze laten we een Node teruggeven. Als er geen herschikking heeft plaats gevonden is deze node gewoon current. In het andere geval is dit de nieuwe topnode. Deze nieuwe node wordt dan bij het terugkeren van de recursieve functie addavl links of rechts gezet. @Override public void add(t object){ Node<T> newnode = new Node<T>(object); root = addavl(root, newnode); // Node wordt toegevoegd en nieuwe topnode wordt teruggegeven (deze kan veranderen) private Node<T> addavl(node<t> current, Node<T> newnode){ if (current == null) return newnode; if (comparator.compare(newnode.data, current.data) < 0){ // moet links komen current.left = addavl(current.left, newnode); // check for imbalance int imbalans = imbalance(current.left, current.right); if (imbalans < -1){ if (comparator.compare(newnode.data, current.left.data) < 0){ System.out.println("When adding "+newnode.data+" outer imbalance at left of "+current.data); // if added left: outer imbalance Node<T> A = current; // zie tekening van cursus Node<T> B = current.left; A.left = B.right; // in de goede volgorde!! B.right = A; Deel 2, Hoofdstuk 6, pag.15

current = B; // dit is de nieuwe topnode van subboom berekenhoogte(a); berekenhoogte(b); else { System.out.println("When adding "+newnode.data+" inner imbalance at left of "+current.data); // if added right: inner imbalance Node<T> A = current; // zie tekening van cursus Node<T> B = current.left; Node<T> C = B.right; A.left = C.right; // in de goede volgorde!! B.right = C.left; C.right = A; C.left = B; current = C; // dit is de nieuwe topnode van subboom berekenhoogte(a); berekenhoogte(b); berekenhoogte(c); else { // rechts current.right = addavl(current.right, newnode); // check for imbalance int imbalans = imbalance(current.left, current.right); if (imbalans > 1){ if (comparator.compare(newnode.data, current.right.data) > 0){ System.out.println("When adding "+newnode.data+" outer imbalance at right of "+current.data); // if added right: outer imbalance Node<T> A = current; // spiegelbeeld van vorige Node<T> B = current.right; A.right = B.left; // in de goede volgorde!! B.left = A; current = B; // dit is de nieuwe topnode van subboom berekenhoogte(a); berekenhoogte(b); else { System.out.println("When adding "+newnode.data+" inner imbalance at right of "+current.data); // if added left: inner imbalance Node<T> A = current; // spiegelbeeld van vorige Node<T> B = current.right; Node<T> C = B.left; A.right = C.left; // in de goede volgorde!! B.left = C.right; C.left = A; C.right = B; current = C; // dit is de nieuwe topnode van subboom berekenhoogte(a); berekenhoogte(b); berekenhoogte(c); Deel 2, Hoofdstuk 6, pag.16

berekenhoogte(current); return current; Merk op dat we berekenhoogte oproepen voor de nodes die deelnemen aan de herschikking. Deel 2, Hoofdstuk 6, pag.17