06.11.2013 Aufrufe

Algorithmen und Datenstrukturen Elementare Datenstrukturen

Algorithmen und Datenstrukturen Elementare Datenstrukturen

Algorithmen und Datenstrukturen Elementare Datenstrukturen

MEHR ANZEIGEN
WENIGER ANZEIGEN

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

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!