Algorithmen & Datenstrukturen

162
1 Algorithmen & Datenstrukturen Prof. Dr. Peter Kneisel Sommersemester 2009

description

Sommersemester 2009. Algorithmen & Datenstrukturen. Prof. Dr. Peter Kneisel. Didaktik: Was will diese Vorlesung. Datenstrukturen und Algorithmen sind die Grundlagen jeder Software-Entwicklung. Datenstrukturen modellieren die (statischen) Strukturen der zu behandelnden Systeme, - PowerPoint PPT Presentation

Transcript of Algorithmen & Datenstrukturen

Page 1: Algorithmen & Datenstrukturen

1

Algorithmen &Datenstrukturen

Prof. Dr. Peter Kneisel

Sommersemester 2009

Page 2: Algorithmen & Datenstrukturen

Didaktik: Was will diese Vorlesung

Datenstrukturen und Algorithmen sind die Grundlagen jeder Software-Entwicklung.

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

Page 3: Algorithmen & Datenstrukturen

3

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 ;-)

Page 4: Algorithmen & Datenstrukturen

4

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.

Page 5: Algorithmen & Datenstrukturen

5

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)

Page 6: Algorithmen & Datenstrukturen

6

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.

Inhalt1. Abstrakte Datentypen (ADTs)2. Suchen: Grundlagen, Algorithmus, Analyse3. Sortieren Grundlagen, Algorithmus, Analyse

Page 7: Algorithmen & Datenstrukturen

7

Überblick und Einordnung

Elemente

OOP

Information

RAGDI

Strukturierung

Sta

tik,

Str

uktu

r

Codes

Zeichen

Zahlen

Datenstrukturen

Verifikation

Komplexität

A&D

Dyn

amik

, A

lgor

ithm

ik

ADTsSuchen

Sortieren

PIS

Page 8: Algorithmen & Datenstrukturen

8

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)

Inhalt1. Wiederholung

2. Was sind ADTs

3. Stacks (Kellerspeicher, Stapel)

4. Queues (Warteschlangen)

5. Einfach verkettete Listen

6. Zweifach verkettete Listen

7. Hashlisten

8. Bäume

9. Graphen

10. Frameworks

Page 9: Algorithmen & Datenstrukturen

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

Page 10: Algorithmen & Datenstrukturen

10

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.

Page 11: Algorithmen & Datenstrukturen

11

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 …

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- bei OO-Sprachen - „Instanz“ genannt

Beispiel:

Imaginäre Zahlen

Page 12: Algorithmen & Datenstrukturen

12

1.1.3 Klassifikation der Datentypen

Datentypen

IdealisierteAbstrakteKonkrete

Einfache StrukturiertePointer(Zeiger)

Boolean(Wahrheitswert)

Integer(Ganzzahl)

Char (Zeichen)

Enumeration (Aufzählung)

Ordinale Real(Fließkomma)

Array (Feld)

Record (Verbund)

Union(Variantenverb.)

...

...

Page 13: Algorithmen & Datenstrukturen

13

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

2 12 15

79

6 61

13

insert (Element)

delete (Element)

search (Element)

Page 14: Algorithmen & Datenstrukturen

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. Grundsätze

2. Algebren

3. Signaturen

4. Axiome

5. Beispiel einer ADT-Schnittstelle

6. Anwendung: Tabelle

14

Page 15: Algorithmen & Datenstrukturen

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

Page 16: Algorithmen & Datenstrukturen

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

Page 17: Algorithmen & Datenstrukturen

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: Konstantesuccessor : 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

Page 18: Algorithmen & Datenstrukturen

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

Page 19: Algorithmen & Datenstrukturen

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

head : list T // Kopf der Liste tail : list list // Liste ohne Kopf length : list integer // Anzahl Listenelementeaxioms: l : list, x : T head ( x : l ) = x

tail ( x : l ) = l

lenght ( [] ) = 0 // [] ist leere Liste

length ( x : l ) = successor ( length (l) )

19

Page 20: Algorithmen & Datenstrukturen

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.

20

key1 info1

key2 info2

keyn infon

Anmerkung:Da die Indizierung von Listen invielen Programmiersprachen mit„0“ beginnt, man aber in der realenWelt mit „1“ zu zählen beginnt, wirddas „0“-te Element oft als Dummy-Element mit einem Dummy-Wertversehen und ignoriert.

Page 21: Algorithmen & Datenstrukturen

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

Page 22: Algorithmen & Datenstrukturen

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

Page 23: Algorithmen & Datenstrukturen

1.3.2 Implementierung eines Stacks

public class ArrayStack implements Stack {

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];

}

// Abfrage auf leeren Stack

public boolean isEmpty() {

return num == 0;

}

public void push(Object obj) throws StackException {

if (num == elements.length)

// KapazitŠt erschöpft

throw new StackException();

elements[num++] = obj;

}

public Object pop() throws StackException {

if (isEmpty()) // Stack ist leer

throw new StackException();

Object o = elements[--num];

elements[num] = null;

return o;

}

public Object top() throws StackException {

if (isEmpty()) // Stack ist leer

throw new StackException();

return elements[num - 1];

}

}

23

Page 24: Algorithmen & Datenstrukturen

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

Page 25: Algorithmen & Datenstrukturen

1.4. Queues

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.

1. Spezifikation

2. Implementierung einer Queue

3. Die Java-Klasse „queue“

25

Page 26: Algorithmen & Datenstrukturen

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

Page 27: Algorithmen & Datenstrukturen

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

// Queue mit vorgegebener Länge erzeugen

public ArrayQueue (int size) {

elements = new Object[size];

}

public boolean isEmpty () {

return l == u;

}

// Zeige das lower Element

public Object front () throws QueueException {

if (isEmpty ())

throw new QueueException ();

return elements[l];

}

// 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.

}

// 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;

}

}27

Page 28: Algorithmen & Datenstrukturen

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

Page 29: Algorithmen & Datenstrukturen

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. class

2. main

3. Methoden

4. Implementierung als Liste

29

Page 30: Algorithmen & Datenstrukturen

1.5.1 class

public class List {

static class Node {

Object obj;

Node next;

public Node(Object o, Node n) { obj = o; next = n; }

public Node() { obj = null; next = null; }

public void setElement(Object o) { obj = o; }

public Object getElement() { return obj; }

public void setNext(Node n) { next = n; }

public Node getNext() { return next; }

}

private Node head = null;

public List() {}

public void addFirst(Object o) {}

public void addLast(Object o) {}

public Object getFirst() throws ListEmptyException {}

public Object getLast() throws ListEmptyException {}

public Object removeFirst() throws ListEmptyException {}

public Object removeLast() throws ListEmptyException {}

public int size() {}

public boolean isEmpty() {}

} 30

Page 31: Algorithmen & Datenstrukturen

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

Page 32: Algorithmen & Datenstrukturen

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 void addLast(Object o) { Node l = head; while (l.getNext() != null) l = l.getNext(); Node n = new Node(o, null); l.setNext(n);}

public Object removeFirst() throws ListEmptyException { if (isEmpty()) throw new ListEmptyException(); Object o = head.getNext().getElement(); head.setNext(head.getNext().getNext()); return o;}

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

Page 33: Algorithmen & Datenstrukturen

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

Page 34: Algorithmen & Datenstrukturen

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. class

2. iterator

3. main

4. Methoden

34

Page 35: Algorithmen & Datenstrukturen

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

Page 36: Algorithmen & Datenstrukturen

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

Page 37: Algorithmen & Datenstrukturen

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

Page 38: Algorithmen & Datenstrukturen

1.6.4 Methoden

public DList () { head = new Node (); // dieser Knoten existiert immer, auch bei leerer Liste tail = new Node (); // dieser Knoten existiert immer, auch bei leerer Liste head.setNext(tail); // head und tail werden initial miteinander verlinkt tail.setPrevious(head); tail.setNext(tail); // 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 getFirst () throws ListEmptyException { if (isEmpty ()) throw new ListEmptyException (); return head.getNext ().getElement ();}

public void addLast (Object o) { Node l = tail.getPrevious (); Node n = new Node (o, l, tail); l.setNext (n); tail.setPrevious (n);}

public Object removeFirst () throws ListEmptyException {

if (isEmpty ()) throw new ListEmptyException (); Object o = head.getNext ().getElement (); head.setNext (head.getNext ().getNext ()); head.getNext ().setPrevious (head); return o;}

public Object removeLast () throws ListEmptyException {

if (isEmpty ()) throw new ListEmptyException (); Node n = tail.getPrevious (); n.getPrevious ().setNext (tail); tail.setPrevious (n.getPrevious ()); return n.getElement ();}

38

Page 39: Algorithmen & Datenstrukturen

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. Grundprinzip des Hashens

2. Die Hashfunktion

3. Behandlung von Kollisionen

4. Implementierung einer Hashliste

39

Page 40: Algorithmen & Datenstrukturen

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

Page 41: Algorithmen & Datenstrukturen

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 7Index 0 1 2 3 4 5 6Element 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

Page 42: Algorithmen & Datenstrukturen

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

Page 43: Algorithmen & Datenstrukturen

1.7.4 Implementierung einer Hashliste

43

public class HashTable {

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;

}

// 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;

}

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"));

}

}

Page 44: Algorithmen & Datenstrukturen

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. Definitionen & Beispiele

2. Spezifikation

3. Datentypen

4. Traversierung

5. Weitere Bäume

44

Page 45: Algorithmen & Datenstrukturen

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

45

… nich‘ so praktisch

… wie sich derInformatiker einenBaum vorstellt

Page 46: Algorithmen & Datenstrukturen

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 einesBlattes + 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

46

Tiefe 0

Tiefe 1

Tiefe 2

Tiefe 3

((1+2)*3)+(2+5)

Page 47: Algorithmen & Datenstrukturen

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

Page 48: Algorithmen & Datenstrukturen

1.8.3 Binäre Bäume: Datentypen

48

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; }

}

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

Page 49: Algorithmen & Datenstrukturen

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

49

Inorder: 1 + 2 * 3 + 2 + 5Preoder: + * + 1 2 3 + 2 5Postorder: 1 2 + 3 * 2 5 + + ( UPN)Levelorder: + * + + 3 2 5 1 2

Page 50: Algorithmen & Datenstrukturen

1.8.4 Binäre Bäume: Traversierung

50

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);

Page 51: Algorithmen & Datenstrukturen

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 ASCII-Werte) 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 251

Page 52: Algorithmen & Datenstrukturen

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

Page 53: Algorithmen & Datenstrukturen

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)

53

1

4

6

5

2

3

1

4

6

5

2

3

1

4 5

2

3

6

2575

2422

20

6430

Page 54: Algorithmen & Datenstrukturen

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

54

1

4 5

2

3

6

2575

2422

20

6430

321 2 3 4 5 6

2

4

2

5

5

6

2 3

Page 55: Algorithmen & Datenstrukturen

1.9.3 Implementierung eines Graphen

55

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) { ... }

}

Page 56: Algorithmen & Datenstrukturen

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

Page 57: Algorithmen & Datenstrukturen

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

Page 58: Algorithmen & Datenstrukturen

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

Stackferweiterert Vector zu eimem LIFO-Stack.

LinkedListDoppelt verkettete Liste, kann auch als Queue (Warteschlange) eingesetzt werden.

HashMapHashliste.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.

TreeSetBalancierter 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

Page 59: Algorithmen & Datenstrukturen

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

Page 60: Algorithmen & Datenstrukturen

Übung 1

1. Implementieren Sie einen stack1. 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 ein1. Traversieren Sie den Baum Inorder, Preorder, Postorder und inline, Geben Sie die

Elemente dabei jeweils aus.

60

Page 61: Algorithmen & Datenstrukturen

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

Page 62: Algorithmen & Datenstrukturen

62

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.

Inhalt1. Wie „gut“ ist ein Algorithmus

2. Die O-Notation

3. Häufige O-Ausdrücke

4. Einige Regeln

5. Quantitatives

6. Platzbedarf

Page 63: Algorithmen & Datenstrukturen

63

2.1.1 Qualität eines Algorithmus

Die Abarbeitung eines Algorithmus benötigt „Ressourcen“, vor allem: Zeit Laufzeit des Algorithmus Platz 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 ...

Page 64: Algorithmen & Datenstrukturen

64

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) cf(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), …

Laufzeit

Problemgröße

g(n)

f(n)

cf(n)

no

g(n) cf(n), für alle n no

Page 65: Algorithmen & Datenstrukturen

65

2.1.3 Die O-Notation: Beispiel

Beispiel: Bei der Analyse eines Algorith-

mus hat sich herausgestellt, dass die Laufzeit: g(n) = 3n2 + 7n – 1ist.

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

Allgemein: 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

Laufzeit

Problemgröße

g(n)

f(n)=n2

cf(n) = 4 n2

no

g(n) cf(n), für fast alle n no

Page 66: Algorithmen & Datenstrukturen

66

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 !

Page 67: Algorithmen & Datenstrukturen

67

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

n cAn2 cB1,5n

1 103 1,5 10-3

10 105 1,8 10-2

20 4 105 3,350 2,5 106 6,4 105

100 107 4,1 1014

Bis hier ist B besser als A

Page 68: Algorithmen & Datenstrukturen

Übung 2.1:

1. Erstellen Sie ein Graphik (mit Excel) in der Sie die Laufzeiten der wichtigsten Komplexitätsklassen “sinnvoll“ darstellen.

68

Page 69: Algorithmen & Datenstrukturen

2.2.. Grundlagen

… bevor es losgeht:

1. Definitionen

2. Beispiele

3. Framework für Implementierungen

69

Page 70: Algorithmen & Datenstrukturen

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

Page 71: Algorithmen & Datenstrukturen

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

Page 72: Algorithmen & Datenstrukturen

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

Page 73: Algorithmen & Datenstrukturen

Übung 2.2:

1. Implementieren Sie dieses Framework und wenden Sie es in einem einfachen Fall an.

73

Page 74: Algorithmen & Datenstrukturen

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. Selection Sort (Sortieren durch Auswählen)

2. Insertion Sort (Sortieren durch Einfügen)

3. Shellsort

4. Bubblesort

5. Vergleich

sorting-algorithms.com

74

Page 75: Algorithmen & Datenstrukturen

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 51 3 6 4 3 9 8 3 7 5 5 1 3 3 4 6 9 8 3 7 5 51 3 3 3 6 9 8 4 7 5 51 3 3 3 4 9 8 6 7 5 51 3 3 3 4 5 8 6 7 9 5 1 3 3 3 4 5 5 6 7 9 81 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

Page 76: Algorithmen & Datenstrukturen

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

Page 77: Algorithmen & Datenstrukturen

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

Page 78: Algorithmen & Datenstrukturen

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

Page 79: Algorithmen & Datenstrukturen

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

Page 80: Algorithmen & Datenstrukturen

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 53 6 3 4 3 9 8 1 7 5 5 3 6 3 4 3 9 8 1 7 5 53 3 6 4 3 9 8 1 7 5 53 3 4 6 3 9 8 1 7 5 53 3 3 4 6 9 8 1 7 5 53 3 3 4 6 9 8 1 7 5 53 3 3 4 6 8 9 1 7 5 5 // swapping of „1“ is exhausting1 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

Page 81: Algorithmen & Datenstrukturen

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

Page 82: Algorithmen & Datenstrukturen

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.

82

// sort array “ITEM[]” between indexes l and rstatic 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; } }

Page 83: Algorithmen & Datenstrukturen

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

Page 84: Algorithmen & Datenstrukturen

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 4h = 4 h = 3 3 6 3 4 3 5 3 1 3 5 3 1 2 33 9 8 1 3 6 5 2 1 3 6 3 3 47 5 5 2 7 9 8 4 5 2 7 5 5 6

9 8 4 9 8 7mit h=1 wird hier abschließend nochmals Insertion-sortiert1 2 3 3 3 4 5 5 6 9 8 7 1 2 3 3 3 4 5 5 7 8 9

84

Page 85: Algorithmen & Datenstrukturen

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

Page 86: Algorithmen & Datenstrukturen

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

Page 87: Algorithmen & Datenstrukturen

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 3 9 8 1 7 5 5 // stoppt bei Gleichheit

1 3 6 3 4 3 9 8 5 7 51 3 6 3 3 4 5 9 8 5 71 3 3 6 3 4 5 5 9 8 71 3 3 3 6 4 5 5 7 9 81 3 3 3 4 6 5 5 7 8 91 3 3 3 4 5 6 5 7 8 91 3 3 3 4 5 5 6 7 8 91 3 3 3 4 5 5 6 7 8 91 3 3 3 4 5 5 6 7 8 91 3 3 3 4 5 5 6 7 8 91 3 3 3 4 5 5 6 7 8 9

87

Page 88: Algorithmen & Datenstrukturen

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 6 3 4 3 9 8 1 7 5 5 // stoppt bei Gleichheit1 3 6 3 4 3 9 8 5 7 51 3 3 6 3 4 5 9 8 5 7

1 3 3 3 6 4 5 5 9 8 7

1 3 3 3 4 6 5 5 7 9 81 3 3 3 4 5 6 5 7 8 9

1 3 3 3 4 5 5 6 7 8 9 // ab hier kein Bubbeln mehr -> stoppen

1 3 3 3 4 5 5 6 7 8 9

1 3 3 3 4 5 5 6 7 8 9

1 3 3 3 4 5 5 6 7 8 9

1 3 3 3 4 5 5 6 7 8 9

88

Page 89: Algorithmen & Datenstrukturen

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

Page 90: Algorithmen & Datenstrukturen

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 00 1 2 3 4 5 6 7 8 9 10 11 12 13 14 (Index)00 30 3 30 0 3 30 0 1 3 30 0 1 1 3 30 0 0 1 1 3 30 0 0 1 1 3 3 30 0 0 0 1 1 3 3 3 0 0 0 0 1 1 2 3 3 3…

90

„Anzahlliste“#0=6,#1=4,#2=2,#3=3, „Anzahlsummenliste“#<0=0,#<1=6#<2=10,#<3=12

Page 91: Algorithmen & Datenstrukturen

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

Page 92: Algorithmen & Datenstrukturen

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

Page 93: Algorithmen & Datenstrukturen

2.3.5 Vergleich

Selection Insertion Shell Bubble Indexsort

min O(n2) O(n) O(n1,2) O(n) O(n)

average O(n2) O(n2) O(n1,2) O(n2) O(n)

max O(n2) O(n2) O(n1,2) O(n2) O(n)

Adaptiv nein ja ja ja nein

Platz (In-situ) O(1) O(1) O(1) O(1) O(n)

Stabil nein ja nein ja 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

Page 94: Algorithmen & Datenstrukturen

Übung 2.3:

1. Implementieren Sie einen einfachen Sortieralgorithmus verwenden Sie dabei Java-arrays aus java.util.array – aber zunächst nicht deren

sort-methodehash(IhrNachname) = Summe(Ascii(Buchstaben)) mod 5hash(IhrNachname) = 0 Selection, 1 Insertion, 2Shell, 3Bubbke, 4Index

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

Page 95: Algorithmen & Datenstrukturen

2.4. Fortgeschrittene Sortieralgorithmen

95

Page 96: Algorithmen & Datenstrukturen

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 4 3 55 6 7 8 9

1 2 3 4 5 6 7 8 9 10 11 12 (Index)1 2 3 3 4 3 5 5 6 7 8 9 Priority Queue (Heap) (not neccessarily completely sorted)

96

Page 97: Algorithmen & Datenstrukturen

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

Page 98: Algorithmen & Datenstrukturen

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

Page 99: Algorithmen & Datenstrukturen

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

Page 100: Algorithmen & Datenstrukturen

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

100

3 6 3 4 3 9 81 7 5 5 2

3 6 3 4 3 2 81 7 5 5 9

3 6 3 4 3 9 81 7 5 5 2

3 6 3 4 3 2 81 7 5 5 9

3 6 2 1 3 3 84 7 5 5 9

3 1 2 4 3 3 86 7 5 5 9

3 6 3 1 3 2 84 7 5 5 9

1 3 2 4 3 3 86 7 5 5 9

Page 101: Algorithmen & Datenstrukturen

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

Page 102: Algorithmen & Datenstrukturen

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

Page 103: Algorithmen & Datenstrukturen

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 23 6 // sorting 1-element lists 3 4 // sorting 1-element lists3 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 lists1 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 lists1 2 3 3 3 4 5 5 6 7 8 9 // final merging

103

Page 104: Algorithmen & Datenstrukturen

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) into

// 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 index // 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 index

// move bigger element of a or b into c and increment correspondent index c[k] = less(a[i], b[j]) ? a[i++] : b[j++];

}

}104

Page 105: Algorithmen & Datenstrukturen

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 41 3 4 7 5 2 -> 3 4 7 5 2 -> 3 4 7 5 -> 4 7 5 -> 7 5 -> 71 2 3 4 5 7

// merge two blocks (l to m and m to r) of array a into array aux 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]; // i at beginning

// reverse copy second block (m to r) to aux

for (j = m; j < r; j++) aux[r+m-j] = a[j+1]; // j at end

// iterate through elements and compare pairwise first and last element

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++]; }

}105

Page 106: Algorithmen & Datenstrukturen

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

Page 107: Algorithmen & Datenstrukturen

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

Page 108: Algorithmen & Datenstrukturen

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 23 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 lists3 3 4 6 // merging 2-element lists 1 3 8 9 // merging 2-element lists 2 5 5 7 // merging 2-element lists1 3 3 3 4 6 8 9 // merging 4-element lists1 2 3 3 3 4 5 5 6 7 8 9 // final merging

108

Page 109: Algorithmen & Datenstrukturen

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

Page 110: Algorithmen & Datenstrukturen

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

Page 111: Algorithmen & Datenstrukturen

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-Wege-Quicksort 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 In-situ-Algorithmus.

111

Page 112: Algorithmen & Datenstrukturen

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

Page 113: Algorithmen & Datenstrukturen

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 4 3 9 8 1 7 5 5 2 3 2 3 4 3 1 5 5 6 9 8 7 1 2 3 3 4 3 5 5 6 9 8 7 1 2 3 3 3 4 5 5 6 7 8 9 3 3 3 4 6 7 8 9

113

Page 114: Algorithmen & Datenstrukturen

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“

Page 115: Algorithmen & Datenstrukturen

Wir wählen mit v ein beliebiges Trennelement (Pivot-Element) .Also z.B. das Element ganz rechts

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.

Wir wiederholen das Ganze bis sich der linke und der rechte Laufindex kreuzen.

Dann muss nur noch das Pivot-Element mit dem linkesten Element des rechten Teils getauscht werden. Damit ist das Pivot-Element am richtigen Platz

2.4.3 Quicksort: Partitionierung

V

rl

kleiner oder gleich v größer oder gleich v V

rl i j

kleiner oder gleich v größer oder gleich v V

rl ij

kleiner oder gleich v größer oder gleich vV

rl ij

Page 116: Algorithmen & Datenstrukturen

2.4.3 Quicksort: Implementierung der Partitionierung

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 intervalstop 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;

}

kleiner oder gleich v größer oder gleich v V

rl i j

Page 117: Algorithmen & Datenstrukturen

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 ( i < j ) {

while (less(a[++i], v)) ;

while (less(v, a[--j]) && j>l); if (i < j) exch(a, i, j);

}

117

Page 118: Algorithmen & Datenstrukturen

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

Page 119: Algorithmen & Datenstrukturen

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 Gesamt-aufwand 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]/2v = (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

Page 120: Algorithmen & Datenstrukturen

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(); // get left and right margin from stack

if (r <= l) continue; // leave loop if list is empty

int i = partition(a, l, r); // 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

Page 121: Algorithmen & Datenstrukturen

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 r7 3 6 5 4 2 8 9 3 5exch(a, (l+r)/2, r-1);

l l-r/2 r-1 r7 3 6 5 3 2 8 9 4 5compExch(a, l, r-1);

l l-r/2 r-1 r4 3 6 5 3 2 8 9 7 5compExch(a, l, r);

l l-r/2 r-1 r4 3 6 5 3 2 8 9 7 5compExch(a, r-1, r);

l l-r/2 r-1 r4 3 6 5 3 2 8 9 5 7

partition(a, l+1, r-1) l+1 r-1 3 6 5 3 2 8 9 5

121

Page 122: Algorithmen & Datenstrukturen

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

Page 123: Algorithmen & Datenstrukturen

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.

2.4.3 Quicksort: 3-Wege Partitionierung

123

gleich v größer v V

rl i j

kleiner v gleichv

p q

Page 124: Algorithmen & Datenstrukturen

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 and reposition i and j

exch(a, i, r); j = i-1; i = i+1;

// new: move equal elements from edges to middle

for (k = l ; k <= p; k++,j--) exch(a, k, j);

for (k = r-1; k >= q; k--,i++) exch(a, k, i);

// recursively call quicksort

quicksort(a, l, j);

quicksort(a, i, r);

}124

Page 125: Algorithmen & Datenstrukturen

2.4.3 Quicksort: Quick-3 Beispiel (1. Rekursion)

i=p j=q

1 2 5 7 3 5 8 9 2 5 9 3 5

----> i j <- // a[i] !< v, a[j] !> v --> swap

1 2 3 7 3 5 8 9 2 5 9 5 5

p q <- // a[q] = v --> swap (although j=q)

1 2 3 7 3 5 8 9 2 5 9 5 5

-> i j <---- // a[i] !< v, a[j] !> v --> swap

1 2 3 5 3 5 8 9 2 7 9 5 5

-> p q // a[p] = v --> swap a[p] with a[i]

5 2 3 1 3 5 8 9 2 7 9 5 5

----> i j <- // a[i] !< v, a[j] !> v --> swap

5 2 3 1 3 2 8 9 5 7 9 5 5

p q <- // a[q] = v --> swap a[q] with a[j]

5 2 3 1 3 2 8 9 9 7 5 5 5

-> i j <------- // swap a[r] with a[i]

5 2 3 1 3 2 5 9 9 7 5 5 8

k=p j i q // swap a[k] with a[j]

2 2 3 1 3 5 5 9 9 7 5 5 8

(k) j i q k // swap a[k] with a[i]

2 2 3 1 3 5 5 5 9 7 5 9 8

j i q,k // swap a[k] with a[i]

2 2 3 1 3 5 5 5 5 7 9 9 8

l j i r // recursively call quicksort[l.j]/[i,r]

125

smaller to left,larger to right,equals to edges

equals at leftedge to middle

equals at rightedge to middle

sorting-elementto middle

Page 126: Algorithmen & Datenstrukturen

2.4.3 Quicksort: Diskussion

Eigenschaften Nicht Stabil Quick-3: Adaptiv (falls Anzahl gleicher Schlüssel konstant)

Aufwand min: O(n) bei 3-Quick mit konstanter Anzahl von Schlüsseln, sonst O (n log n) average: O(n log n) max: O(n2) O(log n) 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

Page 127: Algorithmen & Datenstrukturen

2.4.4 Vergleich

Heap Merge Quick Quick-3

min O(n log n) O(n log n) O(n log n) O(n)

average O(n log n) O(n log n) O(n log n) O(n log n)

max O(n log n) O(n log n) O(n2) O(n2)

Adaptiv nein nein nein ja

Platz (In-situ) O(1) O(n) O(log(n)) O(log(n))

Stabil nein ja nein 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

Page 128: Algorithmen & Datenstrukturen

Übung 2.4:

1. Implementieren Sie einen fortgeschrittenen Sortieralgorithmus verwenden Sie dabei Java-arrays aus java.util.array – aber zunächst nicht deren

sort-methodehash(IhrNachname) = Summe(Ascii(Buchstaben)) mod 3hash(IhrNachname) = 0 Mergesort, 1 Heapsort, 2Quicksort

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

Page 129: Algorithmen & Datenstrukturen

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

Page 130: Algorithmen & Datenstrukturen

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

Page 131: Algorithmen & Datenstrukturen

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-Wege-Quicksort 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 In-situ-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

Page 132: Algorithmen & Datenstrukturen

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 Geschwindigkeit1. Indexsort: bei vorhandener und guter Hashfunktion

2. Quicksort

3. Merge- und Heapsort,

4. Shellsort 132

Page 133: Algorithmen & Datenstrukturen

Ü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

Page 134: Algorithmen & Datenstrukturen

3. Suchen

Sedgewick: „Das Abrufen bestimmter Informationseinheiten aus größeren vorher gespeicherten Datenbeständen ist eine fundamentale Operation, die man als Suchen bezeichnet“.

1. Symboltabellen

2. Elementare Suchalgorithmen

3. Fortgeschrittene Sortieralgorithmen

4. Zusammenfassung

134

Page 135: Algorithmen & Datenstrukturen

3.1 Symboltabellen

Ä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. ADT Symboltabelle

3. Implementierung

4. Zusammenfassung

135

Page 136: Algorithmen & Datenstrukturen

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: Entfernen eines gegebenen Elements select: Auswählen des k-größten Elements (bei gegebem ganzzahligen k) sort: Sortieren der Symboltabelle join: Verknüpfen zweier Symboltabellen

136

Page 137: Algorithmen & Datenstrukturen

3.1.2 Spezifikation

type: stable(T) // T ist die Wertemenge der Elemente

import: boolean

operators:

empty : stack // 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)) = s

search (insert (s,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

Page 138: Algorithmen & Datenstrukturen

3.1.3 Framework (Element)

// interface to Elements

// 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()

}

// example implementation of element

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

Page 139: Algorithmen & Datenstrukturen

3.1.3 Framework (Symboltable)

// interface to 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()

}

// example of a client using symbol list

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

Page 140: Algorithmen & Datenstrukturen

3.2 Elementare Suchalgorithmen

Die im vorhergehenden Unterkapitel beschriebenen Symboltabellen können un auf unterschiedliche Weise umgesetzt werden. Sie implementieren damit auch unterschiedliche Ansätze des Sortierens.In diesem Unterkapitel werden die grundlegenden Verfahren erläutert

1. Schlüsselindizierende Suche

2. Sequenzielle Suche

3. Binäre Suche

4. Vergleich

140

Page 141: Algorithmen & Datenstrukturen

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] != null) N++; }

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] != null && k-- == 0) return st[i]; }

return null;

}

public String toString() {

String s = "";

for (int i = 0; i < st.length; i++) [ if (st[i] != null) s += st[i] + "\n"; }

return s;

}

}141

Page 142: Algorithmen & Datenstrukturen

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

Page 143: Algorithmen & Datenstrukturen

3.2.2 Sequenzielle Suche: sorted array

class ST {

private boolean less(KEY v, KEY w) { return v.less(w); }

private boolean equals(KEY v, KEY w) { return v.equals(w); }

private ITEM[] st;

private int N = 0;

ST(int maxN) { 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

Page 144: Algorithmen & Datenstrukturen

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

Page 145: Algorithmen & Datenstrukturen

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

Page 146: Algorithmen & Datenstrukturen

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

Page 147: Algorithmen & Datenstrukturen

3.2.4 Vergleich

insert(max) insert () search(max) search () select

Indizierte Suche

IndexArray O(1) O(1) O(1) O(1) O(1)

Sequenzielle Suche

SortedArray O(n) O(n/2) O(n) O(n/2) O(1)

SortedList O(n) O(n/2) O(n) O(n/2) O(n)

UnsortedArray O(1) O(1) O(n) O(n/2) O(n log(n))*

UnsortedList O(1) O(1) O(n) O(n/2) O(n log(n))*

Binäre Suche

SortedArray O(n) O(n/2)

* z.B. mit dem Heapsort (k-faches heapifien)147

Page 148: Algorithmen & Datenstrukturen

Übung 3.2

Implementieren Sie unter Verwendung des Frameworks aus 3.1. eine Symboltabelle als geordnete verkettete Liste und einen Client.

148

Page 149: Algorithmen & Datenstrukturen

3.3 Fortgeschrittene Suchalgorithmen

Im vorangegangenen Kapitel haben wir auf linearen Datenstrukturen gesucht. Dort legt aber gerade die sehr Laufzeit-effiziente binäre Suche die Verwendung mehrdimensionaler Strukturen nahe.Das sind insbesondere Bäume und multidimensionale Listen (Skiplisten).Diese Strukturen und die damit verbundenen Implementierungen sind hier beschrieben.

1. Binäre Suchbäume: Die Mutter aller Bäume

2. Ausgeglichene binäre Suchbäume

3. Randomisierte Binärbäume

4. Splay Binärbäume

5. Top-Down 2-3-4 Bäume

6. Rot-Schwarz-Bäume

7. Skiplisten

149

Page 150: Algorithmen & Datenstrukturen

3.3.1 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

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.

150

28

13

3

2

25

9

32

65

42 99

Page 151: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: 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

Page 152: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: Beispiel

Nennen Sie mir 16 Zahlen zwischen 0 und 99

152

Page 153: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: 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

Page 154: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: 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

Page 155: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: 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.

Problem: im alten linkenTeilbaum kann bereits einElement vorhanden sein,welches größer als das neueElement ist.

155

28

13

3 25

32

1515

13

3 25

28

32

Page 156: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: Rotation I

… 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

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;

}156

28

13

3 25

32

15

28

13

3 15

32

25

28

15

13 25

32

3

25

15

13 28

323

rotR(25)

rotL(13)

rotR(28)

h.lh.l

x.rx.r

h.lh.l

x.rx.rh.rh.r

x.lx.lxx

hh

xx

hh

xx

hh

Page 157: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: Rotation II

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

h.lh.l

x.rx.r

xx

hh

h.rh.r

x.lx.l

hh

xx

rotate right

rotate left

Page 158: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: 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

Page 159: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: Auswählen (select)

Page 160: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: Zerlegen (part)

Page 161: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: Entfernen (remove)

Page 162: Algorithmen & Datenstrukturen

3.3.1 Binäre Suchbäume: Verknüpfen (join)