1997-03-04 Einführung
1997-03-05 Aufwandsabschätzung
1997-03-11 Dynamische Datenstrukturen: Listen
1997-03-12 Bäume
1997-04-08 Suchbäume, Quadtrees, Linearisierung von Bäumen, Stacks
1997-04-09 Hash-Tabellen, Queues
1997-04-15 Heap, Stringsuche
1997-04-16 Graphen
1997-04-22 Sortieren (Selection, Insertion, Bubble-, Merge, Heap-, Quicksort)
1997-04-23 Sortieren (Radix Exchange, Distribution Counting)
1997-04-29 Objektorientierte Programmierung
1997-04-30 OOP
1997-05-23 2. Test
Wichtige Adressen:
ad1admin#cg.tuwien.ac.at AlgoDat 1 via E-Mail
www.cg.tuwien.ac.at/~ad1admin Infos im WWW
news:at.tuwien.lva.cg.ad1 Newsgroup
(wp#cg.tuwien.ac.at Prof. Werner Purgathofer)
Übungsgruppen
mindestens 5 Treffen verpflichtend, beim 1. Treffen Anwesenheitspflicht
Anmeldung: Dienstag, 4. März 12:30 - Montag, 10. März 17:00
2 Beispiele in Turbo Pascal 6.0 (Beispielangaben beim 1. Gruppentreffen)
Vorabgabe und Hauptabgabe
2 Abschnitte, je ein Beispiel, jedes Beispiel bis zum Ende des jeweiligen Abschnitts abgeben
Zuerst Vorabgabe, 2 Wo später Hauptabgabe beim/r TutorIn
Tests: 2 notwendig. Beispieltest statt Abgabe von Beispielen möglich, allerdings viel strengeres Beurteilungsschema (aber auch nicht so schlimm).
Listen, Bäume, Stack, Queue, Heap, Graphen, Hashtabelle, Stringsuche, Sortierverfahren, OOP
Mehrere gleichartige Daten können in ein Feld (array) zusammengefaßt werden und mittels einer Nummer (Index) angesprochen werden (indiziert werden):
Pascal:
TYPE Feld=ARRAY[1..n] OF INTEGER;
VAR a: Feld;
Matrixmultiplikation:
FOR i:=1 TO n DO
FOR j:= 1 TO n DO
BEGIN
z[i,j]:=0;
FOR k:=1 TO n DO
Z[i,j]:=z[i,j]+ x[i,k]*y[k,j]
Zusammengehörige verschiedenartige Daten werden zusammengefaßt.
Kriterien:
z.B. sequentielle Suche <=> binäre Suche in einem sortierten Array
sequentiell: Mittel: (max+1)/2 Schritte, worst case: max Schritte
binär: Mittel: (ld max) -1, worst case: ld max Schritte.
Aufwand eines Algorithmus (O):
Eine Funktion (ein Algorithmus) g(N) wird als O(f(N)) bezeichnet, wenn es Konstante c0 und N0 gibt, sodaß g(N)<c0*f(N) für alle N>N0. (Bedeutet: Es gibt eine bestimmte Zahl, ab der das eine Verfahren besser ist)
Wichtige Feststellungen für O
Beispiele: O(7 N2)=O(N2); O(4 lg2N)=O(ld2N); O(3 N2 + N + 100) = O(N2); O(N + ld2N) = O(N)
Übliche O-s von Algorithmen
Beispiele:
Kurze Pause: Prof. Purgathofer erzählt (teils burgenländerfeindliche) Witze. In Zukunft sollen die Studierenden selbst aktiv werden, z.B. Jonglierkünste vorzeigen oder selbst Witze erzählen.
Im Unterschied zu einfachen Arrays "wissen" die Datensätze selbst, wo der nächste Datensatz beginnt. Zugriff auf den Datensatz bei einem Zeiger: p^ (= "Dereferenzieren").
Der Speicher des Elements wird freigegeben und der next-Zeiger
des vorhergehenden Elements wird auf den nächsten Knoten
"verbogen". Wenn die Liste nur in einer Richtung verkettet
ist, muß das vorherige Element umständlich gesucht
werden. Sonderfall: Anfang der Liste soll gelöscht werden:
der Anfang der Liste wird "weitergeschoben".
Beim Einfügen wird gleich die richtige Position gesucht, damit die Liste sortiert bleibt: Es wird das erste Element gesucht, das größer ist als die einzufügende Information. Vor diesem Element wird mit der bekannten Methode eingefügt. (Sonderfälle: Anfang der Liste; Ende der Liste)
Dadurch wird das Suchen durchschnittlich um die Hälfte beschleunigt, daher ist zu überlegen, ob mehr gesucht oder mehr eingefügt ausgelesen wird (Einfügen ist hier viel aufwendiger als in eine unsortierte Liste)
Sie "funktionieren in beide Richtungen", brauchen jedoch dementsprechend mehr Speicher. Sie werden z.B. in Textverarbeitungen gerne eingesetzt, da das Einfügen, aber auch die Bewegung in beide Richtungen ziemlich leicht geht und eine Suche sowieso immer sequentiell sein muß.
Nötig ist auch ein Zeiger für das Ende der Liste.
Durch mehrfache Verkettungen werden für die selben Daten mehrere Sortiermöglichkeiten erreicht.
Einfügen: so wie in eine sortierte Liste, nur für jedes Sortierkriterium.
|
|
|
|
|
Wenn die Systemprozedur Dispose nicht zur Verfügung
steht, muß mensch sie nachbilden. Das passiert mit einer
Freispeicherliste, die vom bereits freigegebenen Speicher neuen
Speicher zurückgibt, soweit das möglich ist.
Bäume bestehen aus Kanten (die eine Richtung haben) und Knoten. Sie enthalten keine "Kreise", also wieder zusammengewachsene Zweige. Begriffe: VorgängerIn, NachfolgerIn, NachbarIn. Ebene: Knoten, die die gleiche Entfernung von der Wurzel haben und nicht Geschwister sein müssen. Teilbaum: Eine Struktur, die im "echten" Baum auch enthalten ist, und deren Wurzel ein Element des "echten" Baumes ist. Endknoten = Blätter: haben keine NachfolgerInnen.
Jeder Zwischenknoten hat höchstens zwei NachfolgerInnen, üblicherweise als li(nks) und re(chts) bezeichnet.
Fundamentales Konzept der Informatik: eine komplizierte oder umfangreiche Aufgabe wird dadurch gelöst, daß mensch sie in einfachere oder kleinere Aufgaben gleichen Typs zerlegt, und mit deren Lösungen die Gesamtaufgabe löst.
Wichtig bei rekursiven Aufrufen:
"Divide and conquer": die Daten werden in 2 "Hälften" geteilt und für jede Hälfte die Operation ausgeführt. Sobald die Operation "einfach" ist, kann sie direkt ausgeführt werden.
Mittelbare Rekursion: Prozedur A ruft B auf, die wiederum A aufruft: Vorsicht!
Türme von Hanoi: jede beliebige Anzahl von Scheiben läßt sich umlegen, der Aufwand ist 2N.
Erste Vorabgabe: 8. 4. 18:10
Tutor: Johannes Riemer
Stacks sind sehr gut für die Auswertung von Ausdrücken.
Hash-Tabellen speichern einen möglichst eindeutigen und in der Grundgesamtheit gleichmäßig verteilten Schlüssel, der auf die eigentlichen Daten zeigt. Voraussetzung ist eine Funktion, die aus den echten Daten auf nachvollziehbare Weise (also ohne Zufallszahlen) Hash-Daten generiert. Hash-Tabellen werden wegen des schnellen Zugriffs in Arrays (in Pascal mit fixer Größe) gespeichert.
Heap ist die linearisierte Version eines binären Baumes, bei dem jeder Knoten größer ist als seine Nachfolger.
Heapbedingung: h[i] > h[2i] und h[i] > [2i + 1]
Aufwand bei "einfacher" Stringsuche: O(N) bis O(N*M)
Aufwand bei Mismatched-Character-Algorithmus: theoretisch O(N*M), bei natürlichen Texten ca. O(N/3).
Wenn eine Wortposition nicht paßt und wenn der dieser Wortposition folgende Buchstabe nicht im Suchwort vorkommt, dann kann mensch um M+1 Stellen weiterrücken.
Graphen bestehen aus Knoten und Kanten.
Ein Weg zwischen zwei Konten X und Y ist eine Liste von Kanten, die X und Y miteinander verbindet.
Ein einfacher Weg enthält keine Knoten doppelt.
Ein Kreis ist ein einfacher Weg, bei dem lediglich der Anfangsknoten und der Endknoten gleich sind.
Ein kreisfreier Graph heißt Baum, wenn er zusammenhängend ist, und Wald, wenn er nicht zusammenhängend ist.
Ein Gerüst eines Graphen ist ein Teilgraph (Teilmenge der Knoten und Kanten), der alle Knoten enthält und ein Baum ist (also nicht alle Kanten enthält).
Ein Graph heißt
+ komplett: wenn alle möglichen Kanten existieren,
+ dünn: wenn sehr wenige Kanten existieren,
+ gerichtet: wen die Kanten eine Richtung haben,
+ gewichtet: wenn die Kanten Werte (Gewichte) haben.
Adjazenzmatrix: eine K*K-Matrix mit logischen Werten, in
der adjmatr[x,y]=TRUE, wenn eine Verbindung vorhanden
ist.
Type GrMatrix = ARRAY [1..K, 1..K] OF BOOLEAN;
Brauchbar, wenn die Anzahl der Kanten nicht viel kleiner ist als die Anzahl der möglichen Kanten.
Adjazenzliste: Für jeden Knoten gibt es eine Liste mit allen durch eine Kante verbundenen Knoten.
TYPE KnoPtr = ^Kno;
Kno = RECORD
data: irgendwas;
next: KnoPtr;
end;
Speicherbedarf: O(K+L) (K: Anzahl der möglichen Verbindungen; L: Anzahl der wirklichen Verbindungen).
Sehr gut, wenn viel weniger Verbindungen existieren, als möglich wäre.
Depth-First-Search: Ausgehend von jedem erreichten Knoten werden
alle angrenzenden Knoten besucht, die noch nicht besucht wurden.
Dazu muß mensch sich merken, welche Knoten bereits besucht
wurden: VAR besucht: ARRAY[1..K] OF BOOLEAN;
PROCEDURE DepthRek(no: INTEGER);
VAR hinf: KnoPtr;
BEGIN
IF NOT besucht[no] THEN
hilf:=adjliste[no];
WHILE hilf<> NIL Donnerstag
BEGIN DepthRek(hilf^.no);
hilf:=hilf^.next
END (*WHILE*)
END(*IF*)
END;
Aufwand proportional zur Anzahl der Kanten (O(L)).
besucht[i]= TRUE
sind, ist der Graph zusammenhängend.
IF NOT besucht[i] jemals FALSE
ist, enthält er Kreise.
Aufgabe in der Pause: 10000, 121, 100, 31, 24, 22, 20, 17, 16, 15, 14, 13, 12, 11, 10, ...
Andere Möglichkeit für Depth-First-Search (mit einem Stack):
PROCEDURE Depth (no: INTEGER);
VAR hilf: KnoPtr;
BEGIN
REPEAT besucht[no]:=TRUE;
hilf:=adjliste[no];
WHILE hilf<>NIL Donnerstag BEGIN
IF NOT besucht[hilf^.no]
THEN Push(hilf^.no);
hilf:=hilf^.next; end;
REPEAT no:=Pop
UNTIL StkEmpty OR NOT besucht[no]
UNTIL StkEmpty
END;(*Depth*)
Nach einem Knoten werden zuerst alle unmittelbaren Nachfolger besucht, bevor deren Nachfolger besucht werden.
Das kleinste Element des noch unsortierten Teiles wird gesucht und an die erste Stelle gestellt (Durch Vertauschen mit dem ersten Element). Aufwand: N2.
Die Elemente werden in ein neues Array gleich in der richtigen Reihenfolge eingefügt, Aufwand N2.
BubbleSort arbeitet so, daß benachbarte Elemente jeweils vertauscht werden, solange das linke Element noch größer ist. Dadurch "wandern" kleine Elemente tendentiell nach links, große nach rechts. Der Aufwand ist immer N2, aber BubbleSort ist bei kleinen Datenmengen (bis ca. 20 Elemente) manchmal schneller als aufwendige Verfahren.
wird eingesetzt, wenn zwei sortierte Teilmengen zusammengefaßt werden. Vorgehensweise:
Dieses Verfahren (Aufwand N) funktioniert zwar nur im angesprochenen Spezialfall, aber ein beliebiges Array kann durch rekursive Aufrufe in kleinste Teile aufgeteilt werden. Dann ist der Aufwand N * ld N.
Proc MergeSort(li, re: Integer);
var Mittwoch: Integer;
Begin
if li < re then begin
mi:=(li+re) DIV 2;
MergeSort(li, mi);
MergeSort(mi+1, re);
Merge(li, mi, re);
end
end;(*MergeSort*)
In ein Heap (das sehr effizientes Einfügen und Entfernen des größten Elements erlaubt) werden erst einmal alle Daten eingetragen und dann rückwärts wieder ausgelesen. Der Aufwand ist N * ld N, aber es ist ein gleich großer Speicher notwendig.
(Hoare, 1960) Klassisches Divide & Conquer-Verfahren.
In jedem Schritt wird ein Element auf seinen endgültigen Platz gebracht. Davor stehen nur kleinere, danach nur größere.
Ausartung: Wenn zufällig immer der kleinste oder größte Datensatz erwischt wird, ist der Aufwand N2/2
Basiert auf der Binärrepräsentation der Zahlen. Einsetzbar, wenn Zahlen sortiert werden und der Wertebereich bekannt ist. Der Vorteil ist, daß Zahlen nicht verglichen werden müssen, sondern gleich bekannt ist, wo sie in der Folge stehen. Aufwand theoretisch N, aber die Differenz zwischen der Obergrenze des Wertebereichs und der größten Zahl fällt stark ins Gewicht. Außerdem kann dieses Verfahren nicht ausarten.
Es existiert ein Feld, das mit seinem Indexbereich den gesamten Wertebereich (Der natürlich ziemlich klein sein sollte) der Schlüssel überdeckt. Dann werden alle Elemente einfach an "ihre" Stelle eingetragen und dort wird ein Zähler für die Anzahl der Elemente erhöht. Anschließend wird das Array für den Definitionsbereich von unten nach oben durchgelesen und die entsprechende Anzahl von Werten ins sortierte Array eingefügt.
Der Aufwand ist (N + max), d.h. bei einem großen Definitionsbereich und wenigen zu sortierenden Werten ist dieses Verfahren allen anderen deutlich unterlegen. Bei sehr großem Definitionsbereich (z.B. Long-Zahlen) ist der Speicherbedarf für praktische Anwendungen zu groß.
Objekte bestehen aus Daten (Eigenschaften) und Methoden (Fähigkeiten).
Kommunikation mit Objekten: Nachricht (message) + Parameter => Objekt => Reaktion (+ Rückgabewerte)
Eigenschaften und Fähigkeiten werden den Subklassen übertragen, diese können sie aber neu implementieren.
Vererbung in Pascal: TYPE SubKlasse
= OBJECT(Superklasse)
Konstruktor: Methode zur Initialisierung eines Objekts
Destruktor: Methode zum ordentlichen Löschen des Objekts
Schlüsselwort CONSTRUCTOR bzw. DESTRUCTOR
statt PROCEDURE
Guter Stil: immer beide implementieren und aufrufen!
Problem bei Polimorphismus in Pascal: Wenn mit einem Pointer auf die Superklasse gearbeitet wird, wird immer (also auch bei Subklassen) die Methode der Superklasse aufgerufen!
"Lösung": Schlüsselwort "VIRTUAL"
nach Methoden, die immer neu definiert werden; immer Constructor
(für die Initialisierung des Laufzeitsystems) notwendig.
Beispiel:
TYPE Haus = OBJECT
CONSTRUCTOR Bau;
FUNCTION Energie: REAL; VIRTUAL;
FUNCTION Kosten: REAL;
END;
Wohnhaus = OBJECT(Haus)
CONSTRUCTOR Bau(leute: INTEGER);
FUNCTION Energie: REAL; VIRTUAL;
END;
HausPtr = ^Haus;
WohnhausPtr = ^Wohnhaus;
CONSTRUCTOR Haus.Bau; BEGIN END;
CONSTRUCTOR Wohnhaus.Bau;
begin einwohner:=leute; END
FUNCTION Haus.Kosten;
BEGIN
Kosten:=500 + Energie * 1.5;
END;
VAR p: HausPtr;
....
p:=New(WohnhausPtr, Bau(12));
x:=p^.Kosten;
Wohnhaus.Kosten ruft Wohnhaus.Energie
auf.
Rat: zuerst alles VIRTUAL definieren, und erst beim
nachträglichen Optimieren ausgewählte Methoden unvirtualisieren.
Fragen:
1. (10 Pkt.)
TYPE
A = OBJECT
FUNCTION D(v: INTEGER): INTEGER; VIRTUAL;
FUNCTION E: INTEGER;
END;(*A*)
B = OBJECT(A)
FUNCTION D(v: INTEGER): INTEGER; VIRTUAL;
END;(*B*)
(...)
FUNCTION A.D;
BEGIN D := v * 2; END;
FUNCTION A.E;
BEGIN E := D(3); END;
FUNCTION B.D;
BEGIN D := v + 1; END;
Was liefert Ainstanz.E + Binstanz.E ?
Welche Funktion wird wie oft aufgerufen?
2. a. (10 Pkt.) Gegeben sind ein verbesserter BubbleSort-Algorithmus und ein Integer-Array: In eine Tabelle muß der Inhalt des Arrays nach jedem Durchlauf der inneren Schleife eingetragen werden.
2. b. (10 Pkt.) Aufwandabschätzung eines QuickSort-Algorithmus:
Gegeben ist, wie lange die Sortierung eines Arrays mit 100, 400,
1000, 1500 Elementen gedauert hat, Frage: wie lange dauert es
mit 2000 Elementen? (O(QuickSort = 1,386 N * ld N))
3. a. (10 Pkt.) Ein Graph ist aufgezeichnet, es muß ein Gerüst eingezeichnet werden. Ist der Graph ein Baum? zusammenhängend? kreisfrei? ein Wald? Eine Adjazenzliste zeichnen.
3. b. (10 Pkt.) Eine Funktion soll geschrieben werden, die ermittelt, ob ein gerichteter ungewichteter Graph ein Baum ist.