Föreläsning 14 Träd och filhantering Träd • Ett träd är en datastruktur som tillåter oss att modellera sådant som vi inte kan modellera med linjära datastrukturer. • Ett datavetenskapligt träd består av noder med pilar emellan. • Ett datavetenskapligt träd ritas upp och ner med roten överst . • Ett träd representerar både en samling noder och någon form av relation mellan dem. 2 Matematisk definition av träd • Ett träd är en ändlig mängd T av en eller flera noder (förbundna med riktade kanter) sådan att: - Exakt en av noderna i T har ingen föregångare. Denna kallas rot. - Resterande noder kan delas in i disjunkta mängder T1, T2, ..., Tm. - Var och en av mängderna Ti är också träd. Dessa mängder Ti kallas subträd till T. (Ett subträd är kopplat till roten via en riktad kant från roten till subträdets rot.) 3 Exempel på träd: arvshierarkier • Alla Javas klasser är delar av ett enda träd med Object-klassen som rot. • Klasserna i Wheels är del av Javas arvshierarki. • Ju närmre roten en klass ligger desto mer generell blir den. AbstractGraphic Shape RectangularShape Ellipse Rectangle Line RoundedRectangle ConversationBubble 4 Exempel på träd: komponenthierarkier • Komponenthierarkier kan också modelleras som träd. • Ju närmre roten desto fler komponenter. JFrame JPanel DrawingPanel JRadioButton ButtonPanel JRadioButton JRadioButton 5 Trädterminologi A • Rotnod: Noden utan B C I föregångare (A i figuren). • Riktad kant (edge): Pil som förbinder en nod med en D E F annan. • Föräldra-, barnnod: En nods föregångare kallas G H föräldranod. En nods efterföljare kallas barnnoder. • Lövnod: En nod utan barn. • Höjden på trädet: Det • Noddjup: Antalet kanter största noddjupet. man måste följa för att Antalet komma från roten till noden. • Nodgrad: barn en nod har. 6 Binära träd och binära sökträd C • Binärt träd: Träd vars noder alla har grad 0, 1 eller 2 (maximalt två barn/nod). • Binärt sökträd: Binärt träd för vilket gäller för alla noder att datat i vänster subträd är mindre än datat i noden och datat i höger subträd är större än det i noden. A B C D B G A E D E F G H F H 7 Jämförelse av datastrukturer • Vilken datastruktur är bäst då vi vill lagra orden i en ordlista eller personer i en telefonkatalog? + sökning - sätta in, ta bort ... Array Länkad lista ... Binärt sökträd + sätta in, ta bort - sökning + sökning, sätta in, ta bort .......... 8 Genomgång av elementen i ett träd • Hur går man igenom dataelementen i ett träd på ett systematiskt sätt? Dataelementen i en linjär struktur går man vanligtvis igenom i ordning från början till slutet. Men hur gör man i ett träd? Med ett träd finns flera möjligheter. Tre av de mest kända för binära träd bygger på att man går igenom trädet genom att följa den röda pilen i figuren. START • På detta sätt kommer alla noder med i C genomgången. B F • Noderna besöks olika antal gånger (1, 2 eller 3 gånger) beroende på A E H hur många barn de har. D G 9 Genomgång av elementen i ett träd • När ska man titta på värdet i noden? • Ska man titta på det första gången man kommer till noden och sedan titta på noderna i vänsterträdet och sedan i högerträdet? (Pre-order: CBAFEDHG) • Eller ska man titta på värdet i noden efter det att man tittat på vänsterträdets noder? (In-order: ABCDEFGH) • Eller, som tredje alternativ, START ska man titta på värdet i noden efter det att man C tittat i vänsterträdets och B F högerträdets noder? (Post-order: ABDEGHFC) A E H D G 10 Praktiska tillämpningar • Det finns praktiska tillämpningar för de olika sätten att gå igenom noderna i ett binärt träd. • Exempelvis kan vi skriva upp aritmetiska uttryck (som 2 * 3) med hjälp av binära träd, där siffrorna blir lövnoder. • Om vi går igenom trädet i in-order får vi den normala infix-versionen av det aritmetiska uttrycket: 2 * 3 • Om vi går igenom trädet i pre-order får vi prefixversionen av det aritmetiska uttrycket: * 2 3 • Om vi går igenom trädet i postorder får vi postfix-versionen av det aritmetiska uttrycket: 2 3 * * • Sammanfattningsvis: 2 3 infix: 2*3 prefix: * 2 3 postfix: 2 3 * 11 Praktiska tillämpningar • Infix-notation: Den vanligste notationen för aritmetiska uttryck och den som är mest vanlig i miniräknare. • Postfix-notation: Används i vissa miniräknare. Man säger även att dessa räknare använder sig av “Reverse Polish Notation” (RPN) efter den polske logikern Lukasiewicz som visade fördelarna med postfix framför infix (inga parenteser behövs i de aritmetiska uttrycken). • Prefix-notation: I programspråket Lisp används prefix-notaionen för aritmetiska uttryck. Fördelen med detta skrivsätt blir att de aritmetiska operatorerna, som + och *, får samma syntax som alla andra funktioner. 12 Praktiska tillämpningar • Infix: (2 + 3) * (5 - 1) • Prefix: * + 2 3 - 5 1 • Postfix: 2 3 + 5 1 - * START * + 2 3 5 1 • Kommentarer: - Med infix-notationen krävs ibland parenteser för att få rätt tolkning. - I Lisp blir prefix-uttrycket: (* (+ 2 3) (- 5 1)) 13 Praktiska tillämpningar START + * 5 2 * / 3 5 4 + 7 8 • Infix: 5 * 2 + 3 / 4 – 5 * (7 + 8) • Prefix: - + * 5 2 / 3 4 * 5 + 7 8 • Postfix: 5 2 * 3 4 / + 5 7 8 + * 14 Praktiska tillämpningar • Att utvärdera ett aritmetiskt uttryck i postfix-notation kan enkelt göras med hjälp av en stack. Man går igenom uttrycket från vänster till höger en gång. När man stöter på ett tal lägger man det på stacken, när man stöter på en operator poppar man stacken två gånger, utför beräkningen med de givna talen och operatorn samt lägger resultatet på stacken. Exempel: Utvärdera uttrycket 5 2 * 3 4 / + 5 7 8 + * - 5 2 5 10 15 5 10 75 10 -65 3 10 4 3 10 0 10 10 5 10 7 5 10 8 7 5 10 15 Java-kod för utvärdering av postfix-uttryck //Pre: Strängen seq är ett aritmetiskt uttryck (med heltal // och operationerna +, ­, * och /) i postfix. //Post: resultat = det aritmetiska uttrycket utvärderat. public int postfix(String [] seq) { Stack<Integer> stack = new Stack<Integer>(); } for (int i = 0; i < seq.length; i++) { try { int tal = Integer.parseInt(seq[i]); stack.push(new Integer(tal)); } catch (NumberFormatException e) { int tal2 = stack.pop().intValue(); int tal1 = stack.pop().intValue(); int res = 0; if (seq[i].equals(“+”)) res = tal1 + tal2; else if (seq[i].equals(“­”)) res = tal1 – tal2; else if (seg[i].equals(“*”)) res = tal1 * tal2; else if (seq[i].equals(“/”)) res = tal1 / tal2; stack.push(new Integer(res)); } } return stack.pop(); 16 Insättning i ett binärt sökträd • Antag att vi vill sätta in talen 7, 10, 1, 0, -3, 45, 15, 8 START i den ordning de anges i ett binärt sökträd. Hur går vi då till väga? • Börja med ett tomt träd och lägg 7 i rotnoden. • Tag sedan 10 och då 10 > 7 sätts 10 in som högerbarnnod till 7. • Fortsätt på samma sätt med resten av talen. • Vad händer om talen i talföljden byter ordning? 7 1 0 -3 10 8 45 15 Preorder: 7, 1, 0, -3, 10, 8, 45, 15 Inorder: -3, 0, 1, 7, 8, 10, 15, 45 Postorder: -3, 0, 1, 8, 15, 45, 10, 7 17 Binärt träd • Antag att vi har ett binärt träd och att det skrivs ut på följande sätt: - inorder: 2, 1, 3, 9, 7, 4, 5, 0, 8 - preorder: 7, 1, 2, 3, 9, 5, 4, 8, 0 • Hur ser trädet ut? (Obs! Trädet är inget binärt sökträd.) START 2 7 1 1 3 2 9 7 3 4 9 5 5 4 0 8 8 7 1 0 2 5 3 4 9 8 0 18 ADT:n Binärt sökträd • Exempel på egenskaper: - Kan innehålla ett obegränsat antal element. • Exempel på operationer: Beskrivning: Skapar ett tomt träd. Pre: sant Post: ett nytt tomt träd har skapats BST() Beskrivning: Kollar om ett element finns i trädet. Pre: sant Post: resultat = sant om elementet i trädet, annars falskt search(in: element, ut: boolskt värde) Beskrivning: Lägger till ett element i trädet. Pre: !search(element) Post: element är insatt i trädet insert(in: element) Beskrivning: Tar bort ett element ur trädet. Pre: search(element) Post: element borttaget ur trädet delete(in: element) Beskrivning: Kollar om trädet är tomt. Pre: sant Post: resultat = sant om trädet är tomt, annars falskt isEmpty(ut: boolskt värde) Beskrivning: Skriver ut trädet i preorder Pre: sant Post: resultat = sträng med trädet i preorder preorder(ut: sträng) 19 Insättning i binärt sökträd START • Exempel: Sätt in 5 i följande binära sökträd: 3 1 7 Pseudokod för insättning: 2 5 insertAux(siffra, rotnod) om rotnod är tom skapa en ny nod och lägg siffra i den sätt rotnod lika med den nya noden annars om siffra < siffran i rotnod vänsterbarn till rotnod = insertAux(siffra, vänsterbarn till rotnod) annars om siffra > siffran i rotnod högerbarn till rotnod = insertAux(siffra, högerbarn till rotnod) 8 skicka tillbaka rotnod 20 Sökning i binärt sökträd START • Exempel: Sök 5 i följande binära sökträd: 3 1 7 Pseudokod för sökning: 2 5 searchAux(siffra, rotnod) om rotnod är tom skicka tillbaka falskt annars om siffra < siffran i rotnod skicka tillbaka searchAux(siffra, vänsterbarn till rotnod) annars om siffra > siffran i rotnod skicka tillbaka searchAux(siffra, högerbarn till rotnod) annars skicka tillbaka sant 8 21 Borttagning ur binärt sökträd Pseudokod för borttagning: • Exempel: Tag bort någon av 2, 5 och 8 ur följande binära sökträd: START 3 1 7 2 5 4 8 6 9 deleteAux(siffra, rotnod) om siffra < siffran i rotnod vänsterbarn till rotnod = deleteAux(siffra, vänsterbarn till rotnod) annars om siffra > siffran i rotnod högerbarn till rotnod = deleteAux(siffra, högerbarn till rotnod) annars om rotnod har inga barn rotnod = null annars om rotnod bara har ett högerbarn rotnod = högerbarn till rotnod annars om rotnod bara har ett vänsterbarn rotnod = vänsterbarn till rotnod annars sök efter det minsta värdet i högerbarnet till rotnod sätt värdet i rotnod till detta minsta värde högerbarn till rotnod = deleteAux( minsta värdet, högerbarn till rotnod) skicka tillbaka rotnod 22 Syftet med lab 6 är att: • • • • ge övning i att skriva rekursiva metoder, ge övning i att implementera datastrukturen träd, träna filhantering och träna hantering av undantag. Laborationen går ut på att komplettera ett befintligt program med vissa metoder. 23 Del av klassdiagrammet till lab 6 _element _left I klassen BST finns en “egenskap”, rotnoden. Observera att metoderna i de allra flesta fall är dubblerade: en metod har ett enkelt gränssnitt och den andra är rekursiv i varje par. _right En nod har tre “egenskaper” och förmågorna består av en konstruktor och set- och getmetoder. 24 Läsa från fil • När man läser från tangentbordet skapar man ett Scanner-objekt enligt: Scanner scanner = new Scanner(System.in); • System.in är ett objekt som motsvarar tangentbordet. • På samma sätt motsvarar File-objekt filer. När man läser från en fil med filnamn fn skapar man ett Scanner-objekt enligt nedan: Scanner scanner = new Scanner(new File(fn)); • Att använda Scanner-objekt till att läsa från filer fungerar ungefär på samma sätt som att använda Scanner-objekt till att läsa från tangentbordet. 25 Läsa från fil • Skillnaden mellan att läsa från en fil och från tangentbordet är att: - filen kanske inte existerar - filen existerar men finns i fel katalog - filen finns i rätt katalog men användaren har skrivit in namnet inkorrekt - filen finns i rätt katalog och är rätt inskriven av användaren, men användaren har inte rättighet att läsa den • Dessutom kan man inte kompilera satsen: Scanner scanner = new Scanner(new File(fn)); Man får följande felmeddelande: Unhandled exception type FileNotFoundException • FileNotFoundException tillhör de undantag som måste tas om hand. 26 En del av Javas Exception-hierarki Throwable Error Exception ClassNotFoundException FileNotFoundException IOException RuntimeException IndexOutOfBoundsException ArrayIndexOutOfBoundsException NullPointerException NoSuchElementException InputMismatchException 27 Läsa från fil import java.io.*; import java.util.*; public class TestIO { public TestIO() throws FileNotFoundException { System.out.println("Enter a file name: "); Scanner kbd = new Scanner(System.in); String fn = kbd.next(); } } Scanner file = new Scanner(new File(fn)); System.out.println(file.nextLine()); file.close(); Alternativ 1: Lägg till throws. Scanner-object kopplat till tangentbordet Scanner-object kopplat till fil public static void main(String [] args) throws FileNotFoundException { new TestIO(); } 28 Läsa från fil import java.io.*; import java.util.*; Alternativ 2: Omslut med try/catch. public class TestRead { public TestRead() { System.out.println("Enter a file name: "); Scanner kbd = new Scanner(System.in); String fn = kbd.next(); try { Scanner file = new Scanner(new File(fn)); System.out.println(file.nextLine()); file.close(); } catch(FileNotFoundException fnfe) { System.out.println("Can't open file"); } } } public static void main(String [] args) { new TestRead(); } 29 Skriva till fil import java.io.*; import java.util.*; public class TestWrite { public TestWrite() { System.out.println("Enter a file name: "); Scanner kbd = new Scanner(System.in); String fn = kbd.next(); try { PrintWriter file = new PrintWriter(new File(fn)); file.println("Hoppsan"); file.close(); } catch(FileNotFoundException fnfe) { PrintWriter har System.out.println("Can't open file"); samma metoder } som System.out } } public static void main(String [] args) { new TestWrite(); } 30 Tillämpning • Använd metoden //Pre: true //Post: resultat = sant om program har balanserade //parentespar (), {}, [], annars falskt public boolean isBalanced(String program) för att kontrollera om ett java-program har balanserade parentespar. • Låt användaren skriva in namnet på filen. • Läs hela filen och lägg innehållet i en sträng med metoden //Pre: true //Post: resultat = en sträng med innehållet i filen som //Scanner-objektet sc är kopplat till public String file2String(Scanner sc) 31 Tillämpning //Pre: true //Post: resultat = en sträng med innehållet i filen //som Scanner-objektet är kopplat till public String file2String(Scanner sc) { boolean cont = true; String str = ""; while (cont) { try { str += sc.nextLine(); } catch (NoSuchElementException e) { System.out.println("End of file"); cont = false; } } return str; } 32 Tillämpning public Test() { StackApp stackApp = new StackApp(); System.out.println("Enter a file name: "); Scanner kbd = new Scanner(System.in); String fn = kbd.next(); try { Scanner file = new Scanner(new File(fn)); String str = file2String(file); if (stackApp.isBalanced(str)) System.out.println("Parenteserna är balanserade"); else System.out.println("Parenteserna är inte balanserade"); file.close(); } catch(FileNotFoundException fnfe) { System.out.println("Can't open file"); } } 33 Binära sökträd till fil • Hur kan man spara undan ett binärt sökträd på en fil för att man senare ska kunna återskapa trädet till dess ursprungsskick? • Studera igen exemplet med vad som händer vid insättning i binära sökträd (se nästa bild). • Vi kan konstatera att ordningen på talen spelar roll för det utseende trädet kommer att få. • Dock kan vi se att trädet kan återskapas om vi sätter in talen i den följd som fås då ursprungsträdet skrivs ut i preorder. • Detta kan vi utnyttja då vi vill skriva ut trädet på fil: skriv ut det i preorder, då kan vi med lätthet återskapa det igen genom att läsa in talen i ordning och sätta in dem i ett nytt träd. 34 Insättning i ett binärt sökträd • Antag att vi vill sätta in talen 7, 10, 1, 0, -3, 45, 15, 8 START i den ordning de anges i ett binärt sökträd. Hur går vi då till väga? • Börja med ett tomt träd och lägg 7 i rotnoden. • Tag sedan 10 och då 10 > 7 sätts 10 in som högerbarnnod till 7. • Fortsätt på samma sätt med resten av talen. • Vad händer om talen i talföljden byter ordning? 7 1 0 -3 10 8 45 15 Preorder: 7, 1, 0, -3, 10, 8, 45, 15 Inorder: -3, 0, 1, 7, 8, 10, 15, 45 Postorder: -3, 0, 1, 8, 15, 45, 10, 7 35