Praktische Informatik: Datenstrukturen

Werbung
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
Herunterladen