Praktische Informatik: Datenstrukturen Vorlesungsmitschrift Praktische Informatik: Datenstrukturen Inhalt VORWORT .........................................................................................................................................................III 1 GRUNDLEGENDE DATENSTRUKTUREN .......................................................................................... 1 1.1 DYNAMISCHE DATENSTRUKTUREN....................................................................................................... 1 1.1.1 Klassen als Referenztypen ............................................................................................................... 1 1.1.2 Einfach verkettete Listen ................................................................................................................. 1 1.1.3 Doppelt Verkettete Liste .................................................................................................................. 4 1.1.4 Listenunterscheidungsmerkmale ..................................................................................................... 5 1.1.5 Anwendungen von Listen: Stack...................................................................................................... 6 1.1.6 Anwendungen von Listen: Queue .................................................................................................... 7 1.2 MENGEN ............................................................................................................................................... 8 1.2.1 Zahlenmengen in Java..................................................................................................................... 8 1.2.2 Implementierung als Bit-Array........................................................................................................ 8 1.2.3 Mengen beliebiger Objekte ............................................................................................................. 8 2 BÄUME ........................................................................................................................................................ 9 2.1 BEGRIFFE .............................................................................................................................................. 9 2.1.1 Eigenschaften von Bäumen: ............................................................................................................ 9 2.1.2 Binärer Suchbaum......................................................................................................................... 10 2.2 MEHRWEGBÄUME ............................................................................................................................... 12 2.3 HEAP .................................................................................................................................................. 13 2.3.1 Einfügen eines Elements................................................................................................................ 14 2.3.2 Entfernen des größten Elementes .................................................................................................. 14 3 GRAPHEN ................................................................................................................................................. 16 3.1 3.1.1 3.2 3.3 3.3.1 3.4 3.4.1 3.4.2 3.4.3 3.5 3.6 3.7 3.7.1 3.8 3.8.1 3.8.2 3.8.3 4 BEGRIFFE ............................................................................................................................................ 16 Speicherdarstellung von Graphen................................................................................................. 16 DEPTH-FIRST-SEARCH (DFS) ............................................................................................................. 18 BREADTH-FIRST-SEARCH (BFS)......................................................................................................... 19 Vergleich BFS/DFS ....................................................................................................................... 19 ALLGEMEINES SCHEMA ZUR GRAPHEN-TRAVERSIERUNG ................................................................... 19 BFS: Saum als Queue verwaltet.................................................................................................... 20 DFS: Saum als Stack verwaltet ..................................................................................................... 20 Allgemein: Saum als Priority Queue (Heap)................................................................................. 21 KLEINSTER SPANNENDER BAUM ......................................................................................................... 21 KÜRZESTER PFAD ............................................................................................................................... 23 TRANSITIVE HÜLLE EINES GRAPHEN .................................................................................................. 23 Implementierung ........................................................................................................................... 24 SERIALISIERUNG VON GRAPHEN ......................................................................................................... 24 Serialisierung von Graphen mit Java-Klassen.............................................................................. 24 Implementierung der Serialisierung eines Baumes ....................................................................... 25 Implementierung der Serialisierung eines Graphen ..................................................................... 25 HASHING .................................................................................................................................................. 26 4.1 4.2 4.2.1 4.2.2 4.2.3 4.3 4.4 4.5 ALLGEMEINES UND ALGORITHMEN .................................................................................................... 26 KOLLISIONEN...................................................................................................................................... 27 Seperate Chaining (Überlauflisten) .............................................................................................. 27 Linear Probing (Lineares Probieren) ........................................................................................... 28 Quadratisches Hashen .................................................................................................................. 28 HASHTABELLE VOLL .......................................................................................................................... 30 LÖSCHEN IN HASHTABELLEN .............................................................................................................. 30 BINÄRBAUM VS. HASHTABELLE ......................................................................................................... 30 5 JAVA KLASSENBIBLIOTHEKEN........................................................................................................ 30 6 STRING SUCHE ....................................................................................................................................... 30 6.1 6.2 6.3 6.4 DARSTELLUNG VON STRINGS.............................................................................................................. 30 BRUTE FORCE SEARCH ....................................................................................................................... 31 BOYER MOORE SUCHE ....................................................................................................................... 31 STRINGVERGLEICH MIT WILDCARDS .................................................................................................. 32 i Praktische Informatik: Datenstrukturen 6.5 7 REGULAR EXPRESSIONS...................................................................................................................... 33 HÖHERE SORTIERALGORITHMEN ................................................................................................. 33 7.1 7.2 7.3 7.4 HEAP SORT ......................................................................................................................................... 33 SORTIEREN LINEARER LISTEN ............................................................................................................. 34 EXTERNES SORTIEREN ........................................................................................................................ 35 TOPOLOGISCHE ALGORITHMEN .......................................................................................................... 35 ii Praktische Informatik: Datenstrukturen Vorwort Die Gelegenheit ein Vorwort zu einem Werk zu verfassen hat nicht jeder. Auch wenn es nur das Vorwort zu einer Vorlesungsmitschrift ist, die man selbst mitgeschrieben hat. Die meisten Vorwörter handeln von der Wichtigkeit des Werkes für die Fachwelt, geben einen Überblick über den Inhalt des Buches. Ich hingegen will in meinem Vorwort zum Ausdruck bringen, dass ich in keiner weise für den Inhalt des vorliegenden Werkes verantwortlich bin. Für den interessierten Leser sei auf die Bücher von Donald E. Knuth hingewiesen. Allerdings sind hier zirka 75€ für jeden der drei Bände zu veranschlagen. Wer allerdings einen Fehler in diesen Werken findet, der hat die Chance von Donald E. Knuth persönlich einen Scheck zu bekommen und so zumindest einen Teil der Ausgaben zurück zu bekommen. Noch einmal den Kern des Vorwortes in aller Kürze: Der Autor dieses Werkes ist in keiner Weise für den Inhalt verantwortlich. Wer Fehler findet darf sie behalten!!!! Martin Vetr iii Praktische Informatik: Datenstrukturen 1 Grundlegende Datenstrukturen 1.1 Dynamische Datenstrukturen Objekte, die mit Zeigern verbunden sind und beliebig wachsen kann. Die Form der Datenstruktur ist nicht vorgegeben und daher beliebig. Es gibt allerdings drei Grundformen: - Lineare Listen („Dynamisches Array“) - Baum (keine Rückwärtsvernetzung) - Graph (beliebiges Geflecht Æ Netze, Wegenetz, …) Der Speicherplatz wird erst auf explizite Anforderung des Benutzers allokiert. 1.1.1 Klassen als Referenztypen class Person{ String name; int phone; } Person p; //Speicherzelle noch leer, nur „Behälter“ p=new Person; //legt ein neues Objekt an, weist dies p zu p.name=“Huber“; //auf Member kann zugegriffen werden Das Freigeben von Objekten wird in Java vom GarbageCollector übernommen, und ist daher sehr einfach. In anderen Sprachen muss der allokierte Speicher manuell freigegeben werden. Die Zuweisung p=q; oder p=null; löst die Referenz auf das ursprüngliche Objekt, falls dies der einzige Zeiger auf das Objekt war löscht der GarbageCollector dieses. Ein anderer Fall ist das Verlassen des Gültigkeitsbereiches einer Variable, beispielsweise beim Verlassen einer Methode. Diese Freigabe kann sich über Verkettete Datenstrukturen fortpflanzen. 1.1.2 Einfach verkettete Listen Bei einfach verketteten Listen kann ein Knoten einen oder keinen Nachfolger haben. Der Vorteil einer linearen Liste gegenüber einem Array ist, dass die Liste ihre Größe verändern kann. D.h. sie kann Wachsen und Schrumpfen. Ein weiterer Vorteil ist, dass ohne größeren Aufwand überall Daten eingefügt werden können. Der Vorteil eines Arrays liegt in der kürzeren Zugriffszeit auf einzelne Objekte (Startadresse + n*Datensatzlänge) während eine Liste, sofern es keinen Zeiger auf das gesuchte Objekt gibt, sequentiell durchgearbeitet werden muss. 1.1.2.1 Unsortierte Liste Für die Nachfolgenden Algorithmen werden folgende Klassen verwendet: class Node{ int val; Node next; Node(int x){val=x;} } class List{ Node head=null; 1 Praktische Informatik: Datenstrukturen void insert(int val){...}; Node delete(int val){...}; Node search(int val){...}; ... } Einfügen am Listenanfang void insert(int val){ Node p=new Node(val); p.next=head; head=p; } Einfügen am Listenende Mithilfe eines neu eingeführten Zeigers tail, der auf das ende der Liste zeigt; void insert(int val){ Node p=new Node(val); if (tail==null) head=p; else tail.next=p; tail=p; } Suchen von Elementen Version Prof. Mössenböck Node search(int val){ Node p=head; while(p!=null&&p.val!=val) p=p.next; return p; } Version Vetr Node search(int val){ Node cnt=head; for(;cnt!=null;){ if (cnt.val==val) return cnt; cnt=cnt.next; } return null; } Löschen am Listenanfang Node remove() { Node p=head; if(head!=null) head=head.next; return p; } 2 Praktische Informatik: Datenstrukturen Löschen eines Knotens mit Schlüssel val Node delete(int val){ Node p=head; Node prev=null; while(p!=null&&p.val!=val){ prev=p; p=p.next; } if (p!=null){ if (p==head) head=head.next; else prev.next=p.next; } return p; } 1.1.2.2 Sortierte Liste Bei der sortierten Liste werden die Daten nach einem bestimmten Kriterium sortiert eingefügt. Einfügen Werte werden nur einmal eingefügt. void insert(int val){ Node p=head; Node prev=0; while(p==null||p.val<val){ prev=p; p=p.next; } if (p!=null&&p.val!=val){ Node q=new Node(val) q.next=p; if (p==head){ head=q; else prev.next=q; } } } Löschen in Sortierten Listen void delete(int val){ Node p=head, prev=null; while(p!=null&&p.val<val){ prev=p; p=p.next; } if (p!=null&& p.val==val){ 3 Praktische Informatik: Datenstrukturen if (p==head) head=p.next; else prev.next=p.next; return p; } return null; } 1.1.2.3 Sortierte Liste mit Kopfknoten Zirkulare Liste mit Dummynode am Listenkopf Initialisierung List(){ head=new Node(0); head.next=head; } Sortiertes Einfügen mit zulassen von Duplikaten void insert(int val){ head.val=val; //stopper Node p=head.next; Node prev=head; while(val>p.val){ prev=p; p=p.next; } // val <= p.val Node q=new Node (val); p.next=p; prev.next=q; } Löschen eines Knoten Node delete(int val){ head.val=val; Node p=head.next; Node prev=head; while (p.val!=val){ prev=p; p=p.next; } if (p==head) return null; prev.next=p.next; return p; } 1.1.3 Doppelt Verkettete Liste Erweiterung der Node um einen Zeiger auf das vorige Element class Node{ int val; 4 Praktische Informatik: Datenstrukturen Node next; Node prev; Node(int x){val=x;} } Initialisierung List(){ head=new Node(0); head.next=head; head.prev=head; } Einfügen in die Liste void insert(int val){ head.val=val; //stopper Node p=head.next; while (val>p.val) p=p.next; //val <=p.val Node q=new Node(val); q.next=p; q.prev=p.prev; p.prev.next=q; p.prev=q; } Löschen Node delete(int val){ head.val=val; Node p=head.next; while (val!=p.val) p=p.next; if (p==head) return null; p.prev.next=p.next; p.next.prev=p.prev; return p; } 1.1.4 Listenunterscheidungsmerkmale Verkettung - einfach - doppelt Ordnung der Elemente - unsortiert - sortiert Aufbau - Linear - Zirkular (Ring) Möglich sind alle Kombinationen dieser sechs Unterscheidungsmerkmale 5 Praktische Informatik: Datenstrukturen 1.1.5 Anwendungen von Listen: Stack Der Stack ist auch unter den Namen Kellerspeicher und LIFO (last in first out) bekannt. z.B. Ausdrucksberechnung ohne Klammern: 5*(9+7): push(5); push(9); push(7); push(pop()+pop()); push(pop()*pop()); Diese Art der Notation wird von HP Taschenrechnern verwendet und ist bekannt unter dem Namen RPN (reverse polnish notation) oder UPN (Umgekehrt Polnische Notation). Stack mit Array, begrenzte Größe class Stack{ int [] stack; int top; Stack(int size){ stack=new int[size]; top=0; } void push(int x){ if (top<stack.length) stack[top++]=x; else panic(now); } int pop(){ if (top>0) return stack[--top]; else panic(now); } } Stack als Liste, Größe nur Hardwarebegrenzt class Stack{ Node top=null; void push(int x){ Node p=new Node (x); p.next=top; top=p; } int pop(){ if (top==null) panic(now); else{ int val=top.val; top=top.next; return val; 6 Praktische Informatik: Datenstrukturen } } } 1.1.6 Anwendungen von Listen: Queue Schlange, Puffer, FIFO (First In First Out) Zyklische Array durch Modulo-Operation des Index (idx%länge) Implementierung mit einem Array (begrenzte Größe) class Queue{ int [] queue; int head, tail; Queue(int size){ queue=new int[size]; head=tail=0; } void put(int x){ int tail1=(tail+1)%queue.length; if (tail1==head) panic(now); else{ queue[tail]=x; tail=tail1; } } void get(){ if (head==tail) panic(now); else{ int val=queue[head]; ++head%=queue.length; return val; } } } Implementierung als Liste mit Dummy-Knoten class Queue{ Node head, tail; Queue(){ head=tail=new Node(0); } void put(int x){ Node p=new Node(x); tail.next=p; tail=p; } int get(){ if (head==tail) panic(now); else{ head=head.next; 7 Praktische Informatik: Datenstrukturen return head.val; } } } 1.2 Mengen 1.2.1 Zahlenmengen in Java BitSet s=newBitSet(); s.set(3); //Hinzufügen s.clean(3); //Entfernen s.get(3); //Element von s? s.size(); s1.or(s2); //S1+S2 s1.and(s2); //S1*S2 s1.xor(s2); // S1+S2\S1*S2 1.2.2 Implementierung als Bit-Array Jedes Element wird durch ein Bit repräsentiert. Ist das entsprechende Bit gesetzt, so ist es in der Menge enthalten. int s=0; s=s|(1<<elem); Für Mengen die durch die Datentypgröße begrenzt sind, im Beispiel sind es 32 Elemente. Mengen mit mehr als 32 Elementen werde durch Arrays dargestellt. Durch eine Division findet man das richtige Element im Array, durch eine Modulo Operation findet man das richtige Bit im Array. class BitSet{ int[] s; void set(int i){ s[i/32]|= 1<<(i%32); } void clear(int i){ s[i/32]&= ~(1<<(i%32)); } boolean get (int i){ return (s[i/32]&(1<<(i%32)))!=0; } } Mengen Alphanumerischer Zeichen werden über Zahlenmengen implementiert. Als Identifikation dienen die jeweils zugewiesenen Zahlenwerte (Code-Tabelle). 1.2.3 Mengen beliebiger Objekte Implementierung über lineare Listen Langsam, komplexe Mengenoperationen Bitmengen (Abbildung ObjektÆZahl) Objekttabelle (Objektarray) 8 Praktische Informatik: Datenstrukturen Abfragen auf Existenz von Objekten laufen über die Bitmengen, während die tatsächlichen Zugriffe auf die Elemente über das Objektarray funktionieren. class Obj{ int nr; static int uniqueNr; static Obj[] obj; Obj(){ nr=uniqueNr; uniqueNr++; obj[nr]=this; } } s.set(x.nr); //Aufnehmen von x in die Menge s if (s.get(x.nr))....; //Ist x in s? for (int i=0; i<s.size();i++) //Alle Elemente von s verarbeiten if (s.get(i)) doSomethingWith(obj[i]); 2 Bäume 2.1 Begriffe Jeder Knoten kann mehrere Nachfolger haben (im Gegensatz zu Listen). Die Anzahl der Nachfolger kann von Node zu Node variieren. Darstellung von Organisationsstrukturen Wurzel/ 1. innerer Knoten Stufe 1 Stufe 2 Stufe 3 Innerer Knoten Blatt Innerer Knoten Blatt Blatt Innerer Knoten Blatt Blatt Binärbaum: alle Knoten haben max. Grad 2 Vollständiger Binärbaum: Binärbaum der in allen Blättern die gleiche Höhe hat. (Balancierter Binärbaum) 2.1.1 Eigenschaften von Bäumen: - enthält keine Zyklen Jeder innere Knoten oder Blattknoten hat genau einen Vater 9 Praktische Informatik: Datenstrukturen - Die Wurzel hat keinen Vater n Knoten Æ n-1 Kanten Jeder vollständige Binärbaum mit n inneren Knoten hat n+1 Blätter 2.1.2 Binärer Suchbaum val val < p.val val > p.val Idealerweise ist der linke Teilbaum gleich groß wie der rechte Teilbaum. Alle Werte im Linken Teilbaum sind kleiner als die des in der aktuellen Node, die Werte des Rechten Teilbaums sind durchwegs größer oder gleich dem Wert der aktuellen Node. class Node{ int val; Node left, right; static Node search(Node p, int val){ if (p==null) return null; if (p.val==val) return p; if (val<p.val) return search (p.left, val); return search (p.right, val); } } Iterative Lösung des binären Suchens static Node search (Node p, int val){ while (p!=null&&p.val!=val){ if (val<p.val) p=p.left; else p=p.right; } return p; } Einfügen von Knoten in Binären Suchbäumen Neue Knoten werden immer als Blatt eingefügt, um den Baum möglichst balanciert zu halten. void insert (int val){ Node p=root; Node father=null; while (p!=null){ father=p; if (val<p.val) p=p.left; 10 Praktische Informatik: Datenstrukturen else p=p.right; } p=newNode(val); if (root==null) root=p; else if (val<father.val) father.left=p; else father.right=p; } Rekursiv Node insert(int val, Node p){ if (p==null) p=newNode(val); else if(val<p.val) p.left=insert(p.left,val); else p.right=insert(p.right, val); return p; } Löschen eines Blattes Man ersetzt den zu löschenden Knoten durch den nächst größeren (oder den nächst kleineren). Die Idee dazu ist nicht allzu komplex, es ergeben sich aber bei genauerer Betrachtung einige Fallunterscheidungen: - kein größerer (rechter) Teilbaum - größerer (rechter) Teilbaum vorhanden Node delete(int val){ Node father=null, p=root; while(p!=null && p.val!= val){ father = p; if (val<p.val) p=p.left; else p=p.right; } if (p!=null{ //suche node x=ersatz für p Node x; if (p.right==null){ x=p.left; }else if(p.right.left==null){ x=p.right; x.left=p.left; }else{ Node xf=p.right; x=xf.left; while(x.left!=null){ xf=x; x=x.left; } x.left=p.left; xf.left=x.right; x.right=p.right; } if (p==root) 11 Praktische Informatik: Datenstrukturen root=x; else if(val<father.val) father.left=x; else father.right=x; } return p; } Traversieren von Bäumen Man unterscheidet drei Arten von Durchläufen für Bäume: Preorder: root, links, rechts Æ +*24/93 Postorder: links, rechts, root Æ 24*93/+ Inorder: links, root, rechts Æ 2*4 + 9/3 void preOrder(Node p){ if (p!=null){ doSomethingWith(p.val); preOrder(p.left); preOrder(p.right); } } void postOrder(Node p){ if (p!=null){ postOrder(p.left); postOrder(p.right); doSomethingWith(p.val); } } void inOrder(Node p){ if (p!=null){ inOrder(p.left); doSomethingWith(p.val); inOrder(p.right); } } 2.2 Mehrwegbäume Mehrwegbäume können als Binärbäume dargestellt werden. Der Vorteil ist die beliebige starke Verzweigung durch die Verwendung von Sohn und Bruder Zeigern. Jeder Knoten hat dabei nur maximal einen Sohn, dieser kann aber, über eine art Verkettete Liste beliebig viele 12 Praktische Informatik: Datenstrukturen Brüder haben. Der Nachteil ist der unbequemere Zugriff im Vergleich zum original Mehrwegbaum. Nächste Kapitel siehe Skript 2.3 Heap PriorityQueue - insert(x) - x=remove() Die Funktion remove() holt das größte Element aus der PQ heraus. Implementierungsmöglichkeiten: - unsortierte Liste (Einfügen funktioniert schnell, entfernen langsam da alle Elemente durchlaufen werden) - sortierte Liste (einfügen langsam, entfernen schnell) - binärer Suchbaum (der Baum degeneriert!!) - Heap (einfügen und entfernen schnell) Architektur des Heaps: Der Heap ist ein Binärbaum, bei dem alle Stufen bis auf die letzte vollständig gefüllt. Die letzte Stufe ist von Links her gefüllt. Der Vater ist größer oder gleich beiden Söhne. Beim Heap handelt es sich nicht um einen Suchbaum, es gibt auch keine Festgelegte Reihenfolge zwischen den Söhnen. Die Wurzel enthält aber in jedem fall den größten Wert. Dieser Baum kann, wie oben zu sehen, in einem Array dargestellt werden. In dieser Darstellung ergibt sich: 13 Praktische Informatik: Datenstrukturen Vater von a[i]: a[i/2] Söhne von a[i]: a[i*2], a[i*2+1] 2.3.1 Einfügen eines Elements - hinten an das Array anfügen heap-Ordnung wiederherstellen void insert(char x){ x++; a[n]=x; upHeap(n); } void upHeap(int pos){ while (pos>1&&a[pos]>a[pos/2]){ char h = a[pos]; a[pos]=a[pos/2]: a[pos/2]=h; pos=pos/2; } } Den ersten Teil der Schleifenbedingung in upHeap kann man einsparen, wenn man in das Element auf der Stelle 0 (Dummy-Element) den größten möglichen Wert einspeichert. 2.3.2 Entfernen des größten Elementes - Wurzel enthält größtes Element 14 Praktische Informatik: Datenstrukturen - Letztes Element an die Wurzel schieben Heap-Ordnung wiederherstellen char remove(){ char x=a[1]; a[1]=a[n]; n--; downHeap(1); return x; } void downHeap(int pos){ char h; for (;;){ int son=2*pos; if (son>n) break; //keine söhne mehr (töchter?) if (son<n&&a[son]<a[son+1])son++; if (a[son]<=a[pos]break; h=a[son]; a[son]=a[pos]; a[pos]=h; pos=son; 15 Praktische Informatik: Datenstrukturen } } Laufzeit: Einfügen und Entnehmen: O(log n) 3 Graphen 3.1 Begriffe - Knoten (Vertex, V), Kanten (Edge, E) Pfad (Verbundene Kantenfolge) Zyklus, einfacher Pfad Zusammenhängender Graph (für alle Knoten x,y: Pfad (x,y)) Gerichteter und ungerichteter Graph Gewichteter Graph Vollständiger Graph: Kanten zwischen allen Knoten (direkte Verbindung aller Knoten) 3.1.1 Speicherdarstellung von Graphen Adjazenzliste: Die Adjazenzliste speichert immer die Nachbarelemente. 16 Praktische Informatik: Datenstrukturen class Graph{ Link first; } class Link{ Node node; Link next; } class Node{ //data Link neighors; } Adjazenzmatrix A B C D A 0 0 1 1 B 0 0 0 1 C 1 0 0 1 D 1 1 1 0 Darstellung gewichteter Graphen mit Hilfe einer Adjazenzmatrix: A B C D 1 a c 2 5 d b 7 A B C D Direkte Verpointerung: class Node{ //data Node left, right; } 0 0 1 5 0 0 0 7 1 0 0 2 5 7 2 0 17 Praktische Informatik: Datenstrukturen 3.2 Depth-First-Search (DFS) Idee: visit(p){ ...process p... for (all sons s of p) visit(s) } Der Algorithmus muss Zyklen erkennen und abfangen. Æ Markierung „ich war da! Tanja + Michael 17.8.2002“. void DFS(Node p){ p.marked=true; ...process p... for (int i=0; p.son[i]!=null; i++) if (!p.son[i].marked) DFS(p.son[i]); } Rücksetzen der Markierung - Extra-DFS mit p.marked=false - Markierungsset - Markieren mit invertieren Invertieren: void DFS(Node p){ p.marked!=p.marked; ...process p... for (int i=0; p.son[i]!=null; i++) if (p.son[i].marked!=p.marked) DFS(p.son[i]); } Markierungsset void DFS(Node p){ markSet.set(p.nr); ...process p... for (int i=0; p.son[i]!=null; i++) if (!markSet.get(p.son[i].nr)) DFS(p.son[i]); } 18 Praktische Informatik: Datenstrukturen 3.3 Breadth-First-Search (BFS) void BFS(Node p){ Queue queue=new Queue(); p.marked=true; do{ ...process p... for (int i=0; p.sun[i]!=null;i++{ Node son=p.son[i]; if (!son.marked){ son.marked=true; queue.put(sun); } } p=queue.get(); }while(p!=null); } 3.3.1 Vergleich BFS/DFS Durch die Verwendung eines Stacks an Stelle der Queue im BFS Algorithmus würde einen DFS Algorithmus erzeugen. Diese Beobachtung legt nahe, dass es sich bei beiden Algorithmen um einen Spezialfall eines allgemeinen Algorithmus handelt. 3.4 Allgemeines Schema zur Graphen-traversierung Die Knoten zerfallen in diesem Schema in drei Knoten: 19 Praktische Informatik: Datenstrukturen - Baumknoten (bereits besuchte Knoten) Saumknoten (unbesuchte Nachbarknoten der Baumknoten) Ungesehene Knoten void traverse(){ Baumknoten={}; //leere Menge Saumknoten={Wurzel}; while (noch nicht alle Knoten Baumknoten){ Mache einen Saumknoten k zum Baumknoten; Mache ungesehene Nachbarknoten von k zu Saumknoten; } } 3.4.1 BFS: Saum als Queue verwaltet Saum: BCD CDE 3.4.2 DFS: Saum als Stack verwaltet Saum: 20 Praktische Informatik: Datenstrukturen BCD BCFG 3.4.3 Allgemein: Saum als Priority Queue (Heap) Nächster Knoten=Knoten mit der höchsten Priorität. 3.5 Kleinster spannender Baum Als kleinster spannender Baum wird jener Baum bezeichnet, der alle Knoten des Graphen verbindet, alle unnötigen Kanten weglässt und nur jene verwendet, sodass das Kantengewicht kleinstmöglich ist. Idee: Priority First Search void ksb(){ p=wurzel(); Baumknoten={p} Saumknoten={} while(noch nicht alle Saumknoten Baumknoten){ mache ungesehen Nachbarn v. p zu Saumknoten; p=Saumknotem mit dem kleinsten Abstand zu irgendeinem Baumknoten dad. Nimm Knoten dad Æ p in KSB auf Mache p zu Baumknoten } } Durchlaufbeispiel: Der Graph hat keinen eindeutigen spannenden Baum, es gibt in manchen Schritten mehrere Knoten mit gleichem Abstand. 21 Praktische Informatik: Datenstrukturen 6 1 1 1 1 4 4 1 6 2 2 2 1 2 6 1 1 1 1 4 4 1 6 2 2 2 1 2 6 1 1 1 1 4 4 1 6 2 2 2 1 6 1 1 4 2 1 2 22 2 Praktische Informatik: Datenstrukturen Identifikation der Knotentypen: Baumknoten: marked Saumknoten: !marked && pqpos>0 Ungesehen: !marked &&pqpos==0 void MST(Node p){ Heap heap=new Heap; while(p!=null){ p.marked=true; for (int i=0; p.son[i]!=null;i++){ Node son=p.son[i]; if (!son.marked){ if(son.pos==0){ son.minWeight=p.weight[i]; son.dad=p; heap.insert(son); }else if(p.weight[i]<son.minWeight){ son.minWeight=p.weight[i]; son.dad=p; heap.upHeap(son.pos); } } } p=heap.remove(); } } 3.6 Kürzester Pfad Geg: 2 Knoten Ges: kürzester weg zwischen diesen Knoten Lösung: PFS minWeight: Kürzeste Distanz zum Anfangsknoten 3.7 Transitive Hülle eines Graphen Transitivität: Pfad(A,B) & Pfad(B,C) Æ Pfad(A,C) Hülle: Pfad(A,B) Æ Kante(A,B) Bei den Schwarzen Kanten handelt es sich um die Originalkanten des Graphen, bei den blauen Kanten handelt es sich um all jene, die durch die Transitive Hülle zum Graphen hinzukommen. 23 Praktische Informatik: Datenstrukturen for (all nodes p) verlängere Kanten nach p auf Nachfolger von p 3.7.1 Implementierung //Adjadzenzmatrix: boolean[][] tab; void closure(){ for (int j=0; j<maxNodes; j++) for (int i=0; i<maxNodes; i++) if (tab[i][j]) //weg von i nach j for (int k=0; k<maxNodes; k++) if (tab[j][k]) tab[i][k]=true; Die Laufzeitkomplexität dieses Algorithmus ist O(n3) Aus der Betrachtung als Bitmatrix erfolgt folgender Algorithmus mit einer um n verringerten Laufzeitkomplexität (daher gilt O(n2)): //Adjadzenzmatrix: boolean[][] tab; void closure(){ for (int j=0; j<maxNodes; j++) for (int i=0; i<maxNodes; i++) if (tab[i][j]) //weg von i nach j tab[i].or(tab[j]); } 3.8 Serialisierung von Graphen Persistenz: Graph überlebt das erzeugende Programm Problem: Abbildungen - Graph in linearisierte Form bzw. linearisierter GraphÆGraph - Zeiger abbilden auf Indizes bzw. Indizes auf Zeiger 3.8.1 Serialisierung von Graphen mit Java-Klassen import java.io; void schreiben(){ ObjectOutputStream s=new ObjectOutputStream(new FileOutputStream("xxx.txt")); s.writeObject(root); s.flush(); } Node lesen(){ ObjectInputStream s=new ObjectInputStream(new FileInputStream("xxx.txt")); Node root=(Node) s.readObject(); return root; 24 Praktische Informatik: Datenstrukturen } Notwendig für die Funktion des obigen Beispiels ist die Implementierung des Serializable Interface in der Node-Klasse. 3.8.2 Implementierung der Serialisierung eines Baumes void writeNode(Node p){ if (p==null) writeInt(0); else{ writeInt(1); ...write p.data... for (int i=0;i<maxSons;i++) writeNode(p.son[i]); } } Node readNode(){ int n=readInt(); Node p; if (n==0) p=null; else{ p=new Node(); ...read p.data... for (int i=0; i<maxNodes;i++){ p.son[i]=readNode(); } } return p; } 3.8.3 Implementierung der Serialisierung eines Graphen Ziel: - jeden Knoten nur einmal zu speichern Alle Zeiger auf einen Knoten müssen nach dem einlesen wieder auf den selben Knoten zeigen. Lösung: - Knoten durchnummerieren (p.n) - Anstelle der Zeiger werden die Knotennummern hinausgeschrieben void writeNode(Node p){ static int n=0; if (p==null)writeInt(0); else if (p.n==0){ //erster Besuch n++; writeInt(n); p.n=-n; ...write p.data... for(int i=0; i<maxSons;i++){ writeNode(p.son[i]); }else{ //da war ich schon mal!!! 25 Praktische Informatik: Datenstrukturen writeInt(p.n); } } } Node readNode(){ Node p; int n=readInt(); if (n==0) p=null; else if (n>0){ p=new Node(); tab[n]=p; p.n=0; ...read data... for(int i=0; i<maxNodes;i++) p.son[i]=readNode; }else{//n<0 p=tab[-n]; } return p; } 4 Hashing 4.1 Allgemeines und Algorithmen Lineare Suche Binäres Suchen Hashen O(n) O(ld n) O(1) Es gibt n Schlüssel aber nur m<<n Adressen. Es gibt daher folgende Anforderungen an den Hash-Algorithmus: - möglichst gute Streuung - effiziente Berechnung Folgende Möglichkeiten: a) Schlüssel hat Wortgröße adr=key%tabSize; die tabSize sollte eine Primzahl sein um eine möglichst gute Streuung zu erhalten b) Schlüssel ist größer als ein Wort Bytes aufaddieren: 26 Praktische Informatik: Datenstrukturen Zeichen Gerade Versetzt A 65 0x01000001 n 110 0x01101110 t 116 0x01110100 o 111 n 110 Adresse 512%499=13 2716%499=221 Eine Permutation der Zeichen im Key ergibt den selben Schlüssel bei geradlinigem Addieren, als Ausweg gibt es die Möglichkeit des versetzten Addierens Source für versetze Hashberechnung: int hash(String key){ int adr=0; for (int i=0; i<key.length(); i++) adr=(2*adr+key.charAt(i))%tabSize; return adr; } Bei langen Schlüsseln ist unter Umständen folgendes Effizienter: adr=(key.charAt(0)*prime+key.charAt(key.length()-1)+key.length())%tabSize; Bsp. Anton: (65*17+110+5)=1220%499=222 4.2 Kollisionen Die Hashfunktion ist keinesfalls Eindeutig. Zur Erkennung der Kollision wird der Key in der Hashtabelle mitgespeichert. In diesem Fall muss für Key2 eine neue Adresse gesucht werden. Allgemein gilt, dass eine Hashtabelle nur zur hälfte gefüllt sein sollte um effizient zu zugreifen zu können. 4.2.1 Seperate Chaining (Überlauflisten) 27 Praktische Informatik: Datenstrukturen Data search(String key){ int adr=hash(key); Node p=tab[adr]; while(p!=null && p.key!=key)p=p.next; if (p!=null) return p.data; return null; } 4.2.2 Linear Probing (Lineares Probieren) Idee: alle Elemente in derselben Tabelle. Versuch i auf adr+i void insert(String key, Data data){ int a =hash(key); while(tab[a] besetzt)a=(a+1)%tabSize; tab[a].key=key; tab[a].data=data; } Diese Technik führt leicht zur Bildung von Clustern. 4.2.3 Quadratisches Hashen Aus dem Linear Probing abgeleitete Technik, die versucht Cluster zu vermeiden, in dem man um das Quadrat der Versuchsnummer weitergeht. Idee: Versuch i auf der Stelle adr+i² Die Sequenz kann aber ohne quadrieren erzeugt werden. 1 1 3 5 7 9 4 9 16 Die Summe (unten keilförmig geschrieben) der ersten n Zahlen ergibt dabei n². void insert(String key, Data data){ int a=hash(key); int d=1; while(tab[a] besetzt){ a=(a+d)%tabSize; d=d+2; 28 Praktische Informatik: Datenstrukturen } tab[a].key=key; tab[a].data=data; } Nachteil dieses Verfahrens ist, dass nicht alle Adressen besucht werden. Ab d==TabSize wiederholen sich die Adressen. Trick, wie alle Listenplätze besucht werden: - d läuft von –tabSize bis +tabSize - tabSize=4*i+3 für beliebige i (tabSize sollte noch immer eine Primzahl sein!) void insert(String key, Data data){ int a=hash(key); int d=-tabSize; while(tab[a] besetzt){ d=d+2; a=(a+Math.abs(d))%tabSize; } tab[a].key=key; tab[a].data=data; } 4.2.3.1 Algorithmen ohne Pseudocode: void insert(String key, Data data){ int a=hash(key); int d=-tabSize; while(tab[a].key!=null&&tab[a].key!=key&&d<tabSize){ d=d+2; a=(a+Math.abs(d))%tabSize; } if (d>=tabSize) panic(now); else{ tab[a].key=key; tab[a].data=data; } } Data search(String key){ int a=hash(key); int d=-tabSize; while(tab[a].key!=null&&tab[a].key!=key&&d<tabSize){ d=d+2; a=(a+Math.abs(d))%tabSize; } if (tab[a].key==key) return tab[a].data; return null; } 29 Praktische Informatik: Datenstrukturen 4.3 Hashtabelle Voll Um die Hashtabelle zu vergrößen wird eine größere (zirka doppelt so große) Hashtabelle angelegt. Nachdem die Adressen über die Tabellengröße verwaltet werden, ist es notwendig alle Schlüssel neu zu berechnen. 4.4 Löschen in Hashtabellen - beim Seperate Chaining ist dies kein Problem, funktioniert wie bei Linearen Listen beim Quadratischen Hashen (und linearen Probieren) ist dies ein Problem Es ist nur ein logisches Löschen möglich, da sonst die Kette unterbrochen werden würde und alle nachkommenden einträge verloren gingen. Der Eintrag XXX wird beim suchen als besetzt und ungültig und beim einfügen als frei zu betrachten. 4.5 Binärbaum vs. Hashtabelle Binärbaum +kann nicht voll werden +einfaches Löschen +liefert Sortierung „gratis“ +kein Speicher verschwendet werden Hashtabelle +schnell 5 Java Klassenbibliotheken Informationen über Java Klassenbibliotheken entnehmen Sie bitte der API oder im Web verfügbaren Büchern wie „Handbuch der Java-Programmierung“ (www.javabuch.de). 6 String Suche 6.1 Darstellung von Strings Pascal verwendet eine Längenterminierung, hat daher eine begrenzte String-Länge. C/C++ verwendet eine 0-Terminierung, dieses Zeichen kommt im normalen Text nicht vor, es ermöglicht beliebig lange Strings. Für alle Beispiele in diesem Kapitel werden 0-Terminierte Strings verwendet. 30 Praktische Informatik: Datenstrukturen 6.2 Brute Force Search Beim Brute Force Ansatz wird der zu durchsuchende String linear durchsucht, bei Übereinstimmung werden die restlichen Zeichen verglichen. int bruteSearch(char[] text, char[] pat){ for (int i=0; text[i]!=0; i++){ if (text[i]==pat[0]){ for(int j=0; pat[j]!=0&&pat[j]==text[i+j];j++); if (pat[j]==0) return i; } } return -1; } Laufzeit: O(textlen*patlen) in der Praxis allerdings nur leicht überlinear: ca. O(textlen); 6.3 Boyer Moore Suche Verfahren: 1. Prüfe Übereinstimmung von text[i] mit pat[last] 2. Wenn keine Übereinstimmung: wo kommt text[i] in pat zuletzt vor? a. Gar nicht: verschiebe pat um seine ganze länge. b. in pat[j]: verschiebe pat[j] unter text[i] Vorausberechnung der Verschiebedistanz: i A B C D ? D C B A else E ? Skip(?) 1 2 3 4 5 j int[] buildSkip(char[]pat, int patlen){ int[] skip=new int[128]; //Standard ASCII Char Set 31 Praktische Informatik: Datenstrukturen int i; for (i=0; i<128; i++) skip[i]=patlen; for (i=0; i<patlen-1; i++) skip[pat[i]]=(patlen-1)-i; return skip; } int boyerMoore(char[] text, char[] pat, int textlen, int patlen){ int[] skip=buildSkip(pat, patlen); int i=patlen-1; char patEnd=pat[i]; while (i<textlen){ char ch=text[i]; if (ch==patEnd){ int j=i; int k=patlen-1; do{ if (k==0) return j; j--; k--; }while(text[j]==pat[k]) } i+=skip[ch]; } return -1; } Laufzeit des Algorithmus: BestCase: O(textlen/patlen) WorstCase: O(textlen*patlen) 6.4 Stringvergleich mit Wildcards Beispiele für Pattern: *.java, *xy*, … Idee: A B C A D E A * D A * D E E D E boolean match(char[] text, char[]pat, int t, int p){ 32 Praktische Informatik: Datenstrukturen while(pat[p]!='*'){ if (pat[p]!=text[t]) return false; if (pat[p]==0) return true; t++; p++; } while(pat[p]=='*')p++; if(pat[p]==0) return true; while(text[t]!=0){ if(match(text, pat, t, p)) return true; t++; } return false; } 6.5 Regular Expressions Bei der suche nach Regulären Ausdrücken werden endliche Automaten implementiert, die diese Aufgabe übernehmen. 7 Höhere Sortieralgorithmen Bekannte Sortierverfahren - Bubble Sort - Insertation Sort - Quicksort - Shellsort Hier besprochene Verfahren - Heap Sort - Sortieren linearer Listen - Externes Sortieren - Topologisches Sortieren 7.1 Heap Sort Idee: insert (5); insert (3); insert (7); remove();//7 remove();//5 remove();//3 Einfache Implementierung: void sort(int[] a){ for(int i=0;i<a.length; i++) heap.insert(a[i]); for(int i=0;i<a.length; i++) a[i]=heap.remove(); } Laufzeit: O(n*log(n)+n*log(n))=O(2n*log(n))=O(n*log(n)) 33 Praktische Informatik: Datenstrukturen Verbesserte Implementierung Die Heap-Ordnung wird nun direkt im Array hergestellt, dadurch wird kein zusätzlicher Speicherplatz verwendet. for (int i=a.length; i>0; i--) downHeap(a,i); //assert a[i]: Root of a Heap Durch die Heap-Ordnung ist es nicht Notwendig alle Elemente umzusortieren, es genügt, wenn man beim Vater des letzten Elementes beginnt: for (int i=a.length/2; i>0; i--) downHeap(a,i); //assert a[i]: Root of a Heap Kompletter, verbesserter Algorithmus: void heapSort(int[]a, int n){ for(int i=n/2;i>0;i--) downHeap(a,n,i); do{ int max=a[1]; a[1]=a[n]; a[n]=max; n--; downHeap(a,n,1); }while(n>1); } Laufzeit: O(n)+O(n log(n)) Quicksort: O(n log(n)) bis O(n²) In der Praxis ist allerdings der immer der Quicksort schneller. 7.2 Sortieren linearer Listen Bekannte Algorithmen lassen sich auf lineare Listen nur schwer anwenden, da das direkte indizieren nicht möglich ist. Hat man allerdings zwei sortiere Liste, so lassen sich diese leicht zu einer sortieren Liste zusammen „mischen“ (Æ Merge-Sort). Annahme: Die Listen werden durch ein spezielles nil-Element terminiert. class Node{ int key; Node next; } Node merge(Node a, Node b){ Node last=nil; do{ if(a.key<b.key){ last.next=a; last=a; a=a.next; }else{ last.next=b; 34 Praktische Informatik: Datenstrukturen last=b; b=b.next; } }while(last!=nil); nil.next=nil; return b; } Mischsortieren 1. Teile die Liste in 2 gleich lange Hälften 2. jede Hälfte wird rekursiv sortiert 3. mischen der beiden sortierten Hälften Node sort(Node c){ if(c.next==nil) return c; Node a=c; Node b=c.next.next; while (b!=nil){ c=c.next; b=b.next.next; } b=c.next; c.next=nil; return merge(sort(a),sort(b)); } 7.3 Externes Sortieren Wenn große Datenmengen vorhanden sind, ist es nicht immer möglich, diese im Hauptspeicher zu Sortieren. aus diesem Grund werden Algorithmen verwendet, die nicht die gesamte Datenmenge auf einmal im Hauptspeicher haben. Diese Verfahren sortieren einzelne Blöcke und sortieren diese dann zusammen (MergeSorting). Das zusammenfügen geschieht ähnlich wie bei den linearen Listen. 7.4 Topologische Algorithmen 35