Datalogi gk 2I1027 - Föreläsning 10 Träd, speciellt binära sökträd presenteras av Jozef Swiatycki, DSV Litteratur: Main, kap. 9 Jozef Swiatycki DSV Bild 1 Träd allmänt Länkad, hierarkisk (icke-linjär) struktur. Består av noder förbundna med kanter. Varje nod har exakt en förälder (utom roten som har ingen). Noder utan barn kallas löv. rot ancestor förälder varelser (förfader) subträd barn syskon fiskar fåglar fyrfota djur kräldjur människor förälder löv löv barn syskon descendent tamdjur syskon vilda djur lärare studenter (ättling) löv löv en nods nivå = antalet steg till roten trädets höjd = maximala nodnivån rotens nivå = 0 tomt träds höjd = -1 (!) Jozef Swiatycki DSV Bild 2 1
Generella träd - en möjlig implementering Varje nod har en array med referenser till sina barn (alternativt kan varje nod ha en länkad lista med referenser till barnen) rot varelse fisk fågel fyrfota djur kräldjur människa tamdjur vilddjur lärare student Jozef Swiatycki DSV Bild 3 Binära träd Maximal utgreningsgrad = 2, dvs varje nod har högst 2 barn Brukar kallas vänster- resp. högerbarn (-subträd). 21 14 21 19 11 15 19 23 31 31 70 70 10 10 22 Jozef Swiatycki DSV Bild 4 2
Binära träd Fullständigt binärt träd: alla påbörjade nivåer är helt fyllda, dvs varje löv har samma nivå och varje icke-löv har två barn 14 21 11 15 19 23 Komplett binärt träd: alla nivåer utom det nedersta är helt fyllda, på den nedersta nivån ligger noderna till vänster 14 21 11 15 19 23 45 13 77 Jozef Swiatycki DSV Bild 5 Binära träd - användningsexempel Beslutsträd Är du hungrig? Ja Nej Gillar du pasta? Ja Nej Vill du gå på bio? Ja Nej Binära sökträd noder i vänstra subträdet har värden som < är mindre än eller lika med förälderns, 14 21 noder i högra subträdet har större < < värden än föräldern 11 15 23 Heap barnens noder har värden som är lägre än eller lika med förälderns, måste vara komplett 47 15 23 15 11 21 Jozef Swiatycki DSV Bild 6 3
Arrayimplementering av kompletta binära träd 47 0 1 2 15 23 3 4 5 15 11 21 data antal 47 15 23 15 11 21 0 1 2 3 4 5 6 7 6 roten finns i data[0] i-te nodens vänstra barn finns i data[2*i+1] (om 2*i+1 < antal) i-te nodens högra barn finns i data[2*i+2] (om 2*i+2 < antal) i-te nodens förälder finns i data[(i-1)/2] (om i > 0) (med i-te nod menar jag noden i data[i]) Jozef Swiatycki DSV Bild 7 Länkad implementering av binära träd Exempel: binärt sökträd find insert BinarySearchTree root 29 Beatrice 47 Stefan 73 Jozef print delete Anna 61 Harald 97 Eskel 8 Urban 52 Mats 67 Tobbe 85 Alex 103 Peter 80 Doris 83 Mia Jozef Swiatycki DSV Bild 8 4
Rekursiv definition av binära träd Ett binärt träd är tomt eller består av en nod och dess vänstersubträd och dess högersubträd och båda subträden är binära träd 47 Stefan Jozef Swiatycki DSV Bild 9 Höjden av ett fullt binärt träd 18 14 25 11 16 20 28 10 13 15 19 23 26 29 Antal noder i trädet 1 3 7 15 Höjd 0 1 2 3 På varje nivå får vi plats med dubbelt så många noder än på nivån ovanför. Antalet noder i ett fullt binärt träd = 2 (höjd+1) -1 Vid ett visst antal noder n blir alltså höjden av ett binärt träd = log 2 (n+1) -1 Vid sökningar och många andra operationer vandrar man ner genom trädet med början i roten och väljer väg (till vänster eller höger) vid varje nod. Trädets höjd blir det maximala antalet steg man behöver vandra - sökoperationerna blir alltså logaritmiskt beroende av antalet noder i trädet. Detta är mycket snabb sökning jämfört med sökning i arrayer eller länkade listor (som är linjärt beroende av antalet element). Jozef Swiatycki DSV Bild 10 5
Nodklass för ett binärt träd (ej återanvändbar) class Node{ int id; String data; Node left, right; Node(int i, String d){ id=i; data=d; left=right=null; // Konstruktor Dataattributen är inte skyddade för att få tydligare kod i metodexemplen, de borde givetvis vara deklarerade som private och avläsas med get-metoder public String tostring(){ return id + ": " + data; // tostring // Node Jozef Swiatycki DSV Bild 11 Klassen BinSearchTree (ej återanvändbar) public Node find(int sought){... public void insert(int id, String data){... public void print(){... void delete(int sought){... // BinSearchTree Jozef Swiatycki DSV Bild 12 6
Rekursiv sökfunktion Node findnode(node node, int sought){ if (node == null) return null; else if (node.id == sought) return node; else if (node.id > sought) return findnode(node.left, sought); else return findnode(node.right, sought); // findnode Jozef Swiatycki DSV Bild 13 Gränssnittsmetoder och privata hjälpmetoder Antag deklarationen BinSearchTree tree = new BinSearchTree(); Antag även att några noder har skapats och lagts till trädet tree. Ett anrop av den rekursiva metoden findnode skulle behöva se ut så här: Node tn = tree.findnode(tree.root, 73); Men en tillämpning har inte åtkomst till tree.root, dessutom känns det ondödigt att behöva ange tree.root vid varje anrop av metoden. Man brukar därför skapa publika gränssnittsmetoder som bara är till för att starta rekursionen och privata rekursiva metoder som gör jobbet: public Node find(int sought){ return findnode(root, sought); private Node findnode(node node, int sought){ // som tidigare Jozef Swiatycki DSV Bild 14 7
Iterativ implementering av sökfunktionen Sökning, insättning och borttag (men inte traversering) i binära sökträd kan även implementeras iterativt, varvid de kan bli mer effektiva. Koden blir dock lite grötig varför jag väljer att visa de elegantare rekursiva lösningarna. Nedan dock ett exempel på iterativ implementering av sökfunktionen: Node findnode(int sought){ Node temp = root; while (temp!= null && temp.id!=sought) if (temp.id > sought) temp=temp.left; else temp=temp.right; return temp; // findnode Jozef Swiatycki DSV Bild 15 Insättning av ny nod i trädet BinarySearchTree 47 Stefan find insert root 29 Beatrice 73 Jozef print delete Anna 61 Harald 97 Eskel 8 Urban 52 Mats 67 Tobbe 85 Alex 103 Peter newnode 50 Ola 80 Doris 83 Mia Jozef Swiatycki DSV Bild 16 8
Kod för insättning av ny nod i trädet public void insert(int id, String data){ Node newnode = new Node(id, data); root=insertnode(root, newnode); private Node insertnode(node node, Node newnode){ if (node == null) node = newnode; else if (node.id > newnode.id) node.left = insertnode(node.left, newnode); else node.right = insertnode(node.right, newnode); return node; Jozef Swiatycki DSV Bild Traversering (genomgång) av trädet Exempel: utskrift i sorteringsordning public void print(){ printtree(root); private void printtree(node node){ if (node!= null){ printtree(node.left); System.out.println(node); printtree(node.right); Jozef Swiatycki DSV Bild 18 9
Träd - traverseringsordning inorder - vänstra subträdet, noden själv, högra subträdet I binära sökträd ger detta traversering i sorteringsordning preorder - noden själv, vänstra subträdet, högra subträdet private void preorderprint(node node){ if (node!= null){ System.out.println(node); preorderprint(node.left); preorderprint(node.right); postorder - vänstra subträdet, högra subträdet, noden själv Jozef Swiatycki DSV Bild 19 Effektivisering av traverseringen På nedersta nivån i trädet finns lika många noder som i hela trädet i övrigt. Med något grötigare kod kan man därför halvera antalet metodanrop: public void print(){ if (root!= null) printtree(root); private void printtree(node node){ if (node.left!= null) printtree(node.left); System.out.println(node); if (node.right!= null) printtree(node.right); Jozef Swiatycki DSV Bild 20 10
Borttagning av en nod från trädet Tre fall: 1 - noden har inget vänsterbarn - ersätt den med dess högerbarn (täcker även fallet inget barn) 2 - noden har inget högerbarn - ersätt den med vänsterbarn 3 - noden har båda barnen - ersätt den med den vänstraste noden i dess högra subträd (eller högraste noden i vänstra subträdet) BinarySearchTree 47 Stefan find insert root 2 29 Beatrice 73 Jozef print delete Anna 1 61 Harald 3 ersätt 97 Eskel 28 Urban 52 Mats 67 Tobbe 85 Alex 103 Peter Jozef Swiatycki DSV Bild 21 Kod för borttagning av en nod från trädet private Node deletenode(node node, int sought){ if (node == null) return null; else if (node.id > sought) node.left = deletenode(node.left, sought); else if (node.id < sought) node.right = deletenode(node.right, sought); else if (node.left == null) node = node.right; else if (node.right == null) node = node.left; else { Node tmp=findleftmost(node.right); node.id = tmp.id; node.data = tmp.data; node.right = deleteleftmost(node.right); return node; Jozef Swiatycki DSV Bild 22 11
Borttagning av en nod från trädet (forts.) public void delete(int sought){ root = deletenode(root, sought); private Node findleftmost(node node){ if (node.left == null) return node; else return findleftmost(node.left); private Node deleteleftmost(node node){ if (node.left == null) return node.right; else { node.left = deleteleftmost(node.left); return node; Jozef Swiatycki DSV Bild 23 Generalisering Om det binära sökträdet ska kunna användas av godtyckliga tillämpningar uppstår det några problem som behöver lösas: strukturen med left/right-referenser i varje nod är en intern angelägenhet för trädklassen, tillämpningar borde inte behöva känna till dem eller få åtkomst till dem. Lösning: separera left/right-referenser från tillämpningens data genom att för varje tillämpningsobjekt skapa ett internt litet objekt, innehållande en referens till det instoppade objektet och left/right-referenser till andra interna objekt. det måste framgå på något sätt hur tillämpningens objekt identifieras. Lösning: separera id-värdet och datavärdet, d.v.s. tillämpningen får lov att stoppa in två värden: referensen till id-objektet och referensen till data-objektet id-värdena måste kunna jämföras (definiera en total ordning) Lösning: kräv att id-värdena uppfyller gränssnittet Comparable och alltså innehåller metoden compareto som tar ett annat objekt som argument och returnerar negativt om det egna objektet är mindre än, noll om det är lika med och positivt om det är större än argumentet tillämpningen måste få iterera över alla sina instoppade objekt för att utföra operationer på dem. Lösning: skapa en iteratorklass som ger tillgång till dataobjekten i sorteringsordning Jozef Swiatycki DSV Bild 24 12
Generalisering, forts. BinarySearchTree find insert iterator root compareto delete compareto compareto compareto compareto Jozef Swiatycki DSV Bild 25 Nodklass för ett återanvändbart binärt sökträd class Node{ Comparable id; Object data; Node left, right; Node(Comparable i, Object d){ id=i; data=d; left=right=null; // Konstruktor public String tostring(){ return id.tostring() + + data.tostring(); // tostring // Node Jozef Swiatycki DSV Bild 26 13
Fragment av återanvändbart binärt sökträd public Object find(comparable sought){ Node node = findnode(root, sought); if (node == null) return null; else return node.data; private Node findnode(node node, Comparable sought){ if (node == null) return null; else { int cmp = node.id.compareto(sought); if (cmp == 0) return node; else if (cmp > 0) return findnode(node.left, sought); else return findnode(node.right, sought); // findnode Jozef Swiatycki DSV Bild 27 Iterator för återanvändbart binärt sökträd Iteratorn för ett återanvändbart sökträd skapas med en kö av nodreferenser som fylls genom en inorder-genomgång av trädet. Varje anrop av iteratorns next-metod tar ut nästa nod ur kön och ger tillämpningen tillgång till nodens data-objekt. Metoden hasnext ger true så länge det finns noder kvar. next hasnext find insert BinarySearchTree root 29 Beatrice 47 Stefan 73 Jozef iterator delete Anna 61 Harald 97 Eskel Jozef Swiatycki DSV Bild 28 14
Användningsexempel class Pers{ String namn, telnr; Person(String n, String t) { namn=n; telnr=t; String getnamn() { return namn; String gettel() { return telnr; class Appl{ public static void main(string[] args){ BinSearchTree tellista = new BinSearchTree(); tellista.insert( Joz, new Pers( Joz, 1616 )); tellista.insert( Bea, new Pers( Bea, 164988 )); Pers p1 = (Pers)tellista.find( Bea ); Iterator iter = tellista.iterator(); while (iter.hasnext()){ Pers p2=(pers)iter.next(); System.out.println(p2.getNamn() + p2.gettel()); Jozef Swiatycki DSV Bild 29 Balansering De presenterade algoritmerna för sökning, insättning och borttag av en nod i ett binärt sökträd har en tidskomplexitet som är beroende av trädets höjd. De blir logaritmiska om trädet är balanserat, d.v.s. skillnaden mellan den minsta nivån för en lövnod och den största nivån för en lövnod är högst 1. Även om trädet bara är någorlunda balanserat (noderna är någorlunda jämt fördelade mellan höger- och vänstersubträden) blir algoritmerna logaritmiska. Men om noderna läggs in t.ex. i sorteringsordning degenererar tidskomplexiteten till att bli linjär. Det finns därför varianter av binära sökträd som automatiskt balanserar sig för varje insättning eller borttag (AVL-träd, black-red-träd). Det finns andra (än binära) självbalanserande trädstrukturer, viktigast av dessa är B-träd (Balanserade träd eller Bayer-träd efter deras skapara Rudolf Bayer). Dessa faller dock utanför ramen för denna presentation. Jozef Swiatycki DSV Bild 30 15