2.3 Version 20.04.2009 - Benutzer-Homepage

Werbung
Algorithmen &
Datenstrukturen
Sommersemester 2009
Prof. Dr. Peter Kneisel
1
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 ;-)
2
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.
3
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)
4
Inhalt
 In „Grundlagen der Informatik“ haben wir uns mit zwei grundlegenden Aspekte
der Informatik befasst:
 Was ist Information und wie kann man diese auf höheren semantischen Ebenen
strukturieren.
 Aus welchen einfachen Elementen ist ein (imperativer) Algorithmus aufgebaut
 „Algorithmen & Datenstrukturen“ nimmt diese Zweiteilung auf:
 Zunächst werden wir die semantische Leiter nach oben steigen und komplexere
semantische Strukturen kennenlernen, die grundlegend für Lösungen vieler typischer
Problemstellungen sind.
 Anschließend werden wir die wichtigsten Algorithmen kennenlernen, die auf diesen
Strukturen arbeiten.
 Inhalt
1. Abstrakte Datentypen (ADTs)
2. Suchen: Grundlagen, Algorithmus, Analyse
3. Sortieren Grundlagen, Algorithmus, Analyse
5
OOP
Sortieren
Suchen
ADTs
A&D
Datenstrukturen
Komplexität
Zahlen
Verifikation
Zeichen
Strukturierung
Codes
Elemente
Information
GDI
Statik, Struktur
PIS
Dynamik, Algorithmik
Überblick und Einordnung
RA
6
Kapitel 1 Abstrakte Datentypen (ADTs)
 In „Grundlagen der Informatik“ haben wir elementare Strukturen kennengelernt
und gesehen, wie daraus mit komplexeren Strukturierungsverfahren
komplexere Strukturen aufgebaut werden können.
Wir haben uns dabei genau auf die Strukturen beschränkt, die den meisten
imperativen Programmiersprachen gemeinsam sind.
In diesem Kapitel gehen wir nun in semantisch höhere Ebenen und erläutern
Strukturen, die häufig verwendet werden, aber nicht im Sprachumfang der
meisten Programmiersprachen liegen (sehr wohl aber in Klassenbibliotheken)
 Inhalt
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
Wiederholung
Was sind ADTs
Stacks (Kellerspeicher, Stapel)
Queues (Warteschlangen)
Einfach verkettete Listen
Zweifach verkettete Listen
Hashlisten
Bäume
Graphen
Frameworks
7
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
8
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.
9
1.1.2 Datentypen
 Typen sind also nicht die intern repräsentierten Objekte, sondern beschreiben
die Eigenschaft einer Gruppe von Objekten.
 Zu diesen Eigenschaften gehören:






Struktur
Wertebereich
anwendbare Operatoren, Funktionen, Relationen
Beziehungen zu anderen Typen
interne Repräsentationsweise
…
Beispiel:
Imaginäre Zahlen
 Einige Anmerkungen::
 Der Begriff „Datentyp“ ist weitergehend als der Begriff „Datenstruktur“
 In der Objektorientierten Programmierung wird statt „Datentyp“ auch der Begriff
„Klasse“ verwendet (Klassen beschreiben mehr Eigenschaften)
 Konkrete Repräsentanten eines Datentyps werden (u.a) „Variable“ oder
- bei OO-Sprachen - „Instanz“ genannt
10
1.1.3 Klassifikation der Datentypen
Datentypen
Konkrete
Einfache
Abstrakte
Pointer(Zeiger)
Idealisierte
Strukturierte
...
Ordinale
Boolean
(Wahrheitswert)
Integer
(Ganzzahl)
Real
(Fließkomma)
Char
(Zeichen)
Array
(Feld)
Record
Union
(Verbund) (Variantenverb.)
...
Enumeration
(Aufzählung)
11
1.1.3 Erläuterung der Klassifikation
 Idealisierte Datentypen
 aus der Mathematik bekannte Datentypen: R, N, Z, ...
 Variablen dieser Typen sind oft nicht endlich darstellbar (Bsp: 2)
 In einem Computer-Algebra-System symbolisch darstellbar (Bsp: 2^( 1/2))
 Konkrete Datentypen
 in einem Rechner von Hard- oder Software bereitgestellte Datentypen
 entweder vordefiniert oder durch den Benutzer definierbar
 Abstrakte Datentypen
 verbergen ihren inneren Aufbau vor dem Benutzer
 bestehen aus beliebigen Strukturen über konkrete/idealisierte Datentypen, sowie aus
Zugriffsfunktionen bzw. Prozeduren
 Beispiel: Baum
13
insert (Element)
6
2
61
12
15
delete (Element)
79
search (Element)
12
1.2
Was sind ADTs
 „Ein abstrakter Datentyp fasst die wesentlichen Eigenschaften und Operationen
einer Datenstruktur zusammen, ohne auf deren eigentlichen Realisierung im
Rechner einzugehen“
Konkrete Datentypen werden aus ordinalen (Basis-) Datentypen konstruiert und
sind somit direkt in einer Implementierung einsetzbar.
1.
2.
3.
4.
5.
6.
Grundsätze
Algebren
Signaturen
Axiome
Beispiel einer ADT-Schnittstelle
Anwendung: Tabelle
13
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.
14
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.
15
1.2.3 Signaturen
 Die Schnittstellen eines (A)DTs - also die Art, wie man den (A)DT verwendet lassen sich durch seine Signatur beschreiben.
 Bsp: betrachten Sie den Datentyp integer:
 integer unterstützt/erzeugt zwei Sorten: integer und bool
 integer unterstützt die Operatoren:
const
:  integer // nullstelliger Operator: Konstante
successor
: integer  integer // einstellige Operation
+, -, x, %
: integer  integer  integer // zweistellige Operation
>, <, ==, != : integer  integer  bool // zweistellige Operation
, 
: bool  bool  bool // zweistellige Operation

: bool  bool // einstellige Operation
 Diese Formalisierung einer Algebra beschreibt die Strukturen und die
Operationen eines (abstrakten) Datentyps und wird Signatur des Datentyps
genannt.
Aus der Signatur eines (A)DTs geht also insbesonder hervor:
 Dessen Wertebereiche in den unterschiedlichen Sorten
 Die Operatoren und deren Stelligkeit
 Die Wertebereiche der bei den Operationen verwendeten Operanten
16
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))
...
17
1.2.5 Beispiel einer ADT-Schnittstelle
type: list(T)
// T ist die Wertemenge der Elemente
// T ist ein sog. Sortenparameter
import: integer
operators:
[]
:  list
_ : _
: T x list  list
//
//
//
//
//
erweitert Liste
_ : _ ist Infix-Operator
Kopf der Liste
Liste ohne Kopf
Anzahl Listenelemente
head
: list  T
tail
: list  list
length
: list  integer
axioms:  l : list,  x : T
head ( x : l )
= x
tail ( x : l )
= l
lenght ( [] )
= 0 // [] ist leere Liste
length ( x : l ) = successor ( length (l) )
18
1.2.6 Anwendung: Tabellen
 Listen repräsentieren oft „Tabellen“:
 Definition:
Eine Tabelle o der Größe n ist eine Folge (z.B. Liste) von n Elementen gleichen
Typs o = (o1, o2, … , on)
 Oft sind die Elemente einer Tabelle nochmals in zwei Teile unterstruktiert:
 Schlüssel-Daten (key)
Die Schlüsseldaten bezeichnen (oft eindeutig) das Element einer Liste.
Der Key kann nochmals unterstrukturiert sein.
 Informations-Daten (info)
Die Informations-Daten geben für das durch den key bezeichnete Element
zusätzliche Informationen an.
Auch info kann nochmals unterstrukturiert sein.
key1
info1
key2
info2
…
keyn
infon
Anmerkung:
Da die Indizierung von Listen in
vielen Programmiersprachen mit
„0“ beginnt, man aber in der realen
Welt mit „1“ zu zählen beginnt, wird
das „0“-te Element oft als DummyElement mit einem Dummy-Wert
versehen und ignoriert.
19
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“
20
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
21
1.3.2 Implementierung eines Stacks
public class ArrayStack implements Stack {
public void push(Object obj) throws
StackException {
if (num == elements.length)
// KapazitŠt erschöpft
throw new StackException();
elements[num++] = obj;
}
private Object elements[] = null; //
Elemente
private int num = 0; // aktuelle Anzahl
// Stack mit vorgegebener Größe erzeugen
public ArrayStack(int size) {
elements = new Object[size];
}
public Object pop() throws StackException {
if (isEmpty()) // Stack ist leer
throw new StackException();
Object o = elements[--num];
elements[num] = null;
return o;
}
// Abfrage auf leeren Stack
public boolean isEmpty() {
return num == 0;
}
public Object top() throws StackException {
if (isEmpty()) // Stack ist leer
throw new StackException();
return elements[num - 1];
}
}
22
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
}
}
}
}
23
1.4. Queues
 Queues (Warteschlangen) sind lineare Listen, deren Elemente nach dem FIFOPrinzip (First in–First Out) ein- bzw. ausgefügt werden
Auch Queues kommen in systemnahen Bereichen vor, insbesondere bei
Betriebssystemen.
1. Spezifikation
2. Implementierung einer Queue
3. Die Java-Klasse „queue“
24
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
25
1.4.2 Implementierung einer Queue
public class ArrayQueue implements Queue {
private Object[] elements; // Elemente
private int l = 0; // „lower“ Zeiger
private int u = 0; // „upper“ Zeiger
// in der Queue sind max. size-1 Elemente
// Einfügen eines Elementes
public void enter (Object obj) throws
QueueException {
if ((elements.length - l + u) %
elements.length
== elements.length - 1)
// Kapazität ist erschöpft (= size-1)
throw new QueueException ();
elements[u] = obj;
// oberen Zeiger aktualisieren
u = (u + 1) % elements.length;
// Modulo, da array zyklisch verwendet.
}
// Queue mit vorgegebener Länge erzeugen
public ArrayQueue (int size) {
elements = new Object[size];
}
public boolean isEmpty () {
return l == u;
}
// Herausnehmen des lower-Elementes
public Object leave () throws QueueException
{
if (isEmpty ())
throw new QueueException ();
Object obj = elements[l];
elements[l] = null;
// unteren Zeiger aktualisieren
l = (l + 1) % elements.length;
return obj;
}
// Zeige das lower Element
public Object front () throws
QueueException {
if (isEmpty ())
throw new QueueException ();
return elements[l];
}
}
26
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.*
27
1.5
Einfach verkettete Liste
 Listen sind (ziemlich) simple Datentypen, die sich statisch durch den konkreten
strukturierten Datentyp „array (Feld)“ darstellen lässt und damit in den meisten
Programmiersprachen implizit vorhanden ist.
In der nicht-imperativen Programmiersprache LISP ist „Liste“ zudem der einzige
strukturierte Datentyp.
Möchte man die Länge einer Liste jedoch zur Laufzeit eines Programmes
dynamisch verändern so muss man auf eigenen Umsetzungen mithilfe eines
ADTs zurückgreifen.
1.
2.
3.
4.
class
main
Methoden
Implementierung als Liste
28
1.5.1 class
public class List {
static class Node {
Object obj;
Node next;
public
public
public
public
public
public
Node(Object o, Node n)
{ obj = o;
next = n; }
Node()
{ obj = null; next = null; }
void
setElement(Object o) { obj = o; }
Object getElement()
{ return obj; }
void
setNext(Node n)
{ next = n; }
Node
getNext()
{ return next; }
}
private Node head = null;
public
public
public
public
public
public
public
public
public
}
List()
void
addFirst(Object o)
void
addLast(Object o)
Object getFirst()
throws
Object getLast()
throws
Object removeFirst() throws
Object removeLast() throws
int size()
boolean isEmpty()
ListEmptyException
ListEmptyException
ListEmptyException
ListEmptyException
{}
{}
{}
{}
{}
{}
{}
{}
{}
29
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());
}
}
30
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;
}
31
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 ();
}
}
32
1.6
Zweifach verkettete Liste
 Aus bestimmten Gründen – vor allem Laufzeit-Effizienz – verwendet man oft
Listen, deren einzelne Elemente nicht nur den jeweiligen Nachfolger, sondern
auch den jeweiligen Vorgänger kennen.
Diese Listen nennt man das „Zweifach bzw. Doppelt verkettete Listen“
1.
2.
3.
4.
class
iterator
main
Methoden
33
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 () {}
}
34
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;
}
}
35
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 ());
}
}
36
1.6.4 Methoden
public DList () {
head = new Node (); //
tail = new Node (); //
head.setNext(tail); //
tail.setPrevious(head);
tail.setNext(tail); //
}
dieser Knoten existiert immer, auch bei leerer Liste
dieser Knoten existiert immer, auch bei leerer Liste
head und tail werden initial miteinander verlinkt
tail.next zeigt auf sich selbst
public void addFirst (Object o) {
Node n = new Node (o, head, head.getNext());
head.getNext ().setPrevious (n);
head.setNext (n);
}
public Object removeFirst () throws
ListEmptyException {
if (isEmpty ())
throw new ListEmptyException ();
Object o = head.getNext ().getElement ();
head.setNext (head.getNext ().getNext ());
public Object getFirst () throws ListEmptyExceptionhead.getNext
{
().setPrevious (head);
if (isEmpty ())
return o;
throw new ListEmptyException ();
}
return head.getNext ().getElement ();
}
public Object removeLast () throws
ListEmptyException {
public void addLast (Object o) {
if (isEmpty ())
Node l = tail.getPrevious ();
throw new ListEmptyException ();
Node n = new Node (o, l, tail);
Node n = tail.getPrevious ();
l.setNext (n);
n.getPrevious ().setNext (tail);
tail.setPrevious (n);
tail.setPrevious (n.getPrevious ());
}
return n.getElement ();
}
37
1.7
Hashlisten
 Hashlisten sind Listenstrukturen, manchmal erweitert durch „weitere“
Strukturen, die sich sehr gut für das Suchen eignen ( Kapitel 2).
Hier seien die grundlegenden Ideen des Hashens dargestellt.
1.
2.
3.
4.
Grundprinzip des Hashens
Die Hashfunktion
Behandlung von Kollisionen
Implementierung einer Hashliste
38
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.
39
1.7.2 Die Hashfunktion
 Die Auswahl der Hashfunktion h hängt natürlich vom zu speichernden Datentyp
(bzw. dessen Wertebereich) und der Auftrittswahrscheinlichkeit der Werte ab.
 Für Integerwerte i wird oft die Modulofunktion verwendet:
 h(i) = i mod n (wobei n die größe der Hashliste ist)
Diese Funktion funktioniert in der Regel nur für große primzahlige n gut
(inbesondere ist n = 2x nicht gut !)
Beispiel:
h(i) = i mod 7
Index
0 1 2 3 4 5 6
Element 28 36 16 66 25 75 27 (danach führt jedes Element zu Kollision)
 Für andere Datentypen kann eine Abbildung auf Integerwerte erfolgen:
 Bei Fließkommazahlen kann man z.B. Mantisse und Exponent addieren
 Bei Strings kann man den ASCII oder Unicode der einzelnen Buchstaben, eventuell
mit einem Faktor gewichtet, miteinander addieren.
 Meist ist eine Gleichverteilung der Bildbereiches der Hashfunktion
wünschenswert, so dass man sich bestimmte Eigenschaften (z.B.
ungleichgewichtige Verteilungen) des Urbildes zu Nutze machen kann und
sollte.
Andererseits geht die Komplexität der Hashfunktion h multiplikativ in die
Gesamtkomplexität ein und sollte daher einfach gehalten werden.
40
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.
41
1.7.4 Implementierung einer Hashliste
public class HashTable {
// sucht Element in Hashliste
public boolean contains (Object o) {
int idx, oidx;
oidx = idx = (o.hashCode () &
0x7fffffff) % table.length;
while (table[idx] != null) {
if (o.equals (table[idx]))
return true;
idx = ++idx % table.length;
if (idx == oidx)
break;
}
return false;
}
Object[] table;
public HashTable (int size) {
table = new Object [size];
}
// fügt Element in Hashliste
public void add (Object o) {
int idx, oidx;
// berechnen Hashfunktion
oidx = idx = (o.hashCode () &
0x7fffffff) % table.length;
// falls Kollision -> suche nächstes Freies
while (table[idx] != null) {
idx = ++idx % table.length;
// fall Suche erfolglos -> Fehler
if (idx == oidx)
throw new HashTableOverflowException ();
}
// trage Wert ein
table[idx] = o;
}
public static void main (String[] args) {
HashTable tbl = new HashTable (20);
tbl.add („Au");
tbl.add („Oh");
tbl.add („Ah");
System.out.println (tbl.contains („Ah"));
System.out.println (tbl.contains („Be"));
}
}
42
1.8
Bäume
 Bäume sind (zumindest) zweidimensionale Strukturen, die viele reale Strukturen
abzubilden Vermögen und zudem sehr gut zum Durchsuchen geeignet sind.
Es gibt daher sehr viele spezielle Arten von Bäumen, von denen hier
stellvertretend vor allem die binären Bäume behandelt werden sollen.
1.
2.
3.
4.
5.
Definitionen & Beispiele
Spezifikation
Datentypen
Traversierung
Weitere Bäume
43
1.8.1. Definitionen & Beispiele
 Ein Baum ist eine Menge von Knoten und (gerichteten) Kanten mit folgenden
Eigenschaften:
 Ein ausgezeichneter Knoten wird als Wurzel bezeichnet
 Jeder Knoten (außer der Wurzel) ist durch genau eine Kante mit seinem
Vorgängerknoten verbunden (Vaterknoten, Elternknoten).
Dieser Knoten wird dann auch als Kind (Sohn, Nachfolger) bezeichnet.
 Ein Knoten ohne Kinder heißt Blatt
 Knoten mit Kindern heißen innere Knoten
Wirbeltiere
(Unterstamm)
Kiefermünder
(Oberklasse)
Kiefermünder
(Oberklasse)
Vögel
(Klasse)
Säugetiere
(Klasse)
Kieferlose
(Oberklasse)
…
(Klassen)
Wirbeltiere
(Unterstamm)
Kieferlose
…
…
(Ordnungen)
Primaten
(Ordnung)
Vögel
Säugetiere
(Klasse)
Primaten
(Ordnung)
…
… nich‘ so praktisch
… wie sich der
Informatiker einen
44
Baum vorstellt
1.8.1. Definitionen & Beispiele
 Ein Pfad in einem Baum ist eine Folge von unterschiedlichen Knoten, in der die
aufeinanderfolgenden Knoten durch Kanten verbunden sind
 Zwischen jedem Knoten und der Wurzel gibt es genau einen Pfad
 Dies bedeutet, dass ein Baum zusammenhängend ist und keine Zyklen besitzt
 Unter dem der Niveau (der Tiefe) eines Knotens versteht man die Länge
dessen Pfades zu der Wurzel
 Die Höhe (Tiefe) eines Baumes entspricht dem maximalen Niveau eines
Blattes + 1 („+1“ da die Wurzel mitzählt)
 Je nach Art und Anzahl von Kindern unterscheidet man zwischen
 n-ären Bäumen, wenn die maximale Anzahl von Kindern gleich n ist
(also z.B. binärer Baum, wenn die maximale Anzahl der Kinder gleich 2 ist)
 geordneten Bäumen, wenn die Kinder entsprechend einer Ordnungsrelation (z.B.
von links nach rechts) angeordnet sind
Tiefe 0
+
*
((1+2)*3)+(2+5)
+
1
3
2
Tiefe 1
+
2
5
Tiefe 2
Tiefe 3
45
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
46
1.8.3 Binäre Bäume: Datentypen
static class TreeNode {
Object
key; // Wert des Knotens
TreeNode left = null; // Referenz auf linken Teilbaum
TreeNode right = null; // Referenz auf rechten Teilbaum
+
*
+
// Konstruktor
public TreeNode (Object e)
{ key = e; }
// getter Methoden
public TreeNode getLeft () { return left; }
public TreeNode getRight () { return right; }
public Object
getKey ()
{ return key;
}
// setter Methoden
public void setLeft (TreeNode n) { left = n; }
public void setRight (TreeNode n) { right = n; }
}
static class BinaryTree {
protected TreeNode root = null;
public BinaryTree () { }
public BinaryTree (TreeNode n) { root = n; }
1
+
3
2
5
2
TreeNode e1 = new TreeNode(“+“);
e1.setleft (new TreeNode(“1“));
e1.setright (new TreeNode(“2“));
TreeNode e2 = new TreeNode(“*“);
e2.setleft (e1);
e2.setright (new TreeNode(“3“));
TreeNode e3 = new TreeNode(“+“);
e3.setleft (new TreeNode(“2“));
e3.setright (new TreeNode(“5“));
TreeNode e = new TreeNode(“+“);
e.setleft (e2);
e.setright (e3);
}
 Bäume baut man „von unten nach oben“ auf
47
1.8.4 Binäre Bäume: Traversierung
 Je nach Reihenfolge unterschiedet man beim Baumdurchlauf folgende
Traversierungsarten.
 Inorder: Hier wird zuerst rekursiv der linke Teilbaum, danach der Knoten selbst, und
schließlich der rechte Teilbaum durchlaufen.
 Preorder: Hier wird zuerst der Knoten, danach zunächst rekursiv der linke Teilbaum
und schließlich rekursiv der rechte Teilbaum durchlaufen.
 Postorder: Hier wird zuerst rekursiv der linke Teilbaum, danach rekursiv der rechte
Teilbaum, schließlich der Knoten durchlaufen.
Diese Traversierungsarten gehen also für jeden Knoten rekursiv in die Tiefen
der beiden Teilbäume und können daher auch Tiefentraversierung genannt
werden.
Daneben gibt es noch eine Traversierungsart, die auf jedem Niveau alle Knoten
berücksicht. Diese Breitentraversierung nennt man:
 Levelorder: erst werden alle Knoten eines Niveaus durchlaufen, danach rekursiv die
beiden Teilbäume
+
*
+
1
+
3
2
2
5
Inorder:
Preoder:
Postorder:
Levelorder:
1+2*3+2+5
+*+123+25
1 2 + 3 * 2 5 + + ( UPN)
+*++32512
48
1.8.4 Binäre Bäume: Traversierung
private void printPreorder (TreeNode n) {
if (n != nullNode) {
System.out.println (n.toString ());
printPreorder (n.getLeft ());
printPreorder (n.getRight ());
}
}
private void printPostorder (TreeNode n) {
if (n != nullNode) {
printPostorder (n.getLeft ());
printPostorder (n.getRight ());
System.out.println (n.toString ());
}
}
protected void printInorder (TreeNode n) {
if (n != nullNode) {
printInorder (n.getLeft ());
System.out.println (n.toString ());
printInorder (n.getRight ());
}
}
private void printLevelorder (Queue q) {
while (! q.isEmpty ()) {
TreeNode n = (TreeNode) q.leave ();
if (n.getLeft () != nullNode)
q.enter (n.getLeft ());
if (n.getRight () != nullNode)
q.enter (n.getRight ());
System.out.println (n.toString ());
}
}
...
// zur Zwischenspeicherung der Knoten ->1.4.2
Queue queue = new ArrayQueue ();
// Initialisierung
queue.enter (root);
// Aufruf
printLevelorder (queue);
49
1.8.5 Weitere Bäume
 Für spezielle Anwendungen des Suchens und Sortierend werden bestimmte
Spezialformen von Bäumen verwendet
 Ausgeglichene (balanced) Bäume: Hier wird beim Auf- und Abbau des Baumes
versucht ,die Tiefen der Teilbäume möglichst ähnlich oder sogar gleich zu halten:
 AVL-Bäume sind binäre Bäume und beschränken die Niveaudifferenz aller
Teilbäume auf 1.
Sie werden vor allem zum Suchen verwendet .
 B-Bäume (b steht für balanciert, buschig, breit) sind n-äre Bäume, bei denen
alle Teilbäume gleichtief sind. Diese sind also meist nicht binär.
Sie werden oft bei Datenbanksystemen zur Indexierung verwendet.
 Digitale Bäume: Das sind n-äre Bäume die eine feste Anzahl von Verzweigungen
(Nachfolgenknoten) unabhängig von den Werten im Baum haben.
 Tries (retrieval): sind n-äre Bäume bei denen die n Werte (z.B. 127 ASCIIWerte) des Knotens als Index für die Nachfolgeknoten verwendet werden.
Sie werden zum Suchen von Worten in Texten verwendet. (
 Patricia-Bäume (Practical Algorithm to Retrieve Information Coded in
Alphanumeric): Spezielle Form von Tries, bei denen Knoten mit nur einem
Nachfolger übersprungen werden können.
Auch Sie werden zum Suchen von Worten in Texten (oder von Gensequenzen
in einem Genom) verwendet.
  Kapitel 2
50
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
51
1.9.1 Arten
 Es gibt (neben anderen) drei wichtige Arten von Graphen
 ungerichtete Graphen: Hier sind Knoten mit ungerichteten Kanten verbunden, d.h.
es gibt kein Nachfolge- oder Vorgänger-Beziehung und auch kein Einschränkungen
bezüglich Anzahl von Kanten pro Knoten.
Anwendungen findet man bei der Modellierung von Straßenverbindungen (ohne
Einbahnstraßen), der Nachbarschaft von Gegenständen oder eines Telefonnetzes.
 gerichtete Graphen: Hier sind Knoten durch gerichtete Kanten verbunden, es kann
also zwischen zwei Knoten bis zu zwei Kanten geben (eine hin, eine zurück).
Anwendungen sind Modelle von Förderanlagen, der Kontrollfluss von Programmre
 gerichtete azyklische Graphen (DAG directed acyclic graphs): dieser Spezialfall
von gerichteten Graphen erlaubt keine Zyklen im Graph, d.h. es darf keinen Pfad
von einem Knoten zu sich selbst geben.
Zusätzlich können Kanten von Graphen noch gewichtet sein (gewichtete
Graphen)
1
2
1
2
3
4
6
5
3
4
6
5
1
20
4
64
6
25
22
2
75
24 3
5
30
52
1.9.2 Umsetzung
 Die interne Darstellung von Graphen erfolgt (historisch) in vier Varianten:
 Knotenliste:
<#Knoten>,<#Kanten>, <Kanteniste> (<Kantenlliste> := <Vorgängerknoten>, <Nachfolgeknoten>)
6, 8, 1,2, 1,4, 3,2, 3,5, 4,5, 4,6, 5,2, 6,3
 Kantenliste:
<#Knoten>,<#Kanten>, <Kantenliste> (<Kantenliste> := <#Nachfolgeknoten>,<Nachfolgeknoten, …>)
6, 8, 2,2,4, 0, 2,2,5, 2,5,6, 1,2, 1,3
 Adjazenzmatrix
0 25 0 20 0 0
0 0 0 0 0 0
0 75 0 0 24 0
0 0 0 0 22 64
0 32 0 0 0 0
0 0 30 0 0 0
 dynamische Adjazenzliste
1
2
3
4
5
6
2
2
5
2
3
4
5
6
25
2 75
32 24 3
22
4
5
64
30
6
1
20
53
1.9.3 Implementierung eines Graphen
public class Graph {
static class Edge {
int dest, cost;
public Edge(int d, int c) {
dest = d; // Nachfolgeknoten
cost = c; // Gewicht
}
}
private ArrayList nodes;
public Graph() {
nodes = new ArrayList();
}
public void addNode(String label) { ... }
public void addEdge(String src, String dest, int cost) { ... }
public Iterator getEdges(int node) { ... }
}
54
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
55
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)
56
1.10.2
Bibliotheken in Java
 In Java sind diverse Klassen definiert, die die hier beschriebenen ADTs
implementieren:
 Vector
funktioniert wie ein array, das bei Bedarf dynamisch wachsen kann.
Nur für Integerwerte.
Generische Variante: ArrayList
 Stack
ferweiterert Vector zu eimem LIFO-Stack.
 LinkedList
Doppelt verkettete Liste, kann auch als Queue (Warteschlange) eingesetzt werden.
 HashMap
Hashliste.
TreeMap kann auch für gehashten (assoziativen) Zugriff verwendet werden, ist
intern als Baum aufgebaut und etwas langsamer – dafür sind die Schlüssel alle
sortiert.
 TreeSet
Balancierter Binärbaum. Die Elemente im Baum sind sortiert
 Diese Klassen befinden sich im Paket: java.util.* und können mit
import java.util.* eingebunden werden.
57
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.
58
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
59
2.1
Wiederholung: Komplexität
 In GDI haben wir den Begriff „Komplexität“ diskutiert und definiert. Komplexität,
insbesomdere Zeitkomplexität (Aufwand) ist nun ein entscheidendes Kriterium
für und wider den Einsatz der im folgenden behandelten Algorithmen und soll
daher hier nochmals kurz wiederholt werden.
 Inhalt
1.
2.
3.
4.
5.
6.
Wie „gut“ ist ein Algorithmus
Die O-Notation
Häufige O-Ausdrücke
Einige Regeln
Quantitatives
Platzbedarf
60
2.1.1 Qualität eines Algorithmus
 Die Abarbeitung eines Algorithmus benötigt „Ressourcen“, vor allem:
 Zeit
 Platz
Laufzeit des Algorithmus
Speicherplatzbedarf des Algorithmus
 Problem bei der Ressourcenermittlung - der Ressourcenbedarf ist Abhängig
von:





der Problemgröße (z.B. Multiplikation einer 10x10 bzw. 100x100 Matrix)
der Eingabewerte (z.B. Sortieren einer bereits sortierten Menge)
der Fragestellung (bester, mittlerer, schlechtester Fall)
der Güte der Implementierung (z.B. (un-)geschickte Typwahl)
der Hard- und Software (z.B. Schneller Rechner, optimierter Compiler)
 Es gibt auch Qualitätsmerkmale eines Algorithmus, der sich nicht am
Ressourcenbedarf festmachen (aber das ist eine andere Geschichte ...)




Wartbarkeit, Wartungsintensität
Robustheit
Eleganz
...
61
2.1.2 Die O-Notation: Definition
 Definition:
Eine Funktion g(n) wird O(f(n)) genannt („Die Laufzeit, der Aufwand, die
Zeitkomplexität von g(n) ist O(f(n))“), falls es Konstanten c und n0 gibt, so dass:
g(n)  cf(n), für fast alle n  no ist
 f(n) ist damit eine obere Schranke für die Laufzeit des Algorithmus (allerdings nur
zusammen mit einem festen c und ab bestimmten n0) !
 Die Problemgröße kann der Umfang der Eingabemenge sein, die Größe des zu
verarbeitenden Objektes (z.B. der Zahl), …
L
a
u
f
z
e
i
t
g(n)  cf(n), für alle n  no
cf(n)
g(n)
f(n)
no
Problemgröße62
2.1.3 Die O-Notation: Beispiel

Beispiel:
 Bei der Analyse eines Algorithmus hat sich herausgestellt, dass
die Laufzeit:
g(n) = 3n2 + 7n – 1
ist.
 Behauptung:
Die Laufzeit von g(n) ist O(n2), also
f(n)=n2,
 Beweis:
Es muss Konstanten c und n0
geben, so dass gilt:
3n2+7n-1  c n2, für alle n  n0
setze n0=7 und c=4, dann gilt:
3n2+7n-1  3n2+7n  3n2+n2 = 4n2

L
a
u
f
z
e
i
t
g(n)  cf(n), für fast alle n  no
cf(n) = 4 n2
g(n)
Allgemein:
f(n)=n2
 g(n) = amnm + am-1nm-1 + … + a0n0
 amnm + am-1nm + … + a0nm
= nm (am + am-1 + … + a0 )
also: g(n)  c nm
mit c = am + am-1 + … + a0
no
Problemgröße 63
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 !
64
2.1.5 Die O-Notation: Achtung
 Achtung !
Die Konstanten c und n0 werden üblicherweise nicht angegeben und können
sehr groß sein
 Beispiel:
Algorithmus A habe eine Laufzeit von O(n2)
Algorithmus B für das gleiche Problem eine Laufzeit von O(1,5n)
Welcher Algorithmus ist besser ?
 schnelle Antwort: A (das stimmt auch für große n)
 bessere Antwort: Wie groß ist n ? Wie groß sind die Konstanten ?
 z.B. für cA=1000 und cB=0,001
Bis hier ist B besser als A
n
cAn2
cB1,5n
1
10
20
50
100
103
105
4  105
2,5  106
107
1,5  10-3
1,8  10-2
3,3
6,4  105
4,1  1014
65
2.2.. Grundlagen
 … bevor es losgeht:
1. Definitionen
2. Beispiele
3. Framework für Implementierungen
66
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.
67
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
68
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; }
}
69
2.3. Elementare Sortieralgorithmen
 … da Sortieren eine so grundlegende Operation in der Informatik ist, gibt es
schon seit einigen Jahrzehnten eingeführte Algorithmen, die teilweise optimiert
wurden und immer noch Einsatz finden:
1.
2.
3.
4.
5.
Selection Sort (Sortieren durch Auswählen)
Insertion Sort (Sortieren durch Einfügen)
Shellsort
Bubblesort
Vergleich
sorting-algorithms.com
70
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 (instabil)
(auch instabil)
36
1
1
1
1
1
1
1
1
1
1
1
6
3
3
3
3
3
3
3
3
3
3
3
3
6
3
3
3
3
3
3
3
3
3
4
4
4
4
3
3
3
3
3
3
3
3
3
3
3
6
6
4
4
4
4
4
4
4
9
9
9
9
9
9
5
5
5
5
5
5
8
8
8
8
8
8
8
5
5
5
5
5
1
3
3
3
4
6
6
6
6
6
6
6
7
7
7
7
7
7
7
7
7
7
7
7
5
5
5
5
5
5
9
9
9
9
8
8
5
5
5
5
5
5
5
8
8
8
9
9
3
1
1
1
1
1
1
1
1
1
1
1
6
6
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
3
4
4
4
4
3
3
3
3
3
3
3
3
3
3
3
3
4
4
4
4
4
4
4
4
9
9
9
9
9
9
5
5
5
5
5
5
8
8
8
8
8
8
8
5
5
5
5
5
1
3
6
6
6
6
6
6
6
6
6
6
7
7
7
7
7
7
7
7
7
7
7
7
5
5
5
5
5
5
5
8
8
8
8
8
5
5
5
5
5
5
9
9
9
9
9
9.
71
2.3.1 Selection Sort: Implementierung 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
}
}
72
2.3.1 Selection Sort: Implementierung 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;
}
73
2.3.1 Selection Sort: Diskussion
 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)
 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.
74
2.3.2 Insertion Sort (Sortieren durch Einfügen)
 Idee: Wie beim Sortieren eines Kartenblattes auf der Hand eines Spielers
werden neue (rechts neben den bereits sortierten) Karten in das bereits
sortierte Kartenblatt an der richtigen Stelle eingefügt.
Angewandt auf eine Liste existiert also immer eine bereits sortierte Teilliste (am
Anfang der Liste), die bei jeder Iteration um ein weiteres korrekt einsortiertes
Element erweitert wird..
 Beispiel:
3 6 3 4 3 9 8 1 7 5 5
3 6 3 4 3 9 8 1 7 5 5
3 6 3 4 3 9 8 1 7 5 5
3 3 6 4 3 9 8 1 7 5 5
3 3 4 6 3 9 8 1 7 5 5
3 3 3 4 6 9 8 1 7 5 5
3 3 3 4 6 9 8 1 7 5 5
3 3 3 4 6 8 9 1 7 5 5 // swapping of „1“ is exhausting
1 3 3 3 4 6 8 9 7 5 5
1 3 3 3 4 5 6 8 9 7 5
1 3 3 3 4 5 5 6 8 9 7
75
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);
}
}
}
76
2.3.2 Insertion Sort: Implementierung Variante 2
// sort array “ITEM[]” between indexes l and r
static void insertion(ITEM[] a, int l, int r)
{ int i;
// initially bring smallest element to front
for (i = r; i > l; i--) compExch(a, i-1, i);
// iterate through list starting with second position
// from left to right
for (i = l+2; i <= r; i++)
{ int j = i;
ITEM v = a[i]; // remember element to be inserted
// Iterate from right to left through already sorted list
// and shift elements to right ...
while (less(v, a[j-1])) // ... stop on correct position
{ a[j] = a[j-1]; j--; }
// insert element to its proper position
a[j] = v;
}
}
77
2.3.2 Insertion Sort: Diskussion
 Variante 2 unterscheidet sich von Variante 1 durch folgende vorteilhafte
Erweiterungen
 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.
 Aufwand:
 Vergleiche : min: O(n), max: O(n2), average O(n2)
 Bewegungen: min: O(n), max: O(n2), average O(n2)
 Der Aufwand des Insertion Sort ist stark abhängig vom Zustand der zu
sortierenden Daten. So ergibt sich der Best-Case bei bereits sortierten oder
stark vorsortierten Daten. Ist dieser Zustand gegeben dann kann dieser
Algorithmus gut verwendet werden.
 Der Insertion Sort ist, bei geschickter Auswahl der Ordnungsrelation, stabil.
78
2.3.3 Shellsort (Donald L. Shell, 1959)
 Motivation: Der Insertion-Sort ist langsam, da nur benachbarte Element
getauscht werden. Insbesondere sehr kleine Elemente müssen dabei häufig
vertauscht werden, um vom Ende an den Anfang zu „rutschen“
 Idee:
Bei den bislang behandelten Algorithmen ist der linke Teil der Liste jeweils
sortiert, als jedes Element links . Beim Shellsort werden Teillisten, bestehend
aus den jeweils h-ten Elementen mit dem Insertion-Sort sortiert . Man
verkleinert h bis es zu 1 wird.
 Die Schrittweite des Vertauschens ist anfangs also groß, so dass Elemente „recht
schnell grob vorsortiert“ werden.
 Beispiel (h-Folge: 4,3,1)
3 6 3 4 3 9 8 1 7 5 5 2 3 5 3 1 3 6 5 2 7 9 8 4
h=4
h=3
3 6 3 4
3 5 3 1
3 5 3 1 2 3
3 9 8 1  3 6 5 2
1 3 6 
3 3 4
7 5 5 2
7 9 8 4
5 2 7 5 5 6
9 8 4 9 8 7
mit h=1 wird hier abschließend nochmals Insertion-sortiert
1 2 3 3 3 4 5 5 6 9 8 7  1 2 3 3 3 4 5 5 7 8 9
79
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;
}
}
}
80
2.3.3 Shellsort: Diskussion
 Der Aufwand von Shellsort hängt von der Wahl der h-Folge ab. Dafür gibt es
unterschiedliche Ansätze:
 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
 Gonnet-Folge (1984)
h1 =  * n, hn =  * hn-1 mit  = 0,45454
 Mit der bislang besten bekannten Folge, der Gonnet-Folge erreicht man
 Vergleiche = Bewegungen : min = max = average O(n1,2)
 Damit war der Shellsort ein Jahr lang (bis 1960) der schnellste bekannte
Sortieralgorithmus
81
2.3.4. Bubblesort
 Idee: Durchlaufe die Datei und vertausche die Elemente solange bis alle
Elemente am richtigen Ort sind
 Dadurch „bubbeln“ kleine Elemente nach oben (links), solange bis sie auf noch
kleinere stoßen, diese „bubbeln“ dann weiter.
 Mit jedem Durchgang wird das kleinste nach oben „gebubbeld“, gleichzeitig werden
dabei auch noch andere kleine „mitgerissen“
 Beispiel:
3 6 3 4
1 3 6 3
1 3 6 3
1 3 3 6
1 3 3 3
1 3 3 3
1 3 3 3
1 3 3 3
1 3 3 3
1 3 3 3
1 3 3 3
1 3 3 3
3
4
3
3
6
4
4
4
4
4
4
4
9
3
4
4
4
6
5
5
5
5
5
5
8
9
5
5
5
5
6
5
5
5
5
5
1
8
9
5
5
5
5
6
6
6
6
6
7
5
8
9
7
7
7
7
7
7
7
7
5
7
5
8
9
8
8
8
8
8
8
8
5
5
7
7
8
9
9
9
9
9
9
9
// stoppt bei Gleichheit
82
2.3.4 Bubblesort: Implementierung
// sort array “ITEM[]” between indexes l and r
static void bubble(ITEM[] a, int l, int r) {
for (int i = l; i < r; i++)
for (int j = r; j > i; j--)
compExch(a, j-1, j);
}
3
1
1
1
1
1
1
1
1
1
1
6
3
3
3
3
3
3
3
3
3
3
3
6
3
3
3
3
3
3
3
3
3
4
3
6
3
3
3
3
3
3
3
3
3
4
3
6
4
4
4
4
4
4
4
9
3
4
4
6
5
5
5
5
5
5
8
9
5
5
5
6
5
5
5
5
5
1
8
9
5
5
5
6
6
6
6
6
7
5
8
9
7
7
7
7
7
7
7
5
7
5
8
9
8
8
8
8
8
8
5 // stoppt bei Gleichheit
5
7
7
8
9
9
9
9
9
9
83
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)
 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.
84
2.3.5 Vergleich
min
max
average
Selection
O(n2)
O(n2)
O(n2)

Insertion
O(n)
O(n2)
O(n2)

Shell
O(n1,2)
O(n1,2)
O(n1,2)

Bubble
O(n)
O(n2)
O(n2)

85
Herunterladen