3.6 Version 16.6.2009 - Benutzer-Homepage

Werbung
Algorithmen &
Datenstrukturen
Sommersemester 2009
Prof. Dr. Peter Kneisel
1
Didaktik: Was will diese Vorlesung
 Datenstrukturen und Algorithmen sind die Grundlagen jeder SoftwareEntwicklung.
 Datenstrukturen modellieren die (statischen) Strukturen der zu behandelnden
Systeme,
 Algorithmen modellieren die (dynamischen) Prozesse, die auf den Strukturen
arbeiten.
Die Systeme und deren Verhalten, die modelliert werden, sind vielfältig.
Dennoch werden in der Informatik immer wiederkehrende Strukturen und
Prozesse zu deren Modellierung verwendet .
 Diese grundlegenden Modelle werden in dieser Vorlesung behandelt und – das
ist fast noch wichtiger – in vielen Aspekten diskutiert.
Daher können Studierende



die grundlegenden Datenstrukturen und Algorithmen sinnvoll auswählen und
umsetzen.
Leistungsparameter von Algorithmen abschätzen und optimieren.
auch weiterführende Datenstrukturen und Algorithmen entwerfen, umsetzen
abschätzen und optimieren.
2
Didaktik: Durchführung
 Diese Vorlesung enthält Übungen
 Die Übungen werden je nach Bedarf durchgeführt.
 Zur Vorbereitung werden Übungsblätter, je nach Vorlesungsverlauf
zusammengestellt.
 Weitere Übungen sind im Foliensatz vorhanden und sollten selbständig und
vollständig bearbeitet werden.
 Vorsicht !
 Kommen Sie in alle Veranstaltungen - machen Sie die Übungen
 … auch wenn vieles auf JAVA zugeschnitten ist, so sind die Konzepte
verallgemeinbar und vielseitig zu verwenden – insb. seien mir syntaktische
„Ungenauigkeiten“ verziehen und sogar zusätzclicher Ansporn für eigene
konstruktive Verbesserungsvorschläge ;-)
3
Didaktik: Folien
 Der Vorlesungsstoff wird anhand von Folien dargelegt
 Die Folien bilden nur einen Rahmen für die Inhalte. Die Folien sollten daher mit Hilfe
eigener Vorlesungsskizzen ergänzt werden - am besten in Form einer
Vorlesungsnachbereitung max. 3 Tage nach der Vorlesung
 Zusätzlich zu den Folien werden Beispiele an der Tafel oder am Rechner gezeigt.
Diese sollten Sie vollständig mitskizzieren.
 Zur vollständigen Nachbereitung, z.B. als Klausurvorbereitung, sind die Folien
einheitlich strukturiert
 Es gibt genau drei Gliederungsebenen: Kapitel, Unterkapitel, Abschnitte
 Die Inhalte jedes Kapitels und jedes Unterkapitels werden jeweils motiviert und sind
verbal beschrieben. Zusätzlich gibt es jeweils ein stichwortartiges Inhaltsverzeichnis
der Unterkapitel, bzw. Abschnitte
 Die Vorlesung wird ständig überarbeitet, so dass sich die Foliensätze ändern
können (und werden)
 Laden Sie sich zur endgültigen vollständigen Klausurvorbereitung nochmals
zusätzlich den kompletten Foliensatz herunter.
4
Literatur
 Diese Veranstaltung ist anhand (wirklich) vieler Bücher und einer Menge
eigener Erfahrungen erstellt worden. Jedes Buch hat dabei Schwerpunkte in
speziellen Bereichen und ist daher sinnvoll. Eine Auflistung aller dieser Bücher
ist nicht sinnvoll.
Stellvertretend für all diese Bücher sei hier ein Buch angeführt:
 Robert Sedgewick: „Algorithmen in Java: Teil 1-4“; Addison-Wesley 2003
viele Programmierbeispiele sind auch aus:
 G.Saake, K.-U. Sattler: „Algorithmen & Datenstrukturen: Eine Einführung mit Java“,
dpunkt.verlag, 2002
der Klassiker ist:
 N.Wirth: „Algorithmen & Datenstrukturen“, Teubner, 1979
 Motivation ist alles !
Haben Sie meine Empfehlungen aus dem ersten Semester beherzigt ?





S.Singh: „Fermats letzter Satz“; DTV, 9.Auflage 2004
M. Spitzer: „Geist im Netz“; Spektrum, Akad. Verlag 2000
H. Lyre: „Informationstheorie“; UTB, 2002
A.Hodges: „Alan Turing, Enigma“; Springer-Verlag, 1983
D.R.Hofstadter: „Gödel, Escher, Bach“; Klett-Cotta, 2006 (Taschenbuch 1991)
5
Inhalt
 In „Grundlagen der Informatik“ haben wir uns mit zwei grundlegenden Aspekte
der Informatik befasst:
 Was ist Information und wie kann man diese auf höheren semantischen Ebenen
strukturieren.
 Aus welchen einfachen Elementen ist ein (imperativer) Algorithmus aufgebaut
 „Algorithmen & Datenstrukturen“ nimmt diese Zweiteilung auf:
 Zunächst werden wir die semantische Leiter nach oben steigen und komplexere
semantische Strukturen kennenlernen, die grundlegend für Lösungen vieler typischer
Problemstellungen sind.
 Anschließend werden wir die wichtigsten Algorithmen kennenlernen, die auf diesen
Strukturen arbeiten.
 Inhalt
1. Abstrakte Datentypen (ADTs)
2. Suchen: Grundlagen, Algorithmus, Analyse
3. Sortieren Grundlagen, Algorithmus, Analyse
6
OOP
Sortieren
Suchen
ADTs
A&D
Datenstrukturen
Komplexität
Zahlen
Verifikation
Zeichen
Strukturierung
Codes
Elemente
Information
GDI
Statik, Struktur
PIS
Dynamik, Algorithmik
Überblick und Einordnung
RA
7
Kapitel 1 Abstrakte Datentypen (ADTs)
 In „Grundlagen der Informatik“ haben wir elementare Strukturen kennengelernt
und gesehen, wie daraus mit komplexeren Strukturierungsverfahren
komplexere Strukturen aufgebaut werden können.
Wir haben uns dabei genau auf die Strukturen beschränkt, die den meisten
imperativen Programmiersprachen gemeinsam sind.
In diesem Kapitel gehen wir nun in semantisch höhere Ebenen und erläutern
Strukturen, die häufig verwendet werden, aber nicht im Sprachumfang der
meisten Programmiersprachen liegen (sehr wohl aber in Klassenbibliotheken)
 Inhalt
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
Wiederholung
Was sind ADTs
Stacks (Kellerspeicher, Stapel)
Queues (Warteschlangen)
Einfach verkettete Listen
Zweifach verkettete Listen
Hashlisten
Bäume
Graphen
Frameworks
8
1.1
Wiederholung
 Wir haben bereits in „Grundlagen der Informatik“ einiges über die Beziehung
von Datentypen erfahren.
Was, wird hier kurz zusammengefasst
1. Datenstrukturen
2. Datentypen
3. KLassifikation von Datentypen
9
1.1.1 Datenstrukturen
 In der Informatik werden Objekte der realen oder abstrakten Welt erfasst
 Bei der Erfassung beschränkt man sich möglichst auf die für den weiteren Transport
/ Speicherung/Verarbeitung/Umsetzung notwendige Information
 Zur internen Repräsentation werden diese Objekte abstrahiert
 Zur Abstraktion gehört die Erkennung von Strukturen - zunächst im Sinne einer
Aggregation.
 Also
 Aus welchen Teilobjekten bestehen Objekte ?
 In welchem Verhältnis stehen die Teilobjekte zueinander ?
 Welches sind die „atomaren“ Teilobjekte ?
 es existieren noch weitere strukturelle Beziehungen (z.B. Vererbung)
 Anschließend werden diese Objekte typisiert.
 Typisierung ist die Einteilung von abstrakten internen Objekten in Gruppen mit
gleichen oder ähnlichen Eigenschaften.
10
1.1.2 Datentypen
 Typen sind also nicht die intern repräsentierten Objekte, sondern beschreiben
die Eigenschaft einer Gruppe von Objekten.
 Zu diesen Eigenschaften gehören:






Struktur
Wertebereich
anwendbare Operatoren, Funktionen, Relationen
Beziehungen zu anderen Typen
interne Repräsentationsweise
…
Beispiel:
Imaginäre Zahlen
Einige Anmerkungen::
Der Begriff „Datentyp“ ist weitergehend als der Begriff
„Datenstruktur“
In der Objektorientierten Programmierung wird statt „Datentyp“
auch der Begriff „Klasse“ verwendet (Klassen beschreiben
mehr Eigenschaften)
Konkrete Repräsentanten eines Datentyps werden (u.a) „Variable“
oder
11
- bei OO-Sprachen - „Instanz“ genannt
1.1.3 Klassifikation der Datentypen
Datentypen
Konkrete
Einfache
Abstrakte
Pointer(Zeiger)
Idealisierte
Strukturierte
...
Ordinale
Boolean
(Wahrheitswert)
Integer
(Ganzzahl)
Real
(Fließkomma)
Char
(Zeichen)
Array
(Feld)
Record
Union
(Verbund) (Variantenverb.)
...
Enumeration
(Aufzählung)
12
1.1.3 Erläuterung der Klassifikation
 Idealisierte Datentypen
 aus der Mathematik bekannte Datentypen: R, N, Z, ...
 Variablen dieser Typen sind oft nicht endlich darstellbar (Bsp: 2)
 In einem Computer-Algebra-System symbolisch darstellbar (Bsp: 2^( 1/2))
 Konkrete Datentypen
 in einem Rechner von Hard- oder Software bereitgestellte Datentypen
 entweder vordefiniert oder durch den Benutzer definierbar
 Abstrakte Datentypen
 verbergen ihren inneren Aufbau vor dem Benutzer
 bestehen aus beliebigen Strukturen über konkrete/idealisierte Datentypen, sowie aus
Zugriffsfunktionen bzw. Prozeduren
 Beispiel: Baum
13
insert (Element)
6
2
61
12
15
delete (Element)
79
search (Element)
13
1.2
Was sind ADTs
 „Ein abstrakter Datentyp fasst die wesentlichen Eigenschaften und Operationen
einer Datenstruktur zusammen, ohne auf deren eigentlichen Realisierung im
Rechner einzugehen“
Konkrete Datentypen werden aus ordinalen (Basis-) Datentypen konstruiert und
sind somit direkt in einer Implementierung einsetzbar.
1.
2.
3.
4.
5.
6.
Grundsätze
Algebren
Signaturen
Axiome
Beispiel einer ADT-Schnittstelle
Anwendung: Tabelle
14
1.2.1 Grundsätze
 Kapselung:
Ein abstrakter Datentyp darf nur über seine Schnittstellen benutzt werden.
Das bedeutet insbesondere,
 dass interne Strukturen von außen nicht direkt zugreifbar sind
 dass interne Strukturen, die nicht über Operationen der Schnittselle zugreifbar sind,
gar nicht von außen zugegriffen werden können.
 Geheimnisprinzip:
Die interne Realisierung eines abstrakten Datentyps ist verborgen.
Das bedeutet insbesondere,
 dass konkrete Umsetzungen von ADTs sehr stark von der verwendeten
Programmiersprache und der geplanten Verwendung abhängen.
 Diese Prinzipen der Kapselung und des Geheimnisprinzips wurden schon in
frühen rein prozeduralen imperativen Programmiersprachen gefordert, aber erst
mit der Einführung objektorientierter imperativer Programmiersprachen ducrh
Sprachkonstrukte mehr oder weniger erzwungen.
 In Pascal konnte man Teilstrukturen eines abstrakten Datentyps jederzeit auch von
außen zugreifen. Die möglichen Operation waren sprachlich nicht mit den Strukturen
verknüpft.
 In Java werden Datenstrukturen als „private“ vor Zugriffen von außen geschützt und
Operationen in Methoden „geheim“ realisiert.
15
1.2.2 Algebren
 Datentypen (auch abstrakte) lassen sich mathematisch als „Algebren“
betrachten ( Vorlesung „Diskrete Strukturen“)
 Eine Algebra ist definiert durch Wertemengen und die Operatoren, die man darauf
anwenden kann.
 Bsp: Betrachten Sie die natürlichen Zahlen.
 darauf lassen sich (zunächst) die Operatoren: +, -, x und % (ganzahliges Teilen)
anwenden, als Ergebnis bekommen Sie Werte aus der Wertemenge der natürlichen
Zahlen
 Sie können aber auch Vergleichsoperatoren: >, <, ==, != anwenden, dann
bekommen Sie als Ergebnis Werte einer anderen Wertemenge, die der bool‘sche
Zahlen: true, false,
 Sie können nun auf die Wertemenge der bool‘schen Werte auch bool‘sche
Operatoren anwenden: , ,  als Ergebnis bekommen Sie wieder bool‘sche Werte.
 Ihre gesamte Algebra verwendet also zwei Sorten von Datenstrukturen (mehrsortige
Algebra): natürliche Zahlen und bool‘sche Werte und kann darauf unterschiedliche
Operatoren anwenden: +, -, x, %, >, <, ==, !=, , ,  wobei nicht jeder Operator auf
jeden Wert (oder Wertepaar) anwendbar ist.
 Eine Algebra ist also definiert durch ihre Sorten, die Operationen und die Art,
wie diese Operationen auf Werte der Sorten anwendbar sind.
16
1.2.3 Signaturen
 Die Schnittstellen eines (A)DTs - also die Art, wie man den (A)DT verwendet lassen sich durch seine Signatur beschreiben.
 Bsp: betrachten Sie den Datentyp integer:
 integer unterstützt/erzeugt zwei Sorten: integer und bool
 integer unterstützt die Operatoren:
const
:  integer // nullstelliger Operator: Konstante
successor
: integer  integer // einstellige Operation
+, -, x, %
: integer  integer  integer // zweistellige Operation
>, <, ==, != : integer  integer  bool // zweistellige Operation
, 
: bool  bool  bool // zweistellige Operation

: bool  bool // einstellige Operation
 Diese Formalisierung einer Algebra beschreibt die Strukturen und die
Operationen eines (abstrakten) Datentyps und wird Signatur des Datentyps
genannt.
Aus der Signatur eines (A)DTs geht also insbesonder hervor:
 Dessen Wertebereiche in den unterschiedlichen Sorten
 Die Operatoren und deren Stelligkeit
 Die Wertebereiche der bei den Operationen verwendeten Operanten
17
1.2.4 Axiome
 Selbst wenn Sie die Signatur eines (A)DT kennen, wissen Sie zwar welche
Operatoren auf welche Wertebereiche (Sorten) anzuwenden sind, Sie wissen
aber immer noch nicht wie die Werte durch die Operatoren verändert werden:
Das beschrieben Sie mit Axiomen.
 Bsp.: Betrachten Sie die natürlichen Zahlen, so gilt z.B. für die Addition
folgendes Axiom:
+ (i,0) = i
+ (i,successor (j)) = succesor (+ (i,j))
 Entsprechend lassen sich für alle Operatoren Axiome aufstellen. Damit ergibt
sich als Spezifikation für den ADT integer: (in Pseudo-Notation)
type: integer
// implizit auch verwendbare Sorte
import: boolean // Sorten, die zusätzlich verwendet werden
operators:
+, -, x, %
: integer  integer  integer
...
axioms:  i,j : integer
+ (i,0) = i
+ (i,successor (j)) = succesor (+ (i,j))
...
18
1.2.5 Beispiel einer ADT-Schnittstelle
type: list(T)
// T ist die Wertemenge der Elemente
// T ist ein sog. Sortenparameter
import: integer
operators:
[]
:  list
_ : _
: T x list  list
//
//
//
//
//
erweitert Liste
_ : _ ist Infix-Operator
Kopf der Liste
Liste ohne Kopf
Anzahl Listenelemente
head
: list  T
tail
: list  list
length
: list  integer
axioms:  l : list,  x : T
head ( x : l )
= x
tail ( x : l )
= l
lenght ( [] )
= 0 // [] ist leere Liste
length ( x : l ) = successor ( length (l) )
19
1.2.6 Anwendung: Tabellen
 Listen repräsentieren oft „Tabellen“:
 Definition:
Eine Tabelle o der Größe n ist eine Folge (z.B. Liste) von n Elementen gleichen
Typs o = (o1, o2, … , on)
 Oft sind die Elemente einer Tabelle nochmals in zwei Teile unterstruktiert:
 Schlüssel-Daten (key)
Die Schlüsseldaten bezeichnen (oft eindeutig) das Element einer Liste.
Der Key kann nochmals unterstrukturiert sein.
 Informations-Daten (info)
Die Informations-Daten geben für das durch den key bezeichnete Element
zusätzliche Informationen an.
Auch info kann nochmals unterstrukturiert sein.
key1
info1
key2
info2
…
keyn
infon
Anmerkung:
Da die Indizierung von Listen in
vielen Programmiersprachen mit
„0“ beginnt, man aber in der realen
Welt mit „1“ zu zählen beginnt, wird
das „0“-te Element oft als DummyElement mit einem Dummy-Wert
versehen und ignoriert.
20
1.3. Stacks (Kellerspeicher, Stapel)
 Stacks (Kellerspeicher, Stapel) sind einfache Abstraktionen von Strukturen, die
in vielen Bereichen der Informatik, insbesondere aber in den systemnahen
Bereichen verwendet werden.
Stacks bezeichnet man manchmal auch als LIFO (Last in – First Out)Schlangen
1. Spezifikation
2. Implementierung
3. Die Java-Klasse „stack“
21
1.3.1 Spezifikation
type: stack(T) // T ist die Wertemenge der Elemente
import: boolean
operators:
empty
:  stack
// erzeugt leeren Stack
push
: stack x T  stack
// Legt Element auf Stack
pop
: stack  stack
// nimmt Element von Stack
top
: stack  T
// zeigt oberstes Element an
is_empty : stack  boolean
// ist Stack leer ?
axions:  s : stack,  x : T
pop (push (s,x)) = s
top (push (s,x)) = x
is_empty (empty) = true // empty ist Wert des Stack
is_empty (push (s,x)) = false
22
1.3.2 Implementierung eines Stacks
public class ArrayStack implements Stack {
public void push(Object obj) throws
StackException {
if (num == elements.length)
// KapazitŠt erschöpft
throw new StackException();
elements[num++] = obj;
}
private Object elements[] = null; //
Elemente
private int num = 0; // aktuelle Anzahl
// Stack mit vorgegebener Größe erzeugen
public ArrayStack(int size) {
elements = new Object[size];
}
public Object pop() throws StackException {
if (isEmpty()) // Stack ist leer
throw new StackException();
Object o = elements[--num];
elements[num] = null;
return o;
}
// Abfrage auf leeren Stack
public boolean isEmpty() {
return num == 0;
}
public Object top() throws StackException {
if (isEmpty()) // Stack ist leer
throw new StackException();
return elements[num - 1];
}
}
23
1.3.3 Die Java-Klasse „stack“
import java.util.*;
public class StackExample {
public static void main(String[] args)
{
Stack s = new Stack(); // ohne Parameter
s.push("Erstes Element"); // Rückgabewert: eingefügtes Element ...
s.push("Zweites Element"); // ... wird ignoriert
s.push("Drittes Element");
while (true) {
try {
System.out.println(s.pop());
//
?
peek() würde Element entfernen
} catch (EmptyStackException e) { // wird beim Lesezugriff auf ...
break;
// ... leeren Stack geworfen
}
}
}
}
24
1.4. Queues
 Queues (Warteschlangen) sind lineare Listen, deren Elemente nach dem FIFOPrinzip (First in–First Out) ein- bzw. ausgefügt werden
Auch Queues kommen in systemnahen Bereichen vor, insbesondere bei
Betriebssystemen.
1. Spezifikation
2. Implementierung einer Queue
3. Die Java-Klasse „queue“
25
1.4.1 Spezifikation
type: queue(T) // T ist die Wertemenge der Elemente
import: boolean
operators:
empty
:  queue
// erzeugt leere Queue
enter
: queue x T  queue // stellt Element ans Ende der Queue
leave
: queue  queue
// nimmt erstes Element von Queue
front
: queue  T
// zeigt erstes Element der Queue
is_empty : queue  boolean
// Ist Queue leer ?
axions:  q : queue,  x : T // empty ist der Wert einer leeren queue
leave (enter (empty,x)) = empty
// (x) ohne Kopf = empty
leave (enter (enter(q,x),y)) = enter (leave (enter (q,x)), y)
// (q,x,y) ohne Kopf = (q,x) ohne Kopf + y -> ((q,x) ohne Kopf,y)
front (enter (empty,x)) = x
// Kopf von (x) = x
front (enter (enter(q,x), y)) = front (enter (q,x))
// Kopf von (q,x,y) = Kopf von (q,x)
is_empty (empty)
= true // is_empty von empty ist true
is_empty (enter(q,x)) = false // is_empty von (q,x) ist falsch
26
1.4.2 Implementierung einer Queue
public class ArrayQueue implements Queue {
private Object[] elements; // Elemente
private int l = 0; // „lower“ Zeiger
private int u = 0; // „upper“ Zeiger
// in der Queue sind max. size-1 Elemente
// Einfügen eines Elementes
public void enter (Object obj) throws
QueueException {
if ((elements.length - l + u) %
elements.length
== elements.length - 1)
// Kapazität ist erschöpft (= size-1)
throw new QueueException ();
elements[u] = obj;
// oberen Zeiger aktualisieren
u = (u + 1) % elements.length;
// Modulo, da array zyklisch verwendet.
}
// Queue mit vorgegebener Länge erzeugen
public ArrayQueue (int size) {
elements = new Object[size];
}
public boolean isEmpty () {
return l == u;
}
// Herausnehmen des lower-Elementes
public Object leave () throws QueueException
{
if (isEmpty ())
throw new QueueException ();
Object obj = elements[l];
elements[l] = null;
// unteren Zeiger aktualisieren
l = (l + 1) % elements.length;
return obj;
}
// Zeige das lower Element
public Object front () throws
QueueException {
if (isEmpty ())
throw new QueueException ();
return elements[l];
}
}
27
1.4.3 Die Java-Klasse „queue“
import java.util.*;
public class QueueExample {
public static void main(String[] args)
{
Queue<String> queue = new LinkedList<String>(); // <...> gibt den Typ
// von Elementen an
queue.offer( "Fischers" );
queue.offer( "Fritze" );
queue.offer( "fischt" );
queue.offer( "frische" );
queue.offer( "Fische" );
queue.poll();
queue.offer( "Nein, es war Paul!" );
while ( !queue.isEmpty() )
System.out.println( queue.poll() );
}
}
// und es gibt noch einige weitere Queues in java.util.*
28
1.5
Einfach verkettete Liste
 Listen sind (ziemlich) simple Datentypen, die sich statisch durch den konkreten
strukturierten Datentyp „array (Feld)“ darstellen lässt und damit in den meisten
Programmiersprachen implizit vorhanden ist.
In der nicht-imperativen Programmiersprache LISP ist „Liste“ zudem der einzige
strukturierte Datentyp.
Möchte man die Länge einer Liste jedoch zur Laufzeit eines Programmes
dynamisch verändern so muss man auf eigenen Umsetzungen mithilfe eines
ADTs zurückgreifen.
1.
2.
3.
4.
class
main
Methoden
Implementierung als Liste
29
1.5.1 class
public class List {
static class Node {
Object obj;
Node next;
public
public
public
public
public
public
Node(Object o, Node n)
{ obj = o;
next = n; }
Node()
{ obj = null; next = null; }
void
setElement(Object o) { obj = o; }
Object getElement()
{ return obj; }
void
setNext(Node n)
{ next = n; }
Node
getNext()
{ return next; }
}
private Node head = null;
public
public
public
public
public
public
public
public
public
}
List()
void
addFirst(Object o)
void
addLast(Object o)
Object getFirst()
throws
Object getLast()
throws
Object removeFirst() throws
Object removeLast() throws
int size()
boolean isEmpty()
ListEmptyException
ListEmptyException
ListEmptyException
ListEmptyException
{}
{}
{}
{}
{}
{}
{}
{}
{}
30
1.5.2 main
public static void main(String args[]) {
List lst = new List();
lst.addFirst("Drei");
lst.addFirst("Zwei");
lst.addFirst("Eins");
lst.addLast("Vier");
lst.addLast("Fünf");
lst.addLast("Sechs");
while (! lst.isEmpty()) {
System.out.println((String) lst.removeFirst());
}
}
31
1.5.3 Methoden
public List() {
head = new Node();
}
public void addFirst(Object o) {
Node n = new Node(o, head.getNext());
head.setNext(n);
}
public Object getFirst() throws ListEmptyException
{
if (isEmpty())
throw new ListEmptyException();
return head.getNext().getElement();
public
Object removeFirst() throws ListEmptyException {
}
if (isEmpty())
throw
new
ListEmptyException();
public
void
addLast(Object
o) {
Object
head.getNext().getElement();
Node l o
= =
head;
head.setNext(head.getNext().getNext());
while (l.getNext() != null)
return
o;
l = l.getNext();
} Node n = new Node(o, null);
l.setNext(n);
public
Object removeLast() throws ListEmptyException {
}
if (isEmpty())
throw new ListEmptyException();
Node l = head;
while (l.getNext().getNext() != null)
l = l.getNext();
Object o = l.getNext().getElement();
l.setNext(null);
return o;
32
1.5.4 Implementierung als Liste
public class ListStack implements Stack {
private List list; // Liste zur Verwaltung der Elemente
public ListStack () {
list = new List ();
}
public void push (Object obj) {
// Element vorn anfŸgen
list.addFirst (obj);
}
public Object pop () throws StackException {
if (isEmpty ())
throw new StackException ();
// Element von vorn entfernen
return list.removeFirst ();
}
public Object top () throws StackException {
if (isEmpty ())
throw new StackException ();
return list.getFirst ();
}
public boolean isEmpty () {
return list.isEmpty ();
}
}
33
1.6
Zweifach verkettete Liste
 Aus bestimmten Gründen – vor allem Laufzeit-Effizienz – verwendet man oft
Listen, deren einzelne Elemente nicht nur den jeweiligen Nachfolger, sondern
auch den jeweiligen Vorgänger kennen.
Diese Listen nennt man das „Zweifach bzw. Doppelt verkettete Listen“
1.
2.
3.
4.
class
iterator
main
Methoden
34
1.6.1 class
public class DList {
static class Node {
Object obj;
Node prev, next;
public Node (Object o, Node p, Node n) { obj = o; prev = p; next = n; }
public Node () { obj = null; prev = next = null; }
...
// Setter und Getter-Methoden
public void
setElement (Object o) { obj = o; }
public Object getElement ()
{ return obj; }
public void
setNext
(Node n)
{ next = n; }
public Node
getNext
()
{ return next; }
public void
setPrevious (Node p)
{ prev = p; }
public Node
getPrevious ()
{ return prev; }
}
private Node head = null;
private Node tail = null;
...
public java.util.Iterator iterator () {}
}
35
1.6.2 iterator
class ListIterator implements java.util.Iterator {
private Node node = null;
public ListIterator () {
node = head.getNext();
}
public boolean hasNext () {
return node.getNext () != tail;
}
public void remove () {
throw new UnsupportedOperationException ();
}
public Object next () {
if (! hasNext ())
throw new java.util.NoSuchElementException ();
Object o = node.getElement ();
node = node.getNext ();
return o;
}
}
36
1.6.3 main
public static void main (String args[]) {
DList lst = new DList ();
java.util.Iterator it = lst.iterator ();
while (it.hasNext ()) {
System.out.println ((String) it.next ());
}
lst.addFirst ("Drei");
lst.addFirst ("Zwei");
lst.addFirst ("Eins");
lst.addLast ("Vier");
lst.addLast ("Fünf");
lst.addLast ("Sechs");
it = lst.iterator ();
while (it.hasNext ()) {
System.out.println ((String) it.next ());
}
}
37
1.6.4 Methoden
public DList () {
head = new Node (); //
tail = new Node (); //
head.setNext(tail); //
tail.setPrevious(head);
tail.setNext(tail); //
}
dieser Knoten existiert immer, auch bei leerer Liste
dieser Knoten existiert immer, auch bei leerer Liste
head und tail werden initial miteinander verlinkt
tail.next zeigt auf sich selbst
public void addFirst (Object o) {
Node n = new Node (o, head, head.getNext());
head.getNext ().setPrevious (n);
head.setNext (n);
}
public Object removeFirst () throws
ListEmptyException {
if (isEmpty ())
throw new ListEmptyException ();
Object o = head.getNext ().getElement ();
head.setNext (head.getNext ().getNext ());
public Object getFirst () throws ListEmptyExceptionhead.getNext
{
().setPrevious (head);
if (isEmpty ())
return o;
throw new ListEmptyException ();
}
return head.getNext ().getElement ();
}
public Object removeLast () throws
ListEmptyException {
public void addLast (Object o) {
if (isEmpty ())
Node l = tail.getPrevious ();
throw new ListEmptyException ();
Node n = new Node (o, l, tail);
Node n = tail.getPrevious ();
l.setNext (n);
n.getPrevious ().setNext (tail);
tail.setPrevious (n);
tail.setPrevious (n.getPrevious ());
}
return n.getElement ();
}
38
1.7
Hashlisten
 Hashlisten sind Listenstrukturen, manchmal erweitert durch „weitere“
Strukturen, die sich sehr gut für das Suchen eignen ( Kapitel 2).
Hier seien die grundlegenden Ideen des Hashens dargestellt.
1.
2.
3.
4.
Grundprinzip des Hashens
Die Hashfunktion
Behandlung von Kollisionen
Implementierung einer Hashliste
39
1.7.1 Grundprinzipien des Hashens
 Das Hashen basiert auf drei Grundprinzipien:
 Die Speicherung der Datensätze erfolgt in einem Feld mit Indexwerten von 0 bis n-1.
wobei die einzelnen Positionen als „Buckets“ (Eimer) bezeichnet werden.
 Eine Hashfunktion h bestimmt für ein zu speicherndes Element e dessen Position
h(e) im Feld
 Diese Hashfunktion h sorgt für eine „gute“ – im besten Fall kollisionsfreie, d.h.
injektive (meist aber „Nur“ kollisionsarme) Abbildung d.h. Verteilung der zu
speichernden Elemente.
 Da normalerweise der Wertebereich der möglicherweise zu speichernden
Element größer ist als die Anzahl der Elemente in der Hashliste kann die
Funktion h (meist) nicht für alle Werte n eindeutige Hashwerte h(n) liefern.
Das führt zu Kollisionen, deren Behandlung die „Qualität“ eines Hashverfahrens
ausmacht.
 Ist die Hashfunktion ungeschickt gewählt, kann das Verfahren „entarten“, was
zu teilweise dramatischen Geschwindigkeitsverlusten führen kann.
40
1.7.2 Die Hashfunktion
 Die Auswahl der Hashfunktion h hängt natürlich vom zu speichernden Datentyp
(bzw. dessen Wertebereich) und der Auftrittswahrscheinlichkeit der Werte ab.
 Für Integerwerte i wird oft die Modulofunktion verwendet:
 h(i) = i mod n (wobei n die größe der Hashliste ist)
Diese Funktion funktioniert in der Regel nur für große primzahlige n gut
(inbesondere ist n = 2x nicht gut !)
Beispiel:
h(i) = i mod 7
Index
0 1 2 3 4 5 6
Element 28 36 16 66 25 75 27 (danach führt jedes Element zu Kollision)
 Für andere Datentypen kann eine Abbildung auf Integerwerte erfolgen:
 Bei Fließkommazahlen kann man z.B. Mantisse und Exponent addieren
 Bei Strings kann man den ASCII oder Unicode der einzelnen Buchstaben, eventuell
mit einem Faktor gewichtet, miteinander addieren.
 Meist ist eine Gleichverteilung der Bildbereiches der Hashfunktion
wünschenswert, so dass man sich bestimmte Eigenschaften (z.B.
ungleichgewichtige Verteilungen) des Urbildes zu Nutze machen kann und
sollte.
Andererseits geht die Komplexität der Hashfunktion h multiplikativ in die
Gesamtkomplexität ein und sollte daher einfach gehalten werden.
41
1.7.3 Behandlung von Kollisionen
 Führt die Hashfunktion für unterschiedlich Werte des Urbildes auf gleiche
Hashwerte, so spricht man von Kollision, die man z.B. mit folgenden Verfahren
behandeln kann:
 Verkettung der Überläufer:
Man erweitert die eindimensionale Listenstruktur der Hashliste um eine zweite
Dimension (z.B. durch eine einfach verkettete Liste), in die man die kollidierenden
Werte ablegt
 Sondieren:
Man legt den kollidierenden Wert an ein andere Stelle in der Hashliste ab, die sich
durch die Berechnung eines Offsets ergeben:
 beim linearen Sondieren wird die nächste freie Position verwendet.
(also als Offset die Werte 1,2,3,4, …)
 beim quadratischen Sondieren ergibt sich der mögliche Offset durch die
Quadratzahlen (also 1,4,9,16,25, …).
Dadurch wir d die „Klumpenbildung“, zu der das lineare Sondieren neigt,
vermieden.
42
1.7.4 Implementierung einer Hashliste
public class HashTable {
// sucht Element in Hashliste
public boolean contains (Object o) {
int idx, oidx;
oidx = idx = (o.hashCode () &
0x7fffffff) % table.length;
while (table[idx] != null) {
if (o.equals (table[idx]))
return true;
idx = ++idx % table.length;
if (idx == oidx)
break;
}
return false;
}
Object[] table;
public HashTable (int size) {
table = new Object [size];
}
// fügt Element in Hashliste
public void add (Object o) {
int idx, oidx;
// berechnen Hashfunktion
oidx = idx = (o.hashCode () &
0x7fffffff) % table.length;
// falls Kollision -> suche nächstes Freies
while (table[idx] != null) {
idx = ++idx % table.length;
// fall Suche erfolglos -> Fehler
if (idx == oidx)
throw new HashTableOverflowException ();
}
// trage Wert ein
table[idx] = o;
}
public static void main (String[] args) {
HashTable tbl = new HashTable (20);
tbl.add („Au");
tbl.add („Oh");
tbl.add („Ah");
System.out.println (tbl.contains („Ah"));
System.out.println (tbl.contains („Be"));
}
}
43
1.8
Bäume
 Bäume sind (zumindest) zweidimensionale Strukturen, die viele reale Strukturen
abzubilden Vermögen und zudem sehr gut zum Durchsuchen geeignet sind.
Es gibt daher sehr viele spezielle Arten von Bäumen, von denen hier
stellvertretend vor allem die binären Bäume behandelt werden sollen.
1.
2.
3.
4.
5.
Definitionen & Beispiele
Spezifikation
Datentypen
Traversierung
Weitere Bäume
44
1.8.1. Definitionen & Beispiele
 Ein Baum ist eine Menge von Knoten und (gerichteten) Kanten mit folgenden
Eigenschaften:
 Ein ausgezeichneter Knoten wird als Wurzel bezeichnet
 Jeder Knoten (außer der Wurzel) ist durch genau eine Kante mit seinem
Vorgängerknoten verbunden (Vaterknoten, Elternknoten).
Dieser Knoten wird dann auch als Kind (Sohn, Nachfolger) bezeichnet.
 Ein Knoten ohne Kinder heißt Blatt
 Knoten mit Kindern heißen innere Knoten
Wirbeltiere
(Unterstamm)
Kiefermünder
(Oberklasse)
Kiefermünder
(Oberklasse)
Vögel
(Klasse)
Säugetiere
(Klasse)
Kieferlose
(Oberklasse)
…
(Klassen)
Wirbeltiere
(Unterstamm)
Kieferlose
…
…
(Ordnungen)
Primaten
(Ordnung)
Vögel
Säugetiere
(Klasse)
Primaten
(Ordnung)
…
… nich‘ so praktisch
… wie sich der
Informatiker einen
45
Baum vorstellt
1.8.1. Definitionen & Beispiele
 Ein Pfad in einem Baum ist eine Folge von unterschiedlichen Knoten, in der die
aufeinanderfolgenden Knoten durch Kanten verbunden sind
 Zwischen jedem Knoten und der Wurzel gibt es genau einen Pfad
 Dies bedeutet, dass ein Baum zusammenhängend ist und keine Zyklen besitzt
 Unter dem der Niveau (der Tiefe) eines Knotens versteht man die Länge
dessen Pfades zu der Wurzel
 Die Höhe (Tiefe) eines Baumes entspricht dem maximalen Niveau eines
Blattes + 1 („+1“ da die Wurzel mitzählt)
 Je nach Art und Anzahl von Kindern unterscheidet man zwischen
 n-ären Bäumen, wenn die maximale Anzahl von Kindern gleich n ist
(also z.B. binärer Baum, wenn die maximale Anzahl der Kinder gleich 2 ist)
 geordneten Bäumen, wenn die Kinder entsprechend einer Ordnungsrelation (z.B.
von links nach rechts) angeordnet sind
Tiefe 0
+
*
((1+2)*3)+(2+5)
+
1
3
2
Tiefe 1
+
2
5
Tiefe 2
Tiefe 3
46
1.8.2. Binäre Bäume: Spezifikation
type: tree (T) // T ist die Wertemenge der Elemente
import: boolean
operators:
empty
:  tree // erzeugt leeren Baum
// verbindet zwei Bäume über neue Wurzel T
bin
: tree x T x tree  tree
left
: tree  tree
// liefert den linken Teilbaum
right
: tree  tree
// liefert den rechten Teilbaum
value
: tree  T
// liefert die Wurzel
is_empty : tree  boolean // ist Baum leer ?
axions:  s : stack,  x : T
left (bin (x,b,y))
= x
// linker Teilbaum
right (bin (x,b,y))
= y
// rechter Teilbaum
value (bin (x,b,y))
= b
// Wurzel
is_empty (empty)
= true // empty ist Wert des Baums
is_empty (bin (x,b,y)) = false
47
1.8.3 Binäre Bäume: Datentypen
static class TreeNode {
Object
key; // Wert des Knotens
TreeNode left = null; // Referenz auf linken Teilbaum
TreeNode right = null; // Referenz auf rechten Teilbaum
+
*
+
// Konstruktor
public TreeNode (Object e)
{ key = e; }
// getter Methoden
public TreeNode getLeft () { return left; }
public TreeNode getRight () { return right; }
public Object
getKey ()
{ return key;
}
// setter Methoden
public void setLeft (TreeNode n) { left = n; }
public void setRight (TreeNode n) { right = n; }
}
static class BinaryTree {
protected TreeNode root = null;
public BinaryTree () { }
public BinaryTree (TreeNode n) { root = n; }
1
+
3
2
5
2
TreeNode e1 = new TreeNode(“+“);
e1.setleft (new TreeNode(“1“));
e1.setright (new TreeNode(“2“));
TreeNode e2 = new TreeNode(“*“);
e2.setleft (e1);
e2.setright (new TreeNode(“3“));
TreeNode e3 = new TreeNode(“+“);
e3.setleft (new TreeNode(“2“));
e3.setright (new TreeNode(“5“));
TreeNode e = new TreeNode(“+“);
e.setleft (e2);
e.setright (e3);
}
 Bäume baut man „von unten nach oben“ auf
48
1.8.4 Binäre Bäume: Traversierung
 Je nach Reihenfolge unterschiedet man beim Baumdurchlauf folgende
Traversierungsarten.
 Inorder: Hier wird zuerst rekursiv der linke Teilbaum, danach der Knoten selbst, und
schließlich der rechte Teilbaum durchlaufen.
 Preorder: Hier wird zuerst der Knoten, danach zunächst rekursiv der linke Teilbaum
und schließlich rekursiv der rechte Teilbaum durchlaufen.
 Postorder: Hier wird zuerst rekursiv der linke Teilbaum, danach rekursiv der rechte
Teilbaum, schließlich der Knoten durchlaufen.
Diese Traversierungsarten gehen also für jeden Knoten rekursiv in die Tiefen
der beiden Teilbäume und können daher auch Tiefentraversierung genannt
werden.
Daneben gibt es noch eine Traversierungsart, die auf jedem Niveau alle Knoten
berücksicht. Diese Breitentraversierung nennt man:
 Levelorder: erst werden alle Knoten eines Niveaus durchlaufen, danach rekursiv die
beiden Teilbäume
+
*
+
1
+
3
2
2
5
Inorder:
Preoder:
Postorder:
Levelorder:
1+2*3+2+5
+*+123+25
1 2 + 3 * 2 5 + + ( UPN)
+*++32512
49
1.8.4 Binäre Bäume: Traversierung
private void printPreorder (TreeNode n) {
if (n != nullNode) {
System.out.println (n.toString ());
printPreorder (n.getLeft ());
printPreorder (n.getRight ());
}
}
private void printPostorder (TreeNode n) {
if (n != nullNode) {
printPostorder (n.getLeft ());
printPostorder (n.getRight ());
System.out.println (n.toString ());
}
}
protected void printInorder (TreeNode n) {
if (n != nullNode) {
printInorder (n.getLeft ());
System.out.println (n.toString ());
printInorder (n.getRight ());
}
}
private void printLevelorder (Queue q) {
while (! q.isEmpty ()) {
TreeNode n = (TreeNode) q.leave ();
if (n.getLeft () != nullNode)
q.enter (n.getLeft ());
if (n.getRight () != nullNode)
q.enter (n.getRight ());
System.out.println (n.toString ());
}
}
...
// zur Zwischenspeicherung der Knoten ->1.4.2
Queue queue = new ArrayQueue ();
// Initialisierung
queue.enter (root);
// Aufruf
printLevelorder (queue);
50
1.8.5 Weitere Bäume
 Für spezielle Anwendungen des Suchens und Sortierend werden bestimmte
Spezialformen von Bäumen verwendet
 Ausgeglichene (balanced) Bäume: Hier wird beim Auf- und Abbau des Baumes
versucht ,die Tiefen der Teilbäume möglichst ähnlich oder sogar gleich zu halten:
 AVL-Bäume sind binäre Bäume und beschränken die Niveaudifferenz aller
Teilbäume auf 1.
Sie werden vor allem zum Suchen verwendet .
 B-Bäume (b steht für balanciert, buschig, breit) sind n-äre Bäume, bei denen
alle Teilbäume gleichtief sind. Diese sind also meist nicht binär.
Sie werden oft bei Datenbanksystemen zur Indexierung verwendet.
 Digitale Bäume: Das sind n-äre Bäume die eine feste Anzahl von Verzweigungen
(Nachfolgenknoten) unabhängig von den Werten im Baum haben.
 Tries (retrieval): sind n-äre Bäume bei denen die n Werte (z.B. 127 ASCIIWerte) des Knotens als Index für die Nachfolgeknoten verwendet werden.
Sie werden zum Suchen von Worten in Texten verwendet. (
 Patricia-Bäume (Practical Algorithm to Retrieve Information Coded in
Alphanumeric): Spezielle Form von Tries, bei denen Knoten mit nur einem
Nachfolger übersprungen werden können.
Auch Sie werden zum Suchen von Worten in Texten (oder von Gensequenzen
in einem Genom) verwendet.
  Kapitel 2
51
1.9. Graphen
 Graphen sind (oft) die komplexesten Grundstrukturen, mit denen man es bei
abstrakten Datentypen zu tun hat.
,,, und tatsächlich sind die im vorherigen Unterkapitel behandelten Bäume
Spezialfälle von Graphen.
1. Arten
2. Umsetzung
3. Implementierung eines Graphen
52
1.9.1 Arten
 Es gibt (neben anderen) drei wichtige Arten von Graphen
 ungerichtete Graphen: Hier sind Knoten mit ungerichteten Kanten verbunden, d.h.
es gibt kein Nachfolge- oder Vorgänger-Beziehung und auch kein Einschränkungen
bezüglich Anzahl von Kanten pro Knoten.
Anwendungen findet man bei der Modellierung von Straßenverbindungen (ohne
Einbahnstraßen), der Nachbarschaft von Gegenständen oder eines Telefonnetzes.
 gerichtete Graphen: Hier sind Knoten durch gerichtete Kanten verbunden, es kann
also zwischen zwei Knoten bis zu zwei Kanten geben (eine hin, eine zurück).
Anwendungen sind Modelle von Förderanlagen, der Kontrollfluss von Programmre
 gerichtete azyklische Graphen (DAG directed acyclic graphs): dieser Spezialfall
von gerichteten Graphen erlaubt keine Zyklen im Graph, d.h. es darf keinen Pfad
von einem Knoten zu sich selbst geben.
Zusätzlich können Kanten von Graphen noch gewichtet sein (gewichtete
Graphen)
1
2
1
2
3
4
6
5
3
4
6
5
1
20
4
64
6
25
22
2
75
24 3
5
30
53
1.9.2 Umsetzung
 Die interne Darstellung von Graphen erfolgt (historisch) in vier Varianten:
 Knotenliste:
<#Knoten>,<#Kanten>, <Kanteniste> (<Kantenlliste> := <Vorgängerknoten>, <Nachfolgeknoten>)
6, 8, 1,2, 1,4, 3,2, 3,5, 4,5, 4,6, 5,2, 6,3
 Kantenliste:
<#Knoten>,<#Kanten>, <Kantenliste> (<Kantenliste> := <#Nachfolgeknoten>,<Nachfolgeknoten, …>)
6, 8, 2,2,4, 0, 2,2,5, 2,5,6, 1,2, 1,3
 Adjazenzmatrix
0 25 0 20 0 0
0 0 0 0 0 0
0 75 0 0 24 0
0 0 0 0 22 64
0 32 0 0 0 0
0 0 30 0 0 0
 dynamische Adjazenzliste
1
2
3
4
5
6
2
2
5
2
3
4
5
6
25
2 75
32 24 3
22
4
5
64
30
6
1
20
54
1.9.3 Implementierung eines Graphen
public class Graph {
static class Edge {
int dest, cost;
public Edge(int d, int c) {
dest = d; // Nachfolgeknoten
cost = c; // Gewicht
}
}
private ArrayList nodes;
public Graph() {
nodes = new ArrayList();
}
public void addNode(String label) { ... }
public void addEdge(String src, String dest, int cost) { ... }
public Iterator getEdges(int node) { ... }
}
55
1.10. Frameworks
 Aufgrund des häufigen Einsatzes dieser ADTs gibt es praktisch für jede
Programmiersprache entsprechende Bibliotheken.
1. ADTs in Programmiersprachen
2. Bibliotheken in Java
56
1.10.1
ADTs in Programmiersprachen
 ADTs werden in vielen
Programmiersprachen unterstützt:
 Diese Bibliotheken sind zwar teilweise
standardmäßig in den
Entwicklungsumgebungen enthalten,
sind aber (meist) nicht Teil des
Sprachumfangs
 Manche Programmiersprachen
besitzen ADTs als Teil des
Sprachumfangs.
(z.B. good ol‘ Pascal: sets)
 Beispiele für C++ und Java:
 C++: Standard Template Library
(Vorsicht: nicht standardisiert !)
(z.B. http://www.sgi.com/tech/stl)
 :Java Collection Framework
(http://java.sun.com/docs/books/tutorial/
collections/index.html)
57
1.10.2
Bibliotheken in Java
 In Java sind diverse Klassen definiert, die die hier beschriebenen ADTs
implementieren:
 Vector
funktioniert wie ein array, das bei Bedarf dynamisch wachsen kann.
Nur für Integerwerte.
Generische Variante: ArrayList
 Stack
ferweiterert Vector zu eimem LIFO-Stack.
 LinkedList
Doppelt verkettete Liste, kann auch als Queue (Warteschlange) eingesetzt werden.
 HashMap
Hashliste.
TreeMap kann auch für gehashten (assoziativen) Zugriff verwendet werden, ist
intern als Baum aufgebaut und etwas langsamer – dafür sind die Schlüssel alle
sortiert.
 TreeSet
Balancierter Binärbaum. Die Elemente im Baum sind sortiert
 Diese Klassen befinden sich im Paket: java.util.* und können mit
import java.util.* eingebunden werden.
58
1.11 Zusammenfassung








„Ein abstrakter Datentyp fasst die wesentlichen Eigenschaften und Operationen einer Datenstruktur
zusammen, ohne auf deren eigentlichen Realisierung im Rechner einzugehen“
Stacks (Kellerspeicher, Stapel) sind einfache Abstraktionen von Strukturen, die in vielen Bereichen der
Informatik, insbesondere aber in den systemnahen Bereichen verwendet werden.
Stacks bezeichnet man manchmal auch als LIFO (Last in – First Out)-Schlangen
Queues (Warteschlangen) sind lineare Listen, deren Elemente nach dem FIFO-Prinzip (First in–First
Out) ein- bzw. ausgefügt werden
Auch Queues kommen in systemnahen Bereichen vor, insbesondere bei Betriebssystemen.
Listen sind (ziemlich) simple Datentypen, die sich statisch durch den konkreten strukturierten Datentyp
„array (Feld)“ darstellen lässt und damit in den meisten Programmiersprachen implizit vorhanden ist.
In der nicht-imperativen Programmiersprache LISP ist „Liste“ zudem der einzige strukturierte Datentyp.
Möchte man die Länge einer Liste jedoch zur Laufzeit eines Programmes dynamisch verändern so
muss man auf eigenen Umsetzungen mithilfe eines ADTs zurückgreifen.
Aus bestimmten Gründen – vor allem Laufzeit-Effizienz – verwendet man oft Listen, deren einzelne
Elemente nicht nur den jeweiligen Nachfolger, sondern auch den jeweiligen Vorgänger kennen.
Diese Listen nennt man das „Zweifach bzw. Doppelt verkettete Listen“
Bäume sind (zumindest) zweidimensionale Strukturen, die viele reale Strukturen abzubilden vermögen
und zudem sehr gut zum Durchsuchen geeignet sind.
Es gibt daher sehr viele spezielle Arten von Bäumen, von denen hier stellvertretend vor allem die
binären Bäume behandelt werden sollen.
Graphen sind (oft) die komplexesten Grundstrukturen, mit denen man es bei abstrakten Datentypen zu
tun hat (Tatsächlich sind die im vorherigen Unterkapitel behandelten Bäume Spezialfälle von Graphen)
Aufgrund des häufigen Einsatzes dieser ADTs gibt es praktisch für jede Programmiersprache
entsprechende Bibliotheken.
59
Übung 1
1. Implementieren Sie einen stack
1. Fügen Sie 10 Elemente ein.
2. Entnehmen Sie die Elemente wieder und geben Sie sie dabei aus.
2. Implementieren Sie eine queue.
1. Fügen Sie 10 Elemente ein.
2. Entnehmen Sie die Elemente wieder und geben Sie sie dabei aus.
3. Implementieren Sie eine Hashliste (der Länge 41) für deutsche Worte mit
quadratischem Sondieren zur Auflösung von Kollisionen
1. Fügen Sie 30 Worte ein.
2. Geben Sie Ihre Hashfunktion an und die Hashwerte für Ihre eingetragen Worte
3. Suchen Sie nach 5 vorhandenen und 5 nicht vorhandenen Worten, geben Sie dabei
jeweils auch den Hashwert an
4. Implementieren Sie einen binären Baum und fügen Sie 30 Element ein
1. Traversieren Sie den Baum Inorder, Preorder, Postorder und inline, Geben Sie die
Elemente dabei jeweils aus.
60
2.
Sortieren
 Suchen und Sortieren sind grundlegende Operationen in der Informatik.
Man schätzt, dass über 50% der Rechenzeiten auf diese Operationen
zurückzuführen sind.
Für diese beiden Operationen gibt es zwar völlig unterschiedliche
Umsetzungen, doch sind beide Operationen mitteinander verwandt, denn oft
basiert ein Suche auf sortierten Strukturen.
Das ist auch der Grund, weshalb das (eher etwas kniffeligere) Sortieren vor
dem Suchen behandelt wird.
1. Wiederholung: Komplexität
2. Grundlagen
3. Elementare Sortieralgorithmen
4. Fortgeschrittene Sortieralgorithmen
5. Zusammenfassung
61
2.1
Wiederholung: Komplexität
 In GDI haben wir den Begriff „Komplexität“ diskutiert und definiert. Komplexität,
insbesomdere Zeitkomplexität (Aufwand) ist nun ein entscheidendes Kriterium
für und wider den Einsatz der im folgenden behandelten Algorithmen und soll
daher hier nochmals kurz wiederholt werden.
 Inhalt
1.
2.
3.
4.
5.
6.
Wie „gut“ ist ein Algorithmus
Die O-Notation
Häufige O-Ausdrücke
Einige Regeln
Quantitatives
Platzbedarf
62
2.1.1 Qualität eines Algorithmus
 Die Abarbeitung eines Algorithmus benötigt „Ressourcen“, vor allem:
 Zeit
 Platz
Laufzeit des Algorithmus
Speicherplatzbedarf des Algorithmus
 Problem bei der Ressourcenermittlung - der Ressourcenbedarf ist Abhängig
von:





der Problemgröße (z.B. Multiplikation einer 10x10 bzw. 100x100 Matrix)
der Eingabewerte (z.B. Sortieren einer bereits sortierten Menge)
der Fragestellung (bester, mittlerer, schlechtester Fall)
der Güte der Implementierung (z.B. (un-)geschickte Typwahl)
der Hard- und Software (z.B. Schneller Rechner, optimierter Compiler)
 Es gibt auch Qualitätsmerkmale eines Algorithmus, der sich nicht am
Ressourcenbedarf festmachen (aber das ist eine andere Geschichte ...)




Wartbarkeit, Wartungsintensität
Robustheit
Eleganz
...
63
2.1.2 Die O-Notation: Definition
 Definition:
Eine Funktion g(n) wird O(f(n)) genannt („Die Laufzeit, der Aufwand, die
Zeitkomplexität von g(n) ist O(f(n))“), falls es Konstanten c und n0 gibt, so dass:
g(n)  cf(n), für fast alle n  no ist
 f(n) ist damit eine obere Schranke für die Laufzeit des Algorithmus (allerdings nur
zusammen mit einem festen c und ab bestimmten n0) !
 Die Problemgröße kann der Umfang der Eingabemenge sein, die Größe des zu
verarbeitenden Objektes (z.B. der Zahl), …
L
a
u
f
z
e
i
t
g(n)  cf(n), für alle n  no
cf(n)
g(n)
f(n)
no
Problemgröße64
2.1.3 Die O-Notation: Beispiel

Beispiel:
 Bei der Analyse eines Algorithmus hat sich herausgestellt, dass
die Laufzeit:
g(n) = 3n2 + 7n – 1
ist.
 Behauptung:
Die Laufzeit von g(n) ist O(n2), also
f(n)=n2,
 Beweis:
Es muss Konstanten c und n0
geben, so dass gilt:
3n2+7n-1  c n2, für alle n  n0
setze n0=7 und c=4, dann gilt:
3n2+7n-1  3n2+7n  3n2+n2 = 4n2

L
a
u
f
z
e
i
t
g(n)  cf(n), für fast alle n  no
cf(n) = 4 n2
g(n)
Allgemein:
f(n)=n2
 g(n) = amnm + am-1nm-1 + … + a0n0
 amnm + am-1nm + … + a0nm
= nm (am + am-1 + … + a0 )
also: g(n)  c nm
mit c = am + am-1 + … + a0
no
Problemgröße 65
2.1.4 Die O-Notation: Schranken
 Die Notation gibt nur eine obere Schranke der Komplexität , das muss nicht
notwendigerweise die beste Schranke sein.
 Beispiel:
Eine weitere obere Schranke für g(n) = 3n2 + 7n - 1 ist auch O(n3), welche sicher nicht
die beste ist.
 Bei der Suche nach der Größenordnung von f(n) wird man versuchen, das
kleinste f(n) zu finden, für das g(n)  c . f(n)
 Dieses ist dann eine kleinste, obere Schranke für den Aufwand
 Zur Bestimmung des tatsächlichen asymptotischen Aufwands wird man also
noch eine größte, untere Schranke h(n) = (g(n)) suchen für die gilt: limn
h(n)/f(n) = 1
 Eine untere Schranke ist die Zeit, die jeder Algorithmus (ab einem n>n0) benötigt
 Das ist im Allgemeinen viel schwieriger !
66
2.1.5 Die O-Notation: Achtung
 Achtung !
Die Konstanten c und n0 werden üblicherweise nicht angegeben und können
sehr groß sein
 Beispiel:
Algorithmus A habe eine Laufzeit von O(n2)
Algorithmus B für das gleiche Problem eine Laufzeit von O(1,5n)
Welcher Algorithmus ist besser ?
 schnelle Antwort: A (das stimmt auch für große n)
 bessere Antwort: Wie groß ist n ? Wie groß sind die Konstanten ?
 z.B. für cA=1000 und cB=0,001
Bis hier ist B besser als A
n
cAn2
cB1,5n
1
10
20
50
100
103
105
4  105
2,5  106
107
1,5  10-3
1,8  10-2
3,3
6,4  105
4,1  1014
67
Übung 2.1:
1. Erstellen Sie ein Graphik (mit Excel) in der Sie die Laufzeiten der wichtigsten
Komplexitätsklassen “sinnvoll“ darstellen.
68
2.2.. Grundlagen
 … bevor es losgeht:
1. Definitionen
2. Beispiele
3. Framework für Implementierungen
69
2.2.1 Definitionen
 Beim Sortieren werden Elemente entsprechend der Werte ihrer Schlüssel
entsprechend einer Ordnungsrelation angeordnet
 Elemente sind Datenstrukturen, die aus mehreren Unterstrukturen bestehen können,
d.h. Element müssen nicht „elementar“ (Int, Real, Char, etc). sein.
Sortieren ist eine „generische“ Operation, d.h. Elemente unterschiedlichsten Typs
können sortiert werden, sofern eine sinnvolle Ordnungsrelation existiert,
 Liegen die Elemente vollständig im Hauptspeichers vor, sprechen wir von
internem Sortieren, ansonsten von externem Sortieren.
 Dabei ist der wesentliche Unterschied, dass beim internen Sortieren leicht auf
beliebige Elemente zugegriffen werden kann. Bein externen Sortieren kann das
nur sequenziell oder allenfalls blockweise geschehen.
 Eine oder mehrere Element-Unterstrukturen definieren den (nicht notwendigerweise
eindeutigen) Schlüssel, der einen eindeutigen Wert besitzt.
Ist der Schlüssel nicht eindeutig, so kann es mehrere auch unterschiedliche
Elemente mit gleichem Schlüssel geben.
 Sortierverfahren die die ursprüngliche Reihenfolge von Elementen gleichen
Schlüssels beibehalten heißen „stabil“.
 Auf dem Wertebereich des Schlüsselwertes muss eine Ordnungsrelation definiert
sein, die die Reihenfolge der Schlüsselwerte festlegt.
70
2.2.2 Beispiele
 Kartenspiel
 Element = Schlüssel
 unterschiedliche Ordnungsrelationen (Für Skat, Doppelkopf, …)
 Telefonbuch: Name, Vorname, Telefonnr
 Element > Schlüssel
 Alphabet als Ordnungsrelation
 …  Tafel
71
2.2.3 Framework für Implementierungen
interface ITEM
{ boolean less(ITEM v); }
class Sort
{
static boolean less(ITEM v, ITEM w)
{ return v.less(w); }
static void exch(ITEM[] a, int i, int j)
{ ITEM t = a[i]; a[i] = a[j]; a[j] = t; }
static void compExch(ITEM[] a, int i, int j)
{ if (less(a[j], a[i])) exch (a, i, j); }
static void sort(ITEM[] a, int l, int r)
{ example(a, l, r); }
static void example(ITEM[] a, int l, int r)
{
for (int i = l+1; i <= r; i++)
for (int j = i; j > l; j--)
compExch(a, j-1, j);
}
}s
class myItem implements ITEM // Key ist int
{ private int key;
public boolean less(ITEM w)
{ return key < ((myItem) w).key; }
void read()
{ key = In.getInt(); }
void rand()
{ key = (int) (1000 * Math.random()); }
public String toString()
{ return key + ""; }
}
class myItem implements ITEM // Key ist string
{ String key;
public boolean less(ITEM w)
{ return key.compareTo(((myItem) w).key)<0; }
void read()
{ key = In.getString(); }
void rand()
{ int a = (int)('a'); key = "";
for (int i = 0; i < 1+9*Math.random(); i++)
key += (char) (a + 26*Math.random());
}
public String toString() { return key; }
}
72
Übung 2.2:
1. Implementieren Sie dieses Framework und wenden Sie es in einem einfachen
Fall an.
73
2.3. Elementare Sortieralgorithmen
 … da Sortieren eine so grundlegende Operation in der Informatik ist, gibt es
schon seit einigen Jahrzehnten eingeführte Algorithmen, die teilweise optimiert
wurden und immer noch Einsatz finden:
1.
2.
3.
4.
5.
Selection Sort (Sortieren durch Auswählen)
Insertion Sort (Sortieren durch Einfügen)
Shellsort
Bubblesort
Vergleich
sorting-algorithms.com
74
2.3.1 Selection Sort (Sortieren durch Auswählen)
 Idee: Suche das kleinste Element (z.B. einer Liste) und tausche es mit dem
Element an der ersten Position. Betrachte dann den Rest der Liste und gehe
ebenso vor
 Beispiel
3 6 3 4 3 9 8 1 7 5 5 instabil: 3 kommt hinter 3
1 6 3 4 3 9 8 3 7 5 5
1 3 6 4 3 9 8 3 7 5 5
1 3 3 4 6 9 8 3 7 5 5
1 3 3 3 6 9 8 4 7 5 5
1 3 3 3 4 9 8 6 7 5 5
1 3 3 3 4 5 8 6 7 9 5
1 3 3 3 4 5 5 6 7 9 8
1 3 3 3 4 5 5 6 7 9 8
1 3 3 3 4 5 5 6 7 9 8
1 3 3 3 4 5 5 6 7 8 9
1 3 3 3 4 5 5 6 7 8 9.
75
2.3.1 Selection Sort: Implementierung Variante 1
// Sorts array a starting from index l up to index r
static void selection(ITEM[] a, int l, int r)
{
// iterates through list
for (int i = l; i < r; i++) {
int min = i; // initialize index to minimum
// iterate through unsorted part of list
for (int j = i+1; j <= r; j++) {
if (less(a[j], a[min])) {
min = j; // index to minimum has changed
}
}
exch(a, i, min); // swap first element with minimum
// even if i=min, i.e. minimum is already
// in front
}
}
76
2.3.1 Selection Sort: Diskussion 1
 Eigenschaften:
 Nicht stabil (gleiche Keys können umgeordnet werden)
 Nicht adaptiv, d.h. Algorithmus „profitiert“ nicht von „günstigen“ Vorgaben:, z.B. von
einer vorhandenen Sortierung.
 Aufwand:




im Beispiel: 11+10+9+8+…+1 Vergleiche = (n*(n+1)) / 2  O(n2)
Im Beispiel: 11 Umordnungen (Einsortierungen) = n  O(n)
Best Case = Worst Case = Average Case = O(n2)
O(1) Platzkomplexität
77
2.3.1 Selection Sort: Implementierung Variante 2 -stabil
// Sorts a linked list, by removing it from in-list (h.next) and
// inserting max in front of the out-list (out)
// (head of list is dummy)
// find node previous to minimum in linked list
private static Node findMin(Node h) {
for (Node t = h; t.next != null; t = t.next)
if (t.next.item < h.next.item) h = t;
return h;
}
// iterate through in-list and move max to head of out-list
static Node selection(Node h) {
Node head = new Node(-1, h), out = null;
while (head.next != null) {
Node min = findMin(head);
Node t = max.next; min.next = t.next; // remove from in-list
t.next = out; out = t;
// put in front of out-list
}
return out;
}
78
2.3.1 Selection Sort: Diskussion 2
 Eigenschaften:
 stabil
 Nicht adaptiv, d.h. Algorithmus „profitiert“ nicht von „günstigen“ Vorgaben:, z.B. von
einer vorhandenen Sortierung.
 Aufwand:
 wie bei Implementierung 1
 schlechtere O(n) Platzkomplexität
 Selection Sort wird (trotz schlechten Aufwandes) eingesetzt für das Sortieren
von Daten mit großen Elementen mit jeweils kleinen Schlüsseln:
 … bei diesen Daten sind die Kosten für den Vergleich sehr viel kleiner als die Kosten
für die Umordnung.
 Der Aufwand für die Umordnungen ist mit O(n) kleiner als in den meisten anderen
Verfahren.
79
2.3.2 Insertion Sort (Sortieren durch Einfügen)
 Idee: Wie beim Sortieren eines Kartenblattes auf der Hand eines Spielers
werden neue (rechts neben den bereits sortierten) Karten in das bereits
sortierte Kartenblatt an der richtigen Stelle eingefügt.
Angewandt auf eine Liste existiert also immer eine bereits sortierte Teilliste (am
Anfang der Liste), die bei jeder Iteration um ein weiteres korrekt einsortiertes
Element erweitert wird..
 Beispiel:
3 6 3 4 3 9 8 1 7 5 5
3 6 3 4 3 9 8 1 7 5 5
3 6 3 4 3 9 8 1 7 5 5
3 3 6 4 3 9 8 1 7 5 5
3 3 4 6 3 9 8 1 7 5 5
3 3 3 4 6 9 8 1 7 5 5
3 3 3 4 6 9 8 1 7 5 5
3 3 3 4 6 8 9 1 7 5 5 // swapping of „1“ is exhausting
1 3 3 3 4 6 8 9 7 5 5
1 3 3 3 4 5 6 8 9 7 5
1 3 3 3 4 5 5 6 8 9 7
80
2.3.2 Insertion Sort: Implementierung Variante 1
// sort array “ITEM[]” between indexes l and r
static void example(ITEM[] a, int l, int r)
{
// iterate through list (starting with second position) from ltr
for (int i = l+1; i <= r; i++)
{ // consider first element after already sorted list.
// Iterate from rtl through already sorted list
// and swap elements if considered one is smaller
for ( int j = i; j > l; j-- )
{
compExch(a, j-1, j);
}
}
}
81
2.3.2 Insertion Sort: Implementierung Variante 2
• Bringt zunächst das kleinste Element nach vorn, so dass der sortierte Teil nicht mehr
vollständig verschoben werden muss, wenn immer wieder „kleinste“ Elemente
einzusortieren sind.
• Die innere Schleife beinhaltest keine Vertauschungen (compExch = drei Zuweisungen)
sondern nur eine Zuweisung (a[j] = a[j-1])
• Die innere Schleife terminiert, sobald die richtige Position gefunden ist.
// sort array “ITEM[]” between indexes l and r
static void insertion(ITEM[] a, int l, int r)
{ int i;
// initially bring smallest element to front
for (i = r; i > l; i--) compExch(a, i-1, i);
// iterate through list starting with second position from left to right
for (i = l+2; i <= r; i++)
{ int j = i;
ITEM v = a[i]; // remember element to be inserted
// Iterate from right to left through already sorted list
// and shift elements to right ...
while (less(v, a[j-1])) // ... stop on correct position
{ a[j] = a[j-1]; j--; }
// insert element to its proper position
a[j] = v;
}
}
82
2.3.2 Insertion Sort: Diskussion
 Eigenschaften
 Stabil
 Adaptiv: O(n) Zeitkomplexität, wenn die Daten stark vorsortiert sind
 kleiner overhead (kompakter Code)
 Aufwand:




Vergleiche :
min: O(n), max: O(n2), average O(n2)
Bewegungen:
min: O(n), max: O(n2), average O(n2)
also O(n) für stark vorsortierte oder sortierte Daten.
O(1) Platzkomplexität
 Der Insertion Sort wird eingesetzt, wenn es auf einen stabilen Algorithmus
ankommt …
 … und die Daten stark vorsortiert sind (da er adaptiv ist)
 … oder die Problemgröße klein ist (da er kompakt ist, also wenig „Overhead“ hat)
83
2.3.3 Shellsort (Donald L. Shell, 1959)
 Motivation: Der Insertion-Sort ist langsam, da nur benachbarte Element
getauscht werden. Insbesondere sehr kleine Elemente müssen dabei häufig
vertauscht werden, um vom Ende an den Anfang zu „rutschen“
 Idee:
Bei den bislang behandelten Algorithmen ist der linke Teil der Liste jeweils
sortiert, als jedes Element links . Beim Shellsort werden Teillisten, bestehend
aus den jeweils h-ten Elementen mit dem Insertion-Sort sortiert . Man
verkleinert h bis es zu 1 wird.
 Die Schrittweite des Vertauschens ist anfangs also groß, so dass Elemente „recht
schnell grob vorsortiert“ werden.
 Beispiel (h-Folge: 4,3,1)
3 6 3 4 3 9 8 1 7 5 5 2 3 5 3 1 3 6 5 2 7 9 8 4
h=4
h=3
3 6 3 4
3 5 3 1
3 5 3 1 2 3
3 9 8 1  3 6 5 2
1 3 6 
3 3 4
7 5 5 2
7 9 8 4
5 2 7 5 5 6
9 8 4 9 8 7
mit h=1 wird hier abschließend nochmals Insertion-sortiert
1 2 3 3 3 4 5 5 6 9 8 7  1 2 3 3 3 4 5 5 7 8 9
84
2.3.3 Shellsort: Implementierung
// sort array “ITEM[]” between indexes l and r
static void shell(ITEM[] a, int l, int r) {
int h;
// compute initial value of h depending on lebgth of array (r-l)
for (h = 1; h <= (r-l)/9; h = 3*h+1);
// dicrease h – by dividing by 3 -> h = ...,364,121,40,13,4,1
for ( ; h > 0; h /= 3) {
// apply insertion sort - increment not 1 but h
for (int i = l+h; i <= r; i++) {
int j = i; ITEM v = a[i];
while (j >= l+h && less(v, a[j-h]))
{ a[j] = a[j-h]; j -= h; }
a[j] = v;
}
}
}
85
2.3.3 Shellsort: Diskussion
 Eigenschaften
 Nicht Stabil
 Adaptiv: O(n log(n)) Zeitkomplexität, wenn die Daten stark vorsortiert sind
 kleiner overhead (kompakter Code)
 Aufwand
 Vergleiche = Bewegungen : min = max = average O(n1,2) für die Gonnet-Folge:
 Gonnet-Folge (1984)
h1 =  * n, hn =  * hn-1 mit  = 0,45454
weitere Folgen (mit leicht schlechterem Aufwand):
 Hibbard-Folge (1969)
2i-1  1,3,7,15,31, … <= h1 mit n/4 < h1 < n/2
 Knuth-Folge (1973)
(3i-1)/2  1,4,13,40,121, … <= h1 mit n/4 < h1 < n/2
 O(1) Platzkomplexität
 Der Shell Sort ist adaptiv, einfach zu implementieren und hat ein besseres
Komplexitätsverhalten als O(n2). Daher wird er bei nicht zu umfangreichen
Daten eingesetzt.
Der Shellsort war zwischen 1959 und 1991 ein Jahr lang der schnellste
bekannte Sortieralgorithmus.
86
2.3.4. Bubblesort
 Idee: Durchlaufe die Datei und vertausche die Elemente solange bis alle
Elemente am richtigen Ort sind
 Dadurch „bubbeln“ kleine Elemente nach oben (links), solange bis sie auf noch
kleinere stoßen, diese „bubbeln“ dann weiter.
 Mit jedem Durchgang wird das kleinste nach oben „gebubbeld“, gleichzeitig werden
dabei auch noch andere kleine „mitgerissen“
 Beispiel:
3 6 3 4
1 3 6 3
1 3 6 3
1 3 3 6
1 3 3 3
1 3 3 3
1 3 3 3
1 3 3 3
1 3 3 3
1 3 3 3
1 3 3 3
1 3 3 3
3
4
3
3
6
4
4
4
4
4
4
4
9
3
4
4
4
6
5
5
5
5
5
5
8
9
5
5
5
5
6
5
5
5
5
5
1
8
9
5
5
5
5
6
6
6
6
6
7
5
8
9
7
7
7
7
7
7
7
7
5
7
5
8
9
8
8
8
8
8
8
8
5
5
7
7
8
9
9
9
9
9
9
9
// stoppt bei Gleichheit
87
2.3.4 Bubblesort: Implementierung
// sort array “ITEM[]” between indexes l and r
static void bubble(ITEM[] a, int l, int r) {
for (int i = l; i < r; i++)
for (int j = r; j > i; j--)
compExch(a, j-1, j);
}
3
1
1
1
1
1
1
1
1
1
1
6
3
3
3
3
3
3
3
3
3
3
3
6
3
3
3
3
3
3
3
3
3
4
3
6
3
3
3
3
3
3
3
3
3
4
3
6
4
4
4
4
4
4
4
9
3
4
4
6
5
5
5
5
5
5
8
9
5
5
5
6
5
5
5
5
5
1
8
9
5
5
5
6
6
6
6
6
7
5
8
9
7
7
7
7
7
7
7
5
7
5
8
9
8
8
8
8
8
8
5 // stoppt bei Gleichheit
5
7
7
8
9
9 // ab hier kein Bubbeln mehr -> stoppen
9
9
9
9
88
2.3.4 Bubblesort: Diskussion
 Der Bubblesort ist zwar sehr einfach zu implementieren und stabil, ist aber i.A.
langsamer als Selection- und Insertion-Sort (und daher diesen nicht
vorzuziehen)
Aufwand:
 Vergleiche : min: O(n), max: O(n2), average O(n2)
 Bewegungen: min: O(n), max: O(n2), average O(n2)
 Platzkomplexität: O(1)
 Der Bubblesort ist sehr ähnlich der Variante 1 des Insertion Sort. Dort wird in
der inneren Schleife allerdings der sortierte linke Teil durchlaufen, beim
Bubblesort der unsortierte rechte.
 Der Bubblesort lässt sich noch etwas optimieren, indem die äußere Schleife
abgebrochen wird, sobald in der inneren keine Vertauschung mehr stattfindet ,
denn dann ist die Folge bereits sortiert.
Dadurch wird er aber auch nicht weniger aufwändig als Selection- oder
Insertionsort.
89
2.3.5 Indexsort (Schlüsselindizierendes Sortieren)
 Idee:
Gibt es für die N zu sortierenden Elemente eine Hashfunktion, die auf c*N
Werte abbildet und die Ordnungsrelation einhält, so kann man innerhalb eines
c*N großen arrays die Elemente direkt sortiert ablegen. Dabei wird für jeden
unterschiedlichen Hashwert ein Block in der Liste belegt – die Werte in den
Blöcken sind also gleich, die Blöcke untereinander sind sortiert.
 Beispiel:
0 3 3 0 1 1 0 3 0 2 0 1 1 2 0
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 (Index)
0
„Anzahlliste“
0
3
#0=6,
0
3 3
#1=4,
0 0
3 3
#2=2,
0 0
1
3 3
#3=3,
0 0
1 1
3 3
„Anzahlsummenliste“
0 0 0
1 1
3 3
#<0=0,
0 0 0
1 1
3 3 3
#<1=6
0 0 0 0
1 1
3 3 3
#<2=10,
0 0 0 0
1 1
2
3 3 3
#<3=12
…
90
2.3.5 Indexsort: Implementierung
// sort array “a[]” between indexes l and r
// assuming: hash-function h is identical function, i.e. h(x)=x
// M: max. number of different keys
static void distCount(int a[], int l, int r)
{
int i
// run-variables
int cnt[] = new int[M];
// Anzahlliste/Anzahlsummenliste
int b[]
= new int[a.length]; // help-list for copying
// initialize „Anzahlliste“
for (i = 0; i < M; i++) cnt[i] = 0;
// compute values of „Anzahlliste“, iterate from l to r
for (i = l; i <= r; i++) cnt[a[i]+1]++; // a[i] starts
// compute values of „Anzahlliste“ by summing up previous elements
for (i = 1; i < M; i++) cnt[i] += cnt[i-1];
// move numbers to block (and increment within block)
for (i = l; i <= r; i++) b[cnt[a[i]]++] = a[i];
// copy helplist b[] back to original list a[]
for (i = l; i <= r; i++) a[i] = b[i-l];
}
91
2.3.3 Indexsort: Diskussion
 Eigenschaften
 Stabil
 Nicht Adaptiv (ist aber egal, da der Aufwand eh‘ klein genug ist)
 Nicht „In-situ“: Indexsort benötigt eine Hilfsliste, „In-situ“-Variante ist nicht stabil
 Aufwand
 Vergleiche = Bewegungen : min = max = average O(n)
 O(n) Platzkomplexität (für Hilfsliste)
 Der Indexsort ist der schnellste Sortieralgorithmus.
 Der Indexsort nur auf solche Daten anwendbar, bei denen der Bereich
unterscheidbarer Schlüsselwerte innerhalb eines konstanten Faktors der Datengröße
bleibt
 Ist die Hashfunktion nichttrivial, so wird der Vorteil des Verfahrens durch einen
hohen konstanten Multiplikationsfaktor im Aufwand, selbst für große n, aufgefressen.
 Bei großen n ist die Platzkomplexität ein Problem.
92
2.3.5 Vergleich
min
average
max
Adaptiv
Platz (In-situ)
Stabil
Selection
O(n2)
O(n2)
O(n2)
nein
O(1)
nein
Insertion
O(n)
O(n2)
O(n2)
ja
O(1)
ja
Shell
O(n1,2)
O(n1,2)
O(n1,2)
ja
O(1)
nein
Bubble
O(n)
O(n2)
O(n2)
ja
O(1)
ja
Indexsort
O(n)
O(n)
O(n)
nein
O(n)
ja
 Schnellster Algorithmus ist der Indexsort – der braucht aber eine effiziente
Hashfunktion und ist zudem nicht In-situ.
 Der Shellsort ist ein schneller „universeller“ in-situ Algorithmus, allerdings nicht
stabil.
 Bubble und Insertion-Sort sind sehr vergleichbar – meist ist der Insertion Sort
schneller.
 Der Selection Sort ist der schlechteste Algorithmus, allerdings benötigt er nur
O(n) Umordnungen – kommt also bei teuren Umordnungsaktionen in Betracht.
 Der Indexsort ist der schnellste Algorithmus, hat aber einen sehr schlechten
Platzbedarf und benötigt eine gute Hashfunktion, die selten verfügbar ist.
93
Übung 2.3:
1. Implementieren Sie einen einfachen Sortieralgorithmus
 verwenden Sie dabei Java-arrays aus java.util.array – aber zunächst nicht deren
sort-methode
hash(IhrNachname) = Summe(Ascii(Buchstaben)) mod 5
hash(IhrNachname) = 0  Selection, 1 Insertion, 2Shell, 3Bubbke, 4Index
2. Generieren Sie 10000 zu sortierende Daten:
 zufällig, sortiert, umgekehrt sortiert
3. Bestimmen Sie für diese Daten jeweils:
(Stellen Sie das Ergebnis in einer Excel-Graphik dar)
 Anzahl der Vergleiche
 Inkrementieren Sie dazu eine globale Variable V
 Anzahl der Zuweisungen
 Inkrementieren Sie dazu eine globale Variable Z
 Laufzeit
 entfernen Sie vorher die Inkrementierungen von V und Z
4. Vergleichen Sie Ihre Laufzeiten mit der array.sort –Methode aus
java.util.array (Erweitern Sie dazu Ihre Excel-Graphik)
5. In welchen Situationen bzw. für welche Daten würden Sie Ihre Algorithmen
verwenden ?
94
2.4. Fortgeschrittene Sortieralgorithmen
95
2.4.1 Heapsort: Der König der in-situ Algorithmen
 Idee (erster Ansatz):
Beim Heapsort werden Elemente so in eine Struktur eingehängt, dass sie
schnell und in sortierter Reihenfolge ausgelesen werden können.
Damit das effizient funktioniert wird beim Einfügen die „Heap-Eigenschaft“
eingehalten, das bedeutet, dass die Daten „logisch“ wie ein binärer Baum
strukturiert sind (aber als z.B. Liste) , wobei kein Nachfolgeknoten kleiner als
sein Vorgängerknoten ist . Damit befindet sich in der Wurzel das kleinste
Element.
Diese Liste nennt man dann Prioritätswarteschlange (Priority-Queue)
 Beispiel für eine Priority Queue für : 3 6 3 4 3 9 8 1 7 5 5 2
1
2
3
3
5
1
1
4
6
2
2
3
3
7
4
3
3
8
5
9
5 6 7 8 9 10 11 12 (Index)
4 3 5 5 6 7 8 9
Priority Queue (Heap)
(not neccessarily completely sorted)
96
2.4.1 Heapsort: Implementierung einer PQ
 Dieser Prototyp implementiert einige grundlegende Teile eines abstrakten
Datentyps „Priority queue“. Insbesondere wir bei „Insert“ das neue Element als
Blatt ganz unten (= ganz hinten) an den Heap angefügt.
Danach muss „nur noch“ die Heap-Eigenschaft sichergestellt werden.
class PQ {
static boolean less(ITEM v, ITEM w)
{ return v.less(w); }
static void exch(ITEM[] a, int i, int j) { ITEM t=a[i]; a[i]=a[j]; a[j]=t; }
private ITEM[] pq; // array holding priority queue
private int N;
// counter, how many elements are in queue
PQ(int maxN)
{ pq = new ITEM[maxN]; N = 0; } // constructor
boolean empty() { return N == 0; }
// is queue empty
// insert at end of array
void insert(ITEM item) {
pq[N++] = item;
// add element as last element, increase counter
...
// heap-property has to be ensured here !
}
// get minimal element from head
ITEM getmin() {
...
// heap-property has to be ensured here !
return pq[1]; // returm minimal element, which is lovated at head
}
};
97
2.4.1 Heapsort: Einhaltung der Heap-Eigenschaft
 Bottom-Up Verfahren:
Bei diesem Verfahren wird ein Element (auf Position k) solange mit seinem
übergeordneten Element (auf Position k/2) vertauscht, bis dieses kleiner ist
private void swim(int k) {
while (k > 1 && less(k, k/2))
{ exch(k, k/2); k = k/2; }
}
 Top-Down Verfahren:
Bei diesem Verfahren wird ein Element (auf Position k) solange mit dem
kleineren seiner beiden untergeordneten Element e (auf den Position 2k und
2k+1) vertauscht, bis diese beide nicht mehr kleiner sind
private void sink(int k, int N) {
while (2*k <= N) {
int j = 2*k;
if (j < N && less(j, j+1)) j++;
if (!less(k, j)) break;
exch(k, j); k = j;
}
}
98
2.4.1 Heapsort: Ein- und Ausfügen
 Mit swim und sink können jetzt die einfügen und ausfüge-Operationen (des
Minimums) implemementiert werden:
class PQ {
...
void insert(ITEM v) {
pq[++N] = v; // new element is added at end of list ...
swim(N);
// and swims to top until smaller element is found
}
ITEM getmin() {
exch(1, N);
// biggest is swapped to top by swapping with smallest
sink(1, N-1);
// this big element on top sinks until it reaches a bigger
return pq[N--]; // return the smallest element, which has been swapped to
// end of list
}
};
99
2.4.1 Heapsort: alternatives „heapify“
 Statt den Heap über aufeinanderfolgende Einfügungen aufzubauen ist es
effizienter, den Heap zu erstellen, indem man ihn rückwärts durchläuft und
dabei kleine Teil-Heaps von der untersten Ebene nach oben hin aufbaut. Dabei
kann man Element der untersten Ebene ignorieren
 Beispiel :
1 2 3 4 5 6 7 8 9 10 11 12 (Index)
3 6 3 4 3 9 8| 1 7 5 5 2
3
3
6
3
4
1
3
7
5
9
5
6
8
2
3
1
4
3
7
5
5
3
1
3
3
7
5
9
5
8
2
2
1
4
3
7
5
3
3
7
5
2
5
5
8
9
2
4
6
3
7
5
3
3
7
5
2
5
3
5
8
9
1
6
1
8
9
1
3
4
3
3
6
1
9
6
3
4
8
3
6
4
2
9
3
8
2
4
6
3
7
5
3
5
9
8
100
2.4.1 Heapsort: Implementierung
 Sortieren kann man nun durch heapify der Liste und anschließendes
sequenzielles Ausfügen., wobei das Ausfügen durch Tausch an das Ende der
Liste (also In-situ) erfolgt.
private void sink(int k, int N) {
while (2*k <= N) {
int j = 2*k;
if (j < N && less(j, j+1)) j++;
if (!less(k, j)) break;
exch(k, j); k = j;
}
}
// heapify the list, starting from N/2 with N=2x with N >= length(list)
for (int k = N/2; k >= 1; k--)
sink(k, N);
// iterate through heap – sorting biggest element first, smallest last
while (N > 1) {
exch(1, N);
// exchange big with smallest (which is at top)
sink(1, --N); // decrease size and heapify heap with big element at top
}
101
2.4.1 Heapsort: Diskussion
 Eigenschaften
 Nicht Stabil
 Nicht (wirklich) Adaptiv
 Aufwand
 Vergleiche = Bewegungen : min = max = average O(n log n) (genauer: < 2n ld n)
 O(1) Platzkomplexität (In-situ)
 Der Heapsort ist gut geeignet die k-kleinsten Elemente zu finden, denn dann
kann man die Ausleseschleife nach k bereits verlassen.
Der Heapsort ist im Mittel der schnellste bekannte in-situ Algorithmus
102
2.4.2 Mergesort: Der König der stabilen Sortieralgorithmen
 Idee:
Die Mergesort-Verfahren sortieren (mischen) bereits sortierte Teildatenmengen
(beginnend mit ein-elementigen Mengen) zusammen zu immer größeren
Datenmengen.
Dabei werden die zu sortierenden Mengen rekursiv geteilt und nach erfolgter
Sortierung der Teilmengen zu Größeren sortiert zusammengemischt .
 Beispiel:
3 6 3 4 3 9 8 1 7 5 5 2
3 6
// sorting 1-element lists
3 4
// sorting 1-element lists
3 3 4 6
// merging 2-element lists
3 9
// sorting 1-element lists
1 8
// sorting 1-element lists
1 3 8 9
// merging 2-element lists
1 3 3 3 4 6 8 9
// merging 4-element lists
5 7
// sorting 1-element lists
2 5 // sorting 1-element lists
2 5 5 7 // merging 2-element lists
1 2 3 3 3 4 5 5 6 7 8 9 // final merging
103
2.4.2 Mergesort: Mischen (Version 1)
 Idee:
vergleiche paarweise die (sortierten) Listen a und b und kopiere den jeweils
kleinsten Wert nach c. Falls eine Liste (a oder b) abgearbeitet ist, können die
noch verbleibenden Elemente der anderen Liste uinbesehen ans Ende von c
kopiert werden
// merge array a[] (from indexes al to ar) and b[] (from index bl to br)
// array c[] (starting from index cl)
static void mergeAB(ITEM[] c, int cl,
ITEM[] a, int al, int ar,
ITEM[] b, int bl, int br )
{ int i = al, j = bl;
for (int k = cl; k < cl + ar-al + br-bl + 1; k++)
{
// copy all element of b into c, if a has been finished
if (i > ar) { c[k] = b[j++]; continue; } // index i larger a’s right
// copy all element of a into c, if b has been finished
if (j > br) { c[k] = a[i++]; continue; } // index j larger b’s right
// move bigger element of a or b into c and increment correspondent
c[k] = less(a[i], b[j]) ? a[i++] : b[j++];
}
}
into
index
index
index
104
2.4.2 Mergesort: Mischen (Version 2)
 Idee:
Die Vergleiche der ersten Version, ob a bzw. b schon abgearbeitet ist sind
aufwändig. Das kann man vermeiden indem man b in umgekehrter Reihenfolge
an a angehängt und dann jeweils das erste mit dem letzten Element vergleicht,
Beispiel:
Mische 1 3 4 in 2 5 7: invertiere 2 5 7 und hänge es an 1 3 4
1 3 4 7 5 2 -> 3 4 7 5 2 -> 3 4 7 5 -> 4 7 5 -> 7 5 -> 7
1
2
3
4
5
7
// merge two blocks (l to m and m to r) of array a
static void merge(ITEM[] a, int l, int m, int r)
{ int i, j;
// copy first block (l to m) to aux
for (i = m+1; i > l; i--) aux[i-1] = a[i-1]; //
// reverse copy second block (m to r) to aux
for (j = m; j < r; j++) aux[r+m-j] = a[j+1]; //
// iterate through elements and compare pairwise
for (int k = l; k <= r; k++) {
// copy smallest element to aux
if (less(aux[j], aux[i])) a[k] = aux[j--];
else
a[k] = aux[i++];
}
}
into array aux
i at beginning
j at end
first and last element
105
2.4.2 Mergesort: Mischen (Version 3: linked list)
 Der Mergesort eignet sich hervorragend für das Sortieren verketteter Listen.
Diese können wie folgt gemischt werden:
// merge two blocks referenced by a and b, return link to sorted list
static Node merge(Node a, Node b)
{
Node dummy = new Node();
Node head = dummy, // head is link to head of list, which is a dummy element
Node c
= head; // c is a running pointer, initialized to head
// iterate through both lists until one of them is empty
while ((a != null) && (b != null))
{
// link c.next to minimim (a or b), link c to this element (end of list),
// increase pointer in list where miniumum has been taken from
if (less(a.item, b.item)) { c.next = a; c = a; a = a.next; }
else
{ c.next = b; c = b; b = b.next; }
}
// add rest of list that is not empty to result
c.next = (a == null) ? b : a;
// do not return 1st dummy element but 1st content element
return head.next;
}
106
2.4.2 Mergesort: Top-Down Sortieren (Rekursiv)
 Idee des Top Down Sortierens (Wie eingangs beschrieben)
Der Algorithmus zerlegt die zu sortierende Datenmenge in zwei Teile und
sortiert diese durch rekursive Aufrufe unabhängig voneinander. Die Ergebnisse
werden dann zusammengemischt
// sort a from index l to r
static void mergesort(ITEM[] a, int l, int r)
{
// stop rekursion if only one element is to be considered
if (r <= l) return;
// split list in two
int m = (r+l)/2;
// recursively call mergesort for first half
mergesort(a, l, m);
// recursively call mergesort for second half
mergesort(a, m+1, r);
// merge the two halfs into one sorted list (by which merge ever)
merge(a, l, m, r);
}
107
2.4.2 Mergesort: Bottom-Up Sortieren (nicht rekursiv)
 Idee des Bottom-Up Sortierens :
Der Algorithmus führt berechnet zunächst alle Sortierungen/Mischungen kleiner
Datenmengen durch, danach werden die Datenmengen verdoppelt, bis die
Gesamtliste sortiert ist. Das ganz funktioniert iterativ:
Beispiel:
3 6 3 4 3 9 8 1 7 5 5 2
3 6
// sorting 1-element lists
3 4
// sorting 1-element lists
3 9
// sorting 1-element lists
8 1
// sorting 1-element lists
5 7
// sorting 1-element lists
2 5 // sorting 1-element lists
3 3 4 6
// merging 2-element lists
1 3 8 9
// merging 2-element lists
2 5 5 7 // merging 2-element lists
1 3 3 3 4 6 8 9
// merging 4-element lists
1 2 3 3 3 4 5 5 6 7 8 9 // final merging
108
2.4.2 Mergesort: Bottom-Up Sortieren (nicht rekursiv)
// procedure to get minimal element of A and B
static int min(int A, int B)
{ return (A < B) ? A : B; }
// sort a from index l to r
static void mergesort(ITEM[] a, int l, int r)
{
// need not sort if list is empty
if (r <= l) return;
// global auxiliary list needed for merging in “merge”
aux = new ITEM[a.length];
// iterate through list by blocks that are doubled each time
for (int m = 1; m <= r-l; m = m+m)
// iterate through each block
for (int i = l; i <= r-m; i += m+m)
// merge left half of block with right half
merge(a, i, i+m-1, min(i+m+m-1, r)); // do not use i+m+m-1 if r is smaller
// important if r-l <> 2x
}
109
2.4.2 Mergesort: Top-Down Sortieren (linked list)
// sorts linked list referenced by c
static Node mergesort(Node c)
{
// need not sort empty list
if (c == null || c.next == null) return c;
Node a = c;
// a is link to 1st element
Node b = c.next; // b is link to 2nd element
// iterate through list until b is at the end (and c in the middle)
while ((b != null) && (b.next != null)) {
c = c.next;
// “increase” c by one
b = (b.next).next; // “increase” b by two
} // c point to the element in the middle
// a still point to 1st element
b = c.next;
// b points to the element right from the middle (c)
c.next = null; // cut of list, right from element in the middle (c)
// recursively call mergesort for a and b and merge it
return merge(mergesort(a), mergesort(b));
}
110
2.4.2 Mergesort: Diskussion
 Eigenschaften
 Stabil (wenn „merge“ stabil)
 Nicht Adaptiv
 kleiner overhead (kompakter Code)
 Aufwand
 Vergleiche = Bewegungen : min = max = average O(n log n)
 O(n) Platzkomplexität (aux-Liste oder Links der verketteten Liste)
 Der Mergesort ist stabil, hat ein besseres Komplexitätsverhalten als O(n2). und
garantiert auch im schlechtesten Fall eine Zeitkomplexität von O(n log n).
 wegen dieser Garantie wird der Mergesort in vielen Bibliotheken verwendet.
 Auch Java.util.array verwendet für sort auf object den
Mergesort (für andere Typen, z.B. int wird ein – wahrscheinlich – 3-WegeQuicksort verwendet).
Dabei garantiert die Java Runtime Environment nicht den verwendeten Algorithmus,
sondern nur die Stabilität des Sortieralgorithmus‘.
Die Entscheidung zwischen Merge- und Heapsort ist eine Entscheidung
zwischen einem stabilen nicht-In-situ (Mergesort) und einem nicht-stabilen Insitu-Algorithmus.
111
2.4.2 Mergesort: Verbesserungen
 Beschränkt man die Rekursionstiefe, so dass dieser nicht bei ein-elementigen
Mengen stoppt, sondern bereits bei mehr-elementigen, so sind die
resultierenden Mengen nicht vollständig sortiert, sondern nur vorsortiert.
Auf dieses Resultat kann man nun den Insertion-Sort anwenden, der für diese
Fälle ein sehr gutes Laufzeitverhalten O(n) hat.
 Dadurch erspart man sich die sehr häufig durchlaufenen „kleinen“ Fälle.
 Dieses Verfahren führt zu einer Leistungssteigerung um 10-15%
 Beim Mergen wird immer wieder in eine Hilfsliste (aux) umkopiert. Das kann
man bei mehrmaligem Aufruf (z.B. in der Rekursion) dadurch vermeiden, dass
man die Rolle der Hilfsliste und der zu mischenden Teillisten jeweils vertauscht
 Damit gewinnt man nochmals ca. 40%
 Auch der Mergesort für verkettete Listen lässt sich in einer Bottom-Up Strategie
realisieren. Dazu kann man z.B. Warteschlangen zur Zwischenspeicherung der
immer größer werdenden Teillisten verwenden.
Diese Realisierung eignet sich, um bereits vorsortierte Datenmengen zu
sortieren. ( Sedgewick, Kapitel 8.7)
112
2.4.3 Quicksort: Der König der Sortieralgorithmen
 1960 wurde von C.A.R. Hoare de bis heute verbreitetste Sortieralgorithmus
entwickelt.
 Idee:
Beim Quicksort wird eine Liste rekursiv in zwei Teile geteilt und diese dann
sortiert. Das Teilen der Liste erfolgt dabei so, dass alle Element im ersten Teil
kleiner oder gleich sind als alle Elemente im zweiten Teil.
Dafür müssen zu große Elemente des ersten Teils mit zu kleinen Element en
des zweitenTeils vertauscht werden. Als Maßstab für „zu groß“ bzw. „zu klein“
gilt dabei ein zufälliges Element a[i].
Es gilt also:
 Das Element a[i] befindet sich an seiner endgültigen Position
 Keines der Elemente a[l] .. a[i-1] ist größer als a[i]
 Keines der Elemente a[i+1] .. a[r] ist kleiner als a[i]
 Beispiel :
3 6 3
3 2 3 4 3
1 2
3 3
1
2
3 3
3
3
4 3 9 8 1 7 5 5 2
1
5 5 6 9 8 7
4 3
5 5
6 9 8 7
3 4
5
5
6 7
8 9
3
4
6
7 8
9
113
2.4.3 Quicksort: Implementierung der Hauptmethode
 Das Vorgehen beim Quicksort ist, in gewissem Sinne, das genaue Gegenteil
des Vorgehens beim Mergesort.
 Beim Mergesort teilt der „Boss“ die Liste unbesehen in zwei Teile und lässt dann
seine „untergebenen“ arbeiten. Erst wenn das Ergebniss vorliegt mischt er die
sortierten Teile zusammen. Das ist „Lazy“.
 Bei Quicksort sortiert der „Boss“ die Liste schon etwas vor, in dem Sinne dass er
zwei Teillisten erstellt, mit jeweils den kleinsten bzw. größten Elementen. Diese
Teillsiten, die aber noch unsortiert sind, reicht er an seine „Untergebenen“
weiter.Damit ist seine Aufgabe erledigt. Das ist „Eager“
Sobald das rekursiv vollständig durchlaufen ist, liegt das Ergebnis vor.
Das ist „eager“
static void quicksort(ITEM[] a, int l, int r) {
if (r <= l) return; // recursion stops if there is one or no element to sort
int i = partition(a, l, r); // r > l => at least two elements => divide
quicksort(a, l, i-1); // sort left part
quicksort(a, i+1, r); // sort right part
}
2.4.3 Quicksort: Partitionierung
 Wir wählen mit v ein beliebiges Trennelement (Pivot-Element) .
Also z.B. das Element ganz rechts
V
r
l
 Wir suchen dann von links ein Element das größer als das Trennelement und
von rechts ein Element das kleiner als das Trennelement ist. Diese beiden
Elemente sind offenbar falsch und werden durch Tauschen korrigiert.
kleiner oder gleich v
größer oder gleich v
V

r
l
i
j
Wir wiederholen das Ganze bis sich der linke und der rechte Laufindex kreuzen.
V
kleiner oder gleich v
größer oder gleich v

r
l
j i
Dann muss nur noch das Pivot-Element mit dem linkesten Element des rechten
Teils getauscht werden. Damit ist das Pivot-Element am richtigen Platz
kleiner oder gleich v
l
V
j i
größer oder gleich v
r
2.4.3 Quicksort: Implementierung der Partitionierung
kleiner oder gleich v
l
V
größer oder gleich v
i
r
j
static int partition(ITEM a[], int l, int r) {
int i = l-1, j = r; // set left index I and right index r
ITEM v = a[r];
// set pivot-element v = a[r] as Sortmarker
for (;;) { // endless loop, left by second break
// iterate from left through all elements a[i] smaller than v
while (less(a[++i], v)) ; // initially starting at l-1, so increase first
// need not explicitely stop at I=r since a[i=r] is not less a[r]
// iterate from right through all elements a[j] bigger than v
while (less(v, a[--j])) // initially starting at r, so decrease first
if (j == l) break; // avoid leaving intervalstop j-iteration at left end
// stop endless loop if left iterator I reaches right iterator j
if (i >= j) break; // second break, I and j crosses
// a[i] reached a value larger than v and a[j] smaller than v
exch(a, i, j); // swap a[i] and a[j]
}
exch(a, i, r); // swap pivot-element a[r] with a[i]
return i;
}




2.4.3 Quicksort: Non-Sedgewick Implementierung ;-)
for (;;) { // endless loop, left by second break
while (less(a[++i], v)) ;
while (less(v, a[--j]))
if (j == l) break;
if (i >= j) break;
exch(a, i, j);
}
=>
while (
while
while
if (i
}
i < j ) {
(less(a[++i], v)) ;
(less(v, a[--j]) && j>l);
< j) exch(a, i, j);
117
2.4.3 Quicksort: Pathologische Fälle
1. Liste ist sortiert , v=a[r] ist das größte Element: 1 2 3 3 4 5 6 7 9




i läuft in while Schleife bis r = j
dann wird j dekrementiert
j ist dann <= i, die Endlosschleife wird verlassen
abschließend wird a[i] mit a[r] getauscht, da i=r bleibt das größte Element wo es war
2. Pivot-Element v=a[r] ist das kleinstes Element: 2 6 4 5 6 7 2 9 1




i wird initial inkrementiert
dann läuft j bis j=i, die j-Schleife wird durch break bei j=l verlassen
j ist dann <= i, die Endlosschleife wird verlassen
abschließend wird a[i]=a[l] mit a[r] getauscht: 1 6 4 5 6 7 2 9 2
3. alle Schlüssel sind gleich: 3 3 3 3 3 3
 i und j laufen jeweils um nur eines nach innen, denn „less“ ist jeweils nicht erfüllt
 dann werden jeweils a[i] und a[j] ausgetauscht und nochmals a[i] und a[r]=v
 abschließend sind i und j gleich, die Endlosschleife wird verlassen und a[r]=v wird
schließlich noch einmal mit a[i] getauscht
 És werden gleiche Elemente getauscht und der Quicksort ist nicht stabil
3 3 3 3 3
3
3 3 3 3 3
3
3 3 3 3 3
3
3 3 3 3 3 3
118
2.4.3 Quicksort: Zwischendiskussion
 Bei unserer der Implementierung werden selbst gleiche Schlüssel getauscht.
Grund dafür ist das stoppen der While-Schleifen für beide Indizes (i,j) bei
Gleichheit.
Interessanterweise ist dieses Verhalten besser als die Alternativen, einen Index
weiterlaufen zu lassen oder sogar beide Indizes weiterlaufen zu lassen. Der
grund liegt in einer statistisch ausgewogeneren Teilung der Liste insb. bei vielen
doppelten Schlüsseln.
 Die Wahl von a[r] als zufälligen Wert führt nur bei einer völlig zufälligern
Wertverteilung zu guten Teilungen.
 Betrachten Sie den pathologischen Fall 1. Dort läuft i immer bis rechts, so dass die
linke Teilliste bis zum vorletzten Element reicht. Damit ergibt sich ein Gesamtaufwand zu: n + (n-1) + (n-2) + … + 1 = (n*(n-1))/2 = O(n2)
Dabei beträgt die Stackgröße maximal n
 Also sollte man dem Zufall auf die Sprünge helfen z.B.: v = a[l+r]/2
v = (a[l]+a[r])/2, v = median (a[l],a[r],a[l+r]/2)
 Wie beim Merge- und Heapsort, so ist auch beim Quicksort eine gewisse
Optimierung durch die Verwendung des Insertion-Sorts für kleine Teildateien
machbar („CutOff“-Verfahren).
119
2.4.3 Quicksort: Nichtrekursive Implementierung
 Wir haben gesehen: im sortierten Fall kann der Stack linear mit n wachsen. Das
kann bestimmten Laufzeitsystemen zum Problem werden (-> stack overflow).
Daher kann man den Quicksort (natürlich) auch nichtrekursiv implementieren
und damit den benötigten Platz aus dem Stack in den Speicher verlagern:
Zus#tzlich wird jeweils der kleinere Teil zuerst abgearbeitet. Die Stackgröße
bleibt dadurch auf O(log n) beschränkt.
static void quicksort(ITEM[] a, int l, int r) {
intStack S = new intStack(50); // get stack as array
S.push(l); S.push(r); // push initial margins on stack
while (!S.empty()) {
r = S.pop(); l = S.pop();
if (r <= l) continue;
int i = partition(a, l, r);
// get left and right margin from stack
// leave loop if list is empty
// partitinate list
// push bigger partial list first on stack, so it is treated after
// smaller part -> stack size is restricted to log n
if (i-l > r-i) { S.push(l); S.push(i-1); } // left part bigger
S.push(i+1); S.push(r);
// right part
if (i-l <= r-i) { S.push(l); S.push(i-1); } // left part smaller
}
}
120
2.4.3 Quicksort: Median – Beispiel
 Um den „entarteten“ Fall einer sortierten
Eingabeliste zu umgehen, wird oft der
Median aus drei Elementen als
Trennelement verwendet.
Zur Ermittlung des Median müssen
diese drei Element sortiert werden.
Ordnet man diese drei Element nun so
an, dass
 das kleinere ganz links in der zu
sortierenden Liste, also bei a[l] steht
 der Median an a[r-1] steht
 der größere ganz rechts, also bei a[r]
steht.
so braucht man bei der anschließenden
Partitionierung das kleinere und größere
Element nicht mehr zu betrachten
l
l-r/2
r-1 r
7 3 6 5 4 2 8 9 3 5
exch(a, (l+r)/2, r-1);
l
l-r/2
r-1 r
7 3 6 5 3 2 8 9 4 5
compExch(a, l, r-1);
l
l-r/2
4 3 6 5 3 2 8
compExch(a, l, r);
r-1 r
9 7 5
l
l-r/2
r-1 r
4 3 6 5 3 2 8 9 7 5
compExch(a, r-1, r);
l
4
3
6
l-r/2
5 3 2
8
r-1 r
9 5 7
partition(a, l+1, r-1)
l+1
r-1
3 6 5 3 2 8 9 5
121
2.4.3 Quicksort: Median-von-drei Quicksort
 Der Median-von-drei Quicksort einen Median, wie es oben beschrieben wurde
und zusätzlich noch einen „CutOff“
private final static int M = 10;
// size of lists that are insertion-sorted
static void quicksort(ITEM[] a, int l, int r) {
if (r-l <= M) return; // do not consider unsorted lists smaller than M
// moves element from the middle to r-1 and sorts l, r-1, r
exch(a, (l+r)/2, r-1);
compExch(a, l, r-1);
compExch(a, l, r);
compExch(a, r-1, r);
// a[l] is already smaller, a[r] already larger than a[r-1] -> l,r need not
// be considered in partitioning
int i = partition(a, l+1, r-1);
// recursively call quicksort
quicksort(a, l, i-1);
quicksort(a, i+1, r);
}
static void hybridsort(ITEM a[], int l, int r)
{ quicksort(a, l, r); insertion(a, l, r); }
122
2.4.3 Quicksort: 3-Wege Partitionierung
 Wenn Sie sich bei „sorting-algorithms.com“ die Laufzeiten für „wenige
unterschiedliche Keys (Few unique) anschauen erkennen Sie, dass der normale
(2-Wege-Partitionierungs-) Quicksort Laufzeitprobleme bei diesen
Eingabewerten hat.
Auch unser „Pathologischer Fall Nr.3“ zeigt, dass selbst eine Liste mit einem
Key noch sortiert werden.
 Idee:
Wenn v das Trennelement ist, so zerlege die Liste in drei Teile, den linken Teil
mit Schlüsseln kleiner als v, den mittleren Teil mit Schlüsseln gleich v und den
rechten Teil mit Schlüsseln größer v (klassisches Programmieraufgabe von
Dijkstra, bekannt als das „Problem der Holländischen Nationalflagge“
)
Danach muss der mittlere Teil nicht mehr sortiert werden.
 1993 wurde eine verfeinerte Methode entwickelt. Dabei werden die gleichen
Schlüssel zunächst an den äußeren Teilen links bzw. rechts angeordnet und in
einem zweiten Durchlauf erst dazwischen eingefügt.
gleich v
l
kleiner v
p
größer v
i
j
gleichv
q
V
r
123
2.4.3 Quicksort: Quick-3 Implementierung
static void quicksort(ITEM a[], int l, int r) {
if (r <= l) return; // r<=1 , i.e. without „CutOff“
ITEM v = a[r];
// „Simple“ Sorting element, i.e. without median
int i = l-1, j = r, p = l-1, q = r, k; // p,q index of equal elements
for (;;) {
// skip smaller elements ltr, resp. larger elements rtl
while (less(a[++i], v)) ;
while (less(v, a[--j])) if (j == l) break;
if (i >= j) break; // stop endless loop if index i,j meet or cross
exch(a, i, j);
// swap a[i] and a[j]
// new: if equal increase/decrease markers p/q and move elements to edges
if (equal(a[i], v)) { p++; exch(a, p, i); } // if equal, increase p
if (equal(v, a[j])) { q--; exch(a, q, j); }
}
// move sorting element to right place
exch(a, i, r); j = i-1; i = i+1;
// new: move equal elements from edges
for (k = l ; k <= p; k++,j--) exch(a,
for (k = r-1; k >= q; k--,i++) exch(a,
// recursively call quicksort
quicksort(a, l, j);
quicksort(a, i, r);
and reposition i and j
to middle
k, j);
k, i);
}
124
2.4.3 Quicksort: Quick-3 Beispiel (1. Rekursion)
i=p
1 2 5
----> i
1 2 3
7
7
3
3
5
5
8
8
9
9
2
2
5
5
p
1
1
-> p
5
5
p
5
2
2
2
2
2
3 7
-> i
3 5
3
3
3
5 2 3
k=p
2 2 3
(k)
2 2 3
2
l
2
3
3
3
5
5
1 3 5
----> i
1 3 2
1
1
1
1
1
3
3
3
j
3
j
3
j
8
8
8
8
9
9
9
9
2
2
5
j
7
2 7
j <5 7
2 8 9 9
-> i
j <------2 5 9 9
j
i
5 5 9 9
i
5 5 5 9
i
5 5 5 5
7
9
3
j
9 5
q
9 5
<---9 5
q
9 5
9 5
q <5 5
j=q
5
<- // a[i]
5
<- // a[q]
5
// a[i]
5
// a[p]
5
// a[i]
5
// a[q]
5
!< v, a[j] !> v --> swap
= v --> swap (although j=q)
!< v, a[j] !> v --> swap
= v --> swap a[p] with a[i]
!< v, a[j] !> v --> swap
= v --> swap a[q] with a[j]
// swap a[r] with a[i]
7
5
q
7 5
q
7 5
q,k
7 9
i
5
8
// swap a[k] with a[j]
5
k
9
8
sorting-element
to middle
equals at left
edge to middle
// swap a[k] with a[i]
8
// swap a[k] with a[i]
9
smaller to left,
larger to right,
equals to edges
8
r
equals at right
edge to middle
// recursively call quicksort[l.j]/[i,r]
125
2.4.3 Quicksort: Diskussion
 Eigenschaften
 Nicht Stabil
 Quick-3: Adaptiv (falls Anzahl gleicher Schlüssel konstant)
 Aufwand




min:
average:
max:
O(log n)
O(n) bei 3-Quick mit konstanter Anzahl von Schlüsseln, sonst O (n log n)
O(n log n)
O(n2)
Platzkomplexität: nicht in-situ, aber ziemlich gut
 Der Quicksort ist ein idealer Allrounder:
 obwohl er nicht in-situ ist, eignet er sich auch für eingebettete Systeme, denn der
Platzverbrauch von O(log n) ist meist auch dafür o.k.
 obwohl er nicht stabil ist, so gibt es doch viele Anwendungsfälle, bei denen es nicht
darauf ankommt, z.B. wenn die zu sortierenden Datentypen einfach sind.
 obwohl der maximale Aufwand quadratisch ist, so ist dieser Fall, insb. bei
Verwendung eines Median-Verfahrens sehr unwahrscheinlich.
… und da der Quicksort im Mittel signifikant schneller als der Heap- und der
Mergesort ist, gilt er zurecht als der „König der Sortieralgorithmen“ ;-)
 mit Quick-3 Implementierung für Daten mit vielfachen Schlüsseln
 mit Median-von-drei und „Cut-Off“ für einfache Schlüssel.
126
2.4.4 Vergleich
min
average
max
Adaptiv
Platz (In-situ)
Stabil
Heap
O(n log n)
O(n log n)
O(n log n)
nein
O(1)
nein
Merge
O(n log n)
O(n log n)
O(n log n)
nein
O(n)
ja
Quick
O(n log n)
O(n log n)
O(n2)
nein
O(log(n))
nein
Quick-3
O(n)
O(n log n)
O(n2)
ja
O(log(n))
nein
 Der Heapsort ist in-Situ aber instabil und langsamer als der Quicksort, garantiert
aber ein maximales Laufzeitverhalten O (n log n)
 Der Mergesort ist stabil aber nicht In-situ und ebenfalls langsamer als der
Quicksort, garantiert aber ebenfalls ein maximales Laufzeitverhalten O (n log n)
 Schnellster Algorithmus ist der Quicksort, der hat aber ein (sehr
unwahrscheinliches aber) garstiges maximales Laufzeitverhalten.
 In java.util.array werden sowohl der Mergesort (für object) als auch der
Quicksort (für alle anderen Typen) verwendet.

Komplexe Objekte haben oft gleiche Schlüssel  Stabilität nötig
127
Übung 2.4:
1. Implementieren Sie einen fortgeschrittenen Sortieralgorithmus
 verwenden Sie dabei Java-arrays aus java.util.array – aber zunächst nicht deren
sort-methode
hash(IhrNachname) = Summe(Ascii(Buchstaben)) mod 3
hash(IhrNachname) = 0  Mergesort, 1 Heapsort, 2Quicksort
2. Generieren Sie 10000 zu sortierende Daten:
 zufällig, sortiert, umgekehrt sortiert
3. Bestimmen für diese Daten jeweils:
(Stellen Sie das Ergebnis in einer Excel-Graphik dar)
 Anzahl der Vergleiche
 Inkrementieren Sie dazu eine globale Variable V
 Anzahl der Zuweisungen
 Inkrementieren Sie dazu eine globale Variable Z
 Laufzeit
 entfernen Sie vorher die Inkrementierungen von V und Z
4. Vergleichen Sie Ihre Laufzeiten mit der array.sort –Methode aus
java.util.array (Erweitern Sie dazu Ihre Excel-Graphik)
5. In welchen Situationen bzw. für welche Daten würden Sie Ihren Algorithmus
verwenden ?
128
2.5
Zusammenfassung
 Wir haben in diesem Kapitel einige Sortieralgorithmen kennengelernt. Dabei gibt
es für fast jeden Algorithmus Situationen in denen genau dieser Algorithmus
vorteilhafter als alle anderen sind.
Jeder Algorithmus hat eine grundsätzliche Idee und die meisten Algorithmen
können auf verschiedene Arten verbessert werden. Insbesondere lassen sich
Sortieralgorithmen auch miteinander kombinieren.
Da es DEN Sortieralgorithmus offenbar nicht gibt, ist die Verwendung eines
„vorgefertigten“ Algorithmuse‘ in einer Bibliothek (z.B. dem aus java.util) mit
Vorsicht zu genießen. Er passt zwar in den allermeisten Fällen sehr gut, aber
eben nicht in allen. In manchen Fällen führt er sogar zu Verletzungen harter
Rahmenbedingungen.
1. Eementarer Algorithmen
2. Fortgeschrittener Algorithmen
3. Vergleich aller Sortieralgorithmen
129
2.5.1 Elementare Algorithmen

Selection Sort wird (trotz schlechten Aufwandes) eingesetzt für das Sortieren von
Daten mit großen Elementen mit jeweils kleinen Schlüsseln:
 … bei diesen Daten sind die Kosten für den Vergleich sehr viel kleiner als die Kosten für
die Umordnung.
 Der Aufwand für die Umordnungen ist beim Selection sort mit O(n) kleiner als in den
meisten anderen Verfahren.

Der Insertion Sort wird eingesetzt, wenn es auf einen stabilen Algorithmus ankommt
…
 … und die Daten stark vorsortiert sind (da er adaptiv ist)
 … oder die Problemgröße klein ist (da er kompakt ist, also wenig „Overhead“ hat)
 … und für den „Cut-Off“-Teil eines fortgeschrittenen Sortieralgorithmuses



Der Shell Sort ist adaptiv, einfach zu implementieren und hat ein besseres
Komplexitätsverhalten als O(n2). Daher wird er bei nicht zu umfangreichen Daten
eingesetzt.
Der Bubblesort ist eigentlich nie zu empfehlen
Der Indexsort ist der schnellste Sortieralgorithmus.
 … aber nur auf solche Daten anwendbar, bei denen der Bereich unterscheidbarer
Schlüsselwerte innerhalb eines konstanten Faktors der Datengröße bleibt .
 Ist die Hashfunktion nichttrivial, so wird der Vorteil des Verfahrens durch einen hohen
konstanten Multiplikationsfaktor im Aufwand, selbst für große n, aufgefressen.
 Bei großen n ist die Platzkomplexität ein Problem.
130
2.5.2 Fortgeschrittene Algorithmen
 Der Heapsort ist gut geeignet die k-kleinsten Elemente zu finden, denn dann
kann man die Ausleseschleife nach k bereits verlassen.
Der Heapsort ist im Mittel der schnellste bekannte in-situ Algorithmus
 Der Mergesort ist stabil, hat ein besseres Komplexitätsverhalten als O(n2). und
garantiert auch im schlechtesten Fall eine Zeitkomplexität von O(n log n).
 wegen dieser Garantie wird der Mergesort in vielen Bibliotheken verwendet.
 Auch Java.util.array verwendet für sort auf object den
Mergesort (für andere Typen, z.B. int wird ein – wahrscheinlich – 3-WegeQuicksort verwendet).
Dabei garantiert die Java Runtime Environment nicht den verwendeten Algorithmus,
sondern nur die Stabilität des Sortieralgorithmus‘.
Die Entscheidung zwischen Merge- und Heapsort ist eine Entscheidung
zwischen einem stabilen nicht-In-situ (Mergesort) und einem nicht-stabilen Insitu-Algorithmus.
 Der Quicksort ist ein idealer Allrounder:
 obwohl er nicht in-situ ist, eignet er sich auch für eingebettete Systeme, denn der
Platzverbrauch von O(log n) ist meist auch dafür o.k.
 obwohl er nicht stabil ist, so gibt es doch viele Anwendungsfälle, bei denen es nicht
darauf ankommt, z.B. wenn die zu sortierenden Datentypen einfach sind.
 obwohl der maximale Aufwand quadratisch ist, so ist dieser Fall, insb. bei
Verwendung eines Median-Verfahrens sehr unwahrscheinlich.
131
2.5.3 Vergleich aller Sortieralgorithmen
Select. Insert. Shell Bubble Index Heap Merge Quick Quick-3
min
■
■
■
■
■
■
■
■
■
average ■
■
■
■
■
■
■
■
■
max
■
■
■
■
■
■
■
■
■
Adaptiv ■
■
■
■
■
■
■
■
■
In-situ
■
■
■
■
■
■
■
■
■
Stabil
■
■
■
■
■
■
■
■
■
 Soll man sich für einen Algorithmus entscheiden, so überprüft man zunächst die
„harten“ Kriterien: In-Situ, Stabilität, akzeptabler Maximalaufwand



Benötigt man In-Situ, scheiden Index- und Mergesort aus. Beim Quicksort ist zu
überlegen, ob O(log n) o,k ist, was meist der Fall sein wird.
Benötigt man Stabilität, scheiden Select, Shell, Heap und Quicksort aus
Select, Insert und Bubble scheiden eigentlich sofort aus. Muss der schlechteste Fall
garantiert zumindest gut sein, scheidet zusätzlich der Quicksort aus.
 Dann entscheidet die durchschnittliche Geschwindigkeit
1.
2.
3.
4.
Indexsort: bei vorhandener und guter Hashfunktion
Quicksort
Merge- und Heapsort,
Shellsort
132
Übung 2.5
Sortieren Sie die Liste 2 7 5 2 3 3 8 9 4 1 6 in aufsteigender Reihenfolge
mit dem
a) Selection Sort (Variante 1)
b) Insertion Sort (Variante 2)
c) Shellsort
d) Bubblesort
e) Indexsort
f) Heapsort (Vorsicht: aufsteigend !)
g) Mergesort (Version 2)
h) Quicksort:
i) Quicksort: Median-von-Drei Quicksort (ohne Cutoff)
j) Quicksort: Quick-3 mit 3-Wege Partitionierung
133
3.
Suchen
 Sedgewick: „Das Abrufen bestimmter Informationseinheiten aus größeren
vorher gespeicherten Datenbeständen ist eine fundamentale Operation, die
man als Suchen bezeichnet“.
1. ADT Symboltabelle
2. Symboltabelle als lineare Struktur
3. Binäre Suchbäume
4. Randomisierte BSTs
5. Splay BSTs
6. Rot-Schwarz-Bäume
134
3.1
ADT Symboltabelle
 Ähnlich wie beim Sortieren sind die Elemente, die gesucht werden
typischerweise unterstrukturiert und bestehen aus einem Schlüssel und
weiteren Attributen. Gesucht wird dann nach dem Schlüssel.
Die Abstrakte Datenstruktur auf der man diese (und andere) Operationen
definieren kann ist eine Symboltabelle
1. Definition
2. Spezifikation
3. Framework
4. Zusammenfassung
135
3.1.1 Definition
 Eine Symboltabelle ist eine Datenstruktur von Elementen mit Schlüsseln, die
zwei grundlegende Operationen unterstützt:
 insert: Einfügen eines neuen Elements
 search: Suchen (Zurückgeben) eines Elements mit einem gegebenen Schlüssel
Symboltabellen bezeichnet man manchmal auch als Wörterbücher
 Neben den grundlegenden Operationen (Einfügen, Suchen) gibt es weitere
sinnvolle Operationen auf einer Symboltabelle:




remove:
select:
sort:
join:
Entfernen eines gegebenen Elements
Auswählen des k-größten Elements (bei gegebem ganzzahligen k)
Sortieren der Symboltabelle
Verknüpfen zweier Symboltabellen
136
3.1.2 Spezifikation
type: stable(T) // T ist die Wertemenge der Elemente
import: boolean
operators:
empty
:  stable
// erzeugt leere Tabelle
insert
: stable x T  stable
// Fügt Element in Tabelle
search
: stable x T  T
// Sucht Element in Tabelle
remove
: stable x T  stable
// entfernt Element aus
Tabelle
join
: stable x stable  stable // verbindet zwei Tabellen
...
is_empty : stable  boolean
// ist Tabelle leer ?
axions:  s : stack,  x : T
remove (insert (s,x), x) = s
search (insert (s,x), x) = x
search ((join (insert(s1,x)),s2),x) = x
search ((join (s1,insert(s2,x))),x) = x
is_empty (empty) = true
is_empty (insert(s,x)) = false
137
3.1.3 Framework (Element)
// interface to Elements
// example implementation of element
// key of the element
class myKey implements KEY {
public boolean less(myKey)
// compare with key
public boolean equals(myKey)
// read key from input
void read()
// generate key randomly
void rand()
// prepare key for output
public String toString()
}
// the element itself
class myItem implements ITEM {
// the key of the element
public KEY key()
// read element from input
void read()
// generate element randomly
void rand()
// prepare element for output
public String toString()
}
class myKey implements KEY {
private int val;
public boolean less(KEY w)
{ return val < ((myKey) w).val; }
public boolean equals(KEY w)
{ return val == ((myKey) w).val; }
public void read()
{ val = In.getInt(); }
public void rand()
{ val = (int) (M * Math.random()); }
public String toString() { return val + ""; }
}
class myItem implements ITEM {
private myKey val;
private float info; // attribute
myItem() { val = new myKey(); }
public KEY key() { return val; }
void read()
{ val.read(); info = In.getFloat(); }
void rand()
{ val.rand(); info = (float) Math.random(); }
public String toString()
{ return "(" + key() + " " + info + ")"; }
138
}
3.1.3 Framework (Symboltable)
// interface to symbol list
// example of a client using symbol list
// key of the element
class ST // ADT interface
{
ST (int)
int count() // nr. of elements
void insert(ITEM)
ITEM search(KEY)
void remove(KEY)
ITEM select(int)
public String toString()
}
class DeDup {
public static void main(String[] args) {
int i,
int N = Integer.parseInt(args[0]),
int sw = Integer.parseInt(args[1]);
ST st = new ST(N);
// construct stable
for (i = 0; i < N; i++) {
myItem v = new myItem();
// either generate list or read it from input
// depending on args[1]
if (sw == 1) v.rand(); else v.read();
// insert element only if key does not exist
if (st.search(v.key()) == null) {
st.insert(v); Out.println(v + "");
}
}
// print no of keys and duplicates
Out.print(N + " keys, ");
Out.println(N-st.count() + " dups");
}
}
139
3.2
Symboltabellen als linearen Strukturen
 der Begriff „Symboltabelle“ suggeriert mit dem Teilbegriff „Tabelle“ eine lineare
Struktur von Elementen.
Tatsächlich kann man Symboltabellen sehr gut linear strukturieren und deren
Operationen auf diesen linearen Strukturen effizient, d.h. laufzeit-optimiert,
umsetzen. Dabei wird im Allgemeinen auf die häufigste Operation optimiert: Auf
das Suchen
1. Schlüsselindizierte Suche
2. Sequenzielle Suche
3. Binäre Suche
4. Vergleich
140
3.2.1 Schlüsselindizierte Suche
 Wenn die Schlüsselwerte positive Ganzzahlen kleiner M sind und die Elemente
eindeutige Schlüssel haben, dann lässt sich der ADT „Symboltablle“ mit
schlüsselindizierten Arrays (Hashlisten) von Elementen implementieren.
 insert, search, remove: O(1)
select, sort,: O(n)
class ST {
private intkeyItem[] st;
ST(int M)
{ st = new intkeyItem[M];
int count() { int N = 0;
for (int i = 0; i < st.length; i++) { if (st[i] !=
return N;
}
void insert(intkeyItem x) { st[x.key()] = x; }
void remove(int key)
{ st[key] = null; }
intkeyItem search(int key) { return st[key]; }
intkeyItem select(int k) {
for (int i = 0; i < st.length; i++) { if (st[i] !=
return null;
}
public String toString() {
String s = "";
for (int i = 0; i < st.length; i++) [ if (st[i] !=
return s;
}
}
}
null) N++; }
null && k-- == 0) return st[i]; }
null) s += st[i] + "\n"; }
141
3.2.2 Sequenzielle Suche: Ansätze
 Wenn die Schlüsselwerte keine positive Ganzzahlen kleiner M sind (und sich
auch nicht auf solche abbilden lassen) oder M zu groß für eine realistische
Speicherung ist, lässt sich eine Symboltabelle auch einfach als z.B. sortiertes
(geordnetes) Array realisieren.
 wird ein Element eingefügt, verschieben sich die größeren Element um eine Position
nach hinten (wie beim insertion-sort)
 Beim Suchen wird das Array sequenziell durchlaufen. Das Durchlaufen kann
beendet werden, sobald das Element oder ein Größeres gefunden wird.
 Auswählen lässt sich durch Zugriff auf das k-te Element leicht implementieren.
 Für das Sortieren ist überhaupt gar keine Aktion notwendig (außer ggf. einer
Ausgabe)
 Es sind auch weitere Realisierungen üblich, als:
 ungeordnetes Array
 geordnete verkettete Liste
 ungeordnete verkettete Liste
142
3.2.2 Sequenzielle Suche: sorted array
class ST {
private boolean less(KEY v, KEY w)
private boolean equals(KEY v, KEY w)
private ITEM[] st;
private int N = 0;
ST(int maxN)
{ return v.less(w); }
{ return v.equals(w); }
{ st = new ITEM[maxN]; }
int count()
{ return N; }
void insert(ITEM x) {
int i = N++; KEY v = x.key(); // “eager approach” for counting
while (i > 0 && less(v, st[i-1].key())) { st[i] = st[i-1]; i--; }
st[i] = x;
}
ITEM search(KEY key) {
int i = 0;
for ( ; i < N; i++) { if (!less(st[i].key(), key)) break; } // leave loop by BREAK
if (i == N)
return null; // ATTENTION: return either HERE
if (equals(key, st[i].key())) return st[i]; //
or HERE
return null;
//
or HERE
}
ITEM select(int k) { return st[k]; }
}
143
3.2.2 Sequenzielle Suche: unsorted list
class ST {
private class Node {
ITEM item; Node next;
Node(ITEM x, Node t) { item = x; next = t; }
}
private Node head;
private int N;
ST(int maxN)
{ head = null; N = 0; }
int count()
{ return N; }
void insert(ITEM x) { head = new Node(x, head); N++; }
private ITEM searchR(Node t, KEY key) {
if (t == null) return null;
if (equals(t.item.key(), key))
return t.item;
return searchR(t.next, key);
}
ITEM search(KEY key) { return searchR(head, key); }
public String toString() {
Node h = head; String s = "";
while (h != null)
{ s += h.item + "\n"; h = h.next; }
return s;
}
}
144
3.2.3 Binäre Suche: Ansatz und Implementierung
 Man kann die Suche in einer sortierten (geordneten) Liste drastisch reduzieren,
indem man nach dem „Teile-und-Herrsche“-Prinzip die zu durchsuchende
Menge halbiert, entscheidet in welcher Hälfte man weitersuchen muss und dort
rekursiv mit dem gleichen Ansatz weitersucht.
// recursive implementation of binary search
// l,r: left resp. right index of array to be searched
// v: key to be searched for
private ITEM searchR(int l, int r, KEY v) {
if (l > r) return null; // list to be searched is empty
int m = (l+r)/2;
// determine middle of list
// better: interpolate index within half (like
//
if (equals(v, st[m].key())) return st[m];
// element found
if (less (v, st[m].key())) return searchR(l, m-1, v); // element in left
else
return searchR(m+1, r, v); // element in right
}
ITEM search(KEY v) {
return searchR(0, N-1, v);
}
// just call recursive implementation of search
145
3.2.3 Binäre Suche: Verbesserung
 Statt „blind“ das mittlere Element zur Teilung zu verwenden, kann man den
„Abstand“ des gesuchten Elements von der linken (bzw. rechten) Seite
verwenden, um den Fundort schneller zu erreichen.
 statt m=(l+r)/2 verwendet man den Werteabstand des gesuchten Elements v zum
linken Element al geteilt durch den Werteabstand zwischen dem linken ar und
rechten Element al also (v - al) / (ar - al)
 Voraussetzung dafür ist, dass die Schlüsselwerte „einigermaßen“ gleichverteilt sind.
 Damit kommt man zu einer durchschnittlichen Zeitkomplexität von O (log(n) * log(n)),
was selbst für große n praktisch konstant ist
 Dieses Vorgehen wendet man z.B. bei der Suche einer Telefonnr. im
Telefonbuch an, denn dort sind die Werte - die Zeichenketten der gesuchten
Nachnamen – ziemlich gleichverteilt.
 mit O (log(n) * log(n)) brauchen Sie zur Suche im Gießener Telefonbuch „praktisch“
gleich lange, wie bei der Suche im Telefonbuch von Mexiko-City
(v - Wl) / (Wr - Wl) 
m = l + (v – a[l].key())*(r-1)
/
( a[r].key() – a[l].key() )
statt:
m = (l+r)/2
146
3.2.4 Vergleich
insert(max) insert () search(max) search ()
Indizierte Suche
IndexArray
O(1)
select
O(1)
O(1)
O(1)
O(1)
Sequenzielle Suche
SortedArray
O(n)
SortedList
O(n)
UnsortedArray
O(1)
UnsortedList
O(1)
O(n/2)
O(n/2)
O(1)
O(1)
O(n)
O(n)
O(n)
O(n)
O(n/2)
O(n/2)
O(n/2)
O(n/2)
O(1)
O(n)
O(n log(n))*
O(n log(n))*
Binäre Suche
SortedArray
O(n/2)
O(n)
O(log(n))
O(n)
* z.B. mit dem Heapsort (k-faches heapifien)
147
Übung 3.2
 Implementieren Sie unter Verwendung des Frameworks aus 3.1. eine
Symboltabelle als geordnete verkettete Liste und einen Client.
148
3.3
Binäre Suchbäume: Die Mutter aller Bäume
 Insert-Operation in geordneten Listen und search-Operationen in ungeordneten
Listen sind teuer, so dass sich Listen (bei einer Mischung dieser Operationen)
nicht zur Implementierung von Symboltabellen eignen.
Abhilfe schaffen binäre Suchbäume
1. Definition
2. Implementierung
3. Beispiel
4. Traversieren
5. nichtrekursives Einfügen
6. Selbstorganisierung
7. Rotation
8. Einfügen an der Wurzel (insert)
9. Auswählen (select)
10. Aufteilen (partition)
11. Entfernen (remove)
12. Verknüpfen (join)
13. BST ausgleichen
149
3.3.1 Definition
 Definition:
Ein binäre Suchbaum oder binary search trees (BST) ist ein Binärbaum, bei
dem mit jedem seiner internen Knoten ein Schlüssel verbunden ist.
Zusätzlich gilt, dass der Schlüssel in jedem beliebigen Knoten größer (oder
größer gleich, bei mehrfachen Schlüsseln) als alle Schlüssel seines linken
Teilbaums ist und kleiner (kleiner gleich) als alle Schlüssel im rechten Teilbaum
ist.
28
13
3
2
32
25
9
65
42
99
150
3.3.2 Implementierung
class ST {
private class Node { // one node of tree
ITEM item; Node l, r;
// key and pointers to two subtrees
Node(ITEM x) { item = x; } // constructing node with value x as key
}
private Node head;
ST(int maxN) { head = null; }
private Node insertR(Node h, ITEM x) {
if (h == null) return new Node(x); // if tree is empty, return new Node
if (less(x.key(), h.item.key())) h.l = insertR(h.l, x);
else
h.r = insertR(h.r, x);
return h;
}
void insert(ITEM x) { head = insertR(head, x); }
private ITEM searchR(Node h, KEY v) {
if (h == null) return null;
if (equals(v, h.item.key())) return h.item;
// key found -> return
if (less (v, h.item.key())) return searchR(h.l, v); // v smaller -> go left
else
return searchR(h.r, v); // v bigger -> go right
}
ITEM search(KEY key) { return searchR(head, key); } // just call recursion
}
151
3.3.3 Beispiel
  Nennen Sie mir 16 Zahlen zwischen 0 und 99
152
3.3.4 Traversieren
 Ein Traversieren in Inorder-Reihenfolge durch einen binären Suchbaum kann
zum Sortieren (und zum „lazy“ Zählen) verwendet werden:
// print values in sorted order
private String toStringR(Node h) {
if (h == null) return "";
String s = toStringR(h.l);
s += h.item.toString() + "\n";
s += toStringR(h.r);
return s;
}
public String toString() { return toStringR(head); }
// count nodes
private int countR(Node h) {
if (h == null) return 0;
return 1 + countR(h.l) + countR(h.r);
}
int count() { return countR(head); }
153
3.3.5 nichtrekursives Einfügen
 Das nichtrekursive Einfügen eines Elementes in einen BST entspricht einer
erfolglosen Suche nach diesem Element. Dann fügt man an die Position, wo die
Suche geendet hat das neue Element ein.
public void insert(ITEM x) {
KEY key = x.key();
// if tree is empty, just add node and return ...
if (head == null) { head = new Node(x); return; }
// ... otherwise
// first, initialize p and q as running elements
Node p = head, q = p;
// walk through tree down to leave – and remember this leave in q
while (q != null) {
if (less(key, q.item.key())) { p = q; q = q.l; }
else
{ p = q; q = q.r; }
}
// add new Node with key x either left or right to q
if (less(key, p.item.key()))
{ p.l = new Node(x); }
else
{ p.r = new Node(x); }
}
154
3.3.6 Selbstorganisierend
 In vielen Anwendungsfällen ist es geschickt, wenn neu eingefügte Elemente
auch schneller gefunden werden, also z.B. dadurch, dass sie nicht unten als
Blatt eingebaut werden, sondern oben in der Wurzel.
Wenn man dann beim suchen das Gefundene unten entfernt und oben wieder
einbaut, hat man eine selbstorganisierte Struktur, bei der häufig gesuchte Werte
schnell gefunden werden.
Fügt man ein kleineres Element als
Wurzel ein, so wird der linke
Teilbaum zum linken Teilbaum des
neuen Elementes und die alte Wurzel
(mit Teilbaum) wird zum rechten
Nachfolgeknoten des eingefügten
Elementes.
15
28
13
3
32
25
15
13
Problem: im alten linken
Teilbaum kann bereits ein
Element vorhanden sein,
welches größer als das neue
Element ist.
3
28
25
32
155
3.3.7 Rotation: Implementierung
 … um das Problem zu umgehen, fügt man das Element unten in den Baum ein
und lässt es, unter Einhaltung der Bedingungen eines binären Suchbaums,
nach oben zur Wurzel wandern … durch Rechts- bzw. Linksrotation
28
13
h
28
32
13
h.l
h
x
32
h.r
h
3
25
h.l
x
15
rotR
(25)
3
rotL
(13)
15
15
28
rotR
(28)
32
x.r
x
13
25
x.l
25
3
x.r
15
private Node rotR(Node h) { // return new (sub)root
Node x = h.l; h.l = x.r; x.r = h;
return x;
}
private Node rotL(Node h) { // return new (sub)root
Node x = h.r; h.r = x.l; x.l = h;
return x;
}
13
3
28
25
32
156
3.3.7 Rotation: Schema
h
rotate right
h.l
h
h.r
x
x
x.r
x.l
rotate left
private Node rotR(Node h) { // return new (sub)root
Node x = h.l; h.l = x.r; x.r = h;
return x;
}
private Node rotL(Node h) { // return new (sub)root
Node x = h.r; h.r = x.l; x.l = h;
return x;
}
157
3.3.8 Einfügen an der Wurzel
 … und das Ganze sieht dann eingebaut so aus:
// inserts item x in (sub)tree that is given by ist root h
private Node insertT(Node h, ITEM x) {
// if x has to be inserted in empty (sub)tree just create new node and
// return it
if (h == null) return new Node(x);
// ... otherwise
// recursively insert it in left or right subtree and rotate after insertion
if (less(x.key(), h.item.key())) { h.l = insertT(h.l, x); h = rotR(h); }
else
{ h.r = insertT(h.r, x); h = rotL(h); }
// return to higher recursion level
return h;
}
// just recursively call insertT
public void insert(ITEM x) { head = insertT(head, x); }
158
3.3.9 Auswählen (select)
 Rekursives Auswählen/Suchen des (k+1)-kleinsten Schlüssels
 0-basierter Index: z.B. k=4  5-kleinster Schlüssel
 der Algorithmus geht von einem „fleißigen“ (eager) Ansatz aus


die Anzahl der Elemente in den Unterbäumen (inkl. Wurzel) ist im jeweiligen Wurzelknoten
abgelegt (hier in: .N)
Nachteil: pro Knoten wird ein weiteres Element benötigt
private ITEM selectR(Node h, int k) {
if (h == null) return null; // no subtree -> return
int t = (h.l == null) ? 0 : h.l.N; // get nr. of elements in left subtree
// nr. of element
if (t > k) return
// nr. of element
if (t < k) return
// nr. of element
return h.item;
in left subtree larger than k -> proceed in left subtree
selectR(h.l, k);
in left subtree smaller than k -> proceed in rightsubtree
selectR(h.r, k-t-1);
in left subtree neither smaller nor larger -> element found
}
ITEM select(int k) { return selectR(head, k); }
// just call recursive select
3.3.9 Auswählen (select): Beispiel
5-kleinste
selectR(&7,4)
k=4, t=3
t<k: selectR(&8,0)
k=0, t=0
t=k: -> &8
3-kleinste:
selectR(&7,2)
k=2, t=3
t>k: selectR(&5,2)
k=2, t=1
t<k: selectR(&6,0)
k=0,t=0
t=k: -> &6
7
.N = 3
5
4
.N = 1
8 .N = 2
6
.N = 1
7
.N = 3
4
.N = 1
.N = 6
5
6
.N = 1
9
.N = 1
.N = 6
8 .N = 2
9
.N = 1
160
3.3.10
Aufteilen (partition)
 Rekursives Aufteilen des Baumes, so dass das k-kleinste Element zur neuen
Wurzel wird.
 wie „select“ aber mit anschließender Rotation, so dass das k-kleinste Element zur
Wurzel rotiert wird
 Rotation nach rechts im linken Teilbaum, Rotation nach links im rechten Teilbaum
Node partR(Node h, int k) {
int t = (h.l == null) ? 0 : h.l.N; // get nr. of elements in left subtree
// nr. of element in left subtree larger than k -> proceed in left subtree
if (t > k) {
partR(h.l, k); // recursively search in left subtree
h = rotR(h);
// rotate right aftet recursive call
}
// nr. of element in left subtree smaller than k -> proceed in rightsubtree
if (t < k) {
partR(h.r, k-t-1); // recursively search in right subtree
h = rotL(h);
// rotate left after recursive call
}
// nr. of element in left subtree neither smaller nor larger -> element found
return h; // just return
}
3.3.11
Entfernen (remove): Vorgehen
 Entfernen der Wurzel in einem binären Suchbaum
1. Überprüfung, ob sich der Knoten im Baum befindet
2. Gefundener Knoten ist Wurzel eines Teilbaums
15
13
3
25
13
3. Entfernen dieser (Teilbaum-) Wurzel
3
32
28
25
4. Rotation des kleinsten Elementes des rechten
(Teil-) Teilbaums zur neuen (Teilbaum-) Wurzel
 das ist willkürlich: man kann auch das größte
Element des linken Teilbaums zur neuen Wurzel
machen. solche Bäume tendieren zu n Tiefe und
damit O(n) Suchaufwand.(statt: log(n) )
28
32
25
13
28
3
32
25
5. Anbinden des linken (Teil-) Teilbaumes an die neue
(Teilbaum-) Wurzel
13
3
28
32
3.3.11
Entfernen (remove): Implementierung
 Entfernt einen Knoten mit dem Wert v
 Durchläuft den Baum rekursiv, bis der gesuchte Knoten v als Wurzel eines
Teilbaumes gefunden wurde
 ersetzt den Knoten durch das Ergebnis der Zusammenfassung seiner zwei
Teilbäume - dabei macht er den kleinsten Knoten im rechten Teilbaum zur neuen
Wurzel und setzt dann dessen linke Verbindung auf den linken Teilbaum
private Node joinLR(Node a, Node b) { // see example on previous slide
if (b == null) return a; // no right subtree -> left subtree is new root
b = partR(b, 0);
// rotate smallest element in right subtree to root
b.l = a;
// link left subtree of new root
return b;
// return new root
}
private Node removeR(Node h, KEY v) {
if (h == null) return null;
KEY w = h.item.key();
if (less(v, w))
removeR(h.l, v);
// if smaller -> search in left subtree
if (less(w, v))
removeR(h.r, v);
// if larger -> search in right subtree
if (equals(v, w)) h = joinLR(h.l, h.r); // found -> delete it
return h;
}
void remove(KEY v) { removeR(head, v); } // removes node with key v
3.3.12
Verknüpfen (join): Vorgehen
15
 Man verknüpft zwei Teilbäume, indem
man
8
1. die Wurzel des ersten Teilbaums wird
in den zweiten Teilbaum „an der
Wurzel“ eingefügt (über „insertT“).



Dabei bleiben im ersten Teilbaum ein
linker und ein rechter Teil-Teil-Baum
übrig
Im zweiten Teilbaum ergeben sich
ebenfalls ein linker und ein rechter
Teil-Teil-Baum
Die jeweils linken Teil-Teil-Bäume
sind kleiner, die jeweils rechten TeilTeil-Bäume sind größer als die
eingefügte Wurzel
2. die jeweils linken Teil-Teil-Bäume
und die jeweils rechten Teil-TeilBäume werden (rekursiv) verknüpft
und links und rechts an die Wurzel
gehängt.
 Aufwand: O(n)
28
27
16
13
3
32
25
15
15
8
27
13
16
3
28
25
32
15
8
27
13
16
3
28
25
32
15
8
3
16
27
13
25
28
32
3.3.12
private
// if
if (b
if (a
Verknüpfen (join): Implementierung
Node joinR(Node a, Node b) {
one of the subtrees is empty, the other is the result
== null) return a; // second node is empty -> first node is result
== null) return b; // first node is empty -> second node is result
// insert root of first tree as root of second tree
insertT(b, a.item);
// recursively join left subtrees and link them as left subtree to root
b.l = joinR(a.l, b.l);
// recursively join right subtrees and link them as right subtree to root
b.r = joinR(a.r, b.r);
// return joined tree
return b;
}
// join tree b with own tree (refered by head)
public void join(ST b) { head = joinR(head, b.head); }
165
3.3.13
BST ausgleichen
 Zum Suchen in BSTs ist es wünschenswert, dass die BSTs möglichst flach
sind, d.h. dass alle Zweige eine max. Tiefe von ld n haben.
 Man kann dies dadurch erreichen, dass beim z.B. bei jedem Einfügen eine
Knotens der Median aller Knoten (über die partR-Methode) zur Wurzel gemacht
wird:
private Node balanceR(Node h) {
if ((h == null) || (h.N == 1)) return h; //nothing to balance
// get median of treee, assuming that no. of subnodes are strored as N in node
h = partR(h, h.N/2);
// recursively call balancedR of left and right subtree
h.l = balanceR(h.l);
h.r = balanceR(h.r);
return h;
}
// return root
166
3.4
Randomisierte BSTs
 Diese Struktur eines BST hängt sehr stark von der Reihenfolge des Einfügens
ab. Insbesondere können in pathologischen Fällen (z.B. sortierten
Eingabelfolge lineare Strukturen (statt Baumstrukturen) entstehen, was zu
linearen (statt logarithmischen) Zeitaufwänden beim Suchen führt.
Ein „guter“ BST ist also abhängig von der Annahme, dass die Eingabefolge
zufällige Werte besitzt.
Diese Zufälligkeit kann man auch im Algorithmus selbst erzeugen und hat es
dann mit Randomisierten BSTs zu tun.
1. Einfügen (insert)
2. Entfernen (remove)
3. Verknüpfen (join)
167
3.4.1 Einfügen (insert)
 Die Idee des Randomisierens ist es, egal wie zufällig die Reihenfolge der
vorgegebene Daten ist, die Knoten so einzufügen, dass jeder Knoten mit der
Wahrscheinlichkeit 1/N ein Wurzelknoten ist.
Damit wird jede Eingabefolge zur zufälligen Eingabefolge
private Node insertR(Node h, ITEM x) {
if (h == null) return new Node(x);
// each N.th node may be root of tree
if (Math.random()*h.N < 1.0) return insertT(h, x); // retrun already here
// otherwise: just insert smaller nodes in left, larger in right subtree
if (less(x.key(), h.item.key())) h.l = insertR(h.l, x);
else
h.r = insertR(h.r, x);
// increase no. of nodes in subtree and return
h.N++;
return h;
}
void insert(ITEM x) {
head = insertR(head, x);
}
168
3.4.2 Entfernen (remove)
 Auch beim Entfernen von Knoten kann das Gleichgewicht des Baumes gestört
werden. So ergibt sich mit der „normalen“ Entfernen (removeR)-Methode eines
BST eine durchschnittliche Baumtiefe von n.
Grund ist auch hier die willkürliche Festlegung, den kleinsten Knotens im
rechten Teilbaum zur neuen Wurzel zu machen. Auch diese Festlegung kann
man randomisieren.
private Node joinLR(Node a, Node b) {
if (a == null) return b;
if (b == null) return a;
// use either biggest node of left subtree or smallest node of right subtree
// with a probality of a.N/(a,N+b.N) or b.N/(a.N+b.N) resp.
if (Math.random()* (a.N + b.N) < a.N) { a.r = joinLR(a.r, b); return a; }
else
{ b.l = joinLR(a, b.l); return b; }
}
private Node removeR(Node h, KEY v) {
... // see removeR from previous slides
}
void remove(KEY v) { removeR(head, v); } // removes node with key v
169
3.4.3 Verknüpfen (join)
 Die Gleichgewicht von BST wird nicht nur durch das Einfügen einzelner
Elemente , sondern auch durch das Verknüpfen von zwei Bäumen (potentiell
negativ) verändert .
Das liegt daran, dass beim „normalen“ Verknüpfen mit der joinR-Methode eines
BSTs immer ein Knoten desselben Teilbaums zur neuen Wurzel wird. Auch das
kann man randomisieren.
public void join(ST b) {
int N = head.N;
// no of nodes in this tree
int M = b.count(); // no of nodes in tree b
// assuming a has N nodes, b
// probability that root has
// probability that root has
if (Math.random()*(N+M) < N)
else
has M nodes
to be taken from a is M/(N+M)
to be taken from b is N/(N+M)
head = joinR(head, b.head);
head = joinR(b.head, head);
}
170
3.5
Splay BSt
 Durch das Randomisieren kann die Wahrscheinlichkeit unausgeglichener
Bäume dramatisch herabgesetzt werden.
Man kann nun, durch eine einfache Modifikation des Einfügen-Operators die
Ausgeglichenheit von BSTs weiter erhöhen: Die Idee dabei ist es Rotationen
zum Ausgleich von Teilbäumen geschickt einzusetzen.
Das Resultat sind Splay-BSTs
1. Idee
2. Rotation
3. Einfügen
4. Eigenschaften
171
3.5.1 Idee
 Man betrachtet jeweils zwei aufeinanderfolgende Rotationen, also solche die
den Nach-Nachfolger zur Wurzel rotieren. Dabei gibt es vier Fälle:
1.
2.
3.
4.
Linksrotation gefolgt von Linksrotation
Rechtsrotation gefolgt von Rechtsrotation
Linksrotation gefolgt von Rechtsrotation
Rechtsrotation gefolgt von Linksrotation
15
28
28
13
32
3
28
15
13
28
15
25
 Diese vier Fälle lassen sich nochmals zu zwei grundsätzlich unterschiedlichen
Fällen zusammenfassen:
 Rotationen gleicher Orientierung: links/links, rechts/rechts
 Rotationen unterschiedlicher Orientierung: links/rechts, rechts/links
 Splay BST verwendet Rotationen mit gleicher Orientierung, um über die
spezielle Splay-Einfüge-Operation die beim Einfügen betrachtet Teilbäume
auszugleichen.
172
3.5.2 Rotationen
 Rotation erst nach links dann nach rechts um Knoten 15 zur Wurzel zu machen
 Rotation zweimal nach rechts um Knoten 13 zur Wurzel zu machen
28
13
3
rotL
(13)
28
32
15
15
13
25
13
3
rotR
(15)
13
3
28
25
32
3
28
32
25
32
25
28
15
rotR
(28)
15
13
3
rotR
(28)
13
32
15
3
28
15
25
32
25
173
3.5.2 Rotationen
 Erst untere Rotation, dann obere Rotation
 Erst obere Rotation, dann untere Rotation -> wird bei Splay-Einfügen verwendet
28
15
13
rotR
(15)
28
32
25
13
3
rotR
(28)
32
3
28
15
3
15
25
28
15
13
3
13
rotR
(28)
25
25
15
32
13
3
rotR
(15)
25
32
13
28
3
15
32
28
25
32
174
3.5.3 Einfügen (Splay-insert)
private Node splay(Node h, ITEM x) {
if (h == null) return new Node(x); // tree empty, return new node
// insert x into tree
if (less(x.key(), h.item.key())) {
// left subtree empty: add x left, rotate right and return
if (h.l == null) { h.l = new Node(x); return rotR(h); }
// case: left/left -> rotate right at root
if (less(x.key(), h.l.item.key())) { h.l.l = splay(h.l.l, x);
// case: left/right -> rotate left at left subtree-node
else
{ h.l.r = splay(h.l.r, x);
return rotR(h); // rotate right at root
} else {
// right subtree empty: add x right, rotate left and return
if (h.r == null) { h.r = new Node(x); return rotL(h); }
// case: right/right -> rotate left at root
if (less(h.r.item.key(), x.key())) { h.r.r = splay(h.r.r, x);
// case: right/left -> rotate right at right subtree-root
else
{ h.r.l = splay(h.r.l, x);
return rotL(h); // rotate left at root
}
}
h = rotR(h); }
h.l = rotL(h.l);}
h = rotL(h); }
h.r = rotR(h.r);}
175
3.5.4 Eigenschaften
 Um einen Splay-BST aus N Einfügeoperationen in einen anfangs leeren Baum
zu erstellen, sind O(n ld n) Vergleiche nötig.
 Das entspricht dem durchschnittlichen Fall bei einem normalen BST. Ist aber besser
als O(n2) bei einer pathologischen Eingabefolge für einen „normalen“ BST.
 Die Anzahl der benötigten Vergleiche für Suchen-Operationn nach M
Einfügeoperationen in einen binären Suchbaum mit N Elementen ist O
((N+M)lod(N+M)
 Eine Splay-Einfüge-Operation verkürzt die Pfadlänge aller beteiligten Knoten auf
die Hälfte.
 Man kann die Splay-Transformation, also das „Durchrotieren“ an die Wurzel mit
Splay-Rotationen auch beim Suchen anwenden.
 Dadurch organsiert sich der Baum ähnlich selbst wie selbstorganisierenden Binären
Suchbaum. Zusätzlich werden die éi der Rotation betroffenen „Nachbarelemente“
näher zur Wurzel gebracht, mithin also ebenfalls schneller gefunden.
 Splay-Bäume geben aber keine Leistungsgarantie für einzelne Operationen in
pathologischen Fällen, sondern eine amortisierte Leistungsgarantie.
 Selbst wenn einzelne Suchoperationen lineare Aufwand aufweisen, so ist garantiert,
dass andere Suchoperationen umso schneller sind.
Insgesamt ergibt sich ein Aufwand von O(n ld n)
176
3.6
Ror-Schwarz Bäume
 Trotz der wahrscheinlichen Leistungsgarantien bei randomisierten Suchbäumen
oder Splay-Suchbäumen besteht, wenn der Teufel es so möchte, die
Möglichkeit, dass einzelne Suchoperationen lineare Zeit benötigen.
Auch der Aufwand für Einfüge-Operationen können dabei linear werden.
Abhilfe schaffen TOP-DOWN-2-3-4 Bäume und die daraus abgeleiteten RotSchwarz-Bäume
1. 2-3-4 Bäume
2. Beispiel eines 2-3-4 Baumes
3. Einfügen in 2-3-4 Bäume
4. Eigenschaften von 2-3-4 Bäumen
5. 2-3-4 Bäume als Rot-Schwarz Bäume
6. Teilen von 4-Knoten in Rot-Schwarz Bäumen
7. Eigenschaften von Rot-Schwarz Bäumen
177
3.6.1 2-3-4 Bäume
 Ein 2-3-4-Suchbaum ist ein Baum, der entweder leer ist oder aus drei
Knotentypen besteht:
1. 2-Knoten: Ein Knoten mit einem Schlüssel,


einer linken Verbindung zu einem Teilbaum mit kleineren Schlüsseln
und einer rechten Verbindung zu einem Teilbaum mit größeren Schlüsseln.
2. 3-Knoten: Ein Knoten mit zwei Schlüsseln



einer linken Verbindung zu einem Teilbaum mit kleineren Schlüsseln,
einer mittleren Verbindung mit Schlüsseln zwischen den zwei Schlüsseln des Knotens
und einer rechten Verbindung mit größeren Schlüssel
3. 4-Knoten: Ein Knoten mit vdrei Schlüsseln




einer ersten (linken) Verbindung zu einem Teilbaum mit kleineren Schlüsseln,
einer zweiten Verbindung zu einem Teilbaum mit Schlüsselwerten zwischen dem ersten
und zweiten Schlüsselwert
einer dritten Verbindung zu einem Teilbaum mit Schlüsselwerten zwischen dem zweiten
und dritten Schlüsselwert
und einer vierten (rechten) Verbindung mit größeren Schlüsseln
 Ein ausgeglichener 2-3-4 Suchbaum ist ein 2-3-4 Suchbaum, bei dem Alle
Verbindungen zu leeren Bäumen mit dem gleichen Abstand von der Wurzel
führen.
178
3.6.2 Beispiel eines 2-3-4 Baumes
 ein ausgeglichener 2-3-4 Suchbaum mit 12 Knoten
 ein 4-Knoten: (15,21,25)
 zwei 3-Knoten, (28,30) bzw. (18,20)
 neun 2-Knoten: (3), (13), (14), (17), (23), (24), (26), (29) , (32)
 Ein 2-3-4 Suchbaum bestehend aus ausschließlich 2-Knoten entspricht einem
binären Suchbaum
15,21,25
x<15
15<x<21
13
17
25<x
21<x<25
23
28,30
x<28
3
14
18,20
24
26
28<x<30
30<x
29
32
179
3.6.3 Einfügen in 2-3-4 Bäume
 Beispiel:
Einfügen von 7,10 und 5 in einen 2-3-4 Suchbaum
 Um einen Schlüssel in einen ausgeglichenen 2-3-4 Suchbaum einzufügen,
sucht man zunächst das Element, an das der einzufügende Schlüssel
anzuhängen wäre.
 Ist dieses Element ein 2 –oder 3-Knoten, so wird der Schlüssel einfach in den
Knoten als zusätzlicher Schlüssel eingeführt.
Aus einem 2-Knoten wird dabei ein 3-Knoten, aus einem 3-Knoten wird ein 4-Knoten.
 Ist dieses Element ein 4-Knoten so teilt man den 4-Knoten, indem man den mittleren
Schlüssel in den Vorgänger zieht und den Knoten anschließend in zwei 2-Knoten
zerteilt. Diese bieten dann Platz für den einzufügenden Schlüssel
Diese Operationen bewahren den ausgeglichenen Zustand.
13
3
13
14
3, 7
13
14
3, 7,10
7,13
14
3, 5
10
14
180
3.6.3 Einfügen in 2-3-4 Bäume
 Was ist aber, wenn der Vorgänger eines 4-Knotens selbst ein 4-Knoten ist
13,17,22
3
14
+ (7) + (10) + (5)
 lazy approach: Man könnte diesen Vorgänger selbst teilen, wobei allerdings
auch dessen Vorgänger wieder ein 4-Knoten sein könnte. Das könnte sich bis
nach ganz oben zur Wurzel fortsetzen.
 eager approach: Im Mittel schneller ist es sicherzustellen, dass der Suchpfad
nicht in einem 4-Knoten endet, dass also kein Blatt ein 4-Knoten ist. Dazu teilt
man jeden 4-Knoten auf dem man auf dem Weg durch den Baum nach unten
begegnet..Dabei wird …
 …. aus einem 2-Knoten als Vorgänger eines 4-Knotens ein 3-Knoten mit zwei 2Knoten als Nachfolger
 … aus einem 3-Knoten als Vorgänger eines 4-Knotens ein 4-Knoten mit zwei 2Knoten als Nachfolger
 ist die Wurzel selbst 4-Knoten, so wird dieser
in einen 2-Knoten mit zwei 2-Knoten als
13,17,22
Nachfolger aufgeteilt.
17
13
22
181
3.6.4 Eigenschaften von 2-3-4 Bäumen
 Der Baum wird jeweils an der Wurzel nach oben erweitert.
Daher nennt man 2-3-4 Bäume auch TOP-DOWN 2-3-4 Bäume
 Mit dem eager-approach wird jeder 4-Knoten aufgelöst
 beim Einfügen eines Schlüssels kann ein 4-Knoten entstehen, der so lange „nach
oben“ aufgelöst wird bis „darüber“ kein 3-Knoten mehr ist..
 Aufwand:
 Einfügeoperationen in 2-3-4 Bäumen mit N Knoten benötigen im ungünstigsten Fall
weniger als ld N+1 Knotenteilungen und „scheinen“ im Durchschnitt weniger als eine
Knotenteilung zu erfordern.
 Suchoperationen in 2-3-4 Bäumen mit N Knoten besuchen höchstens ld N+1 Knoten
 Die Implementierung von 2-3-4 Bäumen ist umständlich und erzeugt einen
gewissen Overhead („den dummen Faktor C) bezüglich z.B. Splay-Bäumen .
… und das nur, um den sehr seltenen Worst-Case eines linearen Such- bzw.
Einfüge-Aufwandes zu vermeiden.
182
3.6.5 2-3-4 Bäume als Rot-Schwarz-Bäume
 Da 2-3-4 Bäume umständlich zu implementieren sind, wurde eine andere
gleichwertige Abstraktionen entwickelt: die Rot-Schwarz-Bäume.
 Statt Binärbäume (mit ausschließlich 2-Knoten) um 3- bzw. 4-Knoten zu
erweitern, verteilt man die Schlüssel von 3- und 4-Knoten auf mehrere Knoten
und kennzeichnet deren „Zusammenhang“ durch besondere Verbindungen: rote
Verbindungen (die man in einem Flag kennzeichnet).
 So lassen sich 3- und 4-Knoten wie folgt darstellen:
es ist also insb. nicht möglich, dass zwei rote Verbindungen direkt nacheinander
in einem Pfad auftreten.
30
28
30<x
28,30
x<28
30<x
28
x<28
28<x<30
28<x<30
x<28
oder
30
28<x<30
30<x
21
x<15
15,21,25
25<x
15
25
25<x
x<15
15<x<21
21<x<25
15<x<21 21<x<25
183
3.6.6 Teilen von 4-Knoten in Rot-Schwarz-Bäumen
!
!
!
184
3.6.7 Implementierung von Rot-Schwarz Bäumen
private static final boolean R = true;
private static final boolean B = false;
private boolean red(Node x) { if (x == null) return false; return x.cbit; }
private Node insertR(Node h, ITEM x, boolean sw) {
if (h == null) { return new Node(x, R); }
if (red(h.l) && red(h.r)) { h.cbit = R; h.l.cbit = B; h.r.cbit = B; }
if (less(x.key(), h.item.key())) {
h.l = insertR(h.l, x, false);
if (red(h)
&& red(h.l)
&& sw) { h = rotR(h); }
if (red(h.l) && red(h.l.l)
) { h = rotR(h); h.cbit = B; h.r.cbit = R; }
} else {
h.r = insertR(h.r, x, true);
if (red(h)
&& red(h.r)
&& !sw) { h = rotL(h); }
if (red(h.r) && red(h.r.r)
) { h = rotL(h); h.cbit = B; h.l.cbit = R;
}
}
return h;
}
void insert(ITEM x)
{ head = insertR(head, x, B); head.cbit = B; }
185
3.6.8 Eigenschaften von Rot-Schwarz Bäumen
 Das Suchen in Rot-Schwarz-Bäumen betrachtet die Färbung von Verbindungen
nicht und funktioniert genau wie bei Binärbäumen.
 eine Suche in einem Rot-Schwarz Baum mit N Knoten benötigt weniger als 2 ld N +
2 Vergleiche, also O(log n).
Dieser Aufwand gilt auch im worst case, denn der Baum ist immer ausgeglichen
 eine Suche in einem Rot-Schwarz Baum mit N Knoten, der aus zufälligen Schlüsseln
aufgebaut ist, benötigt im Durchschnitt ungefähr 1,002 ld N Vergleiche.
… ist also „ziemlich“ Optimal
 Das Einfügen in Rot-Schwarz-Bäumen erzeugt bezügl. dem Einfügen in
Binärbäumen nur einen geringen Overhead, denn wir müssen nur 4-Knotzen
auflösen. Die treten aber nicht oft auf und werden zudem sofort aufgelöst.
 Neben Rot-Schwarz Bäumen gibt es noch andere Ansätze, Ausgeglicheheit zu
garanrtieren. Insbesondere die AVL-Bäume (1962: Adelson-Velsky und Landis)
wurden vor den Rot-Schwarz-Bäumen entdeckt, sind aber etwas
Speicherplatzintensiver.
 Daher wird in Java als Standard „TreeMap“ (in java.util) als Rot-Schwarz-Baum
implementiert
186
Herunterladen