Algorithmen und Datenstrukturen Elementare Datenstrukturen
Algorithmen und Datenstrukturen Elementare Datenstrukturen
Algorithmen und Datenstrukturen Elementare Datenstrukturen
Sie wollen auch ein ePaper? Erhöhen Sie die Reichweite Ihrer Titel.
YUMPU macht aus Druck-PDFs automatisch weboptimierte ePaper, die Google liebt.
<strong>Algorithmen</strong> <strong>und</strong><br />
<strong>Datenstrukturen</strong><br />
Dipl. Inform. Andreas Wilkens<br />
aw@awilkens.com<br />
<strong>Elementare</strong> <strong>Datenstrukturen</strong><br />
Array<br />
Linked List<br />
Stack<br />
Queue<br />
Tree<br />
(Feld)<br />
(Verkettete Liste)<br />
(Stapel)<br />
(Warteschlange)<br />
(Baum)<br />
Einschub: Rekursion<br />
2<br />
1
Rekursion<br />
Rekursion ist keine elementare<br />
Datenstruktur, sondern eine<br />
Programmiertechnik.<br />
3<br />
Rekursion<br />
ein rekursives Programm ruft sich selbst<br />
auf bzw.<br />
eine rekursive Funktion ruft sich selbst<br />
auf<br />
zu jeder Rekursion gehört eine<br />
Abbruchbedingung<br />
4<br />
2
Beispiele für Rekursionen<br />
Das Fernsehbild, das sich selbst enthält<br />
die Fakultätsfunktion n! aus der<br />
Mathematik<br />
5<br />
Fakultätsfunktion n!<br />
n! = n * (n-1)!<br />
für alle natürlichen Zahlen n<br />
größergleich 1<br />
0! = 1<br />
(wichtig: die Abbruchbedingung!)<br />
6<br />
3
Rekursion – Iteration<br />
<br />
<br />
Rekursion<br />
<br />
<br />
ruft sich selbst auf<br />
enthält Abbruchbedingung<br />
Iteration<br />
<br />
<br />
<br />
wiederholtes Durchlaufen von Anweisungen<br />
Abbruch z.B. nach fest vorgegebener<br />
Durchlaufzahl oder durch erfüllte<br />
Abbruchbedingung<br />
Beispiel: Schleifen (for, while)<br />
7<br />
Terminierung<br />
Eine Rekursion muß genauso<br />
terminieren wie eine Iteration<br />
Bei Rekursion muß irgendwann ein<br />
Zustand erreicht werden, in dem kein<br />
weiterer rekursiver Aufruf erzeugt wird<br />
8<br />
4
direkt <strong>und</strong> indirekt<br />
direkte Rekursion<br />
Funktion ruft sich selbst auf<br />
indirekte Rekursion<br />
func_a() ruft func_b() auf, die wieder<br />
func_a() aufruft usw.<br />
9<br />
indirekte Rekursion<br />
void func_a(void)<br />
{ ..<br />
func_b();<br />
..<br />
}<br />
void func_b(void)<br />
{ ..<br />
func_a();<br />
..<br />
}<br />
void main(void)<br />
{ ..<br />
func_a();<br />
..<br />
}<br />
10<br />
5
Speicherarten im PC<br />
<br />
<br />
<br />
<br />
<br />
Codebereich<br />
<br />
enthält Programmcode<br />
Register<br />
<br />
dienen als Ablage zur Programmsteuerung<br />
Globaler Speicher<br />
<br />
Stack<br />
<br />
Heap<br />
<br />
nimmt globale Variablen auf<br />
nimmt u.a. lokale Variablen auf<br />
restlicher Speicher, der vom Programmierer mit „new“<br />
angefordert werden kann<br />
11<br />
Stackframe<br />
beim Aufruf einer Funktion wird ein<br />
Stackframe auf dem Laufzeitstack<br />
angelegt<br />
dieser enthält<br />
die lokalen Variablen<br />
die Parameter<br />
Rückkehrinformationen<br />
wird die Funktion beendet, wird der<br />
Stackframe wieder vom Laufzeitstack<br />
entfernt<br />
12<br />
6
Stackframes bei Rekursion<br />
Laufzeitstack mit 4 rekursiven Instanzen<br />
rekur(4)<br />
Top<br />
rekur(3)<br />
rekur(2)<br />
rekur(1)<br />
main<br />
Bottom<br />
13<br />
Teile <strong>und</strong> herrsche<br />
viele rekursive Programme wenden das<br />
sogenannte „Teile-<strong>und</strong>-herrsche“-<br />
Prinzip an<br />
engl.: divide and conquer<br />
dabei erzeugt die rekursive Funktion<br />
zwei rekursive Aufrufe von sich selbst,<br />
jeweils mit der Hälfte der Eingabewerte<br />
14<br />
7
Beispiel für Teile <strong>und</strong> herrsche<br />
gegeben sei ein Maßstab<br />
dieser soll liniert werden mit<br />
Teilstrichen (siehe Lineal)<br />
die Teilstriche sollen verschiedene<br />
Höhen haben<br />
15<br />
Funktion „liniere“<br />
// zeichnet Teilstriche an eine vorgegebene Strecke<br />
// innerhalb der angegebenen Grenzen l <strong>und</strong> r<br />
// Schema: Mittelstrich erhält die Höhe h,<br />
// die Mittelstriche der entfallenden linken <strong>und</strong><br />
// rechten Hälfte erhalten die Höhe h-1 usw.<br />
void liniere(int l, int r, int h) {<br />
int m = (l+r)/2;<br />
markiere(m, h);<br />
if (h>1) {<br />
liniere(l, m, h-1);<br />
liniere(m, r, h-1);<br />
}<br />
}<br />
16<br />
8
Merkmale der Rekursion<br />
<br />
Rekursion<br />
<br />
<br />
ruft sich selbst auf<br />
enthält Abbruchbedingung<br />
void liniere(int l, int r, int h) {<br />
int m = (l+r)/2;<br />
markiere(m, h);<br />
if (h>1) {<br />
liniere(l, m, h-1);<br />
liniere(m, r, h-1);<br />
}<br />
}<br />
17<br />
Rekursionsbaum für einen<br />
Maßstab der Länge 8<br />
18<br />
9
Noch ein Rekursionsbeispiel<br />
Türme von Hanoi<br />
19<br />
Türme von Hanoi<br />
Scheiben unterschiedlicher Größen<br />
sollen von einem Lagerplatz zu einem<br />
anderen Transportiert werden<br />
Regeln:<br />
es darf nur eine Scheibe zur Zeit<br />
transportiert werden<br />
es darf nur eine kleinere auf eine größere<br />
Scheibe gelegt werden<br />
es steht ein zusätzlicher Hilfslagerplatz zur<br />
Verfügung<br />
20<br />
10
Beispiellösung<br />
Algorithmus zum Lösen des Problems<br />
Türme von Hanoi<br />
Siehe<br />
hanoi.cpp<br />
21<br />
Ablauf für 3 Scheiben:<br />
main: Anzahl der Scheiben: 3<br />
towers( 3, A, B, C );<br />
towers( 2, A, C, B );<br />
towers( 1, A, B, C );<br />
Ausgabe: Scheibe 1 von A nach C<br />
Ausgabe: Scheibe 2 von A nach B<br />
towers( 1, C, A, B );<br />
Ausgabe: Scheibe 1 von C nach B<br />
Ausgabe: Scheibe 3 von A nach C<br />
towers( 2, B, A, C );<br />
towers( 1, B, C, A );<br />
Ausgabe: Scheibe 1 von B nach A<br />
Ausgabe: Scheibe 2 von B nach C<br />
towers( 1, A, B, C );<br />
Ausgabe: Scheibe 1 von A nach C<br />
Fertig<br />
22<br />
11
Rekursionsbaum<br />
Wie sieht der Rekursionsbaum für den<br />
Aufruf towers(3, A, B, C) aus?<br />
23<br />
Türme von Hanoi<br />
die rekursive Funktion „towers“ besteht<br />
nur aus sehr wenigen Zeilen<br />
trotzdem kann damit ein recht hoher<br />
Aufwand geleistet werden<br />
24<br />
12
Frage<br />
Wie kommt man vom Problem „Türme<br />
von Hanoi“ zum Algorithmus?<br />
25<br />
Antwort<br />
durch nachdenken<br />
<strong>und</strong> vereinfachen<br />
26<br />
13
Einfachster Fall<br />
Türme von Hanoi mit nur einer Scheibe<br />
Transportiere die Scheibe vom Quellplatz<br />
zum Zielplatz<br />
Türme von Hanoi mit zwei Scheiben<br />
Transportiere die obere, kleinere Scheibe<br />
vom Quellplatz zum Hilfplatz<br />
Transportiere die untere, größere Scheibe<br />
vom Quellplatz zum Zielplatz<br />
Transportiere die kleinere Scheibe vom<br />
Hilfsplatz zum Zielplatz<br />
27<br />
Allgemeiner Fall<br />
(für die größte Scheibe)<br />
<br />
Um die unterste (größte) Scheibe vom<br />
Startplatz zum endgültigen Zielplatz<br />
transportieren zu können, muss man<br />
1. Erst alle anderen Scheiben aus dem Weg<br />
räumen (zum Hilfsplatz bringen)<br />
2. Jetzt die größte Scheibe zum Zielplatz<br />
bringen.<br />
28<br />
14
Allgemeiner Fall<br />
(für alle Scheiben)<br />
<br />
Um alle Scheiben vom Startplatz zum<br />
Zielplatz zu transportieren, muss man<br />
1. Alle Scheiben (außer der größten) aus dem Weg<br />
räumen (zum Hilfsplatz bringen)<br />
2. Jetzt die größte Scheibe zum Zielplatz bringen.<br />
3. Dann alle anderen (vorher aus dem Weg<br />
geräumten Scheiben) wieder oben auf die<br />
größte Scheibe legen.<br />
(dies gilt immer, unabhängig von der Anzahl der Scheiben)<br />
29<br />
Funktion „towers“<br />
Machen Sie sich die Bedeutung der<br />
(nur) drei Zeilen der Funktion „towers“<br />
deutlich!<br />
<br />
Die drei genannten Punkte auf der<br />
vorangegangenen Folie entsprechen jeder<br />
genau einer Zeile der towers-Funktion!<br />
30<br />
15
Durchlaufen von Binärbäumen<br />
Um z.B. alle Werte eines Binärbaumes<br />
auszugeben, muß dieser durchlaufen<br />
werden.<br />
Das Durchlaufen nennt man auch<br />
“traversieren”.<br />
31<br />
Durchlaufen von Binärbäumen<br />
<br />
Verschiedene Möglichkeiten:<br />
1. WLR – Wurzel-Links-Rechts<br />
2. WRL – Wurzel-Rechts-Links<br />
3. LWR – Links-Wurzel-Rechts<br />
4. LRW – Links-Rechts-Wurzel<br />
5. RWL – Rechts-Wurzel-Links<br />
6. RLW – Rechts-Links-Wurzel<br />
32<br />
16
Beispiel<br />
20<br />
14<br />
33<br />
8 17<br />
26<br />
39<br />
3<br />
11<br />
30<br />
33<br />
Auf einen Blick<br />
derselbe Baum,<br />
verschiedene Durchlaufvarianten:<br />
WLR: 20, 14, 8, 3, 11, 17, 33, 26, 30, 39<br />
LWR: 3, 8, 11, 14, 17, 20, 26, 30, 33, 39<br />
LRW: 3, 11, 8, 17, 14, 30, 26, 39, 33, 20<br />
RWL: 39, 33, 30, 26, 20, 17, 14, 11, 8, 3<br />
34<br />
17
Erkenntnis<br />
Durchläuft man einen Binärbaum nach<br />
LWR (Inorder), so erhält man eine<br />
aufsteigende Sortierung.<br />
Durchläuft man einen Binärbaum nach<br />
RWL, so erhält man eine absteigende<br />
Sortierung.<br />
35<br />
Programmieren<br />
Das Durchlaufen eines binären<br />
Suchbaums kann mittels Rekursion sehr<br />
einfach programmiert werden.<br />
Wiederholung Rekursion:<br />
eine rekursive Funktion ruft sich selbst auf<br />
zu jeder Rekursion gehört eine<br />
Abbruchbedingung<br />
36<br />
18
Datenstruktur eines Knotens<br />
struct node {<br />
ItemType wert;<br />
struct node left, right;<br />
};<br />
37<br />
Preorder (WLR) programmiert<br />
void preorder(node *root)<br />
{<br />
/* Was muß hier stehen ?*/<br />
}<br />
38<br />
19
Preorder (WLR) programmiert<br />
void preorder(node *root)<br />
{<br />
if (root!=NULL)<br />
{<br />
print_value(root);<br />
preorder(root->left);<br />
preorder(root->right);<br />
}<br />
}<br />
39<br />
Inorder (LWR) programmiert<br />
void inorder(node *root)<br />
{<br />
if (root!=NULL)<br />
{<br />
inorder(root->left);<br />
print_value(root);<br />
inorder(root->right);<br />
}<br />
}<br />
40<br />
20
Postorder (LRW)<br />
programmiert<br />
void postorder(node *root)<br />
{<br />
if (root!=NULL)<br />
{<br />
postorder (root->left);<br />
postorder (root->right);<br />
print_value(root);<br />
}<br />
}<br />
41<br />
Löschen im binären Suchbaum<br />
Neben dem Einfügen von Knoten ist<br />
auch das Löschen von Knoten zu<br />
betrachten.<br />
Dabei sind verschiedene Fälle zu<br />
berücksichtigen.<br />
42<br />
21
Löschen im binären Suchbaum<br />
20<br />
14<br />
33<br />
8 17<br />
26<br />
39<br />
3<br />
11<br />
30<br />
Fall 1:<br />
Löschen eines Blattes,<br />
z.B. Knoten mit Wert 30.<br />
Trivial!<br />
43<br />
Löschen im binären Suchbaum<br />
20<br />
14<br />
33<br />
8 17<br />
26<br />
39<br />
3<br />
11<br />
30<br />
Fall 2:<br />
Löschen eines Knotens mit nur einem<br />
Nachfolger (Knoten mit Wert 26)<br />
44<br />
22
Löschen im binären Suchbaum<br />
Fall 2:<br />
Der Nachfolger (-Teilbaum) kann den zu<br />
löschenden Knoten ersetzen.<br />
45<br />
Löschen im binären Suchbaum<br />
Fall 2: Vorgehen:<br />
20<br />
17 26<br />
30<br />
33<br />
39<br />
<br />
<br />
<br />
je ein Arbeitspointer verweist<br />
auf Vaterknoten 33<br />
<strong>und</strong> auf Löschknoten 26<br />
left-Pointer im Vaterknoten<br />
33 wird auf den<br />
Nachfolgerteilbaum<br />
(Knoten 30) des<br />
Löschknotens umgesetzt<br />
Löschknoten 26 wird<br />
entsorgt, nachdem Wert<br />
26 entnommen wurde<br />
46<br />
23
Löschen im binären Suchbaum<br />
20<br />
14<br />
33<br />
8 17<br />
26<br />
39<br />
3<br />
11<br />
30<br />
Fall 3:<br />
Löschen eines Knotens mit zwei Nachfolgeteilbäumen,<br />
z.B. Wurzelknoten 20<br />
47<br />
Löschen im binären Suchbaum<br />
Fall 3, Lösung a:<br />
einer der beiden Teilbäume ersetzt den<br />
Löschknoten<br />
der zweite Teilbaum wird in geeigneter<br />
Weise unter dem ersten Teilbaum gehängt<br />
48<br />
24
Löschen im binären Suchbaum<br />
Fall 3, Lösung a:<br />
<br />
<br />
Teilbaum mit Wurzel<br />
14 ersetzt<br />
Löschknoten 20<br />
Teilbaum mit Wurzel<br />
33 wird als rechter<br />
Nachfolger von<br />
Knoten 17<br />
eingehängt<br />
<br />
Ergebnis:<br />
49<br />
Löschen im binären Suchbaum<br />
<br />
Wie findet man den<br />
richtigen Punkt zum<br />
Einhängen des<br />
Teilbaums mit<br />
Wurzel 33?<br />
50<br />
25
Löschen im binären Suchbaum<br />
Fall 3, Nachteil von Lösung a:<br />
die Höhe des Baums wird größer<br />
im ungünstigsten Fall kann sich die Höhe<br />
verdoppeln<br />
51<br />
Löschen im binären Suchbaum<br />
20<br />
14<br />
33<br />
8 17<br />
26<br />
39<br />
3<br />
11<br />
30<br />
Fall 3:<br />
Löschen eines Knotens mit zwei Nachfolgeteilbäumen,<br />
z.B. Wurzelknoten 20<br />
52<br />
26
Löschen im binären Suchbaum<br />
Fall 3, Lösung b:<br />
Der zu löschende Knoten wird durch den<br />
Knoten mit dem in der Sortierreihenfolge<br />
nächstgrößeren oder nächstkleineren Wert<br />
ersetzt<br />
Achtung, der nächstgrößere oder<br />
nächstkleinere Knoten kann maximal einen<br />
Nachfolger haben, deshalb hierbei<br />
vorgehen wie in Fall 2, Löschen eines<br />
Knotens mit einem Nachfolger<br />
53<br />
Löschen im binären Suchbaum<br />
Ergebnis Fall 3, Lösung b:<br />
Knoten 20 wurde durch nächstgrößeren<br />
(Knoten 26) ersetzt<br />
Knoten 26 wurde an der ursprünglichen<br />
Stelle gelöscht<br />
26<br />
14<br />
33<br />
17<br />
8 30<br />
39<br />
3<br />
11<br />
54<br />
27
Löschen im binären Suchbaum<br />
Fall 3, Vorteil Lösung b:<br />
die Höhe des Baums hat sich nicht<br />
vergrößert<br />
26<br />
14<br />
33<br />
17<br />
8 30<br />
39<br />
3<br />
11<br />
55<br />
Löschen im binären Suchbaum<br />
Ablauf Fall 3, Lösung b:<br />
je ein Arbeitspointer verweist auf den<br />
Löschknoten (20) <strong>und</strong> auf dessen<br />
Vorgänger (hier nicht existent, deshalb<br />
root-Pointer verwenden)<br />
nächstgrößerer Knoten (26) wird lokalisiert<br />
<strong>und</strong> Arbeitspointer auf ihn <strong>und</strong> seinen<br />
Vorgänger (33) gesetzt<br />
Knoten 26 wird herausgeschnitten <strong>und</strong><br />
durch seinen Nachfolgerknoten 30 ersetzt<br />
56<br />
28
Löschen im binären Suchbaum<br />
left-Pointer im Vaterknoten 33 wird auf<br />
Knoten 30 umgebogen<br />
left-Pointer von 26 wird auf 14 gesetzt<br />
right-Pointer von 26 wird auf 33 gesetzt<br />
Vorgängerknoten des Löschknotens (hier<br />
der root-Pointer) wird auf die neue Wurzel<br />
26 umgesetzt<br />
Wert 20 der alten Wurzel wird entnommen<br />
<strong>und</strong> der Knoten gelöscht<br />
fertig<br />
57<br />
Löschen im binären Suchbaum<br />
Fall 3, Fazit von Lösung b:<br />
deutlich aufwendiger zu programmieren als<br />
Lösung a<br />
aber auch deutliche Vorteile beim dadurch<br />
entstehenden Baum, insbesondere bei der<br />
Höhe<br />
deshalb wird Lösung b in der Regel<br />
bevorzugt<br />
58<br />
29