Sortieren und Suchen

Werbung
Kapitel 4
Sortieren und Suchen
4.1
Sortieren
Die Sortierung von Mengen von Datensätzen ist eine häufige algorithmische
Operation auf Mengen bzw. Folgen von gleichartigen Datenobjekten (insbesondere in der betriebswirtschaftlichen Datenverarbeitung). Normalerweise gibt es
einen Schlüsselbegriff nach dem sortiert wird, und zugehörige Attribute, die dem
Schlüsselbegriff zugeordnet sind, d.h. man sortiert records bzw. Objekte.
Im allgemeinen braucht man eine Prä-Ordnungsrelation (transitiv, reflexiv,
total, nicht notwendig antisymmetrisch), die die Schlüsselbegriffe sortiert. Diese ist nicht automatisch gegeben. Z.B. ist die Sortierung von Texten, die im
englischen / amerikanischen einfach auf der internen Darstellung durchgeführt
wird, problematisch bei der Verwendung von Umlauten, da diese normalerweise
aufgrund der internen Darstellung oft falsch einsortiert werden. Für die richtige
Sortierung benötigt man die Definition einer passenden Ordnung auf den Zeichen, die z.B. ü zum u dazusortiert, d.h. t < u < ü < v, oder auch t < u ∼ ü <
v, wie in einem Wörterbuch.
Wir betrachten im folgenden das etwas vereinfachte Problem der Sortierung
einer gegebenen Liste (Menge) von ganzen Zahlen.
Wir betrachten hier vier typische Sortierverfahren.
Sortieren durch Einfügen (Insertion Sort) fügt die restlichen Elemente
nach und nach in die bereits sortierte Liste der abgearbeiteten Zahlen.
Sortieren mit Blasensort (Bubble Sort) Betrachtet jeweils zwei benachbarte Elemente und bringt diese in die richtige Reihenfolge. Der Vergleich
läuft in zwei Schleifen: Die innere startet mit den letzten beiden Elementen und schiebt das Vergleichsfenster nach vorne. Danach ist das kleinste
Element am Anfang der Liste. Dieses Verfahren wird jeweils mit der kleineren Liste wiederholt. Man kann abbrechen, wenn sich in einem inneren
Durchlauf nichts verändert hat.
Quicksort Rekursives Verfahren, das das erste Element als Pivot nimmt, dann
1
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 16. Dezember 2004
2
die Liste zerlegt in eine Liste der kleineren und eine Liste der größeren
Elemente, diese rekursiv sortiert, und dann einfach hintereinander hängt
mit dem Pivot dazwischen.
Mischsort (Merge Sort) Rekursives Verfahren, das die Liste in zwei Hälften
zerlegt, diese rekursiv sortiert und dann wieder zusammenmischt.
Zunächst die Implementierung der vier Sortierverfahren in Haskell. Hierbei
wird die Kernidee der entsprechenden Verfahren demonstriert, nicht jedoch die
Methoden der Speicherausnutzung wie in imperativen Programmiersprachen.
Der Aspekt der Speicherverwaltung bei den Sortierverfahren wird noch angesprochen im Zusammenhang mit einer Implementierung in Python.
---Sortieren von Listen von Zahlen
-- einfuegen
sorteinfuegen xs = sorteinfuegenr xs []
sorteinfuegenr [] ys = ys
sorteinfuegenr (x:xs) [] = sorteinfuegenr xs [x]
sorteinfuegenr (x:xs) ys = sorteinfuegenr xs (sorteinfuegenr1 x ys)
sorteinfuegenr1 x [] = [x]
sorteinfuegenr1 x (y:ys) =
if x <= y then x:y:ys
else y : (sorteinfuegenr1 x ys)
-- Insert-Sort: stabil und
sorteinfuegeno xs = reverse
sorteinfuegenor [] ys = ys
sorteinfuegenor (x:xs) [] =
sorteinfuegenor (x:xs) ys =
O(n) f"ur vorsortierte:
(sorteinfuegenor xs [])
sorteinfuegenor xs [x]
sorteinfuegenor xs (sorteinfuegenor1 x ys)
sorteinfuegenor1 x [] = [x]
sorteinfuegenor1 x (y:ys) =
if x >= y then x:y:ys
else y : (sorteinfuegenor1 x ys)
-- Blasensort (bubblesort)
bubblesort [] = []
bubblesort [x] = [x]
bubblesort xs =
let y:resty = bubblesort1 xs
in y: (bubblesort resty)
bubblesort1 [x] = [x]
bubblesort1 (x:rest) =
let (y:resty) = bubblesort1 rest
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 16. Dezember 2004
in if x
else
> y then y:x: resty
(x: y:resty)
-- Blasensort mit Optimierung
bubblesorto [] = []
bubblesorto [x] = [x]
bubblesorto xs =
let (aenderung,y:resty) = bubblesorto1 xs
in if aenderung then y: (bubblesorto resty)
else xs
bubblesorto1 [x] = (False,[x])
bubblesorto1 (x:rest) =
let (aenderung, y:resty) = bubblesorto1 rest
in if x > y then (True,y:x: resty)
else
(aenderung,x: y:resty)
-- quicksort
quicks [] = []
quicks [x] = [x]
quicks [x,y] = if x <= y then [x,y] else [y,x]
quicks (x:xs) = let (llt,lge) = splitlist x xs
in (quicks llt) ++ (x: (quicks lge))
splitlist x y = splitlistr x y [] []
splitlistr x [] llt lge = (llt,lge)
splitlistr x (y:ys) llt lge =
if y < x
then splitlistr x ys (y: llt) lge
else splitlistr x ys llt (y:lge)
-Versuch einer Optimierung durch variierte Wahl des Pivots.
quicks2 [] = []
quicks2 [x] = [x]
quicks2 [x,y] = if x <= y then [x,y] else [y,x]
quicks2 (x:xs@(y:z:rest)) =
if x >= y && x <= z || x >= z && x <= y
then let (llt,lge) = splitlist x xs
in (quicks2 llt) ++ (x: (quicks2 lge))
else quicks2 (z:x:y:rest)
---
merge-sort
mischsort
mergesort
mergesort
mergesort
xs = mergesort (length xs) xs
_ [] = []
_ [x] = [x]
_ [x,y] = if x <= y then [x,y] else [y,x]
3
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 16. Dezember 2004
4
mergesort len xs =
let lenh = len ‘div‘ 2
in mische (mergesort lenh (take lenh xs))
(mergesort (len -lenh) (drop lenh xs))
mische [] ys = ys
mische xs [] = xs
mische (x:xs) (y:ys) =
if x <= y then x: (mische xs (y:ys))
else y: (mische (x:xs) ys)
Aussage 4.1.1 Sortieren von n Zahlen mittels Vergleich von Zahlen benötigt
im schlechtesten Fall mindestens n ∗ log2 (n) Vergleiche. Genauer: Man nimmt
an: das Programm hat die Eingaben a1 , . . . , an , das Programm ist ein Entscheidungsbaum, der an den Verzweigungen die Frage stellt: ist ai > aj für bestimmte
Indizes i, j, und die Ausgabe am Ende ist die sortierte Permutation der ai .
Dann gibt es immer eine Eingabemöglichkeit, so dass n ∗ log(n) Vergleiche nötig
sind.
Begründung. Der Entscheidungsbaum hat mindestens n! Blätter: pro Permutation der Eingabe ein Blatt. Die Tiefe d des Baumes ist mindestens log2 (n!)
Offenbar gilt n! ≥ |1 ∗ .{z
. . ∗ 1} ∗ n/2 ∗ . . . ∗ n/2 = (n/2)(n/2) . Anwenden des Loga|
{z
}
n/2
n/2
rithmus ergibt
log2 (n!) > log2 ((n/2)(n/2) ) = 0.5 ∗ n ∗ log2 (n/2) = 0.5 ∗ n ∗ (log2 (n) − 1).
D.h. Ω(n ∗ log2 (n)). Eine andere Abschätzung dieser unteren Schranke der minimal notwendigen Anzahl der Vergleiche ist möglich mittels der Stirlingformel
für n!. Der Faktor 0.5 steigt mit wachsendem n, aber bleibt < 1, da ja n! < nn .
Allerdings konnte man noch nicht allgemein nachweisen, dass dies auch eine
untere Schranke ist, wenn man alle Algorithmen zulässt.
In bestimmten Spezialfällen kann es schnellere Algorithmen geben, z.B. wenn
die Länge des Schlüssels nicht zu groß wird.
Bemerkungen zu den Eigenschaften der Sortierverfahren
Sortieren durch Einfügen Ist ein einfaches Verfahren, das für kleinere Listen
schnell genug ist. Die Anzahl Reduktionen (Zeitbedarf) ist im schlechtesten Fall quadratisch: man benötigt für eine Liste der Länge n höchstens:
1 + 2 + . . . + (n − 1) Vergleiche (und Operationen). Dies sind (n − 1) ∗ n/2,
d.h. O(n2 ). Das Verfahren ist auch im besten Fall linear, da der Vergleich
und Einfügen evtl. nur einmal pro Element gemacht werden, wenn die
Liste in der richtigen Reihenfolge vorsortiert ist.
Sortieren mit Bubbleort Ebenfalls quadratisch im schlechtesten Fall: (n −
1) + (n − 2) + . . . + 1 Operationen. Der beste Fall tritt ein, wenn die Liste
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 16. Dezember 2004
5
schon sortiert ist, oder schon fast sortiert ist, da dann abgebrochen werden
kann.
Quicksort Dieses Verfahren hat Vorteile, wenn die Listen groß sind und die
Werte zufällig verteilt sind. Quicksort ist im schlechtesten Fall quadratisch.
Im Mittel ist der Aufwand n ∗ log(n). Beim Experimentieren in Haskell
mit zufällig erzeugten Listen erscheint es als das beste Verfahren.
Mischsort Ist im schlechtesten Fall O(n ∗ log(n)). Beim Experimentieren in
Haskell ist es nur dann besser als Quicksort, wenn Teile der Listen bereits
sortiert sind. In imperativen Sprachen hat es den Nachteil, dass man bei einer naiven Implementierung die Listen jeweils neu aufbauen muss. Es gibt
eine Implementierung, die auch ohne extra Speicher auskommt, und die
effizient ist, und einen angepassten und ausgefeilten Merge-Algorithmus
verwendet.
Wir zeigen, dass der Mischsort einen Zeitbedarf von O(n ∗ log(n)) hat: Dazu
müssen wir redms abschätzen. redms (n) = c + redmische (n) + 2 ∗ redms (n/2)
= c + n + 2 ∗ redms (n/2) ≤ c + n + 2 ∗ (c + n/2 + 1) + 4 ∗ redms (n/4). Da dies
log(n) Schritte erfordert, ergibt sich als Abschätzung:
c ∗ n + n ∗ log(n) + 2 ∗ log(n).
Der Term in der Mitte ist asymptotisch am größten, d.h. der Mischsort ist
O(n ∗ log(n)).
4.1.1
Sortierprogramme in imperativen Programmiersprachen, Speicherverwaltung
In imperativen Programmiersprachen kommt zur eigentlichen Kernidee der Sortieralgorithmen noch der Aspekt der effizienten Speicherverwaltung dazu. Normalerweise ist die Eingabe ein Array der Schlüssel und Daten.
Algorithmen, die die Sortierung nur innerhalb des Arrays vornehmen durch
Tauschvorgänge, nennt man auch In-Place“Verfahren.
”
Die betrachteten Sortierverfahren lassen sich bis auf Misch-Sort leicht als InPlace-Verfahren programmieren. Der entsprechende Programmcode in Python
folgt. Der Merge-Sort kann durch ein effizientes In-Place-Merge-Verfahren ebenfalls implementiert werden, allerdings ist der Algorithmus nicht offensichtlich zu
finden: Man benötigt dazu ein schnelles in-place-Mischen. Ein neuer Algorithmus wurde von Arne Kutzner und Pok-Son Kim gefunden
def Quicksort(soArray):
return Quicksortr(soArray,0,len(soArray)-1)
def Quicksortr(soArray,iLo,iHi):
if (iHi-iLo == 1):
##Optimierung, wenn Teilfeld zwei Elemente enthaelt
if_groesser_then_tausch(soArray,iLo,iHi);
return soArray;
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 16. Dezember 2004
## int Lo, Hi, Mid;
## long T,Mid2;
Lo = iLo;
Hi = iHi;
Mid = (Lo + Hi) / 2;
##
print "Mid: ", Mid;
while (Lo <= Hi):
while (isKleiner(soArray,Lo,Mid)):
Lo = Lo+1
while (isGroesser(soArray,Hi,Mid)):
Hi =
Hi -1;
if (Lo <= Hi):
if (Mid == Hi): Mid = Lo;
elif (Mid == Lo): Mid = Hi;
if (Lo != Hi):
vertausche(soArray,Lo, Hi);
Lo = Lo +1;
Hi = Hi-1;
if (Hi > iLo):
if (Lo < iHi):
return soArray
Quicksortr(soArray,iLo, Hi);
Quicksortr(soArray,Lo, iHi);
def isGroesser(soArray,x,y):
def isKleiner(soArray,x,y):
return (soArray[x] > soArray[y])
return (soArray[x] < soArray[y])
def vertausche(soArray,ind1, ind2):
x =
soArray[ind1];
soArray[ind1] = soArray[ind2];
soArray[ind2] = x;
def if_groesser_then_tausch(soArray,ind1,ind2):
if isGroesser(soArray,ind1,ind2):
vertausche(soArray,ind1, ind2);
Der Bubblesort in Python:
def Bubblesort(soArray):
laenge = len(soArray)
for i in range(0,laenge-1):
aenderung = 0
for j in range(0,laenge-1-i):
6
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 16. Dezember 2004
7
if soArray[j] > soArray[j+1]:
vertausche(soArray,j, j+1)
aenderung = 1
if aenderung == 0:
break
Der Insertsort in Python:
def Insertsort(soArray):
laenge = len(soArray)
for i in range(1,laenge):
for j in range(i,0,-1):
if soArray[j] < soArray[j-1]:
vertausche(soArray,j, j-1)
else: break
Ein Sortierverfahren nennt man stabil, wenn es die Reihenfolge der Eingabe
nicht unnötig ändert, d.h. wenn die Eingaben mit gleichen Schlüsselwerten nicht
in der Reihenfolge vertauscht werden.
Hier verhalten sich die Haskell-Algorithmen anders als die imperativen Algorithmen. Die Haskell-Sortier-Algorithmen sind alle stabil, aber benötigen zusätzliche Listen zum Sortieren.
Die imperativen In-Place-Sortierverfahren sind nur teilweise stabil: Einfügeund Bubble-Sort lassen sich leicht stabil implementieren. Der Merge-Sort kann
ebenfalls stabil implementiert werden. Der imperative Quicksort, so wie oben implementiert, ist durch die nicht-lokalen Vertauschungen nicht stabil. Der Grund
ist, dass beim Tauschen der Elemente um den Pivot herum die Reihenfolge
gleicher Elemente vertauscht wird.
4.2
Datenabstraktion, abstrakte Datentypen
Datenabstraktion ist eine Strukturierungsmethode für Programme und Implementierung, die unabhängig von einer Programmiersprache verwendet werden
kann. Diese wird normalerweise als Modul implementiert, damit man die Funtionalität wiederverwenden kann. Wichtiges Prinzip ist die Trennung von interner
Implementierung der Daten und der Zugriffsfunktionen von deren Verwendung.
Auch dies wird durch Module unterstützt.
Wesentliche Aspekte sind:
• Die Daten sind über klar definierte Zugriffsfunktionen verfügbar
• Die Algorithmen verwenden nur diese Zugriffsfunktionen
• Die Daten haben eine Semantik, die von der Implementierung respektiert
wird.
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 16. Dezember 2004
8
• Die Implementierung der Daten und der eigentlichen Zugriffe ist nicht
sichtbar (Kapselung, information hiding).
Hierzu werden normalerweise Module verwendet, die i.a. einen bestimmten
Datentyp zusammen mit den Zugriffsfunktionen zusammenfassen und implementieren, aber nur ausgewählte Zugriffsfunktionen nach außen hin sichtbar
machen (exportieren).
Es gibt zwei Dinge, die man vermeiden sollte:
• den Durchgriff auf die Implementerung, auch wenn diese kurzfristig die
Effizienz steigert oder bequemer beim Programmieren ist. Die unsaubere Methode des Durchgriffs kann langfristig in Projekten zu Problemen
führen, wenn z.B. die interne Implementierung des Moduls geändert wird.
• Verwenden des durch die Implementierung bedingten Verhaltens, das aber
nicht gerantiert ist. D.h. nicht in der Spezifikation enthalten ist.
Z.B. darf man bei einer Implementierung von Mengen nicht darauf bauen,
dass die Elemente in der Reihenfolge des Einfügens ausgegeben werden.
Will man das ausnutzen, dann benötigt man die Datenstruktur Liste“.
”
Die Funktionen auf einem abstrakten Datentyp kann man grob unterscheiden
in
• Konstruktoren bzw. Eingabe der Objekte
• Fallunterscheidung nach den Konstruktoren und Selektoren; bzw.Frage
nach der logischen Struktur der Datenobjekte, bzw. Ausgabe der Objekte,
• interne Service-Funktionen. Z.B. Operatoren auf Objekten, Gleichheitstests, usw.
• externe Service-Funktionen. Z.B. die Funktion map auf Listen.
• Datenkonversionen
Beispiele für typische Datentypen sind: Zahlen, ganze Zahlen, komplexe Zahlen oder rationale Zahlen.
Beispiel 4.2.1 rationale Zahlen:
• Konstruktion erfolgt durch Eingabe eines Paars von zwei ganzen Zahlen :
Zähler und Nenner.
• Ausgabe entweder durch Abfrage von Zähler bzw. Nenner. Hier ist zu beachten, dass die interne Darstellung im allgemeinen gekürzt ist, d.h. es
gibt keine 1-1 Beziehung zwischen Konstruktion und Selektion.
• Servicefunktion sind z.B. alle arithmetischen Funktionen.
• Eine Datenkonversionen sind z.B. die Konversion einer natürlichen Zahl
in eine rationale, oder umgekehrt, die Konversion eines Bruchs in eine
ganze Zahl, die nicht immer exakt geht. Hierbie kann man runden oder
abschneiden.
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 16. Dezember 2004
9
Oft kann man die Semantik eines abstraktion Datentypen durch Gleichheitsaxiome festlegen.
Beispiel 4.2.2 Listen: Man braucht eigentlich nur die zwei Konstruktoren
cons, nil und die zwei Selektoren head, tail mit den Gleichungen
head (cons x y)
tail (cons x y)
= x
= y
Die Haskell-Implementierung kann erfolgen mittels
head (x:y)
tail (x:y)
cons x y
nil
=
=
=
=
x
y
x:y
[]
Servicefunktionen auf Listen sind z.B. reverse, ++, concat, die man als
intern bezeichnen kann, während map, filter eher externe Servicefunktionen
sind.
Beispiel 4.2.3 Der Datentyp natürliche Zahl“ kann mit den Konstruktoren
”
Null, S aufgebaut werden. As Ausgabe für S(S(S(Null))) kann man dann
S(S(S(Null))) drucken, oder die gemeinte Zahl: 3. Es ist kein Problem, aufbauend auf diese Darstellung alle arithmetischen Operatoren zu definieren.
Die in Programmiersprachen verfügbaren Zahlen sind im allgemeinen auf
binären Strings aufgebaut. Die Änderung der internen Implementierung sollte
(außer Effizienz) keine Änderungen der Funktionalität bewirken.
Beispiel 4.2.4 Der Datentyp ganze Zahlen“ kann auf dem Datentyp der
”
natürlichen Zahlen aufbauen, indem man eine ganze Zahl als Paar (s, n) implementiert, wobei s das Vorzeichen + oder − ist, und n die positive Zahl.
Diese Darstellung hat nur an einer Stelle eine Doppeldeutigkeit, nämlich bei
der internen Darstellung der 0: die kann (+, 0) oder (−, 0) sein.
Beispiel 4.2.5 Fließkommazahlen (Gleitkommazahlen): Es gibt Funktionen
zum Erzeugen und zur Ausgabe, und die mathematischen Operationen:
∗, /, +, −. Hier ist die Semantik nicht die mathematische für reelle Zahlen,
da sich irrationale Zahlen i.a. nicht beliebig genau darstellen lassen. D.h. man
muss Näherungen einführen. Die Semantik der implementierten arithmetischen
Operationen garantiert dann, dass diese die richtigen Werte der Muliplikation möglichst gut annähern. Damit dies auf allen Rechnern eindeutig ist, gibt
es Normierungen (IEEE-Normen) für die internen Berechnungen, Rundungen
und Fehlermeldungen bei Ausnahmen, je nach Genauigkeit.
Beispiel 4.2.6 Gleitkommazahlen in Intervallarithmetik: Es gibt Funktionen
zum Erzeugen, Anzeigen, mathematische Operationen: ∗, /, +, −. Hier ist die
axiomatische Semantik auch der Operationen mathematisch erfassbar: Die Ergebnisse müssen immer in dem angegebenen Intervall liegen. Allerdings gelten
dann nicht mehr alle arithmetischen Gesetze. Division durch ein Intervall, das
die Null enthält muss verboten sein (bzw, einen Fehler ergeben.)
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 16. Dezember 2004
10
Beispiel 4.2.7 Der abstrakte Datentyp (Modul) Menge“ (endliche Mengen
”
mit Elementen von gleichem Typ) sollte folgende Funktionalität bereitstellen:
• Erzeugen von Mengen, gegeben eine endliche Aufzählung der Elemente
• Test auf Enthaltensein eines Elementes in einer Menge
• Drucken einer Menge
• Kardinalität einer Menge
• Bildung von Schnitt, Vereinigung, Differenz, Potenzmenge
• Test auf Gleichheit von Mengen
Bei allen obigen Funktionalitäten benötigt man einen Gleichheitstest der Elemente. Diese ist einfach bei Zahlen, aber bei komplizierteren Datenobjekten muss
ein (evtl. nichttrivialer) Gleichheitstest für die Elemente zur Verfügung stehen.
Beispiel 4.2.8 Der abstrakte Datentyp (Modul) Multimenge“: endliche Multi”
mengen mit Elementen von gleichem Typ, wobei Elemente mehrfach vorkommen
dürfen. Multimengen sind wie Listen, bei denen man von der Reihenfolge der
Elemente absieht; Oder: Multimengen sind wie Mengen, bei denen man mehrfaches Vorkommen der Elemente erlaubt. Die Operatoren sind analog zu Mengen. Z.B. gilt für Multimengen {1, 1, 1, 2, 2} ∩ {1, 1, 2, 2, 2} = {1, 1, 2, 2} und
{1, 1, 1, 2, 2} ∪ {1, 1, 2, 2, 2} = {1, 1, 1, 1, 1, 2, 2, 2, 2, 2}.
Ein Gleichheitstest auf den Elementen ist jetzt nur noch nötig für die Funktion enthalten in“, den Glkeichheitstest von 2 Mutimengen, aber nicht für die
”
Kardinalität.
Die Implementierung von Multimengen ist besonders einfach auf der Basis von Listen. Als Basisfunktionen kann man nehmen: anzahl, einfuegen,
cons, null. Der Datentyp wäre (Multimenge a) mit den Konstruktoren cons,
null. Zwei exemplarische Gleichheiten sind dann:
anzahl null = 0
∀s :: a, t :: M ultimenge a : anzahl(einfuegen s t) = (anzahl t) + 1
Die Listenimplementierung mit anzahl = length, einfuegen = cons ist
dann insoweit korrekt, denn:
anzahl null → length [] → 0
anzahl (einfuegen s t) → length (cons s t) → (length t) + 1.
Eine Implementierungsmöglichkeit für Mengen sind Listen von Elementen.
Da man Schnitte von Mengen und die Kardinalität von Mengen ausrechnen will,
muss man Gleichheit von Elementen testen können. Eine weitere Möglichkeit
ist die Implementierung auf der Basis von Multimengen.
Ein Datentyp “unendliche Liste“ oder “unendliche Menge“ ist möglich, allerdings auch problematisch, denn nicht alle unendlichen Mengen sind endlich
darstellbar. Ein Effekt ist, dass man den Elementtest dann nicht korrekt implementieren kann.
Abstraktionsbarrieren für die Beispiele Mengen / Multimengen / Listen sind:
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 16. Dezember 2004
11
Mengen: Schnitt, ...
Schnittstellenfunktionen
Multimengen: Schnitt, Vereinigung, Gleichheit, ...
Schnittstellenfunktionen
Listen. append, element, ...
Problem: Der Ressourcenbedarf und die Effizienz hängt ab von der Implementierung der Zwischenschichten und oft auch von dem Zusammenspiel der
verschiedenen Zwischenschichten.
4.3
Bäume
Zunächst führen wir Graphen ein. Die einfachste Vorstellung ist, dass ein Graph
gegeben ist als
• eine Menge von Knoten und
• eine Menge von zugehörigen (gerichteten oder ungerichtete) Kanten zwischen den Knoten.
Einige Begriffe für ungerichtete Graphen sind:
12
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
Schlingen
Kanten mit gleichem Anfangs- und Endknoten
Wege
Kantenfolgen (A, B), (B, C), . . .
Kreise
Kantenfolgen (A, B), (B, C), . . . , (Z, A)
Erreichbarkeit
A ist von B aus erreichbar, wenn es einen Weg von A nach B gibt
zusammenhängend Wenn alle Knoten von jedem Knoten aus erreichbar sind.
markierter Graph
Knoten bzw. Kanten haben Markierungen
Ein Baum ist ein (gerichteter oder ungerichteter) Graph, der zusammenhängend ist, ohne Kreise, und mit ausgezeichnetem Knoten (Wurzel)
In Zeichnungen wird meist die Wurzel oben hingezeichnet, die Blätter sind
unten.
Vorgänger
Vater
Tocher
Nachfolger
Blatt
Einige wichtige Begriffe für Bäume:
geordneter Baum
markierter Baum
Rand des Baumes
binärer Baum
Höhe (Tiefe)
balancierter (binärer)
geordneter Baum
Grad eines Knoten
Grad eines Baumes
Es gibt eine Links-Rechts-Ordnung auf den Töchtern
Die Knoten haben Markierung (bzw. Kanten)
Liste der Blattmarkierungen eines geordneten Baumes
Jeder Knoten ist Blatt oder hat genau zwei Töchter
maximale Länge eines Weges von der Wurzel zu einem Blatt
hat unter (binären) Bäumen mit gleichem Rand kleinste Tiefe
Anzahl der Töchter
maximaler Grad eines Knoten
Wir stellen binäre, geordnete Bäume mit folgender Datenstruktur dar:
data Binbaum a = Bblatt
Wr
•
•
•
•
a | Bknoten (Binbaum a) (Binbaum a)
betrachten zunächst Bäume mit folgenden Eigenschaften:
Daten (Markierungen) sind an den Blättern des Baumes
Die Daten sind von gleichem Typ
Jeder (innere) Knoten hat genau zwei Tochterknoten
es gibt einen linken und rechten Tochterknoten (geordnet)
13
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
Beispiel 4.3.1 Der folgende binäre Baum
7
1
3
4
hat eine Darstellung als
Bknoten (Bknoten (Bblatt 1)
(Bknoten (Bblatt 3) (Bblatt 4)))
(Bknoten (Bblatt 7) (Bblatt 8))
Einige Verarbeitungsfunktionen sind:
-- Liste der Markierungen der Blaetter
b_rand (Bblatt x)
= [x]
b_rand (Bknoten bl br) = (b_rand bl) ++ (b_rand br)
-- testet, ob Element im Baum ist
b_in x (Bblatt y)
= (x == y)
b_in x (Bknoten bl br) = b_in x bl
||
b_in x br
-- wendet eine Funktion auf alle Elemente des Baumes an,
-Resultat: Baum der Resultate
b_map f (Bblatt x)
= Bblatt (f x)
b_map f (Bknoten bl br) = Bknoten (b_map f bl) (b_map f br)
--Groesse des Baumes:
b_size (Bblatt x)
= 1
b_size (Bknoten bl br) = 1 + (b_size bl) + (b_size br)
--Anzahl der Bl"atter
b_blattnr (Bblatt x)
= 1
b_blattnr (Bknoten bl br) = (b_blattnr bl) + (b_blattnr br)
8
14
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
--Summe aller Blaetter, falls Zahlen:
b_sum (Bblatt x) = x
b_sum (Bknoten bl br) =
(b_sum bl) + (b_sum br)
-- Erzeugung grosser Baeume
b_mkbt_test 0 k = Bblatt k
b_mkbt_test n k = Bknoten (b_mkbt_test (n-1) (k+1)) (b_mkbt_test (n-1) (2*k))
b_mkbt_testll [x]
= Bblatt x
b_mkbt_testll (x:xs) = Bknoten (Bblatt x) (b_mkbt_testll xs)
b_mkbt_testlr [x]
= Bblatt x
b_mkbt_testlr (x:xs) = Bknoten (b_mkbt_testlr xs) (Bblatt x)
-- schnelles fold "uber bin"are B"aume
foldbt :: (a -> b -> b) -> b -> Binbaum a -> b
foldbt op a (Bblatt x)
= op x a
foldbt op a (Bknoten x y) = (foldbt op (foldbt op a y) x)
--- effizientere Version von b_rand
b_rand_eff = foldbt (:) []
foldbt mit optimiertem Stackverbrauch:
foldbt’ :: (a -> b -> b) -> b -> Binbaum a -> b
foldbt’ op a (Bblatt x)
= op x a
foldbt’ op a (Bknoten x y) = (((foldbt’ op) $! (foldbt’ op a y)) x)
--- effizientere Version von b_rand und b_sum
b_rand_eff = foldbt (:) []
b_sum_eff
= foldbt’ (+) 0
Um zu begründen, warum foldbt relativ schnell ist, betrachte den Zwischenausdruck, der aus einem Baum tr mit der Struktur (((1, 2), 3), (4, 5))
entsteht, wenn man foldbt (+) 0 tr auswertet.: (Wir verwenden hier eine
etwas vereinfachte Notation)
foldbt (+) 0 (((1,2),3),(4 ,5))
--> foldbt (+) (foldbt (+) 0 (4 ,5))
((1,2),3)
--> foldbt (+) (foldbt (+) (foldbt (+) 0 (5)) (4)
--> foldbt (+) (foldbt (+) (5+0) (4)
((1,2),3))
--> foldbt (+) (4+ (5+0))
((1,2),3)
((1,2),3))
15
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
-->
-->
-->
-->
-->
foldbt
foldbt
foldbt
foldbt
1+ (2+
(+)
(+)
(+)
(+)
(3+
(foldbt (+) (4+ (5+0))
(3)) (1,2)
(3+ (4+ (5+0)))
(1,2)
(foldbt (+) (3+ (4+ (5+0))) (2) (1))
(2+ (3+ (4+ (5+0)))) (1)
(4+ (5+0))))
D.h. Wenn ein binärer Baum tr mit Randliste [a1 , . . . , an ]
gegeben
ist,
dann
entspricht
foldbt f a tr
dem
Ausdruck
f a_1 (f a_2 (... (f a_n a) ...s)). D.h. es entspricht einem foldr
(++) [], das z.B.als Definition für concat schneller als foldl (++) [].
Wir zeigen beispielhaft eine Analyse der Funktion b rand:, wobei wir nur
volle binäre Bäume betrachten.
Für diese Bäume gilt:
#(innere Knoten) + 1 = #(Blätter) = 2T ief e
Für die Anzahl der Reduktionen von b rand baum bei n Blättern gilt:
τ (n)
...
=
=
=
=
=
n/2 + 2 + 2 ∗ τ (n/2)
n/2 + 2 + 2 ∗ (n/4 + 2 + 2 ∗ τ (n/4))
n/2 + 2 + n/2 + 2 ∗ 2 + 4 ∗ τ (n/4))
...
n/2 ∗ log2 (n) + 2 ∗ n
Tiefe = log2 (n)
Hinzu kommen noch n Reduktionen für die Blätter .
Da wir diese Analyse mit der von Hugs-Statistik vergleichen
können, nehmen wir noch folgende Funktion is list hinzu und werten
is_list (b_rand testbaum_n) aus.
is_list []
= True
is_list (_:xs) = is_list xs
Deshalb kommen noch (is_list lst) = length lst Reduktionen
dazu. Die folgende Tabelle zeigt die Anzahl der Reduktionen von
is_list (b_rand testbaum_n) allgemein und für einige ausgewählte Werte.
Tiefe
m
10
12
13
14
#Blätter #berechnet
2m
2m + 2m−1 ∗ m + 3 ∗ 2m
n
n + n/2 ∗ log2 (n) + 3 ∗ n
1024
9216
4096
40960
8192
86016
16384
180224
#tatsächliche
2m + 2m−1 ∗ m + 3 ∗ 2m + 14
n + n/2 ∗ log2 (n) + 3 ∗ n + 14
9230
40974
86030
180238
Wir machen auch eine Analyse von b rand eff, um vergleich zu können,
ebenfalls nur für volle binäre Bäume. Zur Erinnerung nochmal die Reduktion
von foldbt:
→
foldbt (:) [] (Bknoten lb rb)
foldbt (:) (foldbt (:) [] rb) lb
16
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
Die Anzahl der Reduktionen bei n Blättern kann man wie folgt abzählen: pro
Bknoten wird ein foldbt-Ausdruck eingesetzt. Dies erfordert n − 1 Reduktionen
zu foldbt-Ausdrücken.
Pro Blatt wird die Reduktion (foldbt (:) rand (Bblatt a)) → a :
rand ausgeführt, d.h. pro Blatt ein Reduktionsschritt.
Die Gesamtanzahl der Reduktionen ist somit in etwa 2 ∗ n.
Folgende Tabelle zeigt die theoretischen und die praktisch ermittelten Werte
des Ausdrucks is_list (b_rand_eff testbaum_n)
Tiefe
m
10
12
13
14
#Blätter #berechnet
2m
2m + 2 ∗ 2m
n
3n
1024
3072
4096
12288
8192
24576
16384
49152
#tatsächliche
2m + 2 ∗ 2m + 15
3n + 15
3087
12303
24591
49167
#Red(b rand)
9230
40974
86030
180238
Man sieht, dass foldbt tatsächlich schneller ist als normales rekursives Programmieren. Der Grund ist der gleiche wie bei der Beschleunigung des mit foldl
programmierten concat durch das mit foldr programmierte. Was man auch
sieht, ist dass lineare Terme die logarithmische Verbesserung etwas dämpfen.
Erst bei sehr großen Bäumen sind die Effekte deutlich sichtbar.
Man kann weitere Funktionen auf Bäumen definieren und auch etwas allgemeinere Bäume als Datenstruktur verwenden. Zum Beispiel kann man dann als
Typ- und Konstruktordefinition verwenden:
data Nbaum a = Nblatt a | Nknoten [Nbaum a]
4.4
Suche und Zugriff
Wir betrachten das in indizierten Dateien und Datenbanken vorkommende Problem, eine Menge von gleichartigen Datenobjekten zu verwalten, Z.B. alle DiplomstudententInnen dieses Jahrgangs, oder ein Lexikon von Worten mit zugehöriger Übersetzung. Wir wollen dieses Problem zunächst vereinfacht betrachten.
Die Objekte (Sätze) seien dargestellt als Paare, wobei das erste Argument
eine Zahl ist (Integer), der sogenannte Schlüssel (key), und das zweite Argument der Inhalt. Die Datenstruktur, die dafür benötigt wird, entspricht in etwa
einer Menge mit markierten Elementen: der Schlüssel bezeichnet das Element,
während die Markierung den zugehörigen Daten entspricht.
Operationen: Erzeugen, Einfügen, Suchen, Drucken, Entfernen.
data Maybe a = Nothing | Just a
data Satz a = Satz Integer a
data Datenbank a = ???
intialisiereDb :: Datenbank a
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
17
einfuegeDb :: Satz a -> Datenbank a -> Datenbank a
istinDb :: Integer -> Datenbank a -> Bool
sucheDb :: Integer -> Datenbank a -> Maybe a
druckeDb :: Datenbank a -> [(Integer,a)]
entferneDb :: Integer -> Datenbank a -> Datenbank a
Unter Benutzung dieser Operationen könnten wir jetzt spezifizieren, was wir
von dieser Datenbank an Verhalten erwarten: Wir formulieren umgangssprachlich:
• Eine neu intialisierte Datenbank ist leer
• Ein eingefügter Satz ist nach dem Einfügen auch zu finden (mit gleichem
Inhalt)
• Die Datenbank enthält genau die eingefügten Sätze.
• Nach dem Entfernen eines Satzes ist dieser nicht mehr zu finden.
• Ein Satz lässt sich nur einmal einfügen.
Eine weitergehende Funktionalität ist eine Anfrageschnittstelle, die logisch
verknüpfte Anfragen beantworten kann. Als mögliche Antworten beschränken
wir uns hier auf Mengen von Sätzen. Ruft man die Anfragen modular auf, so
braucht man auf unterer Ebene eine schnelle Möglichkeit, Mengen von Sätzen zu
schneiden, zu vereinigen, oder zu komplementieren bzw. Differenzen von (markierten) Mengen zu bilden
Wir betrachten verschiedene Möglichkeiten der internen Darstellung:
Assoziationsliste:
Wir beschreiben nur einige der notwendigen Funktionen.
data Satz a = Satz Int
a
data Db a = Dbl [Satz a]
intialisiereDb x = Dbl []
einfuegeDb (Satz x sx) (Dbl db) = Dbl ((Satz x sx): db)
druckeDb (Dbl db) = map (\(Satz x y) -> (x,y)) db
istinDb (Satz x y) (Dbl []) = False
istinDb (Satz x y) (Dbl ((Satz a _): restDb)) =
(x == a) || istinDb (Satz x y) (Dbl restDb)
sucheDb (Satz x y) (Dbl []) = Nothing
sucheDb (Satz x y) (Dbl ((Satz k sk): restDb)) =
if x == k then Just sk
else sucheDb (Satz x y) (Dbl restDb)
entferneDb (Satz x _) (Dbl []) = (Dbl [])
entferneDb (Satz k ky) (Dbl ((Satz x y) : restDb)) =
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
18
if k == x
then entferneDb (Satz k ky) (Dbl restDb)
else let Dbl restDblist = entferneDb (Satz k ky) (Dbl restDb)
in Dbl ((Satz x y) : restDblist)
Eine Schwäche dieser Implementierung ist die Laufzeit O(n) für die Suche
(istinDb) eines Elementes, was bei einer kleineren Menge von Sätzen akzeptabel
ist.
4.4.1
sortierte Liste
Betrachte jetzt eine aufsteigend sortierte Liste, sortiert nach Zahlen:
---
sortierte Liste
einfuege2Db (Satz x sx) (Dbl ((Satz a sa): restDb)) =
if x < a then Dbl ((Satz x sx) : ((Satz a sa): restDb))
else if x == a then error "bereits vorhanden"
else let Dbl restDblist = einfuege2Db (Satz x sx) (Dbl restDb)
in Dbl ((Satz a sa): restDblist)
istin2Db (Satz x y) (Dbl ((Satz a _): restDb)) =
if x < a then False else
if (x == a) then True
else istin2Db (Satz x y) (Dbl restDb)
Diese Methode hat bereits erhebliche Vorteile bei der Bildung von Vereinigungen, Schnitten und Differenzen. Die folgenden Funktionen sind die Schnittbildung, Vereinigung und Differenz auf sortierten Sätzen.
intersect2Dbtop (Dbl xs) (Dbl ys) = Dbl (intersect2Db
xs ys)
intersect2Db
[] ys = []
intersect2Db
xs []
= []
intersect2Db
((Satz k1 d1): xs1) ((Satz k2 d2): xs2)
=
if k1 == k2
then (Satz k1 d1): (intersect2Db xs1 xs2)
else if k1 < k2
then intersect2Db
xs1 ((Satz k2 d2): xs2)
else intersect2Db
((Satz k1 d1): xs1)
xs2
union2Dbtop (Dbl xs) (Dbl ys) = Dbl (union2Db
xs ys)
union2Db
[] ys = ys
union2Db
xs []
= xs
union2Db
((Satz k1 d1): xs1) ((Satz k2 d2): xs2)
=
if k1 == k2
then (Satz k1 d1): (union2Db xs1 xs2)
else if k1 < k2
19
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
then
else
(Satz k1 d1): union2Db
(Satz k2 d2): union2Db
xs1 ((Satz k2 d2): xs2)
((Satz k1 d1): xs1)
xs2
-xs ohne ys
diff2Dbtop (Dbl xs) (Dbl ys) = Dbl (diff2Db
xs ys)
diff2Db
[] ys = []
diff2Db
xs []
= xs
diff2Db
((Satz k1 d1): xs1) ((Satz k2 d2): xs2)
=
if k1 == k2
then (diff2Db xs1 xs2)
else if k1 < k2
then (Satz k1 d1): diff2Db
xs1 ((Satz k2 d2): xs2)
else
diff2Db
((Satz k1 d1): xs1)
xs2
Man kann leicht nachprüfen, dass diese Funktionen einen Zeitbedarf haben,
der linear in der Anzahl der Summe der Elemente der beteiligten Listen ist.
4.4.2
Suchbaum
Diese Implementierung ist komplizierter, allerdings gibt es Vorteile gegenüber
der Assoziationsliste beim Zugriff und Auffinden der schon eingetragenen Sätze.
Jeder Knoten enthält als Markierung den größten Schlüssel des Teilbaumes mit
den kleineren Werten.
5
7
1
7
1
11
3
3
---
5
Suchbaum
data Suchbaum a = Sblatt
Int a | Sknoten (Suchbaum a) Int (Suchbaum a)
| Suchbaumleer
20
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
data DbS a = Suchbaum (Satz a)
intialisiereDbS x = Suchbaumleer
einfuegeDbS (Satz x sx) Suchbaumleer = Sblatt x (Satz x sx)
einfuegeDbS (Satz x sx) (Sblatt k satzk) =
if x < k
then Sknoten (Sblatt x (Satz x sx)) x (Sblatt k satzk)
else if x == k then error " schon eingetragen"
else Sknoten (Sblatt k satzk) k (Sblatt x (Satz x sx))
einfuegeDbS (Satz x sx) (Sknoten l k r) =
if x < k then Sknoten (einfuegeDbS (Satz x sx) l) k r
else if x == k then error " schon eingetragen"
else Sknoten l k (einfuegeDbS (Satz x sx) r)
istinDbS (Satz x y) Suchbaumleer = False
istinDbS (Satz x y) (Sblatt k _)
= (x == k)
istinDbS (Satz x y) (Sknoten bl k br) =
if x < k then istinDbS (Satz x y) bl
else if x == k then True
else istinDbS (Satz x y) br
erzeugeDbS [] = Suchbaumleer
erzeugeDbS (x:xs) = einfuegeDbS (Satz x x)
druckeDbS sb = blaetterSB
foldSB
foldSB
foldSB
foldSB
::
op
op
op
(erzeugeDbS xs)
(a -> b -> b) -> b -> Suchbaum a -> b
a Suchbaumleer = a
a (Sblatt _ x) = op x a
a (Sknoten x _ y) = (foldSB op (foldSB op a y) x)
--- effiziente Version
blaetterSB = foldSB (:) []
Eine Menge M von Suchbäumen hat logarithmische Tiefe, wenn es eine Konstante c gibt, so dass
∀B ∈ M : log(#(Knoten vonB)) ≥ c ∗ (Tiefe von B)
Analyse des Ressourcenbedarfs der verschiedenen Zugriffsfunktionen:
einfuegeDb hat linearen Ressourcenbedarf abhängig von der Tiefe der möglichen Suchbäume. D.h. wenn die Suchbäume logarithmische Tiefe haben,
dann ist der Ressourcenbedarf O(log(n)), wobei n die Anzahl der Einträge
in die Datenbank ist.
21
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
istinDb hat linearen Ressourcenbedarf abhängig von der Tiefe des Suchbaumes. Ist ebenfalls O(log(n)) für Suchbäume mit logarithmischer Tiefe.
Beispiel 4.4.1 Wenn der Suchbaum nicht balanciert ist, dann ist der Zeitbedarf
des Enthaltenseinstest von der Größenordnung O(n). Betrachte einen Baum der
Form:
Sknoten (Sblatt (Satz 1 "eins") 1
Sknoten (Sblatt (Satz 2 "zwei")
2
....
Dann benötigt die Suche darin offenbar genau soviele Weiterschaltungen wie
Blätter in diesem Baum sind. Das Problem ist, dass diese Bäume durchaus
auch durch normales Einfügen entstehen können.
Ein Ausweg sind balancierte Bäume, mit der charakteristischen Eigenschaft,
dass die Tiefe des Baumes nicht zu groß wird. Optimal verteilt sind die Blätter,
wenn die Tiefe am kleinsten ist. Dies ist allerdings beim Einfügen in beliebiger
Reihenfolge nicht so einfach und kann eine völlige Reorganisation des Baumes
beim Einfügen eines einzigen Elementes erzwingen.
Es gibt verschiedene Möglichkeiten, mit logarithmischem Aufwand balancierte Bäume zu verwalten: eine einfache sind 2-3-Bäume, die an jedem inneren
Knoten 2 oder 3 Nachfolger haben dürfen. Dadurch ist es leicht, beim Einfügen
den Baum jeweils so umzuordnen, dass kein ausgearteter Baum entsteht.
Eine Verallgemeinerung sind B-Bäume, die für vorgegebenes k an jedem
inneren Knoten jeweils zwischen k und 2k−1 Nachfolger haben dürfen (natürlich
nicht für zu kleine Bäume).
Eine Datenstruktur mit Operationen, die eine Balance-Bedingung erfüllt,
aber nicht optimal balanciert sind, sind AVL-Bäume (nach Adel’son-Vel’skii
und Landis)
Die Bedingung ist, dass in jedem Knoten die Differenz der Höhe des rechten
und linken Teilbaumes höchstens 1 ist.
Man kann zeigen, dass in AVL-Bäumen die Höhe logarithmisch ist, d.h. die
Funktion f (n) = max{h | h ist die Höhe eines AVL-Baumes mit n Blättern} ist
O(log n).
Man kann durch Anreichern der Datenstruktur um Balancefaktoren
−1, 0, +1 den Aufwand für die beim Einfügen und Löschen erforderlichen Rotationen gering halten. Natürlich muss dann auch jede Veränderung für den
richtigen Wert dieser Faktoren sorgen.
22
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
A
B
h+1
C
h-1
h
h-1
⇓
B
A
C
h+1
h
h-1
h-1
Die Programmierung in Haskell von leicht abgewandelten AVL-Bäumen sieht
folgendermaßen aus:
-------
AVL-Suchbaum nur fuer Zahlen
Ablatt Wert
Aknoten linke-Hoehe linker-Baum
data AVLsuchbaum = Ablatt
rechte-Hoehe rechter-Baum,
Int | Aknoten Int AVLsuchbaum Int AVLsuchbaum
| Abaumleer
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
23
intialisiereAVL x = Abaumleer
-- Resultat:
(Tiefe erh"oht, Baum)
einfuegeAVL x Abaumleer = Ablatt x
einfuegeAVL x (Ablatt k) =
if x < k
then (Aknoten 1 (Ablatt x) x (Ablatt k))
else if x == k then error " schon eingetragen"
else Aknoten 1 (Ablatt k) k (Ablatt x)
einfuegeAVL x (Aknoten _ l k r)
| x < k = let lneu = (einfuegeAVL x l)
ldl = adepth lneu
ldr = adepth r
baumneu = Aknoten (1+ (max ldl ldr)) lneu k r
in if ldl - ldr > 1 then arotate baumneu
else baumneu
| x == k =
error " schon eingetragen"
| otherwise = let rneu = (einfuegeAVL x r)
ldl = adepth l
ldr = adepth rneu
baumneu = Aknoten (1+ (max ldl ldr)) l k rneu
in if ldr - ldl > 1 then arotate baumneu
else baumneu
adepth (Ablatt x) = 0
adepth (Aknoten d _ _ _) = d
arotate (Aknoten d0 (Ablatt s1) k0
(Aknoten d01 s01 k01 t01@(Aknoten _ _ _ _))) =
let dneu1 = (adepth s01) +1
dneu0 = 1+ (max dneu1 (adepth t01))
baumneu = (Aknoten dneu0 (Aknoten dneu1 (Ablatt s1) k0 s01)
k01 t01)
in if abs (dneu1 - adepth t01) > 1
then arotate baumneu else baumneu
arotate (Aknoten d0 (Aknoten d01 s01@(Aknoten _ _ _ _)
k01 t01) k0 (Ablatt s1)) =
let dneu1 = (adepth t01) +1
dneu0 = 1+ (max dneu1 (adepth s01))
baumneu = (Aknoten dneu0 s01 k01
(Aknoten dneu1 t01 k0 (Ablatt s1)))
in if abs(dneu1 - adepth s01) > 1
then arotate baumneu else baumneu
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
24
arotate (Aknoten d0 s0@(Ablatt s1) k0
(Aknoten d01 s01@(Aknoten _ s011 k011 s012)
k01 t01@(Ablatt t011))) =
arotate4 s0 k0 s011 k011 s012 k01 t01
arotate (Aknoten d0 (Aknoten d01 s01@(Ablatt _) k01
(Aknoten _ t11 k11 t12)) k0 s11@(Ablatt s1)) =
arotate4 s01 k01 t11 k11 t12 k0 s11
arotate (Aknoten d0 s1@(Aknoten d01 s01 k01 t01) k0 t1
@(Aknoten d11 s11 k11 t11)) =
if d01 > d11 then arotate3 s01 k01 t01 k0 t1
else arotate3 s1 k0 s11 k11 t11
-- 5 faelle: 544, 454 445, 554,455
arotate3 b1 k1 b2 k2 b3 =
let d1 = adepth b1
d2 = adepth b2
d3 = adepth b3
dd1 = d1-d2
dd2 = d2 -d3
(Aknoten _ b21 k21 b22) = b2
in
if dd1 == 1 && dd2 == 0 || dd1 == 0 && dd2 == 1
then Aknoten (1+ (max d1 (1 + (max d2 d3))))
b1 k1 (Aknoten (1+ (max d2 d3)) b2 k2 b3)
else if dd1 == 0 && dd2 == -1 || dd1 == -1 && dd2 == 0
then Aknoten (1+ (max d3 (1+ (max d1 d2))))
(Aknoten (1+ max d1 d2) b1 k1 b2) k2 b3
else arotate4 b1 k1 b21 k21 b22 k2 b3
-3 faelle: 4444, 4344, 4434
arotate4 b1 k1 b2 k2 b3 k3 b4 =
let d1 = adepth b1
in Aknoten (d1+2) (Aknoten (d1+1) b1 k1 b2) k2
(Aknoten (d1+1) b3 k3 b4)
istinAVL x Abaumleer = False
istinAVL x (Ablatt k)
= (x == k)
istinAVL x (Aknoten _ bl k br) =
if x < k then istinAVL x bl
else if x == k then True
else istinAVL x br
erzeugeAVL [] = Abaumleer
erzeugeAVL (x:xs) = einfuegeAVL x (erzeugeAVL xs)
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
25
foldAVL op a Abaumleer = a
foldAVL op a (Ablatt x) = op x a
foldAVL op a (Aknoten _ x _ y) = (foldAVL op (foldAVL op a y) x)
blaetterAVL = foldAVL (:) []
druckeAVL sb = blaetterAVL
testvarAVL = (erzeugeAVL ([100,99..50] ++ [1..49]))
testmischeqAVL = (blaetterAVL (erzeugeAVL ([1..30]++[60,59..31]++[61..100])))
== mischsort ([1..30]++[60,59..31]++[61..100])
4.4.3
Binäre Suche
Diese Suche hat als Ausgangspunkt ein Feld (oder eine Liste) von sortierten
Elementen. Gegeben ein Element, finde den Index. Implementiert man dies als
Liste, so ist die Suche von linearer Zeitbedarf, da man die Liste durchlaufen
muss, um an die Elemente zu kommen. Dies ist verbesserbar, wenn die Elemente in einem Feld sind: Man kann dann die Suche nach der Methode der
Intervallhalbierung durchführen.
binaere_suche x ar =
let (m,n) = (bounds ar) in binaere_suche_r x ar m n
binaere_suche_r x ar m n =
if m == n then
if (ar!m) == x then (True,m)
else (False, error "binsuche")
else let mid = (m+n) ‘div‘ 2
in if ar ! mid > x binaere_suche_r x ar m mid
else binaere_suche_r x ar mid n
4.5
Schnitt und Vereinigung von Mengen
Die interessanten bisher noch fehlenden Operationen der Datenstruktur Menge
sind Schnitt, Vereinigung, Differenz und Potenzmenge.
unsortierte Listen In diesem Fall ist es zur Berechnung des Schnitts zweier
Mengen A, B notwendig, alle Elemente der Liste A mit allen Elementen
der Liste zu B zu vergleichen. D.h. die Anzahl der Reduktionen ist |A|∗|B|,
d.h. O((|A|+|B|)2 ) bzw. O(n2 ), wenn n die Gesamtgröße der Eingabelisten
ist.
sortierte Listen Die Berechnung des Schnittes oder der Vereinigung erfolgt
durch Mischen (Merge). Hierbei kann man sogar ohne viel Extra-Aufwand
dafür sorgen, dass das Resultat aufsteigend sortiert ist.
26
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
merge_schnitt [] xb = []
merge_schnitt (x:xs) [] = []
merge_schnitt (x:xs) (y:ys) =
if x < y then merge_schnitt xs (y:ys)
else if x == y then x : merge_schnitt xs ys
else merge_schnitt (x:xs) ys
Offenbar wird bei jedem Schritt ein Element verarbeitet, das danach nicht
mehr angefasst wird. D.h.der Zeitbedarf ist O((|A| + |B|)).
balancierte Suchbäume Wir gehen vereinfacht davon aus, dass wir Mengen
von ganzen Zahlen haben. Die Idee zur Berechnung des Schnitts von A
und B ist einfach: man nimmt jedes Element des Suchbaumes für A und
testet, ob es im Suchbaum für B ist.
schnittSB xa xb = schnittSBliste (blaetterSB xa) xb
schnittSBliste [] _ = Suchbaumleer
schnittSBliste (x:xs) sb =
if istinDb x sb
then einfuegeDbS x (schnittSBliste xs
else schnittSBliste xs sb
sb
Der Aufwand berechnet sich folgendermaßen:
Zeit = |A| ∗ log(|B|), d.h. n ∗ log(n) im schlechtesten Fall, wenn n =
|A| + |B|.
Die zweite Möglichkeit ist es, die Misch-Idee auch für Suchbäume zu verwenden:
-Schneiden von Mengen mit Suchbaeumen durch Mischen
-!! Diese haelt nicht die Bedingung ein, dass
--Knotenmarkierung = Maximum der linken
-Schluessel, sondern nur noch die Bedingung: ist in der Mitte
mschnittSB Suchbaumleer _ = Suchbaumleer
mschnittSB _ Suchbaumleer = Suchbaumleer
mschnittSB (Sblatt x a) (Sblatt y _) =
if x == y then (Sblatt x a) else Suchbaumleer
mschnittSB (Sknoten lta ka rta) (Sblatt y b) =
if
y == ka then (Sblatt y b)
else if y < ka then mschnittSB lta (Sblatt y b)
else mschnittSB rta (Sblatt y b)
mschnittSB tl@(Sknoten lta ka rta) tr@(Sknoten ltb kb rtb) =
if
ka == kb
27
Praktische Informatik 1, WS 2004/05, Kapitel 4, vom 28.01.2005
then mappendSB (mschnittSB lta ltb) ka (mschnittSB rta rtb)
else if ka < kb
then mappendSB (mschnittSB lta ltb) ka (mschnittSB rta tr)
else mappendSB (mschnittSB lta tr) ka (mschnittSB rta rtb)
mschnittSB x y = mschnittSB y x
--
Sblatt
+ Sknoten
mappendSB Suchbaumleer _ t = t
mappendSB t _ Suchbaumleer = t
mappendSB l k r = Sknoten l k r
Ein Analyseversuch bei balancierten Bäumen ergibt eine Gleichung
τ (m, n) = τ (m/2, n/2) + τ (m/2, n) für den Zeitbedarf τ . Dies deutet darauf hin, dass diese Methode keinen linearen Zeitbedarf hat, sondern etwas
mehr, vermutlich sogar quadratisch. Nimmt man an, dass bei einem weiteren Schritt der linke und rechte Baum beide zerlegt werden, so ergibt sich
eine Rekursionsgleichung von τ (m, n) = 3 ∗ τ (m/2, n/2). Deren Lösung ist
O(nlog3 ) = O(n1,585 ).
D.h., diese Methode ist offenbar langsamer als das einzelne Testen.
Beste Methode zur Schnitt-Berechnung (Vereinigung,Differenz)
Asymptotisch und im worst-case am besten ist folgende Methode:
• zunächst erzeuge die Liste der Blätter (linear).
• Verarbeite diese mit einer Misch-operation Schnitt, Vereinigung, Differenz
(linear).
• und danach wandle die Liste in ein Feld um (linear). Hier ist allerdings
der logarithmische Faktor im Index versteckt.
• Baue den Baum aus dem Feld wieder auf (linear).
Herunterladen