Programmieren 2 Programmieren 2 (Java) Zusammenfassung v1.0 Kälin Thomas, Abteilung I SS 06 Kälin Thomas, Abt I 1/35 06.07.2006 Programmieren 2 1. 2. Rekursive Methoden........................................................................................................... 6 Iterativ / Rekursiv / Explizit .............................................................................................. 6 2.1. Beispiel Reihen ............................................................................................................... 6 2.2. Zusammenhang: Rekursion / Induktion ........................................................................ 6 3. Designpatterns ................................................................................................................... 6 3.1. Struktur eines Design Patterns ...................................................................................... 6 3.2. Objekt Adapter / Klassenadapter................................................................................... 6 4. Analyse von Algorithmen ................................................................................................... 6 4.1. Laufzeitverhalten ........................................................................................................... 7 4.2. Experimentelle Studien .................................................................................................. 7 4.3. Theoretische Analyse...................................................................................................... 7 4.3.1. Wichtige Funktionen............................................................................................................ 7 4.3.2. Primitive Operationen .......................................................................................................... 7 4.3.3. Pseudocode / Zählen der primitiven Operationen .................................................................. 7 4.3.4. Asymptotische Algorithmus Analyse...................................................................................... 7 4.4. Big-Oh / Big-Omega / Big-Theta.................................................................................... 7 4.4.1. Big-Oh Regeln..................................................................................................................... 8 4.4.2. Beispiele............................................................................................................................. 8 4.4.3. Benötigte Mathematik ......................................................................................................... 8 4.5. Analyse log(n) ................................................................................................................ 8 5. Rekursion ........................................................................................................................... 8 5.1. Endrekursion .................................................................................................................. 8 5.2. Binäre Rekursion ............................................................................................................ 8 6. Stack................................................................................................................................... 8 6.1. Operationen.................................................................................................................... 8 6.2. Exceptions ...................................................................................................................... 8 6.3. Arraybasierter Stack....................................................................................................... 8 7. Queue ................................................................................................................................. 9 7.1. Operationen.................................................................................................................... 9 7.2. Exceptions ...................................................................................................................... 9 7.3. Arraybasierte Queue (Ringspeicher) .............................................................................. 9 8. Linked List .......................................................................................................................... 9 8.1. Operationen.................................................................................................................... 9 8.2. Anwendungen................................................................................................................. 9 9. Array-List............................................................................................................................ 9 9.1. Operationen.................................................................................................................... 9 9.2. Exceptions ...................................................................................................................... 9 9.3. Array-basierte Implementierung ................................................................................. 10 9.4. Array wachsen lassen ................................................................................................... 10 10. Node-List .......................................................................................................................... 10 10.1. Operationen.................................................................................................................. 10 10.2. AddAfter / Remove....................................................................................................... 10 11. Sequence .......................................................................................................................... 10 11.1. Operationen.................................................................................................................. 10 11.2. Implementierung mit Linked List ................................................................................. 10 11.3. Arraybasierte Implementierung................................................................................... 11 12. Iterator ............................................................................................................................. 11 12.1. Operationen.................................................................................................................. 11 12.2. For / Iterator ................................................................................................................ 11 13. XML (eXtended Markup Language).................................................................................. 11 13.1. Beispiel ......................................................................................................................... 11 13.2. Attribute oder Child-Elemente?.................................................................................... 11 13.3. Namen .......................................................................................................................... 11 13.4. CDATA ........................................................................................................................... 11 13.5. Wohlgeformtes XML / Gültigkeit.................................................................................. 11 14. Trees / Bäume .................................................................................................................. 12 Kälin Thomas, Abt I 2/35 06.07.2006 Programmieren 2 14.1. Baum Terminologie ...................................................................................................... 12 14.2. Operationen des Tree ADT............................................................................................ 12 14.3. Traversierungen ........................................................................................................... 12 14.3.1. Preorder ....................................................................................................................... 12 14.3.2. Postorder...................................................................................................................... 12 14.3.3. Inorder (Binäre Bäume)................................................................................................. 12 14.3.4. Euler Tour .................................................................................................................... 12 14.4. Binäre Bäume ............................................................................................................... 12 14.4.1. Eigenschaften echter Binärbäume .................................................................................. 13 14.5. Implementierungen Binärer Bäume............................................................................. 13 14.5.1. Linked List .................................................................................................................... 13 14.5.2. Arraybasiert .................................................................................................................. 13 14.6. Template Method Pattern (Schablonenmuster)........................................................... 13 14.7. Beschreibung von Binär-Bäumen ................................................................................. 13 15. Priority Queue ADT ........................................................................................................... 14 15.1. Operationen.................................................................................................................. 14 15.2. Implementierung mittels unsortierter Liste................................................................. 14 15.2.1. Selection Sort ............................................................................................................... 14 15.3. Implementierung mittels sortierter Liste..................................................................... 14 15.3.1. Insertion Sort................................................................................................................ 14 15.3.2. In-Place Insertion Sort................................................................................................... 14 16. Adaptierbare Priority Queue ............................................................................................ 14 16.1. Operationen.................................................................................................................. 14 16.2. Lokalisierung von Entries ............................................................................................. 14 17. Heap ADT .......................................................................................................................... 14 17.1. Vollständiger Binärbaum .............................................................................................. 14 17.2. Implementierung einer Priority Queue mittels Heap................................................... 15 17.2.1. Einfügen eines Elements................................................................................................ 15 17.2.2. Upheap-Verfahren (Bubbeling)....................................................................................... 15 17.2.3. Entfernen eines Elements .............................................................................................. 15 17.2.4. Downheap-Verfahren .................................................................................................... 15 17.2.5. Heap-Sort ..................................................................................................................... 15 17.3. Implementierung mittels Vector .................................................................................. 15 17.4. Konstruktion eines Heaps............................................................................................. 15 17.4.1. Top-Down .................................................................................................................... 15 17.4.2. Bottom-Up.................................................................................................................... 15 18. Map ADT ........................................................................................................................... 16 18.1. Operationen.................................................................................................................. 16 18.2. Implementierung mittels Linked-List........................................................................... 16 18.3. Sentinel-Trick ............................................................................................................... 16 19. Dictionary ADT.................................................................................................................. 16 19.1. Operationen.................................................................................................................. 16 19.2. Implementierung mittels Linked-List........................................................................... 16 19.3. Implementierung mittels Map...................................................................................... 16 19.4. Binary Search ............................................................................................................... 16 20. Hash-Tabellen .................................................................................................................. 17 20.1. Hash-Funktionen .......................................................................................................... 17 20.1.1. Hash-Funktion .............................................................................................................. 17 20.1.2. Kompressions-Funktion.................................................................................................. 17 20.2. Kollisionsbehandlung ................................................................................................... 17 20.2.1. Geschlossene Adressierung (offenes Hashverfahren) ....................................................... 17 20.2.2. Offene Adressierung (geschlossene Hashverfahren) ........................................................ 17 20.2.3. Doppeltes Hashing (Offene Adressierung)....................................................................... 17 20.3. Performance ................................................................................................................. 17 21. Skip-List............................................................................................................................ 18 21.1. Definition einer Skip List .............................................................................................. 18 21.1.1. Perfekte Skip List .......................................................................................................... 18 Kälin Thomas, Abt I 3/35 06.07.2006 Programmieren 2 21.1.2. Random Skip List .......................................................................................................... 18 21.2. Java-Code ..................................................................................................................... 18 21.2.1. SkipListNode ................................................................................................................. 18 21.2.2. SkipList ........................................................................................................................ 18 21.2.3. Suchen ......................................................................................................................... 18 21.2.4. Einfügen....................................................................................................................... 19 21.2.5. Löschen........................................................................................................................ 19 21.2.6. Zufallsgenerator............................................................................................................ 19 21.3. Implementierung mittels Quad-Nodes......................................................................... 19 21.4. Analysen ....................................................................................................................... 19 21.4.1. Speicherplatz ................................................................................................................ 19 21.4.2. Höhe ............................................................................................................................ 19 21.4.3. Laufzeiten..................................................................................................................... 19 22. Binäre Suchbäume ........................................................................................................... 20 22.1. Höhe des Baums ........................................................................................................... 20 22.2. Implementierung.......................................................................................................... 20 22.3. Suche ............................................................................................................................ 20 22.4. Einfügen........................................................................................................................ 20 22.5. Löschen......................................................................................................................... 20 22.5.1. Symetrischer Nachfolger................................................................................................ 20 22.6. Inorder-Methode .......................................................................................................... 20 23. AVL Baum ......................................................................................................................... 21 23.1. Balance ......................................................................................................................... 21 23.2. Einfügen........................................................................................................................ 21 23.2.1. Cut/Link Restrukturierungs-Algorithmus.......................................................................... 21 23.3. Löschen......................................................................................................................... 21 23.4. Laufzeiten ..................................................................................................................... 21 24. Merge Sort ........................................................................................................................ 22 24.1. Code.............................................................................................................................. 22 25. Quick-Sort ........................................................................................................................ 22 25.1. Laufzeit ......................................................................................................................... 22 25.2. Beispiele ....................................................................................................................... 22 26. Bucket-Sort ...................................................................................................................... 23 27. Radix-Sort ........................................................................................................................ 23 27.1. Beispiel: Sortierung von 3-Bit Integers........................................................................ 23 28. Untere Grenze der Sortierung .......................................................................................... 23 29. Sets................................................................................................................................... 23 29.1. Grundoperationen ........................................................................................................ 23 29.1.1. Vereinigung (Union) ...................................................................................................... 23 29.1.2. Durchschnitt (Intersection) ............................................................................................ 23 29.1.3. Differenz (Subtraction) .................................................................................................. 23 29.2. Operationen.................................................................................................................. 24 29.3. Set-Varianten in Java ................................................................................................... 24 30. Automaten ........................................................................................................................ 24 30.1. Akzeptor ....................................................................................................................... 24 30.2. Transduktor .................................................................................................................. 24 30.2.1. Mealy / Moore – Automaten........................................................................................... 24 31. Pattern Matching.............................................................................................................. 24 31.1. Allgemeines .................................................................................................................. 24 31.2. Brute-Force................................................................................................................... 25 31.2.1. Worst-Case Beispiel....................................................................................................... 25 31.3. Boyer-Moore ................................................................................................................. 25 31.3.1. Last-Occurrence Funktion .............................................................................................. 25 31.3.2. Berechnung der Verschiebung ....................................................................................... 25 31.3.3. Analyse ........................................................................................................................ 26 31.3.4. Zusammenfassung ........................................................................................................ 26 31.4. KMP-Algorithmus.......................................................................................................... 26 Kälin Thomas, Abt I 4/35 06.07.2006 Programmieren 2 31.4.1. KMP-Fehlfunktion .......................................................................................................... 26 31.4.2. Beispiel......................................................................................................................... 26 31.4.3. Analyse ........................................................................................................................ 26 31.4.4. Zusammenfassung ........................................................................................................ 26 32. REGEX............................................................................................................................... 26 32.1. Metazeichen.................................................................................................................. 26 32.2. Tabellen ........................................................................................................................ 27 32.3. Quantoren..................................................................................................................... 27 32.3.1. Beispiele....................................................................................................................... 27 32.4. Java Beispiel ................................................................................................................. 27 33. Graphen ............................................................................................................................ 27 33.1. Begriffe ......................................................................................................................... 27 33.2. Operationen.................................................................................................................. 28 33.3. Kanten-Listen Struktur................................................................................................. 28 33.4. Adjadenz-Listen Struktur ............................................................................................. 28 33.5. Adjazenz-Matrix Struktur ............................................................................................. 29 33.6. Performance ................................................................................................................. 29 33.7. Adjazenz-Matrix ........................................................................................................... 29 34. Tiefensuche (Depth-First Search: DFS)............................................................................ 29 34.1. DFS-Algorithmus .......................................................................................................... 29 34.2. Spezialisierung ............................................................................................................. 30 34.2.1. Pfade finden ................................................................................................................. 30 34.2.2. Zyklen finden ................................................................................................................ 30 34.3. Gerichtete Tiefensuche................................................................................................. 30 35. Breitensuche (Breadth-First Search: BFS) ....................................................................... 30 35.1. BFS-Algorithmus........................................................................................................... 30 35.2. Applikationen ............................................................................................................... 31 36. Gerichteter Graph / Digraph ............................................................................................ 31 36.1. Transitiver Abschluss.................................................................................................... 31 36.2. Floyd-Warshall’s Algorithmus ...................................................................................... 31 36.2.1. Beispiel......................................................................................................................... 31 36.3. Topologische Sortierung............................................................................................... 31 37. Shortest Path / Kürzester Pfad ........................................................................................ 32 37.1. Dijkstra’s Algorithmus .................................................................................................. 32 37.1.1. Beispiel......................................................................................................................... 32 37.1.2. Probleme ...................................................................................................................... 32 37.2. Bellman-Ford Algorithmus............................................................................................ 32 37.3. DAG-basierter Algorithmus .......................................................................................... 33 38. Minimum Spanning Tree .................................................................................................. 33 38.1. Kruskal’s Algorithmus................................................................................................... 33 38.2. Prim-Jarnik’s Algorithmus ............................................................................................ 34 38.3. Baruvka’s Algorithmus ................................................................................................. 34 39. XML: Document Type Definition (DTD) ............................................................................ 34 39.1. Beispiel (interne DTD) .................................................................................................. 34 39.2. Beispiel (externe DTD) ................................................................................................. 35 39.3. Spezielles...................................................................................................................... 35 Kälin Thomas, Abt I 5/35 06.07.2006 Programmieren 2 1. Rekursive Methoden Werte der aktuellen Parameter, für die kein rekursiver Aufruf ausgeführt wird, werden als „base cases“ oder „Verankerung“ bezeichnet. Jede mögliche rekursive Aufrufkette muss einen base case erreichen. Jeder rekursive Aufruf sollte so definiert sein, dass er die Ausführung in Richtung Verankerung bewegt. Beispiel: Fakultät public static int factor(int n) { if (n == 0) { return 1; //Verankerung } else { return n * factor(n-1); } } 2. Iterativ / Rekursiv / Explizit Iterativ lin(n)=sum(i=1;n;i) sqr(n)=sum(i=1;n;i^2) Rekursiv lin(1)=1 lin(n)=n+lin(n-1) sqr(1)=1 sqr(n)=n^2+sum(n-1) Explizit lin(n)=n*(n+1) / 2 sqr(n)=n*(n+1)*(2n+1)/6 2.1. Beispiel Reihen Reihe: 1, Iterativ: Rekursiv: Explizit: 5, an a1 an 9, 13, 17, … = 1 + sum(i=1; n-1; 4) = 1; an = 4 + an-1; = 4n-3 Reihe: 1, Iterativ: Rekursiv: Explizit: 6, sn s1 sn 15, 28, … (Summe von Reihe 1!) = sum(i=1; n; 4n-3); = 1; sn = 4n-3+sn-1; = n*(a1+an)/2 = n*(1+4n-3)/2; //Explizite Formel verw. //Explizite Formel verw. //Explizite Formel verw. 2.2. Zusammenhang: Rekursion / Induktion Die Verankerung entspricht der Abbruchbedingung in der Rekursion und der Induktionsschritt entspricht einem rekursiven Aufruf. Man beweist n+1 durch n und den Induktionsschritt. Dies ist in der Rekursion ebenfalls gegeben durch die Berechnung von n+1 aus n. 3. Designpatterns Ein Designpattern ist ein Lösungsansatz eines „typischen“ Software Designproblems, welches in sehr unterschiedlichen Situationen verwendet werden kann. 3.1. Struktur eines Design Patterns Mustername: Benennung des Patterns Problemabschnitt: Beschreibung, wann das Pattern anzuwenden ist Lösungsabschnitt: Beschreibung der Elemente, aus denen das Pattern besteht. Implementierung, nur ein generelles Konzept. Konsequenzenabschnitt: Liste der Vor- und Nachteile bei der Anwendung des Patterns Keine 3.2. Objekt Adapter / Klassenadapter 4. Analyse von Algorithmen Ein Algorithmus ist ein Schritt-für-Schritt Vorgehen zum Lösen eines Problems mit endlichem Zeitaufwand. Kann gut mit einem Kochrezept verglichen weden. Kälin Thomas, Abt I 6/35 06.07.2006 Programmieren 2 4.1. Laufzeitverhalten Die meisten Algorithmen transferieren Eingaben in Ausgaben. Die Laufzeit eines Algorithmus nimmt typischerweise mit dem Umfang der Eingabe zu. Wir konzentrieren uns bei der Analyse des Laufzeitverhaltens auf den „Worst Case“, da dies vergleichsweise einfach zu analysieren ist. 4.2. Experimentelle Studien Wir schreiben ein Programm, welches den Algorithmus implementiert und lassen dieses mit unterschiedlichen, repräsentativen Eingaben laufen. Dabei zeichnen wir die Laufzeiten beispielsweise mit System.currentTimeMillis() auf und vergleichen die Resultate. Problem: Der Algorithmus muss vollständig Implementiert werden (Zeitaufwand!), die Auswahl der Eingaben ist sehr kritisch und kann zu falschen Schlüssen führen. Natürlich muss immer auf derselben HW und SW getestet werden. 4.3. Theoretische Analyse Hierbei verwendet man einen „high-level“ Beschreibung des Algorithmus (Pseudocode), keine Implementierung. Man sucht eine funktionale Beschreibung (als Funktion der Eingabe). Hierdurch werden alle möglichen Eingaben berücksichtig, ausserdem ist die Methode unabhängig von HW und SW. 4.3.1. Wichtige Funktionen konstant < log < linear < n-log < quadratisch < kubisch < exponentiell 1 < log(n) < n < n*log(n) < n2 < n3 < 2n 4.3.2. Primitive Operationen Dies sind Basis-Operationen, die von einem Algorithmus ausgeführt werden. Sie sind im Pseudocode identifizierbar und unabhängig von der Programmiersprache. Eine Exakte Definition ist eher unwichtig. im RAM (Random Access Machine) Modell benötigen Sie eine typische, konstante Zeit. 4.3.3. Pseudocode / Zählen der primitiven Operationen Algorithm arrayMax(A,n) Input array A of n integers Output maximum element of A currentMax <- A[0] 2 //Zugriff=1, Zuweisung=1 for i<-1 to n-1 do { 1+2n //Zuweisung=1,Vergleichen=n,++=n if (A[i] > currentMax) then { 2(n-1) currentMax <- A[i] } 2(n-1) //Im dümmsten Fall! increment counter i; 2(n-1) //i = i+1!! } return currentMax 1 -> 2 + 1+2n + 2(n-1) + 2(n-1) + 2(n-1) + 1 = 8n-2 4.3.4. Asymptotische Algorithmus Analyse Darunter versteht man das bestimmen des Laufzeitverhaltens in der big-Oh Notation. Dazu suchen wir zuerst das worst-case Verhalten als Funktion primitiver Operationen (8n-2). Diese Funktion beschreiben wir in der big-Oh Notation (arrayMay läuft in O(n) Zeit). 4.4. Big-Oh / Big-Omega / Big-Theta f(n) ist O(g(n)) falls ein c > 0 und ein no >= 1 existiert, so dass: f(n) <= c*g(n) für n>=n0 f(n) ist Ω(g(n)) falls ein c > 0 und ein no >= 1 existiert, so dass: f(n) >= c*g(n) für n>=n0 f(n) ist Θ(g(n)) falls ein c’>0 und c’’0 und ein no >= 1 existiert, so dass: c’*g(n) <= f(n) <= c’’*g(n) n>=n0 Die big-Oh Notation gestattet die Angabe einer oberen Grenze für die Wachstumsfunktion. Die Aussage „f(n) is O(g(n))“ bedeutet: „Die Wachstumsfunktion f(n) wird oben begrenzt durch g(n)“. f(n) ist Ω(g(n)) falls f(n) asymptotisch grösser oder gleich g(n) ist (unten begrenzt durch). f(n) ist Θ(g(n)) falls f(n) asymptotisch gleich wie g(n) ist (oben und unten begrenzt). Kälin Thomas, Abt I 7/35 06.07.2006 Programmieren 2 4.4.1. Big-Oh Regeln Lassen Sie alle tieferen Potenzen weg Lassen Sie alle Konstanten weg Lassen Sie den Koeffizienten der höchsten Potenz weg 4.4.2. Beispiele 7n-2 ist O(n) 3n3+20n2+5 ist O(n3) 3*log(n) + 5 ist O(log(n)) 5n2 ist Ω(n2) 5n2 ist Θ(n2) 4.4.3. logb(x*y) logb(x/y) logb(x^a) logb(a) > > > > > c=7, no=1 c=4, no=21 c=8, no=2 c=5, no=1 c’=5,c’’=5, no=1 Benötigte Mathematik = = = = logb(x) + logb(y) logb(x) – logb(y) a * logb(x) logx(a) / logx(b) a(b+c) a(b-c) abc b bc = = = = = ab * ac ab / ac (ab)c aloga(b) a*c*loga(b) 4.5. Analyse log(n) In einem Intervall der Länge n werden n Werte abgespeichert. Das Intervall wird halbiert. Es stehen jetzt nur noch n/2 Werte zur Verfügung. Jetzt wird ein Teilintervall nochmals halbiert, somit stehen nur noch n/4 Werte zur Verfügung. Abbruch, wenn nur noch ein Wert vorhanden ist. Dieses Verhalten ist typisch für Logarithmen mit O(log(n))! 5. Rekursion Überprüfung der Verankerung (Base Cases). Jede rekursive Aufrufkette muss schliesslich zu einer Verankerung führen. Jeder rekursive Aufruf wird typischerweise in Richtung „base case“ führen. Beim Programmieren rekursiver Methoden ist es wichtig, die Methoden so zu definieren, dass die Rekursion einfach wird. 5.1. Endrekursion Endrekursion tritt dann auf, wenn eine linear rekursive Methode als letzten Schritt den rekursiven Aufruf ausführt. 5.2. Binäre Rekursion Binäre Rekursion tritt immer dann auf, wenn zwei rekursive Aufrufe in allen nicht terminalen Aufrufen ausgeführt werden (Bsp: Fibonacci). Diese Algorithmen haben typischerweise ein Verhalten von O(2n)! 6. Stack Ein Stack speichert beliebige Objekte. Einfügen und Löschen erfolgt gemäss dem LIFO-Schema. Verwendung bei: History im Webbrowser, Undo-Funktion, Methodenaufrufe in der JVM 6.1. Operationen push(Object): Ein Element einfügen Object pop(): Entfernen und Zurückgeben des obersten Elementes. Object top(): Liefert das oberste Element, ohne dieses zu entfernen. int size(): Liefert die Anzahl gespeicherter Elemente bool isEmpty(): Zeigt an, ob Elemente im Stack vorhanden sind. 6.2. Exceptions Bei einem leeren Stack wird EmptyStackException geworfen. 6.3. Arraybasierter Stack Elemente werden von links nach rechts in das Array eingefügt. In einer Variablen wird der Index des obersten Elements abgespeichert. Für den Fall, dass das Array voll ist, muss eine „FullStackException“ programmiert werden (Implementierungsabhängige Exception!). Jede Operation benötigt O(1) Zeit. Kälin Thomas, Abt I 8/35 06.07.2006 Programmieren 2 7. Queue Einfügen und Löschen entspricht dem FIFO-Prinzip. Das Einfügen erfolgt am Ende, das Entfernen am Anfang. In Java ist nur ein Interface für „Queue“ vorhanden. Verwendung bei: Wartelisten, Zugriff auf gemeinsame Ressourcen, Multiprogramming 7.1. Operationen enqueue(Object): Einfügen eines Elementes am Ende der Queue Object dequeue(): Entfernen & zurückgeben d. Elements am Anfang der Queue Object front(): Liefert das erste Element, ohne es zu entfernen int size(): Liefert die Anzahl gespeicherter Elemente. bool isEmpty(): Zeigt an, ob noch Elemente gespeichert sind 7.2. Exceptions Entfernen eines Elements aus einer leeren Queue führt zu einer EmptyQueueException. 7.3. Arraybasierte Queue (Ringspeicher) Wir nutzen ein Array auf zirkuläre Art und Weise. Zwei Variablen merken sich den Anfang und das Ende der Queue. int size(): (n-f+r) % n => (10-3+5) % 10 = 2 Elemente vorhanden bool isEmpty(): (f==r) 8. Linked List Eine einfach verkettete Liste besteht aus einer Sequenz von Knoten (Nodes). Jeder Knoten besitzt ein Element (Inhalt) und einen Link zum nächsten Knoten. Der letzte Knoten zeigt auf null. Alternative: Spezielle Header und Trailer-Elemente definieren, analog zur Node-List. 8.1. Operationen addFirst(Object): Element am Anfang der Liste einfügen addLast(Object): Element am Schluss der Liste einfügen Object removeFirst(): Element am Anfang der Liste auslesen Object removeLast(): Element am Ende der Liste auslesen 8.2. Anwendungen Stack: Mit Hilfe einer einfach verketteten Liste kann ein Stack implementiert werden. Das top()Element entspricht hierbei dem ersten Knoten der Liste. Speicherverbrauch geht mit O(n), jede Stackoperation benötigt O(1) Zeit. Queue: Kann auch mittels einer einfach verketten Liste aufgebaut werden. Das erste Element der Queue (Kopf) entspricht dem ersten Knoten der Liste. Das Endelement der Queue entspricht dem letzten Knoten der Liste. Speicherbedarf ist O(n), jede Operation benötigt O(1) Zeit. 9. Array-List Der Array-List ADT erweitert den Arraybegriff indem beliebige Objekte als Elemente abgespeichert werden dürfen. Ein Element kann eingefügt, gelöscht oder zugegriffen werden, indem sein Index angegeben wird. 9.1. Operationen Element get(int i): Liefert das Element i, ohne es zu entfernen. (O(1)). Element set(int i, Element e): Ersetzt das Element an der Stelle i durch e und gibt das alte zurück. (O(1)). add(int i, Element e): Fügt ein neues Element an der Stelle i ein, ohne das bestehende zu überschreiben. (O(n) wegen Suche). Element remove(int i): Entferne i und liefere das Element zurück. (O(n)) int size(): Grösse der Array-List (O(1)) boolean isEmpty(): Container leer? (O(1)) 9.2. Exceptions Bei Zugriff ausserhalb des erlaubten Bereichs wird eine Exception geworfen. Kälin Thomas, Abt I 9/35 06.07.2006 Programmieren 2 9.3. Array-basierte Implementierung Wir verwenden ein Array V der Grösse N. Eine Variable n speichert die Grösse des Vektors. Operation get(i) kann als O(1) Zeit-Operation implementiert werden: V[i]. Bei add(i,e) müssen wir zuerst Platz schaffen und die Elemente ab Position i nach hinten verschieben. Im schlimmsten Fall (i=0) benötigen wir O(n) Zeit. In der Operation remove(i) müssen wir das entstehende Loch auffüllen, und die Elemente nach vorne verschieben. Wir benötigen wieder O(n) Zeit im schlimmsten Fall. Bei zirkulärer Nutzung benötigen Einfügen und Löschen am Ende / Beginn des Arrays nur O(1) Zeit. 9.4. Array wachsen lassen Wir können das Werfen einer Exception beim Einfügen eliminieren, indem wir das Array wachsen lassen. Dazu gibt es zwei Strategien: Inkrementelle Strategie: Wir erhöhen die Arraygrösse um eine Konstante c. -> O(n) Verdoppelungsstrategie: Wir verdoppeln jedes Mal das Array. -> O(1) 10. Node-List Die Node-List ist stark verwandt mit der Linked-List. Der Unterschied ist, dass die Node-List doppelt verkettet ist, also jeder Node Referenzen auf das vorherige (prev) und nachfolgende (next) Element besitzt. 10.1. Operationen int size(), bool isEmpty(): Generische Methoden Node first(), Node last(): Zugriff auf erster/letzter Node setPrev(Node), setNext(Node): Vorheriger/Nächster Node setzen Node getPrev(), Node getNext(): Vorheriger/Nächster Node abrufen setElement(e): Inhalt vom aktuellen Node mit Element e füllen e getElement(): Inhalt vom aktuellen Node abrufen addBefore(Node,e), addAfter(Node,e): Element e vor/nach Node einfügen. addFirst(e), addLast(e): Element am Anfang/Ende der Node-List einfügen. remove(Node): Element aus der Node-List entfernen 10.2. AddAfter / Remove Node addAfter(p,e) { Node v = new Node(); v.setElement(e); v.setPrev(p); v.setNext(p.getNext()); p.getNext().setPrev(v); p.setNext(v); return v; } 11. Object remove(p) { Object tmp = p.getElement(); p.getPrev().setNext(p.getNext()); p.getNext().setPrev(p.getPrev()); p.setPrev(null); p.setNext(null); return tmp; } Sequence Fasst die Array- und Node-List zusammen. Zugriff möglich über Index oder Position. Generischer Ersatz für Stack, Queue, Array- oder Node-List. 11.1. Operationen int size(), bool isEmpty(): Generische Methoden //Operationen der Array-List (e get(i), set(i,e), …) //Operationen der Node-List (Node first(), Node last(), Node getPrev(), …) //Bridge-Operationen: Node atIndex(i), i indexOf(Node) 11.2. Implementierung mit Linked List Grafik der Implementierung siehe „Node-List“ Knoten implementieren Position und speichern Element, Link auf Vorgänger / Nachfolger. Es existieren je ein spezieller Header und Trailer-Knoten. Index-basierte Zugriffe suchen (abzählen der Schritte) immer vom Anfang / Ende der Liste und sind somit linear in der Zeit (O(n)). Kälin Thomas, Abt I 10/35 06.07.2006 Programmieren 2 11.3. Arraybasierte Implementierung Jeder Arrayeintrag enthält eine Referenz auf eine Position. Diese enthalten den Index und eine Referenz auf das Element. Variablen F und L speichern die erste, bzw. letzte belegte Position. 12. Iterator Ein Iterator abstrahiert den Prozess des Scannens über eine Sammlung von Elementen. Typischerweise wird ein Iterator zusammen mit einer anderen ADT verwendet. Dabei wird das betreffende ADT durch eine Methode „Iterator iterator()“ ergänzt. 12.1. Operationen bool hasNext(): Noch weitere Elemente vorhanden? e next(): Lieft das nächste Element 12.2. For / Iterator For wird bei Arrays verwendet, ein Iterator für komplexe Datenstrukturen (Liste). Nachfolgend noch ein Beispiel einer „For-Each“-Schleife: String arrValues[] = {„Das“, „ist“, „Mist.“}; for (String strValue : arrValues) { //strValue enthält der Reihe nach alle Elemente von arrValues. } 13. XML (eXtended Markup Language) 13.1. Beispiel <?xml version=“1.0“ encoding“ASCII“ standalone=“no“ ?> <!-- Kommentar: XML Dokument muss immer mit obiger Zeile beginnen! --> <root> <example>Das ist ein Beispiel</example> <entitiy>&quot; - &apos; - &lt; - &gt; - &amp;</entitity> <!-- Obige Zeile gib folgendes aus: " – ' - < - > - & --> <attribute value=“Test“>Attribute</attribute> <spezialfall value=“LeererTag“ /> </root> 13.2. Attribute oder Child-Elemente? Es gibt keine grundsätzliche Regel, wann Attribute und wann Child-Elemente verwendet werden. Faustregel: Daten in Elemente, Meta-Daten (ID, URL) in Attribute 13.3. Namen Erlaubt sind: Buchstaben, Zahlen, _-.:. Ausserdem muss Buchstabe / Unterstrich am Anfang sein! 13.4. CDATA Spezielle Kennzeichnung für komplexe Teile. Es können alle möglichen Zeichen verwendet werden! <![CDATA[ for (i=0; i<2; i++) {} ]]> 13.5. Wohlgeformtes XML / Gültigkeit Ein Dokument ist dann wohlgeformt, wenn es der XML-Spezifikation entspricht. Gültigkeit entspricht der DTD (Document Type Definition), kann mit XML-Parser überprüft werden. Das Dokument besitzt nur ein Root Element (Tag auf der obersten Ebene). Zu allen öffnenden Tags existieren schliessende Tags. Die Elemente sind korrekt verschachtelt (schematisch: <a><b>Text</b></a>). Gross-/ Kleinschreibung wird beachtet (Nicht erlaubt: <Anfang>…</anfang>) Attributwerte stehen zwischen Anführungszeichen (<a b="Wert“/> ist erlaubt, <a b=Wert/> nicht). Entities müssen vor ihrem Einsatz deklariert werden (Entities: Texte, welche mehrfach verwendet, einmal definiert werden). Kälin Thomas, Abt I 11/35 06.07.2006 Programmieren 2 14. Trees / Bäume In der Informatik repräsentieren Bäume abstrakte, hierarchische Datenstrukturen. 14.1. Baum Terminologie Wurzel: Knoten ohne Elternknoten (A) Interner Knoten: Knoten mit mindestens einem Kind (A, B, F, C) Externer Knoten: Auch Blatt genannt. Knoten ohne Kind. (E, I, J, K, G, H, D) Vorgänger: Knoten einer höheren Ebene (B ist Vorgänger von E) Nachfolger. Knoten einer tieferen Ebene (E ist Nachfolger / Kind von B) Tiefe eines Knotens: Anzahl Vorgänger (Tiefe A:0, B:1, E:2, K:3) Höhe eines Knotens: Maximale Anzahl Nachfolger bis zu einem Blatt hinunter (Höhe B: 2) Höhe des Baums: Höhe der Wurzel (Höhe A: 3) Subtree: Baum aus einem Knoten und seinen Nachfolgern 14.2. Operationen des Tree ADT int size(): Anzahl Knoten des Baums bool isEmpty(): Knoten enthalten? Iterator elements(): Iterator über die Elemente (Inhalte) Iterator positions(): Iterator über die Knoten Knoten root(): Gibt den Wurzel-Knoten zurück Knoten parent(Knoten): Gibt den Eltern-Knoten eines Knotens zurück Iterator children(Knoten): Iterator über alle Nachfolger eines Knotens bool isInternal(Knoten): Ist dieser Knoten Intern? bool isExternal(Knoten): Ist dieser Knoten ein Blatt? bool isRoot(Knoten): Ist dieser Knoten die Wurzel? E replace(Knoten,E): Ersetze Element eines Knotens. Gibt altes E zurück! 14.3. Traversierungen 14.3.1. Preorder Bei der Preorder Traversierung wird ein Knoten VOR seinen Nachfolgern besucht. Anwedung: Drucken eines strukturierten Dokuments. Beispiel: A, B, E, F, I, J, K, C, G, H, D (Kursiv: Blätter) 14.3.2. Postorder In einer Postorder Traversierung wird ein Knoten NACH seinen Nachfolgern besucht. Anwendung: Angabe des verbrauchten Speichers in einem Verzeichnis. Beispiel: E, I, J, K, F, B, G, H, C, D, A (Kursiv: Blätter) 14.3.3. Inorder (Binäre Bäume) In einer Inorder Traversierung wird ein Knoten NACH seinem linken Subtree und VOR seinem rechten Subtree besucht. Funktioniert NUR bei binären Bäumen. Anwendung: Darstellung von binären Bäumen. Beispiel OHNE Blatt J und D: E, B, I, F, K, A, G, C, H („Von links nach rechts gelesen“). 14.3.4. Euler Tour Generische Traversierung von BINÄREN Bäumen. Jeder Knoten wird 3 mal besucht: Einmal von links (preorder), einmal von unten (inorder), einmal von rechts (postorder). Beispiel OHNE Blatt J und D: LA, LB, LE, UE, UB, LF, LI, UF, LK, UK, RK, RF, RB, UA, LC, LG, UG, … 14.4. Binäre Bäume Ein binärer Baum ist ein Baum mit folgenden Eigenschaften: Jeder Knoten besitzt höchstens zwei Nachfolger o Mit EXAKT zwei Nachfolgern spricht man von einem ECHTEN Binärbaum Die Kinder eines Knotens sind ein geordnetes Paar (linkes Kind / rechtes Kind) Anwendung: o Arithmetische Ausdrücke (interne Knoten sind Operatoren, externe Knoten die Operanden) o Entscheidungsprozesse (interne Knoten sind Fragen, externe Knoten die Entscheide „Ja“/“Nein“) Kälin Thomas, Abt I 12/35 06.07.2006 Programmieren 2 14.4.1. n e i h = = = = Eigenschaften echter Binärbäume Knoten Externe Knoten Interne Knoten Höhe e n h h e <= 2h h >= log2(e) h >= log2(n+1)-1 = i+1 = 2e-1 <= i <= (n-1)/2 14.5. Implementierungen Binärer Bäume 14.5.1. Linked List Jeder Knoten enthält einen Verweis auf das Element (Position ADT), den Elternknoten und die beiden Kindknoten. Aus diesem Knoten kann einfach ein allgemeines Speicherverfahren für normale Bäume abgeleitet werden, indem die beiden Verweise auf die Kindkonten durch einen Verweis auf eine Sequenz von Kindern ersetzt wird. 14.5.2. Arraybasiert Die Knoten werden in einem Array gespeichert. Index 0 wird dabei leer gelassen! Root erhält Index 1. Linkes Kind = 2 * Index des Eltern Elements Rechtes Kind = 2 * Index des Eltern Elements + 1 14.6. Template Method Pattern (Schablonenmuster) Absicht: Definieren eines Rumpfes (Skeletts) eines Algorithmus, wobei einige Schritte erst in Subklassen spezifiziert werden. Das Template Method Muster lässt Subklassen Teile des Algorithmus verfeinern, ohne die Struktur des Algorithmus zu verändern. Problem: Zwei unterschiedliche Komponenten besitzen eine grosse Ähnlichkeit, haben aber kein Interface und keine Implementierung gemeinsam. Falls eine Änderung nötig wird, müsste der Aufwand mehrfach gemacht werden. Lösung: Der Komponentendesigner muss entscheiden, welche Teile des Algorithmus unveränderlich sind und welche angepasst werden können. Die gemeinsamen Teile werden in einer abstrakten Klasse implementiert, die variablen Teile in einer Default-Implementierung oder gar nicht festgehalten. 14.7. Beschreibung von Binär-Bäumen Nur ein Knotentyp. Nachteil: Innere und äussere Knoten können nur mittels „links“ / „rechts“ Abfragen unterschieden werden. Zwei Knotentypen. Nachteil: Baum wird nicht optimal modelliert. Safe Composite Pattern: Ein Knoteninterface. Zwei implementierende Knoten (Blatt und innere Knoten). Gestattet Überprüfung zur Kompilizierzeit. Transparent Composite Pattern: Ein abstrakter Basisknoten. Zwei implementierende Knoten (Blatt und innere Knoten). Muss mit Exceptions arbeiten. Kälin Thomas, Abt I 13/35 06.07.2006 Programmieren 2 15. Priority Queue ADT Eine Priority Queue speichert eine Collection von Entries. Jede Entry besteht aus einem Schlüssel-Wert Paar, wobei derselbe Schlüssel mehrmals vorkommen darf. Der Schlüssel kann ein beliebiges Objekt sein, auf welchem eine Ordnungsrelation definiert ist. 15.1. Operationen void insert(k,v): Neuen Entry mit Schlüssel k und Wert v hinzufügen E removeMin(): Liefert & entfernt die Entry mit kleinstem Schlüssel E min(): Liefert Entry mit kleinstem Schlüssel ohne diese zu entfernen int size(): Anzahl Elemente der PQ bool isEmpty(): Elemente in der PQ vorhanden? 15.2. Implementierung mittels unsortierter Liste Einfügen benötigt O(1) Zeit, da am Ende eingefügt wird. Abfragen benötigt O(n) Zeit, da die ganze Sequenz durchlaufen werden muss, um das Minimum zu finden. 15.2.1. Selection Sort Wir wollen die Elemente einer unsortierten Liste sortiert ausgeben. Dabei müssen wir bei jeder removeMin()-Operation den kleinsten Schlüssel suchen. Deshalb ist die Laufzeit O(n2)! 15.3. Implementierung mittels sortierter Liste Einfügen benötigt O(n) Zeit, da die Einfügestelle gesucht werden muss. Abfragen benötigten dafür O(1) Zeit, da der kleinste Schlüssel immer am Anfang der Liste ist. 15.3.1. Insertion Sort Wir wollen Elemente in eine sortierte Liste einfügen. Bei jedem insert()-Aufruf muss die komplette PQ durchsucht werden, um die entsprechende Position zu finden. Laufzeit ist deshalb auch hier O(n2)! 15.3.2. In-Place Insertion Sort Hierbei werden zuerst alle Elemente in die PQ unsortiert eingefügt und erst anschliessend sortiert, indem die Elemente direkt in der PQ vertauscht werden, bis die Ordnungsrelation stimmt. Auch O(n2)! 16. Adaptierbare Priority Queue Die adaptierbare PQ erweitert die normale PQ um die Fähigkeit, spezifische auf Entries der PQ zuzugreifen. Bei der klassischen PQ ist nur Zugriff auf kleinsten Entry (min()) möglich. 16.1. Operationen E remove(E): Entferne und liefere Entry E k replaceKey(E,k): Ersetzt Key einer Entry. Gibt den alten Key zurück. v replaceValue(E,v): Ersetzt Value einer Entry. Gibt alten Value zurück. 16.2. Lokalisierung von Entries Um aufs spezifische Entries zugreifen zu können, muss jedes Mal die gesamte Datenstruktur abgesucht werden. 17. Heap ADT Ein Heap ist ein Binärbaum, der in seinen Knoten Schlüssel speichert. Ausserdem muss die Bedingung erfüllt sein, dass der Schlüssel eines Kindes immer grösser ist, als der Schlüssel seines Elternknotens. 17.1. Vollständiger Binärbaum Ein Heap ist ein vollständiger Binärbaum. Das heisst, dass neue Knoten immer von links nach rechts angehängt werden und angefangene Zeilen zuerst gefüllt werden, bevor eine neue begonnen wird. Daraus folgt, dass auf den Tiefen i=0..h-1 immer alle Knoten (2^i) vorhanden sind. Es gibt maximal einen Knoten mit nur einem Kind, das ein linkes Kind sein muss. Der letzte Knoten eines vollständigen Binärbaums ist somit der am weitesten rechts stehende Knoten auf Stufe (Tiefe) h. Höhe des Heaps: h = ⎣ lb(n) ⎦. (n = Anzahl Knoten, ⎣ x ⎦ = floor = abrunden). Kälin Thomas, Abt I 14/35 06.07.2006 Programmieren 2 17.2. Implementierung einer Priority Queue mittels Heap Mittels eines Heaps können wir eine Priority Queue implementieren. Wir speichern eine Entry (Key, Value) in jedem Knoten des Heaps. Wegen der Bedingung, dass der tiefste Key immer im RootElement stehen muss, kann mittels min() / removeMin() in O(1) zugegriffen werden. 17.2.1. Einfügen eines Elements Beim Einfügen muss zuerst der „letzte Knoten“ gefunden werden. Dort wird das neue Element eingefügt. Anschliessend muss eventuell die Heap-Eigenschaft mittels „Upheap“ hergestellt werden. 17.2.2. Upheap-Verfahren (Bubbeling) Nach dem Einfügen eines neuen Schlüssels k könnte die Heap-Ordnungseigenschaft verletzt sein. Der Upheap-Algorithmus stellt die Ordnungseigenschaft wieder her, indem der neue Schlüssel k entlang einem Pfad vom Einfügeknoten Richtung Wurzel mit dem jeweils darüberliegenden Knoten vertauscht wird. Upheap wird beendet, sobald der neue Schlüssel k entweder die Wurzel erreicht hat, oder falls die Ordnungsrelation hergestellt ist. Wegen der Höhe des Heaps benötigt das Verfahren O(log(n))-Zeit. 17.2.3. Entfernen eines Elements Beim Entfernen mittels removeMin() wird immer die Wurzel entfernt. Diese wird anschliessend durch den Schlüssel des letzten Knotens ersetzt (vollständiger Binärbaum muss erhalten bleiben!). Folglich ist natürlich die Ordnungsrelation verletzt, welche mittels „Downheap“ hergestellt werden muss. 17.2.4. Downheap-Verfahren Beim Ersetzen des Schlüssels in der Wurzel wird meistens die Heap-Eigenschaft verletzt. Mittels Downlheap-Verfahren wird die Ordnungeigenschaft wieder hergestellt, indem der Schlüssel k entlang einem Pfad in Richtung Äste vertauscht wird. Downheap endet, wenn der Schlüssel in einem Blatt landet oder die Ordnungsrelation hergestellt ist. Wegen der Höhe des Heaps benötigt das Verfahren O(log(n))-Zeit. 17.2.5. Heap-Sort Mit Hilfe einer Heap-basierten Priority Queue kann eine Sequenz von n Elementen innert O(n*log(n))Zeit sortiert werden, da insert() und removeMin() je O(log(n))-Zeit benötigen. 17.3. Implementierung mittels Vector Ein Heap mit n Knoten kann mit einem Vector der Länge n+1 realisiert werden. Linkes Kind wird bei Position 2*i gespeichert, rechtes Kind bei 2*i+1. Insert() entspricht dem Einfügen beim Position n+1. removeMin() entspricht dem Entfernen bei Position 1. Sortierung lässt sich mittels üblichen Verfahren (In-Place Sort) realisieren. 17.4. Konstruktion eines Heaps 17.4.1. Top-Down Beim Top-Down Verfahren wird der Heap von oben (Wurzel) nach unten (Blätter) aufgebaut. Dabei wird aus einer Sequenz von Schlüsseln immer der niedrigste Wert ausgewählt und damit ein vollständiger Binärbaum aufgebaut. Wegen der Suche nach dem niedrigsten Wert benötigt das Verfahren O(n2) Zeit. 17.4.2. Bottom-Up Das Bottom-Up Verfahren wird in mehreren Phasen durchgeführt, wobei der Heap von unten (Blätter) nach oben (Wurzel) aufgebaut wird. Es wird O(n) Zeit benötigt. 1) Hälfte der Elementen (abgerundet) auswählen und platzieren. 2) Hälfte der Elemente (abgerundet) auswählen und oberhalb der Elemente aus Schritt 1 platzieren. 3) Bei den in Schritt 2 entstandenen Binärbäumen muss nun mittels Downheap-Verfahren die HeapEigenschaft wiederhergestellt werden. Schritte 2 und 3 wiederholen, bis keine Elemente mehr vorhanden sind. Kälin Thomas, Abt I 15/35 06.07.2006 Programmieren 2 18. Map ADT Eine Map modelliert eine durchsuchbare Collection von Schlüssel-Wert Entries. Jeder Schlüssel darf nur einmal vorkommen. Anwendung beispielsweise in einem Adressbuch. 18.1. Operationen v get(k): Liefert Value v zum Key k. Key nicht vorhanden: null. v put(k,v): Setzt Value v bei Key k. Gibt alten Value bei Key zurück. v remove(k): Liefert und entfernt Value z zum Key k. int size(): Anzahl Elemente der Map bool isEmpty(): Elemente in der Map vorhanden? C keys(): Liefert iterierbare Collection mit allen Schlüsseln C values(): Liefert iterierbare Collection mit allen Werten C entries(): Liefert iterierbare Collection mit allen Entries 18.2. Implementierung mittels Linked-List Mittels einer unsortierten Liste kann eine MAP effizient implementiert werden. Die Entries der Map können in einer Liste (oder doppelt verketteten Liste) in beliebiger Reihenfolge abgespeichert werden. Beim Abrufen, bzw. Einfügen von Entries wird jeweils über die ganze Liste iteriert und nach dem Key gesucht. Put(), get() und remove() benötigen deshalb immer O(n) Zeit. 18.3. Sentinel-Trick Beim Sentinel-Trick wird der gesuchte Knoten IMMER am Ende der Liste eingefügt. Wir können deshalb bei der Iterationsschleife die Abfrage „hasNext()“ sparen, da wir sicher irgendwann den gesuchten Wert finden werden. Wir müssen nur nach dem Auffinden des Wertes überprüfen, ob es sich bei dem betreffenden Knoten um den Sentinel oder einen echten Eintrag handelt. 19. Dictionary ADT Das Dictionary ADT beschreibt eine Collection, bestehend aus Schlüssel-Wert Entries. Dabei darf, im Gegensatz zur Map, der Key mehrmals vorkommen. Anwendung beispielsweise bei Wort-Definition Paaren oder DNS-Listen. 19.1. Operationen E find(k): Liefert ersten Entry mit Key k. Falls Key nicht vorhanden: null. C findAll(k): Liefert iterierbare Collection der Entries mit dem Key k. E insert(k,v): Fügt neue Entry mit Key k/Value v hinzu. Liefert neue Entry. E remove(E): Entfernt und liefert Entry E. C entries(): Liefert iterierbare Collection aller Entries. int size(): Anzahl Elemente des Dictionary bool isEmpty(): Elemente im Dictionary vorhanden? 19.2. Implementierung mittels Linked-List Mit einer unsortierten (Double)-Linked-List können wir ein Dictionary implementieren. Insert() benötigt dabei O(1) Zeit, da wir neue Entrys einfach am Ende der Liste anfügen. Find(), remove() und findAll() benötigen immer O(n) Zeit, da jeweils die komplette Liste durchsucht werden muss. 19.3. Implementierung mittels Map In einer Map werden Entries vom Typ <Schlüssel, Set> abgelegt. Im Set werden alle Werte, welche zu einem Schlüssel gehören, abgelegt. 19.4. Binary Search Falls das Dictionary mittels einem sortieren Array implementiert ist, können wir die Suchzeit auf O(log(n)) reduzieren. 1) Wert in der Mitte des Arrays auslesen 2) Suchwert mit Wert aus Schritt 1 vergleichen. Falls der Suchwert kleiner ist, wird mit der linken Hälfte des Arrays fortgefahren, andernfalls mit der rechten Seite. 3) Wir fangen mit dem restlichen Teil des Arrays erneut bei Schritt 1 an. Kälin Thomas, Abt I 16/35 06.07.2006 Programmieren 2 20. Hash-Tabellen Bei den Hash-Tabellen wird mittels einer Hash-Funktion h ein Key auf einen Integer in einem fixen Interval abgebildet. Dieser Integer (=Hashwert des Keys) wird als Index in ein Array (Tabelle) verwendet. 20.1. Hash-Funktionen Eine Hash-Funktion besteht für gewöhnlich aus zwei Teilen, der Hash-Funktion und der KompressionsFunktion. Das Ziel der Hash-Funktion ist es, die Schlüssel möglichst zufällig zu verteilen. Die Aufgabe der Kompressions-Funktion lautet, die generierten Schlüssel in ein fixes Intervall zu transformieren. 20.1.1. Hash-Funktion Grundfunktion: h1 = Key -> Integer Eine Hash-Funktion heisst perfekt, wenn es keine Kollisionen gibt. Horner-Schema: Ziel des Horner-Schemas ist es, die Anzahl Rechenoperationen zu reduzieren. Die Laufzeit senkt sich dabei auf O(n). //Grundmuster Horner-Schema P(z) = a0*z0 + a1*z1 + a2*z2 + a3*z3 = a0 + z(a1 + z(a2 + z(a3))) //Beispiel Binärzahlen P(1101) = 1*20 + 0*21 + 1*22 + 1*23 = 13 //3 Add, 7 Mul = 1 + 2(0 + 2(1 + 2(1))) = 13 //2 Add, 3 Mul 20.1.2. Kompressions-Funktion Grundfunktion: h2 = Integer -> [0, N-1] Beispiel Modulo: h2 = y % N oder h2 = (a*y+b) % N Eine typische Kompressions-Funktion ist die Modulo-Division. Als Grösse der Hash-Tabelle wird oft eine Primzahl gewählt, da dadurch die Hash-Funktion optimaler gewählt werden kann. 20.2. Kollisionsbehandlung 20.2.1. Geschlossene Adressierung (offenes Hashverfahren) Bei der geschlossenen Adressierung zeigt jede Zelle der Tabelle auf eine Liste. Dies wird auch „Seperate Chaining“ genannt. Vorteil ist, dass die Verkettung einfach ist. Nachteilig hingegen, dass die Verkettung Speicherplatz ausserhalb der Tabelle benötigt. 20.2.2. Offene Adressierung (geschlossene Hashverfahren) Bei der offenen Adressierung wird für kollidierende Elemente (Überläufer) mittels einer Sondierungsfunktion ein Platz in der Nähe der belegten Zelle gesucht. Jede inspizierte Zelle wird als „probe“ bezeichnet, deshalb auch der Name „Probing“. Lineares Sondieren: Linear vorwärts nach freier Stelle suchen => +1, +2, +3, … Lineares negatives Sondieren: Linear rückwärts nach freier Stelle suchen => -1, -2, -3, … Quadratisches Sondieren: Quadratisch vorwärts nach freier Stelle suchen => +1, +4, +9, … Alternierendes Sondieren: Linear abwechselnd nach freier Stelle Suchen => -1, +2, -3, … Alternierendes quadr. Sondieren: Quadr. abwechselnd nach freier Stelle suchen => -1, +4, -9, … Offene Adressierung erfordert eine besondere Behandlung der Lösch-Operationen. Soll ein Datensatz gelöscht werden, so kann dies die Sondierungsfolge für einen anderen Datensatz unterbrechen. Zu löschende Datensätze dürfen deshalb nicht physisch gelöscht, sondern nur als gelöscht markiert werden. 20.2.3. Doppeltes Hashing (Offene Adressierung) Die Sondierungsfunktion (offene Adressierung) ist eine zweie Hashfunktion, deren Wert linear zur ersten Hash-Funktion addiert wird. 20.3. Performance Lastfaktor (a = Belegte Zellen / Totale Zellen) bestimmt Zeitverhalten. In der Praxis ist Hashing sehr schnell, falls der Lastfaktor nicht zu hoch ist (a < 0.8). Worst-Case: O(n) Zeit für Suchen, Einfügen und Löschen. Optimaler Zugriff in O(1) -> Index! Kälin Thomas, Abt I 17/35 06.07.2006 Programmieren 2 21. Skip-List 21.1. Definition einer Skip List Eine Skip List besteht aus einer Serie von Listen (S0-SH). Jede dieser Listen enthält künstliche Anfangs(+∞) und Endknoten (-∞). Die unterste Liste (S0) enthält alle Keys in nicht absteigender Reihenfolge, die oberste Liste (SH) nur den Anfangs- und Endknoten. 21.1.1. Perfekte Skip List Jede Liste enthält jeweils Knoten in der Mitte der Intervalle der Nachfolgerliste. Die perfekte Skip List wird in Regel nicht verwendet, da der Verwaltungsaufwand zu gross ist. 21.1.2. Random Skip List Anders als bei der perfekten Skip List, wird die Höhe für die einzelnen Listenelemente zufällig bestimmt. Dabei wird versucht, die Höhen gleichmässig und zufällig über die Liste zu verteilen. Je weiter oben die Liste sich befindet, desto weniger Knoten soll sie enthalten. 21.2. Java-Code 21.2.1. SkipListNode Ein Node in einer Skiplist stellt einen „Turm“ der Struktur dar. Höhe kann aus dem Array „next“ herausgelesen werden. class SkipListNode { int key; //Enthält Schlüssel des Nodes SkipListNode[] next; //Referenzen auf nächsten Node jedes Levels } 21.2.2. SkipList class SkipList { int maxHeight; int height; SkipListNode head; SkipListNode tail; SkipListNode[] update; //Maximale Höhe der Liste //Aktuelle Höhe der Liste //Kopf der Liste //Ende der Liste //Hilfsarray, siehe „Einfügen“ SkipList() { maxHeight = 5; //Höhe geht von 0-5! -> 6 Level! height = 0; update = new SkipListNode[maxHeight + 1]; head = new SkipListNode(Integer.MIN_VALUE,maxHeight); tail = new SkipListNode(Integer.MAX_VALUE,0); //Stelle Grundverknüpfungen her for (int i=0; i <= maxHeight; ++i) { head.next[i] = tail; } } } 21.2.3. Suchen search (int key) { //Wir starten im Head beim höchsten Level SkipListNode p = head; for (int i = height; i>= 0; i--) { //Einzelne Levels durchgehen while (p.next[i].key < key) { p = p.next[i]; } } //Der nächste Knoten ist vermutlich der gesuchte Wert! p = p.next[0]; if (p.key == key && p != tail) { return p; } else { return null; } } Kälin Thomas, Abt I 18/35 06.07.2006 Programmieren 2 21.2.4. Einfügen insert (int key) { //Empfohlenerweise zuerst Höhe bestimmen! int newheight = randheight(); if (newheight > height) { height = newheight; } //Wir suchen alle Vorgänger und speichern diese im Hilfsarray SkipListNode p = head; for (int i = height; i >= 0; i--) { while (p.next[i].key < key) { p = p.next[i]; } update[i] = p; } //Wir stellen die Verkettungen für den neuen Knoten her p = new SkipListNode(key, newheight); for (int i = 0; i <= newheight; i++) { p.next[i] = update[i].next[i]; update[i].next[i] = p; } } 21.2.5. Löschen delete(int key) { //Wir suchen alle Vorgänger und speichern diese im Hilfsarray SkipListNode p = head; for (int i = height; i >= 0; i--) { while (p.next[i].key < key) { p = p.next[i]; } update[i] = p; } //Vermuteten Knoten überprüfen p = p.next[0]; if (p.key != key) { return null; } //Schlüssel nicht gefunden //Wir verketten die Elemente neu, zu löschenden Knoten „überspringen“! for (int i = 0; i < p.next.length; i++) { update[i].next[i] = update[i].next[i].next[i]; } //Leere Levels löschen while(height >= 0 && head.next[height] == tail) { height--; } } 21.2.6. Zufallsgenerator int randheight() { //Wahrscheinlichkeit ist 0.5 i height = 0; while (rand() % 2 == 0 && height < maxHeight) { height++; } return height; } 21.3. Implementierung mittels Quad-Nodes Eine Skip List könnte auch mittels einem Quad-Knoten implementiert werden. 21.4. Analysen 21.4.1. Speicherplatz Der Speicherbedarf einer Skip List hängt vom zufälligen Einfügen ab. Die Wahrscheinlichkeit für einen Entry in der Liste SI ist 1/2i. Die erwartete Grösse der Liste SI ist n*p, also n/2i. Über alle Stufen aufsummiert ergibt das einen Speicherplatz < 2n, also O(n). (Siehe Grafik) 21.4.2. Höhe Die Skip List besitzt eine Höhe von O(log n). 21.4.3. Laufzeiten Suchen, Einfügen und Löschen benötigt O(log n) Zeit. Kälin Thomas, Abt I 19/35 06.07.2006 Programmieren 2 22. Binäre Suchbäume Ein binärer Such-Baum ist ein binärer Baum, welcher Entries vom Typ Key/Value in seinen internen Knoten speichert. Die Blätter speichern keine Daten. Ausserdem muss die Bedingung erfüllt sein, dass die Inorder-Traversierung die Keys in nicht absteigender Reihenfolge besucht. Das heisst also: o Linkes Kind <= Knoten <= Rechtes Kind 22.1. Höhe des Baums Die Höhe ist im besten Fall O(log(n)). Dabei ist der Baum völlig ausbalanciert. Im schlechtesten Fall kann die Höhe jedoch auch O(n) sein, heisst also, dass auf jeder Ebene des Baums nur ein Knoten liegt. Wir haben also so etwas ähnliches wie eine verkettete Liste. 22.2. Implementierung Die Implementierung eines Knotens ist denkbar einfach: Ein Knoten aggregiert intern zwei Knoten (linkes / rechtes Kind) und zusätzlich ein Entry (Key/Value). Im Baum selber wird nur ein Knoten für die Wurzel und die Zugriffsmethoden gespeichert. 22.3. Suche TreeSearch(Key, Knoten) { if (Knoten.isExternal()) { return null; } //Element nicht gefunden if (Key<Knoten.Key) { return TreeSearch(Key, Knoten.leftChild()); } else if (Key>Knoten.Key) { return TreeSearch(Key, Knoten.rightChild()); } return Knoten; //Aktueller Knoten ist gesuchtes Element! } 22.4. Einfügen Wir gehen analog zur Suche vor. Jedoch wird bei der isExternal()-Abfrage kein null zurückgegeben, sondern ein neuer Knoten passend zum Key generiert. 22.5. Löschen Beim Löschen müssen wir 3 Fälle unterscheiden. Der Knoten v mit Schlüssel k ist…: Ein Knoten mit zwei Blättern (Bsp.: 1). Hierbei ist das Löschen einfach. Der Knoten kann entfernt und durch „null“ ersetzt werden. Ein Knoten mit einem Blatt (Bsp.: 9). Hier kann der zu löschende Knoten durch den Kind-Knoten ersetzt werden, der kein Blatt ist. Ein Knoten ohne Blätter (Bsp.: 2). Dies ist etwas aufwändiger. Der zu löschende Knoten muss durch den Knoten ersetzt werden, der in der Inorder-Traversierung als nächstes folgen würde (siehe Hilfsmethode). Der Knoten, der den Platz des zu löschenden Knotens übernimmt, muss im Anschluss auch wieder den Löschen-Algorithmus durchlaufen. 22.5.1. Symetrischer Nachfolger Hilfsmethode zum Auffinden des Inorder-Nachfolgers. Knoten SymNach(Knoten p) { if (p.rightson.leftson != null) { p = p.rightson; while (p.leftson.leftson != null) { p = p.leftson; } } } 22.6. Inorder-Methode Die Hilfsmethode führt eine Inorder-Traversierung durch void Inorder(Knoten p) { if (p.leftson != null) { Inorder(p.leftson); } System.out.print(„(„ + p.getKey() + „)“); if (p.rightson != null) { Inorder(p.rightson); } } Kälin Thomas, Abt I 20/35 06.07.2006 Programmieren 2 23. AVL Baum Ein AVL Baum ist ein binärer Suchbaum, der versucht die Verteilung der Knoten zu balancieren. Deshalb muss für jeden internen Knoten v von T gelten, dass die Höhe der Kinder von v sich höchstens um 1 unterscheiden. Für die Höhe eines Baums gilt O(log(n)). 23.1. Balance B(Knoten) = |Höhe(Links) – Höhe(Rechts)| Falls nach dem Einfügen eines neuen Knotens B(K) >= 2 ist, muss der Baum umstrukturiert werden. Den Prozess des Ausgleichens bezeichnet mal als Rotation. 23.2. Einfügen Das Einfügen erfolgt wie beim binären Suchbaum. Es wird also immer ein externer Knoten expandiert. Nach dem Einfügen muss vom Knoten in Richtung Wurzel traversiert werden. Bei jedem Vorgänger muss die Balance neu berechnet werden. Ist der Baum nicht mehr balanciert, muss ein Ausgleich vorgenommen werden. 23.2.1. 17 N N Cut/Link Restrukturierungs-Algorithmus Die einfachste Methode zur Ausbalancierung ist der Cut/Link-Algorithmus. Ich erläutere diesen anhand eines Beispiels. 44 Der nebenstehende Baum ist offensichtlich unbalanciert. Nehmen wir an, wir hätten gerade 88 eingefügt. 62 1) 1. Unbalancierten Knoten von (88) Richtung Wurzel suchen: (44) = Z 50 78 2) Kind von Z mit grössere Höhe: (62) = Y 3) Kind von Y mit grösserer Höhe: (78) = X 48 54 88 4) T0-T3 festlegen (restliche Subbäume!). Nummerierung von LnR! 5) Knoten X-Z nach Inorder-Reihenfolge mit A-C benennen N N N N N N N Wir erhalten den links abgebildeten Baum. Beim Programmieren werden diese 7 Elemente nach folgendem Schema in einem Array abgelegt. T0 A T1 B T2 C T3 Aus diesem Muster kann jetzt der neue Baum gemäss folgendem Muster gezeichnet werden: Hier sehen wir nun den komplett umstrukturierten Baum. Diese Methode führt zum selben Ergebnis wie die normalen (Doppel-)Rotationen! Die Umstrukturierung kann eventuell eine neue Unbalance generieren. Deshalb unbedingt weiter bis zur Wurzel überprüfen! 23.3. Löschen Das Löschen erfolgt nach demselben Prinzip wie bei den binären Suchbäumen. Natürlich muss auch hier wieder die Balance des Baumes überprüft werden. 23.4. Laufzeiten Eine einzelne Restrukturierung benötigt O(1) Zeit, unter Benutzung eins verlinkten Binärbaums find(), insert() und delete() sind O(log(n)), da bei jeder Aktion zuerst durch den Baum gesucht werden muss, was durch die Höhe des Baums (log(n)) bestimmt wird. Kälin Thomas, Abt I 21/35 06.07.2006 Programmieren 2 24. Merge Sort Merge Sort ist ein Sortier-Algorithmus basierend auf dem Teile-und-Herrsche Paradigma (Divide and Conquer). Die Laufzeit beträgt O(n log(n)). Ein grosser Vorteil des Merge-Sort ist, dass dessen Verhalten absolut stabil ist. Die Sortierreihenfolge der Eingangssequenz hat auf die Laufzeit überhaupt keinen Einfluss. 24.1. Code Sequenz mergeSort(Sequenz S) { | merge(Sequenz S1, Sequenz S2) { if (S.size() > 1) { | Sequenz S = new Sequenz(); //Temp (S1,S2) = Teile(S,n/2); | while (!S1.empty() && !S2.empty()) { S1 = mergeSort(S1); | if (S1.first() < S2.first()) { S2 = mergeSort(S2); | S.insert(S1.remove(S1.first()); S = merge(S1,S2); | } else { } | S.insert(S2.remove(S2.first()); return S; | } } | } | while (!S1.isEmpty()) { | S.insert(S1.remove(S1.first()); | } | while (!S2.isEmpty()) { | S.insert(S2.remove(S2.first()); | } | return S; 25. Quick-Sort Quick-Sort ist ein Sortier-Algorithmus basierend auf Divide-and-Conquer. Aus einer gegebenen Sequenz wird ein Element ausgewählt, das sog. Pivot. Die restlichen Elemente werden jeweils mit diesem Pivot verglichen und abhängig davon in eine der 3 Gruppen (Less, Equals, Greater) eingeteilt. Diese drei Gruppen werden dann wieder mit diesem Verfahren behandelt (Rekursion). 25.1. Laufzeit Die Laufzeit des Algorithmus ist stark abhängig vom gewählten Pivot. Der Worst-Case tritt dann auf, wenn das Pivot auf jeder Rekursionsstufe genau das Minimum- oder Maximum-Element ist. In diesem Fall tritt eine Laufzeit von O(n2) auf, da auf jeder Ebene alle Elemente ausser dem Pivot in dieselbe Gruppe (Less oder Greater) eingeteilt werden. Im Best-Case beträgt die Laufzeit O(n log(n)). Nachfolgend noch einige Beispiele: Sortiere Sequenz -> (1,2,3,4): Falls das letzte oder erste Element als Pivot gewählt wird: O(n2)! Gleiche Elemente -> (1,1,1,1): Auch O(n2), da alle Elemente nach „Equals“ gehen. 25.2. Beispiele Binärbaum Auf jeder Ebene wird von „links nach rechts“ gelesen. Beispiel Wurzel: Zuerst werden alle Elemente vom linken Kind gelesen und vor das Pivot gehängt (2, 4). Anschliessend kommt das Pivot (6), darauf alle Elemente vom rechten Kind (7, 9). Kälin Thomas, Abt I 1. 2. 3. 4. 5. 6. 7. 8. 9. In-Place 2 4 1 7 4 9 4 9 2 4 1 7 4 9 2 4 1 7 1 9 2 4 4 7 1 9 2 4 4 7 1 9 2 4 4 7 1 2 9 4 4 7 Pivot wird festgelegt Suche von links bis Elemente > 2 gefunden -> 4 Suche von rechts bis Element < 2 gefunden -> 1 Tausche Elemente 4 und 1 Suche von links bis Elemente >2 gefunden -> 9 Suche von rechts bis Pivot erreicht -> 2 Tausche Element 9 mit Pivot Pivot erreicht Endposition (rot). Mit linker / rechter Seite von Pivot fortfahren 22/35 06.07.2006 Programmieren 2 26. Bucket-Sort Beim Bucket-Sort wird in einer ersten Phase eine Sequenz von (Key, Value)-Elementen auf verschiedene Bereiche (Buckets = Eimer) verteilt. In der zweiten Phase werden diese einzelnen Container der Reihe nach ausgelesen und zu einer neuen Sequenz zusammengefügt. Die Phase 1 benötigt O(n) Zeit, Phase 2 benötigt O(n+N) Zeit, wobei N die Anzahl Container ist. Stabile Sort Eigenschaft: Die relative Ordnung von zwei Items mit demselben Key werden durch den Algorithmus nicht verändert, falls keine Zwischenphase (Sortierung) durchgeführt wird. Eine Variante des Bucket-Sort besteht darin, dass zwischen Phase 1 und Phase 2 eine Sortierung der Elemente in einem Bucket erfolgt. Eine Anwendung des Bucket-Sort wäre z.B. eine Namensliste sortieren. Jeder Bucket ist hierbei ein Buchstabe aus dem Alphabet und der Key ist der Anfangsbuchstabe eines Namens. 27. Radix-Sort Radix-Sort ist eine Spezialisierung des lexikographischen Sortierens (Sortieren nach Dimensionen), welcher Bucket-Sort als stabilen Sortier-Algorithmus für jede Dimension benutzt. Radix-Sort läuft in O(d*(n+N)) Zeit, wobei d für die Anzahl Dimensionen steht. 27.1. Beispiel: Sortierung von 3-Bit Integers 1 111 001 100 28. 2 100 111 001 3 100 001 111 4 001 100 111 1) Ursprungs-Zustand der ungeordneten Elemente. 2) Es wurde mit Bucket-Sort nach der 1.Bit-Position sortiert. Dabei wurde die relative Ordnung (Stabilität) der Elemente nicht verändert. 3) Sortierung mit Bucket-Sort nach der 2.Bit-Position. 4) Sortierung mit Bucket-Sort nach der 3.Bit-Position. Da wir nun alle Dimensionen (Bitpositionen) durchlaufen haben, sind wir am Ende des Radix-Sorts angelangt. Die Bitfolgen sind sortiert. Untere Grenze der Sortierung Zur Herleitung einer unteren Grenze (Lower Bound) der Laufzeit zählen wir die Anzahl Vergleiche. Jeder mögliche Durchgang ergibt einen Entscheidungsbaum. Die Höhe des Entscheidungsbaumes entspricht der unteren Grenze der Laufzeit. Jeder vergleichsbasierte Sortier-Algorithmus hat im Worst-Case als untere Grenze die Laufzeit von Ω(n*log(n)). 29. Sets Ein Set wird durch eine sortierte Sequenz seiner Elemente repräsentiert. 29.1. Grundoperationen Alle Set-Operationen können mit generischem Mischen implementiert werden. Die Laufzeit einer Operation mit den Sets A und B sollte maximal O(nA + nB) sein, daraus folgt O(n). 29.1.1. Vereinigung (Union) Es wird immer das kleine Element ins neue Set eingefügt. A.first() < B.first() -> V.insertLast(A.removeFirst()) A.first() > B.first() -> V.insertLast(B.removeFirst()) A.first() = B.first() -> V.insertLast(A.removeFirst()); B.removeFirst(); 29.1.2. Durchschnitt (Intersection) Es werden nur gleiche Elemente A.first() < B.first() A.first() > B.first() A.first() = B.first() 29.1.3. eingefügt. -> A.moveNext(); -> B.moveNext(); -> V.insertLast(A.removeFirst()); B.removeFirst(); Differenz (Subtraction) Ersetze A durch Differenz von A und B. Es werden also nur Elemente eingefügt, welche nicht in beiden Mengen vorkommen. Kälin Thomas, Abt I 23/35 06.07.2006 Programmieren 2 29.2. Operationen bool add(E): Element hinzufügen, falls noch nicht vorhanden bool addAll(C): Alle Elemente einer Collection hinzufügen. bool remove(E): Element aus dem Set entfernen bool removeAll(C): Alle Elemente in der Collection aus dem Set entfernen bool retainAll(C): Löscht alle Elemente, die nicht in der Collection sind. bool contains(E): Überprüft, ob ein Element im Set vorhanden ist. bool containsAll(C): Überprüft ob ALLE Elemente vorhanden sind. void clear(): Alle Elemente des Sets löschen. bool isEmpty(): Elemente im Set vorhanden? int size(): Anzahl Elemente des Sets E[] toArray(): Erzeugt ein Array aus dem Set 29.3. Set-Varianten in Java AbstractSet, EnumSet, HashSet, LinkedHashSet, TreeSet (sortiert!). 30. Automaten 30.1. Akzeptor Ein Akzeptor hat die Aufgabe, eine Folge von Eingaben auf Ihren Erfolg / Gültigkeit zu prüfen. Ist der Automat nach Abarbeitung einer Eingabe in einem Endzustand, war die Eingabe gültig. Ein Akzeptor besitzt keine Ausgabe. Q = {z0,z1,z2} Σ = {a,b} δ = (z0,a,z0) (z0,b,z1) (z1,a,z0) (z1,b,z2) … q0 = z 0 F = {z2} 30.2. Transduktor Ein Transduktor hat die Aufgabe, auf eine Folge von Eingaben eine definierte Ausgabe zu erzeugen. Er besitzt keine Endzustände. 30.2.1. Mealy / Moore – Automaten Moore- und Mealy-Automaten sind gleichwertig. Der eine kann in den jeweils anderen überführt werden. In der Praxis werden meistens Mischmodelle benutzt. Mealy Moore Im Mealy-Modell werden Eingabeaktionen benutzt, die Ausgabe hängt also vom Zustand und der Eingabe ab. 31. Im Moore-Modell werden nur die Eingangsaktionen verwendet, die Ausgabe hängt also nur vom Zustand ab. Pattern Matching 31.1. Allgemeines Substring: Ein Substring P[i..j] von P ist die Subsequenz von P, bestehend aus den Zeichen mit Rang zwischen und inklusiv i und j. Präfix: Ein Präfix von P ist ein Substring vom Typ P[0..i] Suffix: Ein Suffix von P ist ein Substring vom Typ P[i..m-1] Kälin Thomas, Abt I 24/35 06.07.2006 Programmieren 2 31.2. Brute-Force Beim Brute-Force Algorithmus wird das Such-Pattern einfach immer um eine Position vorgeschoben, falls keine Übereinstimmung gefunden wurde. Der Algorithmus benötigt O(n*m) Zeit, wobei m die Länge des Patterns ist. 31.2.1. Worst-Case Beispiel T = aaaaaaaaaaaaaaaaaaaaaah ||| P = aaah -> P = aaah -> P = aaah -> … Bei jeder Position von T werden jeweils 3 a’s verglichen, bevor an der 4. Position eine Nicht-Übereinstimmung (h) gefunden wird. Dies wird für alle Positionen von T so fortgeführt. Da wir für n-Zeichen von T m-Vergleiche (m ist die Länge des Patterns) durchführen müssen, ergibt sich O(n*m). 31.3. Boyer-Moore Der Boyer-Moore Algorithmus basiert auf zwei Heuristiken: Looking-Glass: Vergleiche Pattern P mit einer Subsequenz von T. Starte dabei am ENDE des Patterns. Character-Jump: Fall 1 Fall 2 Falls bei einem Vergleich das Zeichen c aus Kommt c in P nicht vor, verschiebe P aufs dem Text T im Pattern P vorkommt, verschiebe nächste Feld nach c in T. P bis das letzte Auftreten von c in P mit c in T übereinstimmt. Achtung: Nicht immer liefert die „Schlechtes-Zeichen“-Strategie ein gutes Eregnis. In Sonderfällen kann eine negative Verschiebung entstehen. Dies kann umgangen werden, indem man in diesen Fällen stattdessen umf 1 nach vorne schiebt. 31.3.1. Last-Occurrence Funktion Boyer-Moore’s Algorithmus analysiert zuerst das Pattern P und das Alphabet Σ, um auf die „lastoccurence“-Funktion L aufzubauen. Die Funktion lässt sich in O(m+s) berechnen, wobei m die Länge des Patterns und s die Anzahl Zeichen in Σ ist. S = {a, b, c, d} c a b c d P = abacab L(c) 4 5 3 -1 31.3.2. Berechnung der Verschiebung Das Zeichen T[i] kommt im Pattern P vor (Fall 1). i = i + m – (last(T[i]) + 1) i = Indexpos., m = Patternlänge i = 0123456789 -> i = i + m – (last(T[i]) + 1) T = aabababacb -> i = 3 + 5 – (last(a) + 1) P = cabbb -> i = 3 + 5 – (1 + 1) = 3 + 5 – 2 = 6 cabbb --> Das Zeichen T[i] kommt im Pattern P vor, ist allerdings schon vorbei (Ausnahme). i = i + m – j i = Indexpos., m = Patternlänge, j = Pos. im Pattern i = 0123456789 -> i = i + m - j T = aabababacb -> i = 3 + 5 - 2 P = cabba -> i = 6 cabba --> Unviversal-Methode i = i + m – ( min(j,last(T[i]) + 1) Wir nehmen entweder j oder die Lastfunktion, je nach dem, welches den tieferen Wert liefert. Kälin Thomas, Abt I 25/35 06.07.2006 Programmieren 2 31.3.3. Analyse Der Boyer-Moore-Algorithmus benötigt O(n*m + s) Zeit, wobei n die Länge des Textes, m die Länge des Patterns und s die Anzahl Zeichen des Alphabets sind. Im Worst-Case (T = aa…a, P = baaa) funktioniert der Algorithmus genau wie Bruteforce, da immer nur um eine Stelle verschoben wird. 31.3.4. Zusammenfassung Analyse beginnt mit dem Ende des Patterns Algorithmus verwendet Last-Occurence Funktion Zwei Fälle: Verschiebung mit Hilfe der Last-Funktion oder Verschiebung um 1 (Brute-Force!) 31.4. KMP-Algorithmus Der Knurr-Morris-Pratt Algorithmus vergleich das Muster gegen den Text von links-nach-rechts, aber schiebt das Muster intelligenter als beim Brute-Force. T = abaabx P = abaaba j = F(j-1) = F(5-1) = F(4) = 2 -> Neue Pos im Pattern abaaba --> 31.4.1. KMP-Fehlfunktion In einer Vorlaufsphase sucht der Algorithmus Übereinstimmungen vom Präfix des Musters im Muster selbst. Die Funktion kann in O(m) Zeit berechnet werden, wobei m die Länge des Patterns ist. P[0] = a -> 0 -> j = 0 1 2 3 4 5 P[1] = ab -> 0 P[j] = a b a a b a P[2] = aba -> 1 F(j) = 0 0 1 1 2 3 P[3] = abaa -> 1 P[4] = abaab -> 2 P[5] = abaaba -> 3 31.4.2. Beispiel 31.4.3. Analyse Der KMP-Algorithmus benötigt im optimalen Fall O(m + n) Zeit, wobei m die Länge des Patterns und n die Länge des Suchtextes ist. 31.4.4. Zusammenfassung Analyse beginnt mit dem Anfang des Patterns Verwendet Failure-Funktion, welche mit Hilfe der Ränder des Patterns berechnet wird. 32. REGEX Reguläre Ausdrücke werden an verschiedenen Orten verwendet: Compilerbau, Suchprogramme, … Reguläre Ausdrücke unterstützen genau 3 Operationen: Alternative (ODER), Aneinanderreihung (UND) und Wiederholung (Kleene-Star). 32.1. Metazeichen Metazeichen beschreiben den regulären Ausdruck und werden interpretiert. ( ) [ ] { } \ ^ $ | ? * + . Kälin Thomas, Abt I 26/35 06.07.2006 Programmieren 2 32.2. Tabellen [abc] [^abc] [a-zA-Z] [a-d[m-p]] [a-z&&[def]] [a-z&&[^bc]] [a-z&&[^m-p]] Character Classes a, b oder c Beliebiger Char ausser a,b,c a bis z oder A-Z a bis d oder m bis p. ([a-dm-p]) d, e oder f. (Durchschnitt) a bis z ohne b und c. ([ad-z]) a bis z nicht m bis p ([a-lq-z]) ^ $ \b \B \A \G \Z \z Boundary Matchers Zeilenanfang Zeilenende Wortgrenze Grenze (nicht eines Wortes) Beginn der Eingabe Ende des vorgängigen Matches Ende der Eingabe (aber nicht Datei) Ende der Eingabe . \d \D \s \S \w \W Predefinied Character Classes Beliebiges Zeichen Ziffer: [0-9] Zeichen ausser Ziffer: [^0-9] Whitespace: [\t\n\x0b\f\r] Zeichen ausser Whitespace: [^\s] Wort: [a-zA-Z_0-9] Zeichen ausser Wort: [^\w] Quantifiers Posses. Meaning X?+ X, einmal oder nie X*+ X, kein oder mehrfach X, einmal oder X+ X+? X++ mehrfach X{n} X{n}? X{n}+ X, genau n Mal X{n,} X{n,}? X{n,}+ X, mindestens n Mal X, mindestens n aber X{n,m} X{n,m}? X{n,m}+ nicht mehr als m Mal. Greedy X? X* Lazy X?? X*? 32.3. Quantoren Greedy: Versucht, so viele Zeichen wie möglich zu erfassen. Lazy (Reluctant): Versucht, so wenig Zeichen wie möglich bei einem Match zu erfassen. Possesive: Versucht genau einmal, ob eine Übereinstimmung vorliegt. 32.3.1. Beispiele GROSSPAPAGROSSMAMAPAPAMAMA (25 Zeichen) Greedy: Lazy: Possesive: .*MA -> Match von 0..26 .*?MA -> Match bei 0..16, 16..18, 18..24, 24..26 .*+MA -> Muster wird nicht gefunden (nur ein Match!) 32.4. Java Beispiel import java.util.regex; public static String find(String strPattern, CharSequence strInput) { Pattern pattern = Pattern.compile(strPattern); //Statische Klassenmethode Matcher matcher = pattern.matcher(strInput); if (matcher.find()) { return matcher.group(); } return null; } //Greedy strMatch = find(„A.*c“,“AbcAbc“); //AbcAbc strMatch = find(„A.+“,“AbcAbc“); //AbcAbc //Non-Greedy strMatch = find(„A.*?c“,“AbcAbc“); //Abc strMatch = find(„A.+?“,“AbcAbc“); //Abc 33. Graphen 33.1. Begriffe Gerichtete Kanten: Geordnetes Paar von Vertices. 1.Knoten entspricht dem Start, 2.Knoten dem Ziel Ungerichtete Kanten: Ungeordnetes Paar {V1,V2} von Vertices. Gerichteter Graph: Alle Kanten sind gerichtet Ungerichteter Graph: Alle Kanten sind ungerichtet Endknoten: V ist Endknoten von a und b Kälin Thomas, Abt I 27/35 06.07.2006 Programmieren 2 Inzident: a, d und b sind inzident (enden in) in V Adjazente: U und V sind adjazent (benachbart) Grad: Anzahl indizenter Kanten. Grad von X ist 5 Parallele Kanten: h und i sind parallele Kanten Schleife: j ist eine Schleife Pfad: Sequenz von alternierenden Knoten und Kanten. Bsp: (U,c,W,e,X,g,Y,f,W,d,V) Einfacher Pfad: Alle Knoten und Kanten sind unterschliedlich. Bsp: (V,b,X,h,Z) Zyklus: Zirkuläre Sequenz alternierender Knoten und Kanten. Bsp: (U,c,W,e,X,g,Y,f,W,d,V,a) Einfacher Zyklus: Alle Knoten und Kanten sind unterschiedlich. Bsp: (V,b,X,g,Y,f,W,c,U,a) Subgraph: Die Kanten und Knoten eines Subgraphes sind Teilmengen des ursprünglichen Graphen. Aufspannender Subgraph: Der Subgraph enthält alle Knoten des ursprünglichen Graphen. Verbundener Graph: Zwischen jedem Paar von Knoten existiert ein Pfad Baum: Ein Graph, der verbunden ist und keine Zyklen aufweist Aufspannender Baum: Ein aufspannender Subgraph, der auch ein Baum ist. 33.2. Operationen V[] endVertices(E): Array mitden beiden Endknoten V einer Kante E V opposite(V,E): Gegenüberliegender Endknoten von V bei Kante E bool areAdjacent(V1,V2): Überprüft, ob zwei Knoten benachbart sind void replace(V,O): Inhalt des Knoten V mit Element O ersetzen void replace(E,O): Inhalt der Kante E mit Element O ersetzen void insertVertex(O): Knoten mit Element O hinzufügen void insertEdge(V1,V2,O): Kante von V1 nach V2 mit Element O hinzufügen void removeVertex(V): Knoten V und die inzident Kanten entfernen void removeEdge(E): Kante E entfernen Coll incidentEdges(V): Collection der inzident Kanten des Knoten V Coll vertices(): Collection aller Knoten im Graph Coll edges(): Collection aller Kanten im Graph 33.3. Kanten-Listen Struktur Knoten-Objekt: Besteht aus einem Element (Inhalt) und einer Referenz auf die Position in der Knoten-Sequenz Kanten-Objekt: Besteht aus einem Element (Inhalt), einer Referenz auf den Ursprungsknoten, einer Referenz auf den Zielknoten und einer Referenz auf die Postion in der Kantenfolge. Knoten-Sequenz: Sequenz der einzelnen Knotenobjekte (gelb). Kanten-Sequenz: Sequenz der einzelnen Kantenobjekte (blau). 33.4. Adjadenz-Listen Struktur Knoten-Objekt: Besteht aus einem Element (Inhalt), einer Referenz auf die Position in der Knoten-Sequenz und einer Referenz auf eine Sequenz der inzidenten Kanten (grüne Pfeile). Kanten-Objekt: Besteht aus einem Element (Inhalt), einer Referenz auf den Ursprungsknoten, einer Referenz auf den Zielknoten, einer Referenz auf die Postion in der Kantenfolge, zwei Referenzen auf die Listenelemente der Endknoten (grüne Pfeile). Knoten-Sequenz: Sequenz der einzelnen Knotenobjekte (gelb). Kanten-Sequenz: Sequenz der einzelnen Kantenobjekte (blau). Kälin Thomas, Abt I 28/35 06.07.2006 Programmieren 2 33.5. Adjazenz-Matrix Struktur Knoten-Objekt: Besteht aus einem Element (Inhalt), einer Referenz auf die Position in der Knoten-Sequenz und einem Integer Key, der als Index in eine Tabelle dient Kanten-Objekt: Besteht aus einem Element (Inhalt), einer Referenz auf den Ursprungsknoten, einer Referenz auf den Zielknoten und einer Referenz auf die Postion in der Kantenfolge. 2D-Adjazenz-Array: Referenziert auf die Kantenobjekte für adjazente (benachbarte) Knoten. Falls keine Kante vorhanden ist, wird null eingetragen. Knoten-Sequenz: Sequenz der einzelnen Knotenobjekte (gelb). Kanten-Sequenz: Sequenz der einzelnen Kantenobjekte (blau). 33.6. Performance Rahmenbedingungen: n Knoten, m Kanten, keine parallen Kanten, keine Schleifen Kanten-Liste Adjazenz-Liste Adjazenz-Matrix Speicherbedarf n+m n+m n2 incidentEdges(V) m deg(V) n areAdjacent(V1,V2) m min(deg(V1), deg(V2)) 1 insertVertex(O) 1 1 n2 insertEdge(V1,V2,O) 1 1 1 removeVertex(V) m deg(V) n2 removeEdge(E) 1 1 1 33.7. Adjazenz-Matrix b (A) --- (B) | | a | | c | | (C) --- (D) d 34. -> -> -> -> |0 |1 |1 |0 1 0 0 1 1 0 0 1 0| 1| 1| 0| -> -> -> -> |0 |b |a |0 b 0 0 c a 0 0 d 0| c| d| 0| Tiefensuche (Depth-First Search: DFS) DFS ist eine allgemeine Technik für die Traversierung eines Graphen. Sie entspricht in etwa der EulerTour bei binären Bäumen und läuft in O(n+m) Zeit ab, sofern n Knoten und m Kanten vorhanden sind. Dabei bilden die markierten, besuchten Kanten einen aufspannenden Baum. 34.1. DFS-Algorithmus In einer Initialisierungsphase werden alle Knoten und Kanten mit dem Label „UNEXPLORED“ initialisiert. Danach läuft der folgende Algorithmus (rekursiv) ab: DFS(Graph G, Knoten V) { setLabel(V, VISITED); foreach (Kante E : G.incidentEdges(V)) { if (getLabel(E) == UNEXPLORED) { Knoten V2 = opposite(V,E); if (getLabel(V2) == UNEXPLORED) { setLabel(E, DISCOVERY); DFS(G,V2); } else { setLabel(E, BACK); } } } } Kälin Thomas, Abt I 29/35 06.07.2006 Programmieren 2 34.2. Spezialisierung 34.2.1. Pfade finden Wir können den DFS Algorithmus spezialisieren, um einen Pfad zwischen zwei gegebenen Knoten zu finden. Dabei rufen wir den DFS-Algorithmus mit dem Startknoten auf, und merken uns den Pfad zwischen dem Startknoten und dem aktuellen Knoten auf einem Stack. Sobald der Zielknoten gefunden wurde, geben wir den Pfad mit Hilfe des Stacks aus. 34.2.2. Zyklen finden Wir können den DFS Algorithmus spezialisieren, um Zyklen in einem Graph zu finden. Wir merken uns den Pfad zwischen dem Startknoten und dem aktuellen Knoten auf einem Stack. Sobald wir auf eine BACK-Kante treffen, die auf den Startknoten zeigt, geben wir den Zyklus als Teil des Stacks aus. 34.3. Gerichtete Tiefensuche Wie können den DFS-Algorithmus für gerichtete Graphen spezialisieren, indem Kanten nur in die erlaubten Richtung traversiert werden. Dabei existieren 4 Verschiedene Kanten: • Discovery (rot): Der aufspannende Baum bei der Tiefensuche • Back (gestrichelt): Kanten von einem Nachfolger zu einem Vorgänger. • Forward (strich-punkte): Kanten von einem Vorgänger zu einem seiner Nachfolger. • Cross (gepunktet): Kanten zwischen zwei Teilästen des aufspannenden Baums. 35. Breitensuche (Breadth-First Search: BFS) BFS ist eine generelle Technik für die Traversierung eines Graphen. Die Discovery-Kanten des BFS bilden einen aufspannenden Baum vom G. BFS benötigt bei einem Graphen mit n Knoten und m Kanten O(n+m) Zeit, sofern der Graph mithilfe seiner Adjazenzliste dargestellt wird. 35.1. BFS-Algorithmus In einerInitialisierungsphase werden alle Knoten und Kanten mit dem Label „UNEXPLORED“ initialisiert. Danach läuft der folgende Algorithmus ab: BFS(Graph G, Knoten s) { L[0] = new List(); L[0].insertLast(s); setLabel(s,VISITED); int i = 0; while (!L[i].isEmpty()) { L[i+1] = new List(); foreach (Knoten v : L[i].elements()) { foreach (Kante e : G.incidentEdges(v)) { if (getLabel(e) == UNEXPLORED) { Knoten w = G.opposite(v, e); if (getLabel(w) == UNEXPLORED) { setLabel(e,DISCOVERY); setLabel(w,VISITED); L[i+1].insertLast(w); } else { setLabel(e,CROSS); } } } } i++; } } Kälin Thomas, Abt I 30/35 06.07.2006 Programmieren 2 35.2. Applikationen Mit Anwendung der BFS-Traversierung können wir folgende Probleme lösen: Bestimmen der verbundenen Komponenten von G Bestimmen eines aufspannenden Waldes von G Bestimmen eines einfachen Zyklus in G Findes eines Pfades in G zwischen den beiden Knoten mit einer minimalen Anzahl Kanten, oder bestimmen, ob überhaupt so ein Pfad existiert. 36. Gerichteter Graph / Digraph Ein gerichteter Graph (directed graph, digraph) ist ein Graph, dessen Kannten ALLE gerichtet sind. Anwendungen findet er bei Einbahnstrassen, Fluglinien und Task-Scheduling. 36.1. Transitiver Abschluss Der transitive Abschluss stellt die gesamte Erreichbarkeitsinformation über einen Digraph zur Verfügung. Der transitive Abschluss lässt sich berechnen, indem eine Tiefensuche von jedem Vertex aus gestartet wird. Laufzeit ist deshalb O(n*(n+m)). In nebenstehenden Beispiel beschreiben die blauen Kanten den ursprünglichen Graphen G, die roten Kanten ergänzen ihn zum transitiven Abschluss G*. 36.2. Floyd-Warshall’s Algorithmus Floyd-Warshall’s Algorithmus nummeriert die Knoten von G als v1…vn und berechnet eine Serie von Digraphen G0…Gn, wobei Gn der transitive Abschluss darstellt. Die Laufzeit ist O(n3), sofern areAdjacent() in O(1) läuft. 36.2.1. Beispiel In diesem Beispiel steht der Knoten V3 (rot) im Mittelpunkt. Wie wir sehen, trifft beim Knoten V3 eine Kante von V6 ein. Ausserdem geht eine Kante von V3 nach V4 weg. Da V6 und V4 noch nicht direkt verbunden sind, erstellen wir eine neue Kante. Dies führen wir solange durch, bis alle eingehenden mit allen ausgehenden Kanten durchprobiert wurden. Danach gehen wir zum nächsten Vertex (V4) und wiederholen das Prozedere. 36.3. Topologische Sortierung Dies ist eine spezialisierte Anwendung der Tiefensuche. Der Algorithmus ist identisch mit dem DFS, mit dem Unterschied, dass nach der foreach-Schleife (also am tiefsten Punkt des aufspannenden Baumes!) jeweils der Knoten noch mit einer Zahl beschriftet wird. Diese Zahl beginnt mit der Anzahl an Knoten und wird dann bei jeder „Beschriftung“ um 1 dekrementiert. Wir nummerieren also rückwärts. Kälin Thomas, Abt I 31/35 06.07.2006 Programmieren 2 37. Shortest Path / Kürzester Pfad Gegeben sei ein gewichteter Graph mit zwei Knoten u und v. Wir möchten den kürzesten Weg zwischen diesen beiden Knoten finden. Dies ist der Pfad, mit der geringsten Summe der Gewichte. Eigenschaft 1: Ein Teilweg eines kürzesten Weges ist selbst auch ein kürzester Weg Eigenschaft 2: Es existiert ein Baum von kürzesten Wegen, von einem Start-Vertex zu allen anderen Vertices. 37.1. Dijkstra’s Algorithmus Dieser Algorithmus berechnet die Distanzen zu allen Vertizes von einem Start-Vertex s aus und basiert auf der Greedy-Methode. Wir gehen davon aus, dass der Graph verbunden, ungerichtet und ohne negative Kantengewichte ist. Im ersten Schritt wird eine Wolke um den Startknoten gebildet. Anschliessend wird in jeder Phase die lokal billigste Kante gesucht, welche „aus der Wolke hinaus“ führt. Diese wird dann in die Wolke mit einbezogen. Laufzeit ist O((n+m)*log(n)). 37.1.1. Beispiel 37.1.2. Probleme Der Algorithmus funktioniert nicht für negative Kantengewichte. Wenn ein Vertex mit einer Kantegewicht <0 später der Wolke hinzugefügt wird, bringt dies die Distanzen für die Knonten durcheinander. Man könnte somit Zyklen erstellen, welche die Pfäde ungültig „verbessern“. 37.2. Bellman-Ford Algorithmus Funktioniert auch mit negativen Kantengewichten, allerdings nur dann, wenn der Graph gerichtet ist. Laufzeit ist O(n*m). Berechnet von jedem Knoten aus das Gewicht zum verbundenen Knoten (Start + Kante = Gewicht). Ist dieses kleiner als dasjenige des Zielknotens, wird es überschrieben. Kälin Thomas, Abt I 32/35 06.07.2006 Programmieren 2 37.3. DAG-basierter Algorithmus Funktioniert wird der Bellmand-Ford Algorithmus auch mit negativen Kantengewichten, jedoch nur bei gerichteten Graphen. Zusätzlich zum vorherigen Algorithmus werden die Knoten in topologischer Reihenfolge angesprochen, was die Laufzeit auf O(n+m) verbessert. 38. Minimum Spanning Tree Ein MST ist ein aufspannender Baum eines gewichteten Graphen mit minimalem totalem Kantengewicht. 38.1. Kruskal’s Algorithmus Der Algorithmus bildet in der ersten Phase um jeden Vertex eine eigene Wolke. Anschliessend werden alle Kanten in eine Priority Queue eingetragen. Als Schlüssel dient hierbei das Kantengewicht. Somit wird bei jedem lesen der PQ die Kante mit dem minimalen Gewicht ausgelesen. Die Wolken um die Endknoten der ausgelesen Kante werden miteinander verglichen. Sind diese unterschiedlich, so werden Sie zu einer einzelnen Wolke vereinigt. Nun wird das nächste Element aus der PQ gelesen und solange wiederholt, bis diese leer ist. Kanten 2,3,7 bilden den MST. Die Kante 6 wurde weggelassen, da über Kanten 2 und 3 ein billigerer Weg möglich ist. Kälin Thomas, Abt I 33/35 06.07.2006 Programmieren 2 38.2. Prim-Jarnik’s Algorithmus Dieser Algorithmus funktioniert ähnlich wie Djikstra’s Algorithmus. Es gibt jedoch einen kleinen Unterschied: Hier wird jedes Mal der Knoten mit dem tiefsten Gewicht zur Wolke hinzugefügt. (Als Erinnerung: Bei Djikstra war das Kriterium das Kantengewicht). Dies kann natürlich wieder optimal mittels einer PQ programmiert werden, welche die Knoten ausserhalb der Wolke mit der Distanz als Schlüssel beinhaltet. Laufzeit ist O(m*log(n)). 38.3. Baruvka’s Algorithmus Ähnlich wie Kruskal’s Algorithmus, bildet dieser Algorithmus mehrere Wolken auf einmal. Das Vorgehen ist hierbei wie folgt: 1) Wählen einen Knoten V aus 2) Suche alle mit V verbundenen Knoten. Für jeden dieser Knoten wird die Kante markiert, welche das kleinste Gewicht aufweist. Ist diese Kante nicht in derselben Wolke wie V, wird die Kante verbunden. 39. XML: Document Type Definition (DTD) Eine zu einem gegebenen XML-Dokument zugeordnete DTD beschreibt, welche Elemente und welche Entitäten in diesem Dokument vorkommen müssen und dürfen. Gültigkeitsüberprüfung: Der Parser prüft, ob die in der DTD definierten Anforderungen vom Dokument erfüllt sind. Ein Dokument kann zwar wohlgeformt sein, aber dennoch ungültig (falls die DTD nicht eingehalten sind). 39.1. Beispiel (interne DTD) <?xml version=“1.0“ encoding=“UTF-8“ ?> <!DOCTYPE person [ //(#PCDATA) bedeutet nur geparste Zeichendaten, keine Child-Elemente <!ELEMENT first_name (#PCDATA)> <!ELEMENT last_name (#PCDATA)> <!ELEMENT profession (#PCDATA)> //Geordnete Folge von Child-Elementen, alle müssen vorkommen (Reihenfolge) <!ELEMENT name (first_name, last_name)> //Quantoren: ? -> (0-1) * -> (0-n) + -> (1-n) <!ELEMENT person (name, profession*)> ]> <person> <name> <first_name>Thomas</first_name> <last_name>Kaelin</last_name> </name> <profession>drug dealer</profession> Kälin Thomas, Abt I 34/35 06.07.2006 Programmieren 2 <profession>information technologist</profession> </person> 39.2. Beispiel (externe DTD) <?xml version=“1.0“ encoding“UTF-8“ standalone=“no“ ?> <!DOCTYPE person SYSTEM „external.dtd“ > 39.3. Spezielles //Kreditkarte oder Bar-Child möglich <!ELEMENT Zahlungsbetrag (Kreditkarte|Bar)> <Zahlungsbetrag><Kreditkarte>Visa</Kreditkarte></Zahlungsbetrag> //Leere Elemente werden mit EMPTY deklariert <!ELEMENT BR EMPTY> <BR></BR> //Beliebige Elemente können Child-Elemente sein: ANY <!ELEMENT Weltall ANY> <Weltall>Unendlich</Weltall> <Weltall><Planet>Erde</Planet></Weltall> Kälin Thomas, Abt I 35/35 06.07.2006