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