Einführung in die Programmierung Wintersemester 2008/09 Prof. Dr. Günter Rudolph Lehrstuhl für Algorithm Engineering Fakultät für Informatik TU Dortmund Kapitel 8: Elementare Datenstrukturen Kapitel 8 Inhalt ● Definition: Abstrakter Datentyp (ADT) ● ADT Keller ● ADT Schlange ● ADT Liste ● ADT Binärer Suchbaum ● ADT Graph ● Exkurse: - Einfache Dateibehandlung - C++ Strings G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 2 Kapitel 8 Elementare Datenstrukturen Definition: Abstrakter Datentyp (ADT) ist ein Tripel (T, F, A), wobei ● T eine nicht leere Menge von Datenobjekten ● F eine Menge von Operationen, ● A eine nicht leere Menge von Axiomen, die die Bedeutung der Operationen erklären. ■ Abstrakt? ● Datenobjekte brauchen keine konkrete Darstellung (Verallgemeinerung). ● Die Wirkung der Operationen wird beschrieben, nicht deren algorithmische Ausprägung. → „WAS, nicht WIE!“ G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 3 Elementare Datenstrukturen Kapitel 8 Beispiel: ADT bool F: Operationen true false not and or : : : bool : bool x bool : bool x bool → bool → bool → bool → bool → bool Festlegung, welche Funktionen es gibt A: Axiome not(false) not(true) and(false, false) and(false, true) and(true, false) and(true, true) or(x, y) = true = false = false = false = false = true = not(and(not(x), not(y))) Festlegung, was die Funktionen bewirken G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 4 Kapitel 8 Elementare Datenstrukturen Eigenschaften ● Wenn man ADT kennt, dann kann man ihn überall verwenden. ● Implementierung der Funktionen für Benutzer nicht von Bedeutung. ● Trennung von Spezifikation und Implementierung. ● Ermöglicht späteren Austausch der Implementierung, ohne dass sich der Ablauf anderer Programme, die ihn benutzen, ändert! Nur Operationen geben Zugriff auf Daten! → Stichwort: Information Hiding! G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 5 Elementare Datenstrukturen Kapitel 8 Lineare Datenstrukturen: Keller bzw. Stapel (engl. stack) create push pop top empty : : : : : Keller x T Keller Keller Keller empty(create) = true empty(push(k, x)) = false pop(push(k, x)) = k top(push(k, x)) =x → Keller → Keller → Keller →T → bool Aufräumen: Kiste in Keller, oben auf Haufen. Etwas aus Keller holen: Zuerst Kiste, weil oben auf Haufen. Stapel Teller LIFO: Last in, first out. G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 6 Kapitel 8 Elementare Datenstrukturen Implementierung: (Version 1) const int maxSize = 100; struct Keller { T data[maxSize]; // data array int sp; // stack pointer }; void create(Keller &k) { k.sp = -1; } T top(Keller k) { return k.data[k.sp]; } bool empty(Keller k) { return k.sp == -1; } void push(Keller &k, T x){ k.data[++k.sp] = x; } void pop(Keller &k) { k.sp--; } Probleme: Arraygrenzen! G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 7 Kapitel 8 Elementare Datenstrukturen Wann können Probleme auftreten? Bei pop, falls Keller leer ist: → Stackpointer wird -2, anschließendes push versucht auf data[-1] zu schreiben Bei top, falls Keller leer ist: → es wird undefinierter Wert von data[-1] zurückgegeben Bei push, falls Keller voll ist: → es wird versucht auf data[maxsize] zu schreiben ⇒ diese Fälle müssen abgefangen werden! Fehlermeldung! void error(char *info) { cerr << info << endl; exit(1); } gibt Fehlermeldung info aus und bricht das Programm durch exit(1) sofort ab und liefert den Wert des Arguments (hier: 1) an das Betriebssystem zurück G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 8 Kapitel 8 Elementare Datenstrukturen Implementierung: (Version 2, nur Änderungen und Zusätze bei Funktionen) const int maxSize = 100; struct Keller { T data[maxSize]; // data array int sp; // stack pointer }; unverändert T top(Keller k) { if (!empty(k)) return k.data[k.sp]; else error(“leer“); } void push(Keller &k, T x){ if (!full(k)) k.data[++k.sp] = x; else error(“voll“); } void pop(Keller &k) { if (!empty(k)) k.sp--; else error(“leer“); } bool full(Keller k) { return k.sp == maxSize-1; } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 9 Kapitel 8 Elementare Datenstrukturen Lineare Datenstrukturen: Schlange (engl. queue) FIFO: First in, first out. create enq deq front empty Schlange an der Supermarktkasse: : : : : : → Schlange Schlange x T → Schlange Schlange → Schlange Schlange →T Schlange → bool empty(create) empty(enq(s, x)) deq(enq(s, x)) front(enq(s, x)) Wenn Einkauf fertig, dann hinten anstellen. Der nächste Kunde an der Kasse steht ganz vorne in der Schlange. = true = false = empty(s) ? s : enq(deq(s), x) = empty(s) ? x : front(s) Eingehende Aufträge werden „geparkt“, und dann nach und nach in der Reihenfolge des Eingangs abgearbeitet. G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 10 Kapitel 8 Elementare Datenstrukturen Implementierung: (Version 1) const int maxSize = 100; struct Schlange { T data[maxSize]; // data array int ep; // end pointer }; void create(Schlange &s){ s.ep = -1; } void enq(Schlange &s, T x) { s.data[++s.ep] = x; } bool empty(Schlange s) { return s.ep == -1; } void deq(Schlange &s) { for (int i=0; i<s.ep; i++) s.data[i] = s.data[i+1]; s.ep--; } T front(Schlange s) { return s.data[0]; } Probleme: Arraygrenzen! G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 11 Kapitel 8 Elementare Datenstrukturen Implementierung: (Version 2, nur Änderungen und Zusätze bei Funktionen) const int maxSize = 100; struct Schlange { T data[maxSize]; // data array int ep; // end pointer }; T front(Schlange s) { if (!empty(s)) return s.data[0]; error(“leer“); } unverändert void enq(Schlange &s, T x) { if (!full(s)) s.data[++s.ep] = x; else error(“full“); } void deq(Schlange &s) { if (empty(s)) error(“leer“); for (int i=0; i<s.ep; i++) s.data[i] = s.data[i+1]; s.ep--; } bool full(Schlange s) { return s.ep == maxSize-1; } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 12 Kapitel 8 Elementare Datenstrukturen Benutzer des (abstrakten) Datentyps Schlange wird feststellen, dass 1. fast alle Operationen schnell sind, aber 2. die Operation deq vergleichsweise langsam ist. Laufzeit / Effizienz der Operation deq void deq(Schlange &s) { if (empty(s)) error(“leer“); for (int i=0; i < s.ep; i++) s.data[i] = s.data[i+1]; s.ep--; } s.ep = Anzahl Elemente in Schlange s.ep viele Datenverschiebungen Worst case: (maxSize – 1) mal G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 13 Kapitel 8 Elementare Datenstrukturen Idee: Array zum Kreis machen; zusätzlich Anfang/Start markieren (s.sp) s.ep 7 s.sp 0 6 1 5 2 4 3 G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 14 Kapitel 8 Elementare Datenstrukturen Implementierung: (Version 3) const int maxSize = struct Schlange { T data[maxSize]; int sp; int ep; }; 100; // data array // start pointer // end pointer void create(Schlange& s) { s.sp = 0; s.ep = maxSize; } bool empty(Schlange s) { return s.ep == maxSize; } bool full(Schlange s) { int d = s.ep – s.sp; if ((d == -1) || (d == maxSize-1)) return; } T front(Schlange s) { if (!empty(s)) return s.data[s.sp]; error(“leer“); } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 15 Kapitel 8 Elementare Datenstrukturen Implementierung: (Version 3) void enq(Schlange &s, T x) { if (!full(s)) { s.ep = (s.ep + 1) % maxSize; s.data[s.ep] = x; } else error(“full“); } void deq(Schlange &s) { if (empty(s)) error(“leer“); else if (s.sp == s.ep) create(s); else s.sp = (s.sp + 1) % maxSize; } Laufzeit: unabhängig von Größe der Schlange Laufzeit: unabhängig von Größe der Schlange G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 16 Kapitel 8 Elementare Datenstrukturen Unbefriedigend bei der Implementierung: Maximale festgelegte Größe des Kellers bzw. der Schlange! → Liegt an der unterliegenden Datenstruktur Array: Array ist statisch, d.h. Größe wird zur Übersetzungszeit festgelegt und ist während der Laufzeit des Programms nicht veränderbar! Schön wären dynamische Datenstrukturen, d.h. Größe wird zur Übersetzungszeit nicht festgelegt und ist während der Laufzeit des Programms veränderbar! ⇒ Dynamischer Speicher! (Stichwort: new / delete ) G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 17 Kapitel 8 Wiederholung: ADT Schlange Lineare Datenstrukturen: Schlange (engl. queue) create enq deq front empty : : : : : → Schlange Schlange x T → Schlange Schlange → Schlange Schlange →T Schlange → bool create : erzeugt leere Schlange enq : hängt Element ans Ende der Schlange deq : entfernt Kopf der Schlange front : gibt im Kopf der Schlange gespeichertes Element zurück empty : prüft, ob Schlange leer ist → Implementierung mit statischen Speicher ersetzen durch dynamischen Speicher G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 18 Kapitel 8 Wiederholung: Dynamischer Speicher Bauplan: Datentyp *Variable = new Datentyp; (Erzeugen) delete Variable; (Löschen) Bauplan für Arrays: Datentyp *Variable = new Datentyp[Anzahl]; (Erzeugen) delete[] Variable; (Löschen) Achtung: Dynamisch erzeugte Objekte müssen auch wieder gelöscht werden! Keine automatische Speicherbereinigung! G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 19 Kapitel 8 Elementare Datenstrukturen Vorüberlegungen für ADT Schlange mit dynamischen Speicher: Wir können bei der Realisierung der Schlange statt statischen (Array) nun dynamischen Speicher verwenden … Ansatz: new int[oldsize+1] … bringt uns das weiter? → Größe kann zwar zur Laufzeit angegeben werden, ist aber dann fixiert! Falls maximale Größe erreicht, könnte man 1. größeres Array anlegen 2. Arraywerte ins größere Array kopieren und ineffizient! 3. kleineres Array löschen. G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 20 Kapitel 8 Elementare Datenstrukturen Vorüberlegungen für ADT Schlange mit dynamischen Speicher: Objekt Datenfeld „Kopf “ + Zeiger „Schwanz der Schlange“ Schlange 0 Start zeigt auf Null (nichts): Nullpointer Ende G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 21 Elementare Datenstrukturen Kapitel 8 Implementierung: (Version 4) struct Objekt { T data; Objekt *tail; }; struct Schlange { Objekt *sp; Objekt *ep; }; // Datenfeld // Zeiger auf Schwanz der Schlange // Start // Ende Schlange *create() { Schlange *s = new Schlange; s->ep = 0; return s; } Speicher für Schlange allokieren Initialisierung! Rückgabe des Zeigers auf Schlange bool empty(Schlange *s) { return s->ep == 0; } true falls s->ep == 0, sonst false G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 22 Elementare Datenstrukturen Kapitel 8 Implementierung: (Version 4) struct Objekt { T data; Objekt *tail; }; struct Schlange { Objekt *sp; Objekt *ep; }; // Datenfeld // Zeiger auf Schwanz der Schlange // Start // Ende T front(Schlange *s) { if (!empty(s)) return (s->sp)->data; error(“leer“); } void clear(Schlange *s) { while (!empty(s)) deq(s); } Was bedeutet s->sp->data ? identisch zu (*(*s).sp).data vorderstes Element entfernen bis Schlange leer G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 23 Elementare Datenstrukturen Kapitel 8 Implementierung: (Version 4) struct Objekt { T data; Objekt *tail; }; struct Schlange { Objekt *sp; Objekt *ep; }; // Datenfeld // Zeiger auf Schwanz der Schlange // Start // Ende void enq(Schlange *s, T x) { Objekt *obj = new Objekt; obj->data = x; obj->tail = 0; if (empty(s)) s->sp = obj; else s->ep->tail = obj; s->ep = obj; } neues Objekt anlegen (dyn. Speicher) neues Objekt initialisieren neues Objekt vorne, falls Schlange leer sonst hinten anhängen Aktualisierung des Endezeigers G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 24 Elementare Datenstrukturen Kapitel 8 Implementierung: (Version 4) struct Objekt { T data; Objekt *tail; }; struct Schlange { Objekt *sp; Objekt *ep; }; // Datenfeld // Zeiger auf Schwanz der Schlange // Start // Ende void deq(Schlange *s) { if (empty(s))error(“leer“); Objekt *obj = s->sp; s->sp = s->sp->tail; if (s->sp == 0) s->ep = 0; delete obj; } Zeiger auf zu löschendes Objekt retten Startzeiger auf 2. Element setzen falls kein 2. Element, dann Schlange leer ehemals 1. Element löschen G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 25 Elementare Datenstrukturen Kapitel 8 int main() { Testprogramm! Schlange *s = create(); if (empty(s)) cout << "Schlange leer" << endl; for (int i = 0; i < 10; i++) enq(s, i); if (!empty(s)) cout << "Schlange nicht mehr leer" << endl; cout << "vorderstes Element: " << front(s) << endl; while (!empty(s)) { cout << front(s) << " "; deq(s); } cout << endl; if (empty(s)) cout << "Schlange jetzt leer" << endl; for (i = 0; i < 100; i++) enq(s,i); if (!empty(s)) cout << "Schlange nicht mehr leer" << endl; clear(s); if (empty(s)) cout << "Schlange wieder leer" << endl; } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 26 Elementare Datenstrukturen Kapitel 8 G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 27 Elementare Datenstrukturen Kapitel 8 ADT Liste (1. Version) Liste wird nur durch einen Zeiger auf ihren Listenkopf repräsentiert struct Liste { T data; Liste *next; }; Operationen: create empty append prepend clear is_elem : : : : : : Liste T x Liste T x Liste T x Liste → Liste → bool → Liste → Liste → Liste → bool hängt am Ende an vor Kopf einfügen ist Element enthalten? G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 28 Elementare Datenstrukturen Kapitel 8 ADT Liste (1. Version) struct Liste { T data; Liste *next; }; Liste wird nur durch einen Zeiger auf ihren Listenkopf repräsentiert Liste *create() { return 0; } Laufzeit: unabhängig von Listenlänge bool empty(Liste *liste) { return liste == 0; } Laufzeit: unabhängig von Listenlänge void clear(Liste *liste) { if (empty(liste)) return; clear(liste->next); delete liste; } rekursives Löschen von „hinten“ nach „vorne“ Laufzeit: proportional zur Listenlänge G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 29 Kapitel 8 Elementare Datenstrukturen ADT Liste (1. Version) struct Liste { T data; Liste *next; }; bool is_elem(T x, Liste *liste) { if (liste == 0) return false; if (liste->data == x) return true; return is_elem(x, liste->next); } Liste *prepend(T x, Liste *liste) { Liste *L = new Liste; L->data = x; L->next = liste; return L; } rekursiver Durchlauf von „vorne“ nach „hinten“ Laufzeit: proportional zur Listenlänge Laufzeit: unabhängig von Listenlänge G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 30 Kapitel 8 Elementare Datenstrukturen ADT Liste (1. Version) struct Liste { T data; Liste *next; }; Liste *append(T x, Liste *liste) { Liste *tmp = liste; Liste *L = new Liste; L->data = x; L->next = 0; if (liste == 0) return L; while (liste->next != 0) liste = liste->next; liste->next = L; return tmp; } Zeiger auf Listenkopf retten iterativer Durchlauf von „vorne“ nach „hinten“ Laufzeit: proportional zur Listenlänge G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 31 Kapitel 8 Elementare Datenstrukturen ADT Liste (1. Version) Zusammenfassung: 1. Laufzeit von clear proportional zur Listenlänge → kann nicht verbessert werden, weil ja jedes Element gelöscht werden muss → unproblematisch, weil nur selten aufgerufen 2. Laufzeit von is_elem proportional zur Listenlänge → kann bei dieser Datenstruktur nicht verbessert werden → später verbessert durch ADT BinärBaum 3. Laufzeit von append proportional zur Listenlänge → kann durch Veränderung der Implementierung verbessert werden → zusätzlicher Zeiger auf das Ende der Liste G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 32 Elementare Datenstrukturen Kapitel 8 ADT Liste (2. Version) struct Liste { Element *head; Element *foot; }; struct Element { T data; Element *next; }; Liste *create() { Liste *L = new Liste; L->head = L->foot = 0; return L; } bool empty(Liste *liste) { return liste->foot == 0; } Liste besteht aus 2 Zeigern: Zeiger auf Listenkopf (Anfang) Zeiger auf Listenfuß (Ende) Nutzdaten Zeiger auf nächstes Element Laufzeit: unabhängig von Listenlänge Laufzeit: unabhängig von Listenlänge G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 33 Kapitel 8 Elementare Datenstrukturen ADT Liste (2. Version) struct Liste { Element *head; Element *foot; }; struct Element { T data; Element *next; }; bool is_elem(T x, Liste *liste) { if (empty(liste)) return false; Element *elem = liste->head; while (elem != 0) { if (elem->data == x) return true; elem = elem->next; } return false; } iterativer Durchlauf von „vorne“ nach „hinten“ Laufzeit: proportional zur Listenlänge → keine Verbesserung (OK) G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 34 Elementare Datenstrukturen Kapitel 8 ADT Liste (2. Version) struct Liste { Element *head; Element *foot; }; struct Element { T data; Element *next; }; void clear(Liste *liste) { if (empty(liste)) return; Element *elem = liste->head; liste->head = elem->next; clear(liste); delete elem; } rekursives Löschen von „hinten“ nach „vorne“ Laufzeit: proportional zur Listenlänge → keine Verbesserung (OK) G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 35 Kapitel 8 Elementare Datenstrukturen ADT Liste (2. Version) struct Liste { Element *head; Element *foot; }; struct Element { T data; Element *next; }; Liste *prepend(T x, Liste *liste) { Element *elem = new Element; elem->data = x; elem->next = liste->head; if (empty(liste)) liste->foot = elem; liste->head = elem; return liste; } Laufzeit: unabhängig von Listenlänge G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 36 Kapitel 8 Elementare Datenstrukturen ADT Liste (2. Version) struct Liste { Element *head; Element *foot; }; struct Element { T data; Element *next; }; Liste *append(T x, Liste *liste) { Element *elem = new Element; elem->data = x; elem->next = 0; if (empty(liste)) liste->head = elem; else liste->foot->next = elem; liste->foot = elem; } Laufzeit: unabhängig von Listenlänge → Verbesserung! G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 37 Kapitel 8 Elementare Datenstrukturen ADT Liste (2. Version) Zusammenfassung: 1. Laufzeit von clear proportional zur Listenlänge → kann nicht verbessert werden, weil ja jedes Element gelöscht werden muss → unproblematisch, weil nur selten aufgerufen 2. Laufzeit von is_elem proportional zur Listenlänge → kann bei dieser Datenstruktur nicht verbessert werden → verbessern wir gleich durch ADT BinärBaum 3. Laufzeit von append unabhängig von Listenlänge → war proportional zur Listenlänge in 1. Version → Verbesserung erzielt durch Veränderung der Implementierung G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 38 Elementare Datenstrukturen Kapitel 8 ADT Binäre Bäume Vorbemerkungen: Zahlenfolge (z. B. 17, 4, 36, 2, 8, 19, 40, 6, 7, 37) soll gespeichert werden, um später darin suchen zu können Man könnte sich eine Menge A vorstellen mit Anfrage: Ist 40 ∈ A ? Mögliche Lösung: Zahlen in einer Liste speichern und nach 40 suchen … … aber: nicht effizient, weil im schlechtesten Fall alle Elemente überprüft werden müssen! Bessere Lösungen? G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 39 Kapitel 8 Elementare Datenstrukturen ADT Binäre Bäume Beispiel: kleiner : nach links Zahlenfolge 17, 4, 36, 2, 8, 19, 40, 6, 7, 37 größer : nach rechts 17 4 36 2 8 19 6 40 37 z.B. „Suche, ob 42 enthalten“ 7 benötigt nur 3 Vergleiche bis zur Entscheidung false G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 40 Kapitel 8 Elementare Datenstrukturen ADT Binäre Bäume Beispiel: kleiner : nach links Zahlenfolge 17, 4, 36, 2, 8, 19, 40, 6, 7, 37 größer : nach rechts 17 4 36 2 8 19 6 40 37 z.B. „Suche, ob 42 enthalten“ 7 benötigt nur 3 Vergleiche bis zur Entscheidung false G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 41 Kapitel 8 Elementare Datenstrukturen ADT Binäre Bäume: Terminologie keine Wurzel und kein Blatt ⇒ innerer Knoten Wurzel L R linker Unterbaum rechter Unterbaum Blätter G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 42 Elementare Datenstrukturen Kapitel 8 ADT Binäre Bäume: Datenstruktur struct BinTree { T data; BinTree *lTree, *rTree; }; // Nutzdaten // linker und rechter Unterbaum Falls ein Unterbaum nicht existiert, dann zeigt der Zeiger auf 0. bool IsElement(int key, BinTree *tree) { if (tree == 0) return false; if (tree->data == key) return true; if (tree->data < key) return IsElement(key, tree->rTree); return IsElement(key, tree->lTree); } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 43 Elementare Datenstrukturen Kapitel 8 ADT Binäre Bäume: Einfügen BinTree *Insert(int key, BinTree *tree) { if (tree == 0) { BinTree *b = new BinTree; b->data = key; b->lTree = b->rTree = 0; return b; } else { if (tree->data < key) tree->rTree = Insert(key, tree->rTree); else if (tree->data > key) tree->lTree = Insert(key, tree->lTree); return tree; } } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 44 Kapitel 8 Elementare Datenstrukturen ADT Binäre Bäume: Aufräumen void Clear(BinTree *tree) { if (tree == 0) return; // Clear(tree->lTree); // Clear(tree->rTree); // delete tree; // } 3 6 4 u.s.w. 19 2 7 20 1 18 Rekursionsabbruch linken Unterbaum löschen rechten Unterbaum löschen aktuellen Knoten löschen 17 8 16 9 10 5 15 12 11 13 14 G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 45 Elementare Datenstrukturen Kapitel 8 ADT Binäre Bäume Höhe := Länge des längsten Pfades von der Wurzel zu einem Blatt. Höhe(leerer Baum) = 0 Höhe(nicht leerer Baum) = 1 + max { Höhe(linker U-Baum), Höhe(rechter U-Baum) } Anmerkung: rekursive Definition! (U-Baum = Unterbaum) G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 46 Kapitel 8 Elementare Datenstrukturen ADT Binäre Bäume 1 2 3 4 Auf Ebene k können jeweils zwischen 1 und 2k-1 Elemente gespeichert werden. ⇒ In einem Baum der Höhe h können also zwischen h und Elemente gespeichert werden! G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 47 Kapitel 8 Elementare Datenstrukturen ADT Binäre Bäume 1 2 3 4 ● Ein vollständiger Baum der Höhe h besitzt 2h – 1 Knoten. Man braucht maximal h Vergleiche, um Element (ggf. nicht) zu finden. Bei n = 2h – 1 Elementen braucht man log2(n) < h Vergleiche! ● Ein degenerierter Baum der Höhe h besitzt h Knoten (= lineare Liste). Man braucht maximal h Vergleiche, um Element (ggf. nicht) zu finden. Bei n = h braucht man also n Vergleiche! G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 48 Kapitel 8 Exkurs: Einfache Dateibehandlung Datei := speichert Daten in linearer Anordnung Zwei Typen: ● ASCII-Dateien - sind mit Editor les- und schreibbar - Dateiendung („suffix“ oder „extension“) meist .txt oder .asc - betriebssystem-spezifische Übersetzung von Zeichen bei Datentransfer zwischen Programm und externem Speicher ● Binär-Dateien - werden byteweise beschrieben und gelesen - lesen / schreiben mit Editor ist keine gute Idee - schnellerer Datentransfer, da keine Zeichenübersetzung G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 49 Kapitel 8 Exkurs: Einfache Dateibehandlung Hier: einfache Dateibehandlung! ● Dateien können gelesen oder beschrieben werden. ● Vor dem ersten Lesen oder Schreiben muss Datei geöffnet werden. ● Man kann prüfen, ob das Öffnen funktioniert hat. ● Nach dem letzten Lesen oder Schreiben muss Datei geschlossen werden. ● Bei zu lesenden Dateien kann gefragt werden, ob Ende der Datei erreicht ist. ● Beim Öffnen einer zu schreibenden Datei wird vorheriger Inhalt gelöscht! ● Man kann noch viel mehr machen … wir benötigen: #include <fstream> bzw. <fstream.h> G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 50 Kapitel 8 Exkurs: Einfache Dateibehandlung ● Eingabe-Datei = input file ● Ausgabe-Datei = output file ifstream Quelldatei; ofstream Zieldatei; Datentyp Datentyp Bezeichner ● Öffnen der Datei: Bezeichner ● Öffnen der Datei: Quelldatei.open(dateiName); Zieldatei.open(dateiName); ist Kurzform von Quelldatei.open(dateiName, modus); ist Kurzform von Quelldatei.open(dateiName, modus); wobei fehlender modus bedeutet: ASCII-Datei, Eingabedatei (weil ifstream) wobei fehlender modus bedeutet: ASCII-Datei, Ausgabedatei (weil ofstream) G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 51 Exkurs: Einfache Dateibehandlung Kapitel 8 modus: ios::binary binäre Datei ios::in öffnet für Eingabe (implizit bei ifstream) ios::out öffnet für Ausgabe (implizit bei ofstream) ios::app hängt Daten am Dateiende an ios::nocreate wenn Datei existiert, dann nicht anlegen Warnung: teilweise Compiler-abhängig (nocreate fehlt in MS VS 2003, dafür trunc) Man kann diese Schalter / Flags miteinander kombinieren via: ios::binary | ios::app (öffnet als binäre Datei und hängt Daten an) G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 52 Kapitel 8 Exkurs: Einfache Dateibehandlung ● Datei öffnen file.open(fileName) bzw. file.open(fileName, modus) falls Öffnen fehlschlägt, wird Nullpointer zurückgegeben ● Datei schließen file.close() sorgt für definierten Zustand der Datei auf Dateisystem; bei nicht geschlossenen Dateien droht Datenverlust! ● Ende erreicht? ja falls file.eof() == true ● Lesen (von ifstream) file.get(c); file >> x; liest ein Zeichen liest verschiedene Typen ● Schreiben (von ofstream) file.put(c); schreibt ein Zeichen file << x; schreibt verschiedene Typen G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 53 Kapitel 8 Exkurs: Einfache Dateibehandlung Merke: 1. Auf eine geöffnete Datei darf immer nur einer zugreifen. 2. Eine geöffnete Datei belegt Ressourcen des Betriebssystems. ⇒ Deshalb Datei nicht länger als nötig geöffnet halten. 3. Eine geöffnete Datei unbekannter Länge kann solange gelesen werden, bis das Ende-Bit (end of file, EOF) gesetzt wird. 4. Der Versuch, eine nicht vorhandene Datei zu öffnen (zum Lesen) oder eine schreibgeschützte Datei zu öffnen (zum Schreiben), führt zu einem Nullpointer. ⇒ Das muss überprüft werden, sonst Absturz bei weiterer Verwendung! 5. Dateieingabe und -ausgabe (input/output, I/O) ist sehr langsam im Vergleich zu den Rechenoperationen. ⇒ I/O Operationen minimieren. “The fastest I/O is no I/O.“ Nils-Peter Nelson, Bell Labs G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 54 Exkurs: Einfache Dateibehandlung Kapitel 8 #include <iostream> #include <fstream> using namespace std; int main() { ifstream Quelldatei; ofstream Zieldatei; // zeichenweise kopieren Quelldatei.open("quelle.txt"); if (!Quelldatei.is_open()) { cerr << "konnte Datei nicht zum Lesen öffnen\n"; exit(1); } Zieldatei.open("ziel.txt"); if (!Zieldatei.is_open()) { cerr << "konnte Datei nicht zum Schreiben öffnen\n"; exit(1); } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 55 Exkurs: Einfache Dateibehandlung Kapitel 8 while (!Quelldatei.eof()) { char c; Quelldatei.get(c); Zieldatei.put(c); } Quelldatei.close(); Zieldatei.close(); } offene Datei Start aktuelle Position eof() == true G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 56 Kapitel 8 Exkurs: C++ Strings Bisher: Zeichenketten wie char str[20]; → Relikt aus C-Programmierung! → bei größeren Programmen mühevoll, lästig, … → … und insgesamt fehlerträchtig! Jetzt: Zeichenketten aus C++ → sehr angenehm zu verwenden (keine 0 am Ende, variable Größe, …) → eingebaute (umfangreiche) Funktionalität wie benötigen: #include <string> und using namespace std; G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 57 Kapitel 8 Exkurs: C++ Strings Datendefinition / Initialisierung string s1; // leerer String string s2 = "xyz"; // initialisieren mit C-String string s3 = s2; // vollständige Kopie! string s4("abc"); // initialisieren mit C-String string s5(s4); // initialisieren mit C++-String string s6(10, ‘*‘); // ergibt String aus 10 mal * string s7(1‚‘x‘); // initialisieren mit einem char string sx(‘x‘); // FEHLER! string s8(""); // leerer String G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 58 Kapitel 8 Exkurs: C++ Strings Eingebaute Funktionen ● Konvertierung C++-String nach C-String via c_str() const char *Cstr = s2.c_str(); ● Stringlänge length() cout << s2.length(); ● Index von Teilstring finden int pos = s2.find(“yz“); ● Strings addieren s1 = s2 + s3; s4 = s2 + “hello“; s5 += s4; ● Strings vergleichen if (s1 == s2) s3 += s2; if (s3 < s8) flag = true; ● substr(), replace(), erase(), … G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 59 Kapitel 8 Elementare Datenstrukturen ADT Binäre Bäume: Anwendung Aufgabe: Gegeben sei eine Textdatei. Häufigkeiten der vorkommenden Worte feststellen. Alphabetisch sortiert ausgeben. Strategische Überlegungen: Lesen aus Textdatei → ifstream benutzen! Sortierte Ausgabe → Binärbaum: schnelles Einfügen, sortiert „von selbst“ Worte vergleichen → C++ Strings verwenden! Programmskizze: Jeweils ein Wort aus Datei lesen und in Binärbaum eintragen. Falls Wort schon vorhanden, dann Zähler erhöhen. Wenn alle Wörter eingetragen, Ausgabe (Wort, Anzahl) via Inorder-Durchlauf. G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 60 Kapitel 8 Elementare Datenstrukturen struct BinBaum { string wort; int anzahl; BinBaum *links, *rechts; }; gelesenes Wort wie oft gelesen? BinBaum *Einlesen(string &dateiname) { ifstream quelle; quelle.open(dateiname.c_str()); if (!quelle.is_open()) return 0; string s; BinBaum *b = 0; while (!quelle.eof()) { quelle >> s; b = Einfuegen(s, b); } quelle.close(); return b; } Datei öffnen Worte einzeln auslesen und im Baum einfügen Datei schließen + Zeiger auf Baum zurückgeben G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 61 Elementare Datenstrukturen Kapitel 8 BinBaum *Einfuegen(string &s, BinBaum *b) { if (b == 0) { b = new BinBaum; b->wort = s; b->anzahl = 1; b->links = b->rechts = 0; return b; } if (b->wort < s) b->rechts = Einfuegen(s, b->rechts); else if (b->wort > s) b->links = Einfuegen(s, b->links); else b->anzahl++; return b; } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 62 Kapitel 8 Elementare Datenstrukturen void Ausgabe(BinBaum *b) { if (b == 0) return; Ausgabe(b->links); cout << b->anzahl << " " << b->wort.c_str() << endl; Ausgabe(b->rechts); } Dies ist die Inorder-Ausgabe. Präorder: Postorder: cout …; Ausgabe(…); Ausgabe(…); Ausgabe(…); Ausgabe(…); cout …; G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 63 Elementare Datenstrukturen Kapitel 8 Hauptprogramm: #include <iostream> #include <fstream> #include <string> using namespace std; int main() { string s("quelle.txt"); BinBaum *b = Einlesen(s); Ausgabe(b); return 0; } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 64 Elementare Datenstrukturen Durchlaufstrategien: Kapitel 8 z.B. Ausdruck des Knotenwertes ● Tiefensuche („depth-first search“, DFS) - Präorder Vor (prä) Abstieg in Unterbäume die „Knotenbehandlung“ durchführen - Postorder Nach (post) Abstieg in bzw. Rückkehr aus Unterbäumen die „Knotenbehandlung“ durchführen - Inorder Zwischen zwei Abstiegen „Knotenbehandlung“ durchführen ● Breitensuche („breadth-first search“, BFS; auch: „level search“) auf jeder Ebene des Baumes werden Knoten abgearbeitet, bevor in die Tiefe gegangen wird G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 65 Kapitel 8 Elementare Datenstrukturen Breitensuche Beispiel: eingegebene Zahlenfolge 17, 4, 36, 2, 8, 40, 19, 6, 7, 37 17 4 36 2 8 19 6 40 37 7 Ausgabe: 17, 4, 36, 2, 8, 19, 40, 6, 37, 7 Implementierung: → Praktikum! G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 66 Kapitel 8 Elementare Datenstrukturen ADT Graph ● Verallgemeinerung von (binären) Bäumen ● Wichtige Struktur in der Informatik ● Zahlreiche Anwendungsmöglichkeiten - Modellierung von Telefonnetzen, Versorgungsnetzwerken, Straßenverkehr, … - Layout-Fragen bei elektrischen Schaltungen - Darstellung sozialer Beziehungen - etc. ● Viele Probleme lassen sich als Graphenprobleme „verkleiden“ und dann mit Graphalgorithmen lösen! G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 67 Kapitel 8 Elementare Datenstrukturen Definition EIn Graph G = (V, E) besteht aus einer Menge von Knoten V („vertex, pl. vertices“) und einer Menge von Kanten E („edge, pl. edges“) mit E ⊆ V x V. 1 2 6 Δ(G) δ(G) 5 Schlinge 7 3 = 6 = 3 4 Eine Kante (u, v) heißt Schlinge (“loop“), wenn u = v. Der Grad („degree“) eines Knotens v ∈ V ist die Anzahl der zu ihm inzidenten Kanten: deg(v) = | { (a, b) ∈ E : a = v oder b = v } | . Maxgrad von G ist Δ(G) = max { deg(v) : v ∈ V } Mingrad von G ist δ(G) = min { deg(v) : v ∈ V } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 68 Kapitel 8 Elementare Datenstrukturen Definition Für vi ∈ V heißt (v0, v1, v2, …, vk) ein Weg oder Pfad in G, wenn (vi,vi+1) ∈ E für alle i = 0, 1, …, k-1. Die Länge eines Pfades ist die Anzahl seiner Kanten. Ein Pfad (v0, v1, v2, …, vk) mit v0 = vk wird Kreis genannt. Distanz dist(u, v) von zwei Knoten ist die Länge des kürzesten Pfades von u nach v. Durchmesser diam(G) eines Graphes G ist das Maximum über alle Distanzen: diam(G) = max { dist(u, v) : (u, v) ∈ V x V }. Graph ist zusammenhängend, wenn ∀ u, v ∈ V mit u ≠ v einen Pfad gibt. G heißt Baum gdw. G zusammenhängend und kreisfrei. G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 69 Kapitel 8 Elementare Datenstrukturen Darstellung im Computer ● Adjazenzmatrix A mit aij = 1 falls (vi,vj) ∈ E 0 sonst Problem: Da | E | ≤ | V |2 = n2 ist Datenstruktur ineffizient (viele Nullen) wenn | E | verschwindend klein. ● Adjazenzlisten: Für jeden Knoten v eine (Nachbarschafts-) Liste L(v) mit L(v) = { u ∈ V : (v, u) ∈ E } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 70 Kapitel 8 Elementare Datenstrukturen c Beispiel b a Adjazenzlisten L(a) = ( b, e ) L(b) = ( a, c, d ) L(c) = ( b, d ) L(d) = ( b, c, e ) L(e) = ( a, d ) d e Adjazenzmatrix ADT Liste a b c d e a 0 1 0 0 1 b 1 0 1 1 0 c 0 1 0 1 0 d 0 1 1 0 1 e 1 0 0 1 0 Array[][] G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 71 Kapitel 8 Elementare Datenstrukturen Mögliche Funktionalität typedef unsigned int uint; typedef Datentyp TypName; Graph *createGraph(uint NoOfNodes); void addEdge(Graph *graph, uint Node1, uint Node2); void deleteEdge(Graph *graph, uint Node1, uint Node2); bool hasEdge(Graph *graph, uint Node1, uint Node2); uint noOfEdges(); uint noOfNodes(); void printGraph(); G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 72 Kapitel 8 Elementare Datenstrukturen AdjList Graph *createGraph(uint NoOfNodes) { Graph *graph = new Graph; graph->NoOfNodes = NoOfNodes; graph->AdjList = new List*[NoOfNodes]; for (uint i = 0; i < NoOfNodes; i++) graph->AdjList[i] = create(); return graph; } List List List // Zeiger auf Zeiger auf Listen List struct Graph { uint NoOfNodes; List **AdjList; }; Speicher reservieren initialisieren! G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 73 Kapitel 8 Elementare Datenstrukturen AdjList List List List // Zeiger auf Zeiger auf Listen List struct Graph { uint NoOfNodes; List **AdjList; }; uint noOfNodes(Graph *graph) { return graph == 0 ? 0 : graph->NoOfNodes; } uint noOfEdges(Graph *graph) { if (noOfNodes(graph) == 0) return 0; unsigned int cnt = 0, i; for (i = 0; i < noOfNodes(graph); i++) cnt += size(graph->AdjList[i]); return cnt / 2; } Ineffizient! Falls häufig benutzt, dann besser Zähler NoOfEdges im struct Graph G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 74 Elementare Datenstrukturen Kapitel 8 AdjList List List List // Zeiger auf Zeiger auf Listen List struct Graph { uint NoOfNodes; List **AdjList; }; bool hasEdge(Graph *graph, uint Node1, uint Node2) { if (graph == 0) error("no graph"); uint n = noOfNodes(graph); if (Node1 >= n || Node2 >= n) error("invalid node"); return is_elem(Node2, graph->AdjList[Node1]); } void addEdge(Graph *graph, uint Node1, uint Node2) { if (!hasEdge(graph, Node1, Node2)) { append(Node2, graph->AdjList[Node1]); append(Node1, graph->AdjList[Node2]); } } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 75 Kapitel 8 Elementare Datenstrukturen AdjList List List List // Zeiger auf Zeiger auf Listen List struct Graph { uint NoOfNodes; List **AdjList; }; void printGraph(Graph *graph) { if (noOfNodes(graph) == 0) return; unsigned int i, n = noOfNodes(graph); for (i = 0; i < n; i++) { cout << i << ": "; printList(graph->AdjList[i]); } } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 76 Kapitel 8 Elementare Datenstrukturen AdjList List List List // Zeiger auf Zeiger auf Listen List struct Graph { uint NoOfNodes; List **AdjList; }; Graph *clearGraph(Graph *graph) { if (graph == 0) return 0; unsigned int cnt = 0, i; for (i = 0; i < noOfNodes(graph); i++) clearList(graph->AdjList[i]); delete[] graph->AdjList; delete graph; return 0; } G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 77 Kapitel 8 Elementare Datenstrukturen int main() { Graph *graph = createGraph(7); addEdge(graph, 0, 2); addEdge(graph, 1, 2); addEdge(graph, 3, 2); addEdge(graph, 2, 4); addEdge(graph, 4, 6); addEdge(graph, 6, 5); addEdge(graph, 5, 2); addEdge(graph, 2, 3); addEdge(graph, 3, 0); addEdge(graph, 6, 3); addEdge(graph, 0, 1); cout << "nodes: " << noOfNodes(graph) cout << "edges: " << noOfEdges(graph) printGraph(graph); graph = clearGraph(graph); cout << "nodes: " << noOfNodes(graph) cout << "edges: " << noOfEdges(graph) return 0; } 0 6 1 5 2 4 << endl; << endl; << endl; << endl; nodes: edges: 0: 2 1 1: 2 2: 4 3 3: 2 0 4: 6 5: 2 6: 5 3 nodes: edges: 3 7 11 0 0 G. Rudolph: Einführung in die Programmierung ▪ WS 2008/09 78