Träd En lista är en struktur som är enkel att hantera men som inte är så effektiv ur söksynpunkt. Att leta efter en viss nod i en lista med n noder kommer i genomsnitt att kräva n/2 jämförelser. Detta är inte så effektivt. En mer avancerad struktur som blir mer komplex att hantera men som blir mer effektiv är ett träd. Ett träd är en struktur som har grenar. Detta leder till att vi får fler korta sökvägar istället för en lång. Ett träd kan se ut på detta sätt: Ett träd är uppbyggt med en mängd noder sammanbundna genom pekare på ett hierarkiskt sätt. Varje nod har en ingående pekare och ett antal utgående pekare. 1 2 Ett binärt träd är ett träd där varje nod har en ingående pekare och högst två utgående pekare. Kan alltså se ut på följande sätt: Man talar också i termer av släkträd så att underliggande noder kallas för barn, i ett binärt träd talar man om vänsterbarn och högerbarn. Vi talar om förälder eller fader och förfader. Vi talar också om olika nivåer i trädet, rotnoden utgör nivå noll, sedan ökar nivån nedåt. rot nod En nod i ett binärt träd kan se ut på följande sätt: inre nod Nyckeldel Infodel Vänsterpek löv 3 4 Högerpek Ett binärt sökträd är ett binärt träd där nodernas placering baseras på deras nyckel så att en nods vänsterpekare pekar ut en nod med en lägre nyckel och en nods högerpekare pekar ut en nod med en högre nyckel. Det kan se ut på detta sätt: 2 8 4 6 1 Om vi har ett träd med 8 nivåer kan vi i det ideala fallet lägga in femhundraelva noder och hitta en godtycklig av dessa noder med max 8 jämförelser. 9 7 Vad ska man ha ett träd till? Det finns ju flera goda skäl, t.ex utgör de en stor bit av denna kurs. Det finns ju en del andra skäl också. Ett sökträd är en effektiv struktur att söka i. För att finna talet 4 i vårt binära träd så behövs det 3 jämförelser. Vi ser att vi kan hitta vilket tal som helst i trädet med högst 4 jämförelser trots att vi har tio noder. Ett träd blir en logproportionelig struktur till skillnad mot en lista som blir linjär. 5 3 Vi antar här att nycklarna är unika. Om de inte är det kan man införa ett räknarfält i noderna eller låta nodernas infodel vara en länkad lista med alla dubletter. 10 5 Ett träd verkar vara komplicerat att underhålla och att lägga in noder i. Detta är inte fallet. I själva verket är det tämligen trivialt att lägga in noder i ett binärt sökträd. Däremot är det lite knepigare att ta bort något ur ett träd. Trädets effektitivet avgörs av dess utseende. Det ska vara balanserat för att vara effektivt. Sämsta möjliga är om vi bara har en lång pinne åt ett håll. Då har vi ju en länkad lista. 6 Hur kan vi då implementera träd? Datastrukturen blir ju mycket lik en lista, det enda som skiljer i själva noden är att vi har två pekare. Vi kan alltså göra en post eller en klass som innehåller tillämpliga delar. Nämligen: Nyckeldel Infodel Vänsterpekare Högerpekare Det visar sig att det kan vara fördelaktigt att i varje nod ha en referens till sin “förälder” också. Det ingår egentligen inte i trädet, men förenklar vissa saker. Vad ska då ingå i ADT‘n? 7 8 Blir ungefär som i listfallet. Vi börjar med en lågnivådel , trädnoden, som mest innehåller konstruktorer Sökning i ett binärt sökträd. Vi kan sedan utan större problem bygga upp högre nivårutiner som lite grand kan beror på vad vi avser att göra. Sådana kan vara: Börja i roten. search insert delete print leta efter en nod i ett träd lägger in en ny nod i ett träd tar bort en ny nod ur ett träd skriver ut hela trädets infodelar Oväntat nog blir dessa rätt triviala trots att trädet verkar så komplext. Undantaget är delete. Vi antar att alla nycklar har en unik förekomst. Repetera Jämför nycklarna Om lika klara Annars om söknyckeln för stor, gå ett steg till höger Annars gå ett steg till vänster tills vi hittat rätt eller tills vi inte kan gå längre Om vi hittat en nollpekare så fanns inte sökt nod. Vi kan naturligtvis formulera detta annorlunda: Om trädet är tomt så fanns nyckeln inte Annars jämför rotnoden med sökt data Om lika så är vi klara Annars finns den ev. till vänster eller till höger . 9 En rekursiv formulering 10 Hur lägger vi in noder i ett binärt träd då? Verkar ju svårt. Resonemang; Har vi en tom pekare så fanns inte noden. Om vi har en eller flera noder så måste letar vi först i roten. Finns den inte där så letar vi i de båda subträden och hittar den kanske då. Om vi har ett binärt sökträd kan algoritmen förenklas en del. Vi kan ju då utesluta ett av subträden i den rekursiva sökningen. Antag att vi vill lägga in talen 12 4 15 9 1 8 20 Det blir på detta viset: Blir då så här: a) Om trädet är tomt så fanns nyckeln inte Annars Jämför rotnoden med sökt data Om lika så är vi klara Annars om för liten leta till höger annars leta till vänster b) 12 12 4 4 11 c) 12 12 15 12 12 d) 4 4 15 1 9 f) 15 9 8 12 4 1 15 g) 12 e) 4 9 15 1 9 20 8 13 14 Vi kan notera en viktig sak. All inläggning av noder sker längst ned i trädet. Vi lägger aldrig in en nod mitt i trädet. Det behövs inte. Det betyder å andra sidan att trädets utseende kommer att bero på i vilken ordning talen läggs in. Om vi tar samma tal som nyss men i ordningen 1 20 4 8 9 12 15 får vi följande träd Man kan formulera en inläggningsalgoritm på följande sätt: Om trädet är tomt så lägg in noden direkt. Annars Repetera Jämför vår nyckel med nodens nyckel om vår nyckel större försök gå till höger annars försök gå till vänster tills du inte kan gå längre lägg in noden där Man kan naturligtvis formulera samma sak rekursivt Trivialfallet: roten är en nollpekare, d. v. s. trädet är tomt sätt roten att peka på vår nya nod Generella fallet: Om nyckeln mindre än rotens nyckel lägg in den nya noden i vänster subträd Annars lägg in i höger subträd. Test: Eftersom subträdet till slut blir en noll-pekare så konvergerar alltid det generella fallet mot trivialfallet. 1 20 4 8 9 12 15 15 16 Hur kan det fungera? Vi kommer först att anropa rutinen nånting så här: Om vi går vidare med att skriva ut alla noder i ett träd så kommer vi in på något som kallas för traversering av ett träd. Att traversera ett träd innebär att man går igenom hela trädet så att man besöker alla noder en gång. root = insert(root, nynod) Man brukar tala om tre standardmetoder att göra detta: Sedan kommer insert att anropa sig själv ungefär så här: inorder postorder preorder root.left = insert(root.left, nynod) Så småningom blir det första argumentet en nollpekare och den kommer då att ändras till att peka på min nya nod. Eftersom root.left är en pekare som ingår i trädet så kommer min nya nod att noggrannt hängas upp i julgranen på lämplig gren. in betyder emellan något och betyder i detta fallet att roten är i mitten. Alltså, vänster-roten-höger. post betyder ju efter och då får vi, vänster-höger-roten eftersom pre betyder före så får vi i det sista fallet rotenvänster-höger. Detta appliceras rekursivt på trädet. 17 18 Man kan exemplifiera detta med följande träd: Man kan formulera en sekventiell inorder algoritm på följande sätt: Skaffa en stack av noder tillräckligt stor. Sätt pek till roten. Repetera så längre inte pek är nollpekare pusha pek pek = pek.left om stacken inte är tom poppa pek gör något med utpekad nod pek = pek.höger tills pek är nollpekare och stacken är tom. 5 3 2 8 4 6 9 7 1 Inorder : 1 2 3 4 5 6 7 8 9 10 Preorder: 5 3 2 1 4 8 6 7 9 10 Postorder: 1 2 4 3 7 6 10 9 8 5 19 10 Det blir ju så här ungefär: pusha 5 pusha 3 pusha 2 pusha 1 poppa 1 skriv ut 1 poppa 2 skriv ut 2 poppa 3 skriv ut 3 pusha 4 poppa 4 skriv ut 4 20 poppa 5 skriv ut 5 pusha 8 pusha 6 poppa 6 skriv ut 6 pusha 7 poppa 7 skriv ut 7 poppa 8 skriv ut 8 pusha 9 poppa 9 skriv ut 9 pusha 10 poppa 10 skriv ut 10 klart Hur blir en rekursiv formulering då? Trivialfallet: Tomt träd, gör inget Generella fallet: Ta först hand om vänster subträd Skriv sedan ut roten Ta sedan hand om höger subträd Konvergens? Ja subträden blir nollpekare så småningom. blir såhär ungefär Notera att detta kan förenklas genom vår föräldrapekare! Då behövs inte stacken. inorder_print( root : trädpekare) om rot inte nollpekare inorder_print(root.left) skriv(root.info) inorder_print(root.right) end Ingen stack ingen repetition bara några få enkla rader. Medge att det blir snyggt! 21 Vi ser att om vi har ett binärt sökträd så får vi stigande nyckelordning om vi gör en inorder traversering. Om vi inte bryr oss om ordningen utan bara vill besöka alla noder en gång och utföra något, spelar det då någon roll vilken traverseringsordning jag väljer. 22 Vi kan implementera ett binärt träd på olika sätt. Som i listfallet börjar vi med att definera en nod. Den innehåller en datadel och två pekare samt ett antal enkla funktioner på dessa. Det kan bli något åt det här hållet: package trad; // // // // En trädnodklass med heltal som datadel använd paketåtkomst för att TreeNode och Tree ska kunna komma åt varandra direkt, stäng ute de som är utanför paketet class TreeNode { int val; TreeNode parent; TreeNode left; TreeNode right; // // // // datadel förälder vänsterpekare högerpekare TreeNode() { val = 0; parent = null; right = null; left = null; } // std konstruktor // nollställ TreeNode(int val) { this.val = val; parent = null; right = null; left = null; } // konstruktor // sätt värde // // TreeNode(int val, TreeNode parent) { this.val = val; // sätt värde this.parent = parent; 23 24 En Iterator för vårt träd kan se ut som right = null; left = null; } package trad; // kopiera ett träd rekursivt public TreeNode copy() { TreeNode t = new TreeNode(); // kopia t.val = val; // kopiera datadelen public interface Iterator { public boolean hasNext(); public int next(); public void remove(); // klona barnen if(left != null) { t.left = left.copy(); // vänsterträdet t.left.parent = t; // t är förälder } if(right != null) { t.right = right.copy(); t.right.parent = t; } return t; // kopian } } }; 25 26 // hämta nästa nod, flytta referenserna Vi kan om vi är mindre renläriga direkt arbeta med TreeNode klassen och låta ett träd vara en pekare till en TreeNode. public int next() { } // // // // Det är dock snyggare att explicit skapa en trädtyp som vi kan deklarera och använda. Ett binärt sökträd kan bli ungefär så här ta bort aktuell nod. Om den har två barn så kommer den att ersättas av sin efterföljare, därför backar vi ett steg i det fallet. public void remove() { } package trad; // En klass för binära sökträd, trädnodklassen // finns i samma paket med paketåtkomst för // att underlätta arbetet }; // Standardkonstruktor public Tree() { root = null; } public class Tree { private TreeNode root; // trädets rot // en iterator private class TreeIterator implements Iterator { // aktuell nod och nästa nod // Kopiera trädet public Object clone() { Tree t = new Tree(); t.root = (TreeNode) root.copy(); return t; } private TreeNode lastReturned = null,next; // ny iterator // konstruktor public Iterator iterator() { return new TreeIterator(); } public TreeIterator() { } // Inläggning, bara en "wrapper" // kolla om det finns fler noder public boolean hasNext() { } public void insert(int val) { root = insert(root, val); } // Rekursiv inläggning. Eftersom vi inte kan ha // referensparametrar i Java så måste vi returnera 27 28 // resultatet istället och lägga in på rätt ställe } TreeNode insert(TreeNode root, int val) { } // Leta upp angiven nod, returnera en referens // till den // Kolla om tomt träd private TreeNode nodeSearch(int val) { } public boolean empty() { return root == null; } // ta bort nod med angiven datadel public void remove(int val) { } // Töm trädet, bara en wrapper public void clear() { } // den som gör själva jobbet private void deleteEntry(TreeNode p) { } // Ta bort alla noder ur trädet, behövs kanske inte? }; private void clear(TreeNode root) { } // Leta efter angivet värde i trädet, icke rekursivt public boolean search(int val) { } // Traversera trädet, bara en wrapper public void traverse() { traverse(root); } // inorder traversering void traverse(TreeNode root) { } // ta fram efterföljande nod i inorder mening. // En hjälpfunktion private TreeNode successor(TreeNode e) { 29 Innan vi implementerar detta ska vi se hur man kan ta bort saker ur ett träd. När vi ska ta bort något ur ett träd så blir det vissa svårigheter. Det inses lätt att det inte är trivialt att såga bort en bit mitt i ett träd och samtidigt behålla trädet intakt. Med lite datoriserad ympningsteknik kan man dock fixa till det också. 30 Antag att vi har trädet på sid 19. Man inser att det är lätt att ta bort ett löv t. ex. noden med värdet 1, men också överkomligt att ta bort en nod med bara ett barn, t. ex. 2’an. I detta fall får vi limma fast 1’an direkt under 3’an. Om vi vill ta bort noden med värdet 8 blir det svårare, vi kan inte hänga upp båda barnen under roten eftersom den då får 3 barn vilket inte är tillåtet. Istället erätter vi noden med en annan nod som är enklare att ta bort, utan att rubba trädets grundstruktur. Finns det en sådan nod. Ja två stycken, de som har värden närmast aktuell nod. Av konvention brukar man ta den som är närmast större än aktuell nod, i det här fallet 9’an. Vi vet säkert att denna nod inte kan ha något vänsterbarn. 31 32 Varför? Jo om den hade det så skulle denna nod ha ett lägre värde än vår nod men samtidigt högre än 8’an. (Följer av dess läge). Men då är den ju närmare 8’an än vårt tal vilket strider mot grundantagandet. Vi ersätter alltså 8’an med 9’an och tar bort den gamla noden med talet 9. Hur kan vi nu implementera detta? sökning ganska enkelt enligt tidigare beskrivning // Leta efter angivet värde i trädet, // rekursivt public boolean search(int val) { Notera att resonemanget lika gärna kan appliceras på noden närmast mindre än aktuell nod. // kolla om sökt tal finns i roten if (root.val == val) return true; Det är inte speciellt tidskrävande att ta bort noder ur ett träd, det är bara det att metodiken blir lite strulig. // Nej, leta vidare Hur hittar vi rätt nod? Ta ett steg nedåt till höger, gå sedan så långt åt vänster som det går. Då har vi hittat rätt nod. TreeNode l = root; // så länge vi kan fortsätta och // så länge vi inte hittat rätt Notera att vi har en successor metod i vår klass, den är till för att hitta efterföljaren. Använs kanske också av iteratorn. while (l != null && l.val != val) { // gå till vänster eller höger if (val < l.val) l = l.left; else l = l.right; } return (l != null); // svaret } 33 34 } Ta bort ett träd, enkelt med hjälp av destruktorerna // Töm trädet, bara en wrapper public void clear() { // tomt inget att göra if (root == null) return; else { clear(root); root = null; } empty, trivial funktion public boolean empty() { return root == null; } } // Ta bort alla noder ur trädet, // behövs kanske inte? private void clear(TreeNode root) { // ta bort subträden rekursivt if (root.left != null) { clear(root.left); root.left.parent = null; root.left = null; } if (root.right != null) { clear(root.right); root.right.parent = null; root.right = null; } 35 36 Utskrift av alla noder i inorder ordning nodeSearch och successor används bl. a. av remove. public void traverse() { traverse(root); } // Leta upp angiven nod, // returnera en referens till den // inorder traversering void traverse(TreeNode root) { if (root != null) { // tomt? traverse(root.left); System.out.println(root.val); traverse(root.right); } } private TreeNode nodeSearch(int val) { TreeNode t = root; while (t != null) { if (t.val == val) return t; else if (t.val > val) t = t.left; else t = t.right; } return null; } // ta fram efterföljande nod i inorder // mening. En hjälpfunktion private TreeNode successor(TreeNode e) { // tomt träd, ingen efterföljare if (e == null) return null; // ta ett steg åt höger om det går else if (e.right != null) { // sedan åt vänster så långt det går TreeNode p = e.right; 37 while (p.left != null) p = p.left; return p; } 38 Borttagning görs på detta sätt: // ta bort nod med angiven datadel // inget högerbarn public void remove(int val) { TreeNode p = nodeSearch(val); else { // kolla om den fannse // saknar högerbarn // vandra uppåt så länge du // bara är högerbarn. Stanna när // du hittar ett barn som är //vänsterbarn, returnera dess // förälder. TreeNode p = e.parent; TreeNode ch = e; while (p != null && ch == p.right) { ch = p; p = p.parent; } return p; } } if (p == null) throw new NoSuchElementException(); deleteEntry(p); } // själva arbetshästen private void deleteEntry(TreeNode p) { // kolla om p har två barn, i så fall // byt ut innehållet och // ta bort en annan nod if (p.left != null && p.right != null) { TreeNode s = successor(p); p.val = s.val; // flytta s data till p p = s; // ta bort s istället } // Nu vet vi att p har högst ett barn TreeNode replace; // om vi har ett vänsterbarn, spara referens // annars spara referens till högerbarn if(p.left != null) replace = p.left; else 39 40 replace = p.right; // om p har ett barn, länka //utbytesnoden till föräldern if (replace != null) { // finns ett barn replace.parent = p.parent; // farfar blir pappa // se till att farfar adopterar barnbarnet // när vi slår ihjäl pappa. Kolla dock att // farfar existerar först if (p.parent == null) root = replace; // ingen farfar // om p är vänsterbarn, lägg in utbytesnod // där istället Vanliga binära sökträd lider av de är så känsliga för inläggningsordningen. Det leder till att effektiviteten är svår att förutse och den kan bli allt från log(n) till n-proportionell i ett träd. För att avhjälpa detta kan vi använda oss av olika metoder att balansera trädet. Obalans uppstår vid inläggning och borttagning av noder. Vi kan modifiera dessa operationer så att balans uppnås. En metod att göra detta är s. k. AVL-träd, uppkallade efter de ryska kamraterna Adelson-Velskij och Landis. else if (p == p.parent.left) p.parent.left = replace; // annars högerbarn else p.parent.right = replace; } // inga barn, kolla om jag är // den enda i hela världen else if (p.parent == null) root = null; // // // // ja, nu utrotad! nej finns fler än jag, kolla om jag är vänster eller högerbarn till min förälder. Nollställ aktuellt ställe. else { if (p == p.parent.left) p.parent.left = null; else p.parent.right = null; } } 41 42 Principen för ett AVL-träd är ganska enkel. Vi inför en balansräknare i varje nod som anger skillnaden i längd mellan nodens vänster och högergren. I ett balanserat träd skall denna ha något av värdena -1, 0 eller +1. I ett AVL-träd modifierar vi sedan insert och remove så att balans alltid bibehålls oavsett inläggningsordning. 43 44