Algorithmen und Datenstrukturen

Algorithmen und Datenstrukturen
Dr. Beatrice Amrhein
9. Februar 2015
ii
¨
Zur umfassenden Ausbildung eines Software-Ingenieurs gehoren
grundlegende Kenntnisse der wichtigsten Datenstrukturen und wie man diese verarbeitet (Algorithmen). Das Kennen von geeigneten Datenstrukturen hilft dem Programmierer, die Informationen richtig zu organisieren und besser strukturierte
Programme zu schreiben.
Lerninhalte
-
Abstrakte Datentypen, Spezifikation
¨ von Algorithmen,
Komplexitat
Algorithmen-Schemata: Greedy, Iteration, Rekursion
¨
Wichtige Datenstrukturen: Listen, Stacks, Queues, Baume,
Heaps
Suchen und Sortieren, Hash-Tabellen
¨ Sprachen, Pattern Matching
Endliche Automaten, regulare
Kontextfreie Grammatiken, Parser
Lernziele
Die Studierenden kennen die wichtigsten Datenstrukturen mit ihren Methoden. Sie kennen die klassi¨
¨
¨
¨
schen Algorithmen und konnen
sie anwenden. Ausserdem knnen
sie Komplexitatsabsch
atzungen
von
Algorithmen vornehmen.
Informationen zum Unterricht
Grundlage ist ein Skript, das die wichtigsten Lerninhalte umfasst.
Unterrichtssprache: Deutsch (Fachliteratur zum Teil in Englisch)
¨
¨
Umfang: 12 halbtagige
Blocke
a` 4 Lektionen
Dozentin: Beatrice Amrhein,
Empfohlene Literatur:
- Reinhard Schiedermeier Programmieren mit Java, Eine methodische Einfuhrung.
Pearson Studi¨
um ISBN 3-8273-7116-3.
- Robert Sedgewick Algorithms in Java. Addison-Wesley Professional; 2002 ISBN 978-0-20136120-9
- M. T. Goodrich & R. Tamassia Algorithm Design: Foundations, Analysis, and Internet Examples.
John Wiley & Sons, Inc.
ISBN: 0-471-38365-1.
- Gunter Saake, Kay-Uwe Sattler Algorithmen und Datenstrukturen, Eine Einfuhrung
mit Java.
¨
dpunkt, 2004. ISBN 3-89864-255-0.
Inhaltsverzeichnis
1
Einfuhrung
¨
1-1
1.1 Die wichtigsten Ziele dieses Kurses . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1-1
1.2 Einige Begriffe: Datenstrukturen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1-2
1.3 Einige Begriffe: Algorithmen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1-9
1.4 Algorithmen Schema: Iteration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1-12
1.5 Algorithmen Schema: Greedy (die gierige Methode) . . . . . . . . . . . . . . . . . . . . 1-12
1.6 Algorithmen Schema: Rekursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1-15
¨
1.7 Ubung
1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1-20
2
3
¨ von Algorithmen
Komplexitat
¨
2.1 Komplexitatstheorie
. . . . . . . .
¨
2.2 Komplexitatsanalyse
. . . . . . . .
¨ . . . . .
2.3 Asymptotische Komplexitat
¨
2.4 Ubung
2 . . . . . . . . . . . . . .
2-1
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
2-1
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
2-2
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
2-5
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
2-9
Datentypen: Listen, Stacks und Queues
3-1
3.1 Array Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3-1
3.2 Doppelt verkettete Listen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3-5
3.3 Stacks und Queues
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3-11
3.4 Iteratoren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3-13
¨
3.5 Ubung
3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3-14
4
¨
Datentypen: Baume,
Heaps
4-1
¨
4.1 Baumdurchlaufe
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4-4
¨ Suchbaume
¨
4.2 Binare
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4-8
¨
4.3 B-Baume
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4-10
4.4 Priority Queues . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4-15
¨
4.5 Ubung
4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4-21
5
Suchen
5-1
5.1 Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5-1
5.2 Lineare Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
¨ Suche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.3 Binare
5-2
5-3
5.4 Hashing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5-5
¨
5.5 Ubung
5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5-14
6
Sortieren
6-1
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6-2
6.2 Insertion Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6-4
6.3 Divide-and-Conquer Sortieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6-5
6.4 Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6-6
6.1 Selection Sort
iv
Inhaltsverzeichnis
6.5 Sortieren durch Mischen (Merge Sort) . . . . . . . . . . . . . . . . . . . . . . . . . . . 6-9
¨
6.6 Ubung
6 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6-12
7
Pattern Matching
7-1
¨ Ausdrucke
7.1 Beschreiben von Pattern, Regulare
¨
. . . . . . . . . . . . . . . . . . . . . .
7-1
7.2 Endliche Automaten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7-3
¨
7.3 Automaten zu regularen
Ausdrucken
. . . . . . . . . . . . . . . . . . . . . . . . . . . . 7-7
¨
¨
7.4 Ubung
7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7-10
8
Top Down Parser
8.1 Kontextfreie Grammatik
8-1
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8-2
8.2 Top-Down Parser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8-7
¨
8.3 Ubung
8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8-14
9
Kryptologie
9-1
9.1 Grundlagen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9-2
9.2 Einfache Verschlusselungmethoden
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
¨
9-3
9.3 Vernamchiffre, One Time Pad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9-5
9.4 Moderne symmetrische Verfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9-6
9.5 Asymmetrische Verfahren: Public Key Kryptosysteme . . . . . . . . . . . . . . . . . . . 9-7
¨
9.6 Ubung
9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9-12
1
Einfuhrung
¨
1.1 Die wichtigsten Ziele dieses Kurses
Die wichtigsten Ziele des Algorithmen und Datenstrukturen Kurses sind:
¨
• Die Studierenden kennen die wichtigsten Datenstrukturen, konnen
damit arbeiten, und kennen deren
Vor- und Nachteile sowie deren Anwendungsgebiete.
¨
• Die Studierenden erhalten die Grundlagen, um wahrend
der Design Phase die richtigen Datenstruk¨
turen auszuwahlen
und dann richtig einzusetzen.
¨
• Die Studierenden kennen die wichtigsten Komplexitatsklassen
und deren Einfluss auf das Laufzeitverhalten eines Systems.
¨
• Die Studierenden kennen die klassischen Algorithmen und konnen
diese anwenden. Sie kennen
•
¨
deren Einsatzgebiete (wann soll welcher Algorithmus benutzt werden) und kennen die Komplexitat
¨
dieser Algorithmen (in Abhangigkeit
der darunterliegenden Datenstrukturen).
¨
¨
Die Studierenden erhalten einen Uberblick
uber
verschiedene Vorgehensweisen bei Problemlosun¨
¨
¨
gen und kennen deren Starken
und Schwachen.
1-2
1 Einfuhrung
¨
1.2 Einige Begriffe: Datenstrukturen
Definition: Daten sind Information, welche (maschinen-) lesbar und bearbeitbar sind und in einem Bedeutungskontext stehen. Die Information wird dazu in Zeichen oder Zeichenketten codiert. Die Codierung
¨ klarer Regeln, der sogenannten Syntax.
erfolgt gemass
Daten sind darum Informationen mit folgenden Eigenschaften:
¨ den semantischen Teil (die Bedeutung) des Datenobjekts.
1. Die Bezeichnung erklart
2. Die Wertemenge bestimmt die Syntax (die Form oder Codier-Regel) des Datenobjekts.
3. Der Speicherplatz lokalisiert das Datenobjekts im Speicher und identifiziert dieses eindeutig.
Definition: Ein Datentyp ist eine (endliche) Menge (der Wertebereich des Typs) zusammen mit einer
Anzahl Operationen.
Der Wertebereich eines Datentyps bestimmt, was fur
¨ Werte ein Objekt dieses Typs annehmen kann. Die
Elemente des Wertebereichs bezeichnet man auch als Konstanten des Datentyps.
¨
Dazu gehoren
die Methoden oder Operatoren, welche auf dem Wertebereich definiert sind und somit
¨
auf Objekte dieses Typs angewandt werden konnen.
Beispiel:
Der Wertebereich des Datentyps int besteht aus
Auf diesem Datentyp gibt es die Operationen
Es ist wichtig, dass wir zwischen der (abstrakten) Beschreibung eines Datentyps (Spezifikation) und
¨
dessen Implementierung unterscheiden. Wenn wir komplizierte Probleme losen
wollen, mussen
wir von
¨
¨
den Details abstrahieren konnen.
Wir wollen nicht wissen (mussen)
wie genau ein Datentyp implementiert
¨
¨
ist, sondern bloss, wie wir den Datentyp verwenden konnen
(welche Dienste er anbietet).
1.2 Einige Begriffe: Datenstrukturen
1-3
Jedes Objekt besitzt einen Datentyp, der bestimmt, welche Werte dieses Objekt annehmen kann und welche Operationen auf diesen Werten erlaubt sind. In allen Programmiersprachen gibt es nun Variablen,
¨
¨
welche diese Objekte reprasentieren
konnen.
Es stellt sich nun die Frage, ob auch den Variablen zwingend ein Datentyp zugewiesen werden soll. Diese Frage wird in verschiedenen Programmiersprachen
unterschiedlich beantwortet.
In untypisierten Sprachen wird den Variablen keinen Datentyp zugeordnet. Das heisst, jede Variable
¨
kann Objekte von einem beliebigen Typ reprasentieren.
Die Programmiersprachen Smalltalk und Lisp
¨
sind typische Reprasentanten
dieser Philosophie.
In untypisierten Sprachen kann der Compiler keine sogenannten Typentests durchfuhren.
Zur Kompi¨
¨
lationszeit sind alle Operationen auf allen Variablen moglich.
Es wird also zur Compilationszeit nicht
nachgepruft,
erlaubt sind. Unerlaubte Operationen fuhren
zu Lauf¨ ob gewisse Operationen uberhaupt
¨
¨
zeitfehlern.
In typisierten Sprachen wird allen Variablen ein Datentyp zugeordnet. Entweder mussen
alle Variablen
¨
deklariert werden, wie in den Sprachen Pascal, C, C++ oder Eiffel, oder der Datentyp wird aus der Notation der Variablen klar wie etwa in der Sprache Fortran oder Basic (in Basic sind Variablen, welche mit
dem Zeichen % enden, vom Typ Integer).
In einer typisierten Sprache kann schon der Compiler entscheiden, ob die angegebenen Operationen
typkorrekt sind oder nicht.
Als atomare Typen bezeichnen wir Datentypen, die in einer Sprache schon vordefiniert sind. Die atomaren Typen sind die grundlegenden Bausteine des Typsystems einer Programmiersprache. Aus diesen
¨
atomaren Typen konnen
mit Hilfe von Mengenoperationen (Subtypen, Kartesische Produkte, Listen, ...)
¨
¨
weitere Typen abgeleitet werden. Welche atomaren Typen zur Verfugung
stehen, hangt
von der gewahl¨
ten Programmiersprache ab.
In allen wichtigen Programmiersprachen existieren die atomaren Typen Integer (ganze Zahlen), Float (reelle Zahlen, Fliesskomma), Boolean (logische Werte) und Char (Schriftzeichen). Dabei ist zu bemerken,
dass diese atomaren Typen naturlich
nur eine endliche Teilmenge aus dem Bereich der ganzen, bzw. der
¨
¨
reellen Zahlen darstellen konnen.
1-4
1 Einfuhrung
¨
Beispiel: Der strukturierte Typ Array wird aus zwei gegebenen Datentypen, einem Grundtyp und einem
Indextyp konstruiert. Der Grundtyp ist ein beliebiger atomarer oder abgeleiteter Datentyp. Der Indextyp
ist normalerweise ein Subtyp (oder Intervall) des Typs int .
Auf Arrays ist immer ein Selektor definiert, welcher es erlaubt, ein einzelnes Element des Arrays zu lesen
oder zu schreiben.
Definition: Ein strukturierter Datentyp (eine Klasse) entsteht, wenn wir Elemente von beliebigen
Typen zu einer Einheit zusammenfassen. Ein solcher Typ ist formal gesprochen das kartesische Produkt
von beliebigen Datentypen.
DT = DT1 × DT2 × DT3 × . . . × DTn
¨
Die Datentypen DT1 , . . . , DTn konnen
atomare oder auch strukturierte Typen sein.
¨ ausserdem die Spezifikation der zugehorigen
¨
Dazu gehort
Operationen oder Methoden auf DT .
Beispiel: Wir definieren ein einfaches Interface PushButton als Basis fur
¨ einen Button auf einer Be¨
nutzeroberflache).
1.2 Einige Begriffe: Datenstrukturen
1-5
Abstrakter Datentyp
Der abstrakte Datentyp ist ein wichtiges Konzept in der modernen Informatik: Die Philosophie der objektorientierten Sprachen basiert genau auf dieser Idee. Der abstrakte Datentyp dient dazu, Datentypen
¨
unabhangig
von deren Implementation zu definieren.
Die Idee des abstrakten Datentyps beruht auf zwei wichtigen Prinzipien: dem Geheimnisprinzip und dem
Prinzip der Wiederverwendbarkeit.
Geheimnisprinzip: Dem Benutzer eines Datentyps werden nur die auf diesem Datentyp erlaubten Operationen (mit deren Spezifikation) bekanntgegeben. Die Implementation des Datentyps bleibt fur
¨ den
Benutzer verborgen (abstrakt, Kapselung).
Die Anwendung dieses Prinzips bringt folgende Vorteile:
¨
• Der Anwender kann den Datentyp nur im Sinne der Definition verwenden. Er hat keine Moglichkeit,
Eigenschaften einer speziellen Implementation auszunutzen.
¨
• Die Implementation eines Datentyps kann jederzeit verandert
werden, ohne dass die Benutzer des
Datentyps davon betroffen sind.
• Die Verantwortungen zwischen dem Anwender und dem Implementator des Datentyps sind durch
die Interface-Definitionen klar geregelt. Die Suche nach Fehlern wird dadurch erheblich vereinfacht.
Wiederverwendbarkeit: Ein Datentyp (Modul) soll in verschiedenen Applikationen wiederverwendbar
¨
¨ werden mussen.
sein, wenn ahnliche
Probleme gelost
¨
Die Idee hinter diesem Prinzip ist klar. Es geht darum, die Entwicklungszeit von Systemen zu reduzieren.
Das Ziel ist, Softwaresysteme gleich wie Hardwaresysteme zu bauen, das heisst, die einzelnen Komponenten eines Systems werden eingekauft, eventuell parametrisiert und zum Gesamtsystem verbunden.
1-6
1 Einfuhrung
¨
Ein abstrakter Datentyp definiert einen Datentyp nur mit Hilfe des Wertebereichs und der Menge der
Operationen auf diesem Bereich. Jede Operation ist definiert durch ihre Spezifikation, also die Input- und
Output-Parameter und die Vor- und Nachbedingungen.
¨
Die Datenstruktur ist dann eine Instanz eines (abstrakten) Datentyps. Sie beinhaltet also die Reprasentation der Daten und die Implementation von Prozeduren fur
¨ alle definierten Operatoren.
Wir sprechen hierbei auch von der logischen, bzw. der physikalischen Form von Datenelementen. Die
Definition des abstrakten Datentyps ist die logische, deren Implementation die physikalische Form des
Datenelements.
Der abstrakte Datentyp spezifiziert einen Typ nicht mit Hilfe einer Implementation, sondern nur als eine
Liste von Dienstleistungen, die der Datentyp dem Anwender zur Verfugung
stellt. Die Dienstleistungen
¨
nennt man auch Operationen, Methoden oder Funktionen.
Ein abstrakter Datentyp kann viele verschiedene Implementationen oder Darstellungen haben. Der abstrakte Datentyp gibt darum nicht an, wie die verschiedenen Operationen implementiert oder die Daten
¨
reprasentiert
sind. Diese Details bleiben vor dem Benutzer verborgen.
Beispiel: Der abstrakte Datentyp Stack wird durch die Menge der angebotenen Dienste definiert: Einfugen
eines Elements (push ), entfernen eines Elements (pop ), lesen des obersten Elements (peek ) und
¨
prufen
auf leer (empty ).
¨
Eine solche Beschreibung berucksichtigt
also nur, was ein Stack dem Anwender zu bieten hat.
¨
Bei den verschiedenen Methoden muss stehen, was die Methoden tun oder bewirken (Nachbedingung)
¨
und was fur
Vorbedingungen) an die Verwendung der Methoden
¨ Voraussetzungen (Einschrankungen,
gestellt sind.1
¨
In Java konnte
ein Interface fur
¨ einen Stack wie folgt aussehen:
1 Optimalerweise steht noch dabei, welchen Aufwand die Methode hat.
1.2 Einige Begriffe: Datenstrukturen
1-7
public interface Stack<E> {
/**
* Pushes an item onto the top of this stack.
* @param item
the item to be pushed onto this stack.
* @return the item argument.
*/
E push(E item);
/**
* Removes the object at the top of this stack and returns that
* object as the value of this function.
* @return
The object at the top of this stack (the last
*
item of the Vector object).
* @exception EmptyStackException if this stack is empty.
*/
E pop();
/**
* Looks at the object at the top of this stack without removing
* it from the stack.
* @return
the object at the top of this stack (the last
*
item of the Vector object).
* @exception EmptyStackException if this stack is empty.
*/
E peek();
/**
* Tests if this stack is empty.
* @return true if and only if this stack contains no items;
*/
boolean empty();
}
Bei den Methoden push und empty gibt es keine Vorbedingungen. Die Methoden pop und peek werfen
eine Runtime-Exception, wenn der Stack leer ist.
1-8
1 Einfuhrung
¨
¨
¨
Die Spezifikation eines Datentyps muss vollstandig,
prazise
und eindeutig sein. Weiter wollen wir keine
Beschreibung, die auf der konkreten Implementation des Datentyps basiert, obwohl diese die geforderten
¨
Kriterien erfullen
wurde.
Eine Beschreibung, die auf der Implementation basiert, fuhrt
zu einer Uberspe¨
¨
¨
zifikation des Datentyps.
¨
Konkret konnen
wir den Datentyp Stack zum Beispiel als Arraystruktur (mit einem Zeiger auf das aktuelle
oberste Element head des Stacks) implementieren. Flexibler ist allerdings die Implementation mit Hilfe
einer Listenstruktur..
1.3 Einige Begriffe: Algorithmen
1-9
1.3 Einige Begriffe: Algorithmen
Ein Algorithmus2 beschreibt das Vorgehen oder eine Methode, mit der eine Aufgabe oder ein Problem
¨ werden kann, bzw. mit der eine Funktion berechnet werden kann. Ein Algorithmus besteht aus
gelost
¨
einer Folge von einfachen (Rechen-)Schritten (Anweisungen), welche zur Losung
der gestellten Aufgabe
fuhren.
Der Algorithmusgedanke ist keine Besonderheit der Informatik. In fast allen Naturwissenschaften
¨
¨
aber auch im Alltag werden Arbeitsvorgange
mit Hilfe von Algorithmen beschrieben.
Jeder Algorithmus muss die folgenden Eigenschaften erfullen:
¨
1. Er muss aus einer Reihe von konkret ausfuhrbaren
¨
Schritten bestehen.
2. Er muss in einem endlichen Text beschreibbar sein.
¨
3. Er darf nur endlich viele Schritte benotigen
(Termination).
¨
4. Er darf zu jedem Zeitpunkt nur endlich viel Speicherplatz benotigen.
5. Er muss bei der gleichen Eingabe immer das selbe Ergebnis liefern.
¨
6. Nach der Ausfuhrung
eines Schrittes ist eindeutig festgelegt, welcher Schritt als nachstes
auszufuh¨
¨
ren ist.
7. Der vom Algorithmus berechnete Ausgabewert muss richtig sein (Korrektheit).
Bemerkung: Die Forderung nach Eindeutigkeit wird etwa in parallelen oder probabilistischen Algorith¨
men zum Teil fallengelassen. Nach dem Abschluss eines einzelnen Schrittes ist der nachste
Schritt nicht
¨
¨
eindeutig bestimmt, sondern es existiert eine endliche Menge von (moglichen) nachsten Schritten. Die
¨
Auswahl des nachsten
Schrittes aus der gegebenen Menge ist nichtdeterministisch.
Der Anspruch, dass alle Algorithmen terminieren mussen,
bedeutet, dass nicht alle von uns benutzten
¨
Programme Algorithmen sind. Editoren, Shells oder das Betriebssystem sind alles Programme, die nicht
(von selber) terminieren.
¨
Wir konnen
aber jedes dieser Programme als Sammlung von verschiedenen Algorithmen betrachten,
welche in verschiedenen Situationen zur Anwendung kommen.
¨ 825 vor Christus ein
ˆ
2 Das Wort Algorithmus stammt vom Persischen Autor Abu Ja’far Mohammed ibn Mus
ı, welcher ungefahr
ˆ aˆ al-Khowarizmˆ
Buch uber
arithmetische Regeln schrieb.
¨
1-10
1 Einfuhrung
¨
Algorithmen werden der Einfachheit halber oft in einer Pseudocode Sprache formuliert. Damit erspart
man sich alle technischen Probleme, welche die konkrete Umsetzung in eine Programmiersprache mit¨
bringen konnte.
¨
Beispiel: Grosster
gemeinsamer Teiler von m und n: Die kleinere der beiden Zahlen wird so lange von
¨
der grosseren
subtrahiert, bis beide Werte gleich sind. Dies ist dann der GgT von m und n.
Initialisiere m und n
Wiederhole solange m und n nicht gleich sind
Ja
Ist m > n ?
Verringere m um n
Verringere n um m
Gib m aus
Siehe auch [4]: Programmieren in Java, Kapitel 3.
Beispiele von Algorithmen in Java
int proc( int n )
{
return n/2;
}
bool isPrim( int n )
{
return false;
}
//
Nein
return true if n is a prime
1.3 Einige Begriffe: Algorithmen
¨
Das nachste
Beispiel stammt von L. Collatz (1937):
long stepNum( long n )
{
// return number of steps
long m = 0;
while( n > 1 )
{
if( n%2 == 0 ){ n = n/2; }
else { n = 3*n + 1; }
m++;
}
return m;
}
1-11
1-12
1 Einfuhrung
¨
1.4 Algorithmen Schema: Iteration
Unter einem Algorithmen-Schema verstehen wir ein Verfahrens-Muster oder eine allgemeine Methode,
¨ werden kann. Nicht jede Methode ist fur
mit welcher ein Problem gelost
¨ jedes Problem gleich gut geeignet. Umso wichtiger ist es also, die verschiedenen Algorithmen-Schemata zu kennen.
¨
¨
Ein Problem wird durch Iteration gelost,
falls der zugehorige
Algorithmus einen Loop (while- oder forSchleife) benutzt. Iteration ist zum Beispiel dann sinnvoll, wenn die Daten in einem Array (oder einer
3
Liste) abgelegt sind und wir mit jedem Element des Array die gleichen Schritte durchfuhren
mussen
.
¨
¨
Beispiel: Das Addieren zweier Vektoren kann wie folgt implementiert werden:
public DVector sum(DVector v1) throws VectorException {
if (v1.size != size)
throw new VectorException("Incompatible vector length");
DVector res = new DVector(size);
for (int i = 0; i < size; i++)
res.value[i] = v1.value[i] + value[i];
return res;
}
1.5 Algorithmen Schema: Greedy (die gierige Methode)
¨
¨
Greedy-Verfahren werden vor allem dann erfolgreich eingesetzt, wenn von n moglichen
Losungen
eines
¨
Problems die bezuglich
einer Bewertungsfunktion f optimale Losung
gesucht wird (Optimierungsproble¨
me).
Die Greedy-Methode arbeitet in Schritten, ohne mehr als einen Schritt voraus- oder zuruckzublicken.
Bei
¨
¨
jedem Schritt wird aus einer Menge von moglichen
Wegen derjenige ausgesucht, der den Bedingungen
des Problems genugt
¨ und lokal optimal ist.
3 Solche Algorithmen lassen sich oft auch sehr einfach parallelisieren.
1.5 Algorithmen Schema: Greedy (die gierige Methode)
1-13
Wir wollen die Arbeitsweise dieser Methode an einem anschaulichen Beispiel illustrieren. Wir nehmen
¨
an, dass jemand sich irgendwo auf einem Berg befindet und so schnell wie moglich
zum Gipfel kommen
¨
mochte.
Eine einfache Greedy-Strategie fur
¨ dieses Problem ist die folgende: Bewege dich immer entlang
¨
¨
der grossten
Steigung nach oben bis dies nicht mehr moglich
ist, das heisst, bis in allen Richtungen die
Wege nur noch nach unten fuhren.
¨
Dieser Ansatz ist in der Abbildung 1.1 dargestellt. Es ist ein typischer Greedy-Ansatz. Man schaut nicht
¨ jeweils die lokal optimale Strategie.
zuruck
¨ und wahlt
Abbildung 1.1: Hill climbing
Maximum
Lokales Maximum
Abbildung 1.2: Erreichen eines lokalen Maximums mit Greedy
In der Abbildung 1.2 sehen wir aber, dass diese Strategie nicht unbedingt zum (optimalen) Ziel fuhrt.
Hat
¨
der Berg mehrere Nebengipfel, so bleiben wir vielleicht auf einem solchen Nebengipfel stehen.
¨
¨
Bei Problemen dieser Art liefert oft nur ein exponentieller Algorithmus eine global beste Losung,
wahrend
4
¨
ein heuristischer Ansatz mit Greedy nicht immer die beste Losung
liefert, dies aber in polynomialer Zeit.
¨
Ahnliche
Probleme sind das Finden von kurzesten
Wegen, oder besten (Spiel-)Strategien, Verpackungs¨
¨
probleme (moglichst
viele verschieden grosse Kisten in einen Lastwagen packen) oder Scheduling von
verschieden langen Prozessen auf Mehrprozessor-Rechnern.
4 Eine Heuristik ist eine Richtlinie, ein generelles Prinzip oder eine Daumenregel, welche als Entscheidungshilfe benutzt werden kann.
1-14
1 Einfuhrung
¨
Ein weiteres Problem dieser Art ist das Suchen eines minimalen Pfades in einem allgemeinen Graphen.
¨
¨
Um eine optimale Losung
zu finden, mussten
wir im wesentlichen samtliche
Pfade abgehen und deren
¨
¨ das Problem viel schneller, indem er jeweils lokal
Gewichte aufschreiben. Ein Greedy-Algorithmus lost
¨
den kurzesten
(leichtesten) Pfad wahlt.
Allerdings findet man mit dieser Methode nicht unbedingt den
¨
insgesamt kurzesten
Pfad.
¨
Es existieren aber auch Probleme, bei denen der Greedy-Ansatz zum optimalen Ergebnis fuhrt.
Ein
¨
¨ das folgende Problem: Finde ein minimales (maximales) Gerust
Greedy-Algorithmus lost
¨ in einem ge¨ man jeweils die Kante, die das kleinste (grosste)
¨
wichteten Graphen. Dabei wahlt
Gewicht hat und keinen
¨
Zyklus verursacht. Der Algorithmus ist fertig, sobald ein zusammenhangender
Teilgraph entstanden ist.
9
9
3
3
7
5
6
6
5
5
x
7
5
4
4
6
8
7
6
8
7
3
5
1
8
8
7
8
8
4
3
5
1
4
y
3
7
9
5
x
1.6 Algorithmen Schema: Rekursion
1-15
1.6 Algorithmen Schema: Rekursion
Rekursion ist ein fundamentales Konzept der Informatik. Eine Prozedur heisst rekursiv, wenn sie sich
direkt oder indirekt selber aufruft. Dabei mussen
wir darauf achten, dass eine Abbruchbedingung existiert,
¨
damit die Prozedur in jedem Fall terminiert.
¨
Beispiele: Die rekursive Implementation der Fakultatsfunktion:
long factorial( int n )
{
if( n <= 1 )
return 1;
return n*factorial(--n);
}
Der rekursive Aufruf kann auch indirekt erfolgen:
int proc( int a, int b )
{
if( b-a < 5 )
return sub( b );
return a * proc(a-1, b/2)
}
int sub( int c )
{
if( c%2 == 0 )
return c*c;
return proc(c-2,c+1);
}
Bei einer rekursiven Prozedur sind die folgenden Punkte besonders zu beachten:
• Die Rekursion darf nicht unendlich sein. Es muss also in der Prozedur ein Instruktionszweig existie¨ Diesen Teil der Prozedur nennt man den Rekursionsren, der keinen Aufruf der Prozedur enthalt.
anfang. Bei indirekter Rekursion (Prozedur A ruft Prozedur B auf und B ruft wieder A auf) ist jeweils
besondere Vorsicht geboten.
• Es muss sichergestellt sein, dass die Anzahl der hintereinander ausgefuhrten
rekursiven Aufrufe
¨
(also die Rekursionstiefe) vernunftig
bleibt, da sonst zu viel Speicher verwendet wird. Beim rekursiven
¨
¨ sein.
Sortieren von n Elementen sollten zum Beispiel nur O(log(n)) rekursive Aufrufe notig
¨
• Rekursion soll dann angewandt werden, wenn die Formulierung der Losung
dadurch klarer und
¨
¨
kurzer
wird. Auch darf der Aufwand der rekursiven Losung
in der Ordnung nicht grosser
werden
¨
1-16
1 Einfuhrung
¨
¨
als der Aufwand der iterativen Losung.
Insbesondere kann die Rekursion leicht eliminiert werden,
¨ und dieser Aufruf die letzte Instruktion der
wenn die Prozedur nur einen rekursiven Aufruf enthalt
Prozedur ist (tail recursion, diese wird von einem optimierenden Compiler normalerweise automatisch eliminiert.)
Beispiel Die Fibonacci Funktion ist wie folgt definiert:
fibonacci(0)
= 1
fibonacci(1)
= 1
fibonacci(n + 2)
= fibonacci(n + 1) + fibonacci(n)
Diese Definition kann direkt in dieser Form als Rekursion implementiert werden:
Diese Implementierung fuhrt
zu einem exponentiellen Aufwand5 . Auf jeder Stufe sind zwei rekursive Auf¨
¨
¨
rufe notig,
welche jeweils unabhangig
voneinander die gleichen Funktionswerte berechnen. Eine bessere
¨
¨
Implementation (ohne Rekursion) benotigt
nur linearen Aufwand (vgl. Ubung).
1.6.1 Rekursionselimination
¨
Wie bereits vorher erwahnt,
soll Rekursion nur dann verwendet werden, wenn dadurch die Programme
¨ nicht grosser
¨
¨
einfacher lesbar werden und die Komplexitat
als die der iterativen Losung
ist.
¨
¨
Ist ein Problem durch eine (unnotig
aufwandige)
Rekursion formuliert, stellt sich die Frage, ob und wie
¨
sich die Rekursion allenfalls eliminieren lasst.
Prinzipiell kann dies durch folgendes Vorgehen versucht
werden:
¨
¨
5 Die Prozedur benotigt
zum Berechnen von fib(n) in der Grossenordnung
von 2·fib(n) rekursive Aufrufe.
1.6 Algorithmen Schema: Rekursion
1-17
Umdrehen der Berechnung (von unten nach oben).
Abspeichern der Zwischenresultate in einen Array, eine Liste oder einen Stack.
Beispiel: Gegeben ist die folgende rekursive Funktion, die wir in eine nichtrekursive Prozedur umschreiben wollen:
long rekFunction(int x, int y)
{
if( x <= 0 || y <= 0 )
return 0;
return x + y + rekFunction(x-1, y);
}
Etwas komplizierter wird die Rekursionselimination, wenn die Funktion, wie im folgenden Beispiel, von
¨
zwei Parametern abhangt:
long Pascal(int x, int y)
{
if( x <= 0 || y <= 0 )
return 1;
return Pascal(x-1, y) + Pascal(x, y-1);
}
1-18
1 Einfuhrung
¨
1.6.2 Divide and Conquer
¨
Die Divide and Conquer Methode (kurz: DAC) zerlegt das zu losende
Problem in kleinere Teilprobleme
¨
(divide) bis die Losung
der einzelnen Teilprobleme (conquer) einfach ist. Anschliessend werden die
¨
¨
Teillosungen
zur Gesamtlosung
vereinigt (merge)6 .
¨ werden,
Da das Problem in immer kleinere Teilprobleme zerlegt wird, welche alle auf die gleiche Art gelost
¨
ergibt sich normalerweise ein Losungsansatz
mit Rekursion.
Ein DAC-Algorithmus hat also folgende allgemeine Form:
void DAC( problem P ) {
¨
if( Losung
von P sehr einfach ) {
¨
return Losung(P)
// conquer
}
else {
divide( P, Teil1 , . . . ,Teiln );
return combine( DAC(Teil1 ),. . .,DAC(Teiln ) );
}
}
¨
DAC-Algorithmen konnen
grob in die beiden folgenden Kategorien unterteilt werden.
¨
• Das Aufteilen in Teilprobleme (divide) ist einfach, dafur
¨ ist das Zusammensetzen der Teillosungen
(merge) schwierig.
¨
• Das Aufteilen in Teilprobleme (divide) ist schwierig, dafur
¨ ist das Zusammensetzen der Teillosungen
(merge) einfach.
¨
Wenn sowohl das Aufteilen in Teilprobleme als auch das Zusammensetzen der Teillosungen
schwierig
ist, ist Divide and Conquer vermutlich nicht der richtige Ansatz.
¨
6 Das Divide and Conquer Schema eignet sich vor allem auch zum parallelen oder verteilten Losen
von Problemen.
1.6 Algorithmen Schema: Rekursion
1-19
Bekannte Beispiele fur
¨ Divide and Conquer sind die Sortieralgorithmen Quicksort und Mergesort.
¨ einem Pivotelement in verschiedene
Quicksort : (Hard Split Easy Join) Die Elemente werden gemass
Mengen aufgeteilt. Das Einsammeln ist dann trivial.
Mergesort : (Easy Split Hard Join) Die Elemente werden in beliebige (gleichgrosse) Mengen aufgeteilt.
Beim Einsammeln der verschiedenen (sortierten) Mengen muss nachsortiert werden.
void Sort( Menge P ) {
if( P besteht aus wenigen Elementen ) // zum Beispiel aus weniger als 10
{
verwende einfachen (linearen) Sortieralgorithmus und gib sortierte Menge zuruck
¨
}
else {
divide( P, Teil1 , . . . ,Teiln ); // Zerteile P in n Teile
// Fuge
die sortierten Mengen zusammen (trivial oder durch Nachsortieren).
¨
return merge( Sort(Teil1 ),. . .,Sort(Teiln ) );
}
}
1-20
1 Einfuhrung
¨
¨
1.7 Ubung
1
1. Nichtdeterministischer Primzahltest Das folgende Verfahren testet, ob ein Kandidat P eine Prim¨
¨
zahl ist: Wahlen
Sie eine genugend
grosse Menge beliebiger (zufalliger)
Zahlen zi und versuchen
¨
Sie nacheinander, P durch zi zu teilen. Falls keine der Zahlen zi ein Teiler ist, geben Sie true zuruck,
¨
andernfalls false.
Formulieren Sie fur
¨ das Verfahren einen Algorithmus in Pseudocode (Initialisierung, sequenzelle Anweisungen, if, while, ...)
2. Rekursionselimination: Gegeben ist die folgende Implementation der Fibonacci-Funktion:
public long fibonacci( int n ) {
if( n < 2 )
return 1;
return fibonacci(n-1) + fibonacci(n-2);
}
Finden Sie eine effizientere Implementierung ohne Rekursion fur
¨ die Berechnung der Fibonacci-Zahlen.
3. Rekursionselimination:
Eliminieren Sie aus den der folgenden Prozedur die Rekursion:
public long procRek(int n) {
if(n<=3)
return 2;
else
return 2*procRek(n-1) + procRek(n-2)/2 - procRek(n-3);
}
¨
4. Rekursion: Zahlen
der Knoten eines Baumes
Implementieren Sie eine Methode countNodes(), welche mit Hilfe einer Rekursion die Anzahl Knoten
eines Baumes berechnet.
Die Anzahl Knoten eines Baumes sind rekursiv wie folgt definiert: Wenn ein Baum nur aus einem
Blatt (leaf) besteht, dann gilt countNodes(leaf) = 1.
Sonst gilt countNodes(node) = 1 + sum(countNodes(c): c the children of node)
Rahmenprogramme finden Sie unter www.sws.bfh.ch/
∼amrhein/AlgoData/uebung1
2
¨ von Algorithmen
Komplexitat
¨
2.1 Komplexitatstheorie
¨
Nicht alle (mathematischen) Probleme (Funktionen) sind algorithmisch losbar
(berechenbar). Ausser¨
dem sind unter den berechenbaren Funktionen viele, deren Berechnung sehr aufwandig
und deshalb
undurchfuhrbar
ist.
¨
In diesem Abschnitt wollen wir nun die prinzipiell berechenbaren Probleme weiter unterteilen: in solche,
¨
die mit vernunftigem
Aufwand losbar
sind und die restlichen.
¨
Alle Funktionen
Berechenbare Funktionen
Durchführbare Algorithmen
Abbildung 2.1: Durchfuhrbare
Algorithmen
¨
¨
¨
Fur
Probleme ist es wichtig zu wissen, wieviele Betriebsmittel (Ressourcen) fur
¨ losbare
¨ ihre Losung
¨
erforderlich sind. Nur solche Algorithmen, die eine vertretbare Menge an Betriebsmitteln benotigen, sind
¨
tatsachlich
von praktischem Interesse.
¨ von Algorithmen
2 Komplexitat
2-2
¨
Die Komplexitatstheorie
stellt die Frage nach dem Gebrauch von Betriebsmitteln und versucht diese zu
beantworten. Normalerweise werden fur
¨ einen Algorithmus die Betriebsmittel Zeit- und Speicherbedarf
¨
untersucht. Mit Zeitbedarf meint man die Anzahl benotigter
Rechenschritte1 .
¨
2.2 Komplexitatsanalyse
¨
¨
Mit Hilfe der Komplexitatsanalyse
konnen
wir die Effizienz verschiedener Algorithmen vergleichen, bzw.
¨
versuchen zu entscheiden, ob ein Algorithmus das Problem im Allgemeinen innert nutzlicher
Frist lost.
¨
¨
¨
Eine Moglichkeit,
die Effizienz verschiedener Algorithmen zu vergleichen ware,
alle Algorithmen zu im¨
plementieren und die benotigte
Zeit und den Platzverbrauch zu messen. Allerdings ist dieses Verfahren
¨
¨ viel programmiert werden. Wir konnen
¨
¨
hochst
ineffektiv. Es muss unnotig
auch nicht einschatzen,
ob nicht
ein Algorithmus schlechter programmiert wurde als die anderen oder ob die Testbeispiele eventuell einen
2
Algorithmus begunstigen
.
¨
¨
¨
Auch mit Hilfe einer Komplexitatsanalyse
konnen
wir nicht wirklich entscheiden, ob ein Programm schnell
laufen wird. Vielleicht kann ein optimierender Compiler den einen Code besser unterstutzen
als den an¨
¨
¨
deren. Vielleicht sind gewisse Speicherzugriffe ubers
Netz notig,
die den Code langsam machen. Mogli¨
cherweise ist der Algorithmus auch einfach schlecht implementiert.
¨
Dennoch kann eine Komplexitatsanalyse
einen Hinweis geben, ob ein Algorithmus uberhaupt
prinzipi¨
¨
¨
¨
ell fur
der Anzahl notiger
Rechenschritte konnen
wir
¨ unser Problem in Frage kommt. Durch das Zahlen
zumindest verschiedene Algorithmen einigermassen fair vergleichen. Ein Rechenschritt besteht dabei
aus einer einfachen Operation, einer Zuweisung oder einem Vergleich (was normalerweise in einer Programmzeile steht).
Algorithmen nehmen Eingabedaten entgegen und fuhren
mit diesen eine Verarbeitung durch. Die Anzahl
¨
¨
¨
¨
Rechenschritte hangt
normalerweise von der Lange
(Grosse)
der Eingabedaten ab.
¨ gelost
¨ werden. Fur
• Ein Problem kann durch verschiedene Algorithmen mit verschiedener Komplexitat
¨
¨ werden mussen,
Probleme, welche sehr oft gelost
ist es von grossem Interesse, einen Algorithmus
¨
¨
zu finden, welcher moglichst
wenig Betriebsmittel erfordert.
¨ eines Algorithmus ist naturlich
¨
1 Die Komplexitat
unabhangig
von der Geschwindigkeit des verwendeten Computers.
¨
¨
2 Wir mussten
fairerweise alle moglichen
Eingaben testen, was naturlich
nicht machbar ist.
¨
¨
¨
2.2 Komplexitatsanalyse
2-3
¨ eines Algorithmus hangt
¨
¨
¨
• Die Komplexitat
von der Grosse
der Eingabedaten ab. Je grosser
die Di¨
mension n der Matrizen, desto langer
wird die Ausfuhrung
des Algorithmus dauern. Im Allgemeinen
¨
¨
¨ eines Algorithmus als Funktion der Lange
¨
konnen
wir die Komplexitat
der Eingabedaten angeben.
¨
Als Vereinfachung betrachten wir normalerweise nicht die (exakte) Lange
der Eingabe (zum Beispiel in
¨
Anzahl Bytes), sondern grossere,
fur
Einheiten. Man spricht dann von der naturli¨ das Problem naturliche
¨
¨
¨
¨
¨ eines Algorithmus
chen Lange
des Problems. Will man nur eine Grossenordnung
fur
¨ die Komplexitat
¨ man auch nicht alle Operationen, sondern nur diejenigen, welche fur
¨
angeben, so zahlt
des
¨ die Losung
Problems am wichtigsten (zeitintensivsten) sind. In der folgenden Tabelle sind Probleme mit ihrer naturli¨
¨
chen Lange
und ihren wichtigsten Operationen angegeben.
Problem
naturliche
Einheit
¨
Operationen
Algorithmen auf ganzen Zahlen
(z.B. Primzahlalgorithmen )
Suchalgorithmen
Sortieralgorithmen
Algorithmen auf reellen Zahlen
Matrix Algorithmen
Anzahl Ziffern
Operationen in
Anzahl Elemente
Anzahl Elemente
¨
Lange
der Eingabe
Dimension der Matrix
Vergleiche
Vergleiche, Vertauschungen
Operationen in IR
Operationen in IR
¨ der folgenden Prozeduren, indem wir die Anzahl Aufrufe von
Beispiel: Wir berechnen die Komplexitat
¨
¨
do something() (abhangig
vom Input n ) zahlen.
int proc1( int n )
{
int res = 0;
for( i = 0; i < n; i++ )
res = do_something(i);
return res;
}
int proc2( int n )
{
int res = 0;
for( i = 0; i < n; i++ )
for( j = 0; j < n; j++ )
res = do_something(i, j);
return res;
}
¨
¨
Wir verandern
die Prozedur etwas und berechnen wiederum die Komplexitat.
¨ von Algorithmen
2 Komplexitat
2-4
int proc3( int n )
{
int res = 0;
for( i = 0; i <= n; i++ )
for( j = 0; j <= i; j++ )
res = do_something(i, j);
return res;
}
¨
Wir zahlen
auch hier, wie oft do something()
aufgerufen wird.
2.2.1 Best-Case, Average-Case, Worst-Case Analyse
¨
Je nach Input kann die Anzahl benotigter
Rechenschritte sehr stark schwanken. Dies geschieht beispiels¨
weise dann, wenn die Prozedur Fallunterscheidungen (if/else) enthalt.
int proc3( int n )
{
int res = 1;
if( n % 2 == 0 )
return res;
for( int i=0; i<n; i++ )
res = do_something(res, i);
return res;
}
¨
2.3 Asymptotische Komplexitat
2-5
¨
¨
Bei Suchalgorithmen zahlen
wir die Anzahl notiger
Vergleiche.
public int indexOf( Object elem ) {
// lineare Suche, elem != null
for( int i=0; i < size; i++ ) {
if( elem.equals(elementData[i]) )
return i;
}
return -1;
}
n = size
int procRek( int n )
{
int res = do_something(n);
if( n <= 1 ) return res;
if( n % 2 == 0 )
return procRek(n/2);
return procRek(n+1);
}
¨
2.3 Asymptotische Komplexitat
¨ eines
Eine Vereinfachung ergibt sich, wenn man nur das asymptotische Verhalten der Komplexitat
¨
Algorithmus betrachtet. Das asymptotische Verhalten eines Polynoms entspricht dessem grossten
Term.
Konstante Faktoren werden dabei ignoriert.
Beispiel: In der Funktion
f (n) = 2n2 − 10n + 20
¨ fur
fallt
dem Ausdruck 2n2 immer weniger ins Gewicht.
¨ wachsendes n der Ausdruck 10n + 20 gegenuber
¨
Der dominierende Ausdruck ist in diesem Fall 2n2 .
¨ von Algorithmen
2 Komplexitat
2-6
10000
200
8000
150
6000
100
4000
50
0
2000
2
4
6
8
10
12
0
14
20
40
n
60
80
100
n
Das asymptotische Verhalten von f ist also n2 . Man schreibt auch f (n) ∈ O(n2 ) um das Wachstumsverhalten einer Funktion zu klassifizieren.
2
n
2n
1600
2
10n log(n)
1400
20 n
1200
1000
800
10 n
600
400
log(n)
200
10
20
30
40
50
Wir sagen, eine Funktion f hat exponentielles Wachstumsverhalten, wenn der dominierende Term von
f (n) von der Form kcn ist, f hat polynomiales Wachstum, falls er von der Form knc ist (c fest!), lineares
Wachstum, falls er von der Form kn ist und logarithmisches Wachstum, falls der dominierende Term von
der Form k log(n) ist.
¨
¨ nur das proportionale
Wie schon vorher erwahnt,
interessiert uns bei der asymptotischen Komplexitat
Verhalten. Die O-Notation gibt uns ein Mittel, dies mathematisch auszudrucken:
¨
¨
2.3 Asymptotische Komplexitat
2-7
Definition: [O-Notation] Eine Funktion f (n) ist aus O(g(n)), falls es Konstanten c und N gibt, so dass
fur
¨ alle m > N die Beziehung f (m) < cg(m) gilt.
Die Notation sagt genau das aus, was wir vorher schon etwas salopp formuliert hatten: Eine Funktion
¨ zu O(g(n)), falls sie (bis auf eine Konstante) nicht schneller wachst
¨
f (n) gehort
als g(n).
Man sagt auch, f hat das gleiche asymptotische Verhalten wie g.
¨
So gehoren
zum Beispiel die Funktionen 300n2 + 2n − 1, 10n + 12 und 5n3/2 + n
¨
Hingegen gehoren
die Funktionen 2n oder n3 nicht zu O(n2 ).
alle zu O(n2 ).
¨ nichts uber
Umgekehrt sagt das Wissen, dass eine Funktion f zu O(g) gehort,
die Konstanten c und
¨
¨
N aus. Diese konnen
sehr gross sein, was gleichbedeutend damit ist, dass ein Algorithmus mit dieser
¨ eventuell erst fur
(asymptotischen) Komplexitat
¨ sehr grosse Eingabewerte sinnvoll einsetzbar ist 3 .
Nachfolgend sind einige wichtige Regeln (ohne Beweis) angegeben:
• Die Ordnung des Logarithmus ist kleiner als die Ordnung einer linearen Funktion.
log(n) ∈ O(n) n ̸∈ O(log(n))
¨
• Die Ordnung eines Polynoms ist gleich der Ordnung des Terms mit der hochsten
Potenz.
ak nk + ak−1 nk−1 + . . . + a1 n + a0 ∈ O(nk )
• Fur
¨ zwei Funktionen f und g gilt:
O( f + g) = max{O( f ), O(g)}
O( f ∗ g) = O( f ) · O(g)
¨
• Die Ordnung der Exponentialfunktion ist grosser
als die Ordnung eines beliebigen Polynoms. Fur
¨ alle
c > 1 und k gilt:
cn ̸∈ O(nk )
3 Der FFT-Algorithmus fur
¨ Langzahlarithmetik ist zum Beispiel erst fur
¨ Zahlen, die mehrere hundert Stellen lang sind, interessant.
¨ von Algorithmen
2 Komplexitat
2-8
¨ der folgenden Prozeduren.
Beispiel: Wir berechnen die asymptotische Komplexitat
int proc4( int n )
{
int res = 0, m = n*n;
for( i = m; i > 1; i=i/2 )
res = do_something(res, i);
return res;
}
¨
Wir zahlen
wieder, wie oft do something()
aufgerufen wird:
¨
¨ im folgenden Beispiel. Der EinfachEine andere Methode benotigen
wir zum Berechnen der Komplexitat
heit halber nehmen wir an, n sei eine Zweierpotenz (n = 2k ).
int procRec( int n )
{
int res = 0;
if(n <= 1)
return res;
for( int i = 0; i < n; i++ )
res = do_something(res, i);
return procRec(n/2);
}
¨
Wir zahlen
wiederum, wie oft do something()
aufgerufen wird.
¨
2.4 Ubung
2
2-9
¨
2.4 Ubung
2
¨ von einfachen Prozeduren
1. Komplexiat
¨
¨ der Prozeduren 1 bis 4. Wie oft wird do something() aufgerufen? UberBerechnen Sie die Komplexitat
¨
¨
prufen
Sie Ihre Losungen,
indem Sie die Prozeduren in Java implementieren und einen Zahler
einbauen.
¨
void procedure1 ( int n )
{
for(int i=0; i<=n; i++)
do something(i,n);
for(int j=n; j>=0; j--)
do something(j,n);
}
void procedure2 ( int n )
{
for(int i=0; i<n; i++)
for(int j=0; j<2*i; j++)
do something(i,j,n);
}
void procedure3 ( int n )
{
for(int i=0; i<n; i++)
{
int j = 0;
while( j < 2*n )
{
j++;
do something(i,j,n);
}
}
}
void procedure4 ( int n )
{
int j=n;
while( j > 0 )
{
j = j/2;
do something(i,j,n);
}
}
¨ von Algorithmen
2 Komplexitat
2-10
¨ rekursiver Prozeduren
2. Komplexitat
¨ der folgenden rekursiven Prozeduren. Wie oft wird do something() ausBerechnen Sie die Komplexitat
¨
gefuhrt?
Wahlen
Sie fur
¨
¨ n eine Zweierpotenz: n = 2k .
void procRec1( int n )
{
if( n<=1 )
return;
int procRec2( int n, int res )
{
res = do_something(res, n);
if( n <= 1 )
return res;
res = procRec2(n/2, res);
res = procRec2(n/2, res);
return res;
do_something(n)
procRec1(n/2);
}
}
¨ verschiedener Java Methoden
3. Komplexitat
¨ der MeBestimmen Sie von den Java Klassen ArrayList und LinkedList die asymptotische Komplexitat
thoden
-
public
public
public
public
public
public
public
boolean contains(Object o)
E get(int index)
E set(int index, E element)
boolean add(E o)
void add(int index, E element)
E remove int(index)
boolean remove(Object o)
¨
Sie mussen
dazu die Algorithmen nicht im Detail verstehen. Es genugt,
¨
¨ die Iterationen (auch der benotig¨
¨
ten Hilfsfunktionen) zu zahlen
(wir werden diese Algorithmen in einem spateren
Kapitel noch genauer
betrachten).
3
Datentypen: Listen, Stacks und Queues
¨
Listen, Stacks und Queues konnen
entweder arraybasiert oder zeigerbasiert implementiert werden. Die
Implementierung mit Hilfe von Arrays hat den Vorteil, dass ein wahlfreier Zugriff besteht. Der Nachteil
¨
hingegen ist, dass wir schon zu Beginn wissen mussen,
wie viele Elemente die Liste maximal enthalt.
¨
¨
¨
¨
Viele Kopieraktionen sind notig,
wenn der gewahlte
Bereich zu klein gewahlt
wurde, oder wenn in der
¨
Mitte einer Liste ein Element eingefugt
werden soll.
¨ oder geloscht
Eine flexiblere Implementation bietet die Realisation von Listen mit Hilfe von Zeigerstrukturen.
3.1 Array Listen
In einer Array Liste werden die einzelnen Elemente (bzw. die Referenzen auf die Elemente) in einen Array
(vom generischen Typ E) abgelegt.
initialCapacity
E[ ] elementData
....
size
Der Vorteil von Array Listen ist der direkte Zugriff auf das n-te Element. Der Nachteil ist allerdings, dass
¨
bei jedem Einfugen
oder Loschen
von Elementen der Array (in sich) umkopiert werden muss. Ausserdem
¨
3-2
3
Datentypen: Listen, Stacks und Queues
¨
muss der Array in einen neuen, grosseren
Array umkopiert werden, sobald die initiale Anzahl Elemente
uberschritten
wird.
¨
Die ArrayList benutzt also einen Array von (Zeigern auf) Elementen E als Datenspeicher:
public class ArrayList<E> extends AbstractList<E> {
private Object[] elementData;
private int size; // The number of elements.
/** Constructs an empty list with the specified initial capacity. */
public ArrayList(int initialCapacity) { ... }
/** Returns true if this list contains no elements.
public boolean isEmpty() { ... }
*/
/** Returns the index of the first occurrence of the specified element. */
public int indexOf(Object elem) { ... }
/** Returns the element at the specified position in this list. */
public E get(int index) { ... }
/**
Inserts the element at the specified position in this list.
Shifts any subsequent elements to the right. */
public void add(int index, E element) { ... }
/**
Removes the element at the specified position in this list.
Shifts any subsequent elements to the left. */
public E remove(int index) { ... }
/** Increases the capacity of this ArrayList instance. */
public void ensureCapacity(int minCapacity) { ... }
...
}
3.1 Array Listen
3-3
¨
Im Konstruktor wird der elementData Array mit Lange
initialCapacity initialisiert:
public ArrayList(int initialCapacity) {
if (initialCapacity < 0) throw new IllegalArgumentException( ... );
elementData = new Object[initialCapacity];
}
Der Zugriff auf ein Element an einer gegebenen Stelle ist direkt und damit sehr schnell.
public E get(int index) {
if (index >= size || index < 0) throw new IndexOutOfBoundsException( ... );
return (E) elementData[index];
}
¨
Das Einfugen
von neuen Elementen in den Array hingegen ist aufwandig,
da der hintere Teil des Array
¨
umkopiert werden muss.
arrayCopy
....
add
public void add(int index, E element) {
if (index > size || index < 0) throw new IndexOutOfBoundsException(...)
ensureCapacityInternal(size + 1);
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
3-4
3
Datentypen: Listen, Stacks und Queues
¨
¨
Das Gleiche gilt fur
von Elementen aus einer ArrayList. Alle Elemente hinter dem geloschten
¨ das Loschen
Element mussen
umkopiert werden.
¨
public E remove(int index) {
if (index >= size || index < 0) throw new IndexOutOfBoundsException( ... );
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData,
index, numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
Sobald der aktuell angelegte Array voll ist, muss ein neuer Datenspeicher angelegt und der gesamte
Array umkopiert werden.
public void ensureCapacity(int minCapacity) {
if (minCapacity - elementData.length > 0) // -> grow
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// copy all elements to new (larger) memory area
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
3.2 Doppelt verkettete Listen
3-5
3.2 Doppelt verkettete Listen
In einer doppelt verketteten Liste besteht jedes Listenelement aus einem Datenfeld (bzw. einer Referenz
auf ein Datenfeld) (element) und zwei Zeigern (next und prev).
Als Listenelemente dient die Klasse Node.
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Die Klasse Node ist eine innere Klasse von List und wird einzig zum Verpacken der Datenelemente
bentutzt.
3-6
3
Datentypen: Listen, Stacks und Queues
Eine (doppelt) verkettete Liste entsteht dann durch Zusammenfugen
einzelner Node Elemente. Beson¨
dere Node Elemente bezeichnen dabei den Listenanfang und das Ende.
Die Definition einer Liste sieht dann zum Beispiel wie folgt aus:
public class
transient
transient
transient
LinkedList<E> {
Node<E> first;
Node<E> last;
int size = 0;
/**
* Returns true if this list contains no elements.
*/
boolean isEmpty(){ ... };
/**
* Returns the element at the specified position in this list.
* Throws IndexOutOfBoundsException if the index is out of range.
*/
E get(int index){ ... };
/**
* Inserts the element at the specified position in this list.
* Throws IndexOutOfBoundsException if the index is out of range.
*/
void add(int index, E element){ ... };
/**
* Removes the element at position index in this list.
* Returns the element previously at the specified position.
* Throws IndexOutOfBoundsException if the index is out of range.
3.2 Doppelt verkettete Listen
*/
E remove(int index){ ... };
/**
* Returns the index of the first occurrence of the specified
* element, or -1 if this list does not contain this element.
*/
int indexOf(Object o){ ... };
. . .
}
¨
Wir betrachten hier je eine Implementation fur
¨
und fur
eines Elementes.
¨ das Einfugen
¨ das Loschen
Suchen einer bestimmten Stelle
Node<E> node(int index) {
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
3-7
3-8
Einfugen
¨
an einer bestimmten Stelle
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
}
3
Datentypen: Listen, Stacks und Queues
3.2 Doppelt verkettete Listen
void linkBefore(E e, Node<E> succ) {
final Node<E> pred = succ.prev;
// assert succ != null;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
}
void addFirst(E e) {
// oder linkFirst
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
}
public void add(E e) {
// add at the end
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
}
Am effizientesten ist also das nicht-sortierte Einfugen,
das heisst am Ende oder am Anfang.
¨
3-9
3-10
3
Datentypen: Listen, Stacks und Queues
¨
Loschen
¨
Beim Loschen
von Elementen muss gepruft
¨ werden, ob ev. first und/oder last korrigiert werden mussen.
¨
public E remove(int index) {
if(index >= 0 && index < size)
return unlink(node(index));
else
throw new IndexOutOfBoundsException( ... );
}
E unlink(Node<E> x) {
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
}
x.item = null;
size--;
return element;
}
x.prev = null;
x.next = null;
3.3 Stacks und Queues
3-11
3.3 Stacks und Queues
Ein Interface fur
¨ einen Stack hatten wir im Abschnitt 1.2 bereits gesehen. Stacks sind einfache Listen¨
strukturen, bei denen bloss am Kopf Elemente eingefugt,
gelesen, bzw. geloscht
werden durfen.
¨
¨
Wir betrachten hier die Implementation eines Stacks mit Hilfe von Zeigerstrukturen.
public class Stack<E> {
private Node<E> first;
private int size = 0;
public E push(E item) {
first = new Node<E>(item, first);
size++;
return item;
}
public E pop() {
if (size==0) throw new EmptyStackException();
Node<E> e = first;
E result = e.element;
first = e.next;
e.element = null;
e.next = null;
size--;
return result;
}
public E peek() {
if (size==0) throw new EmptyStackException();
return first.element;
3-12
3
Datentypen: Listen, Stacks und Queues
}
public boolean empty() {
return size == 0;
}
private static class Node<E> {
E element;
Node<E> next;
Node(E element, Node<E> next) {
this.element = element;
this.next = next;
}
}
}
¨
¨
In einer Queue konnen
Elemente nur am Ende angefugt
Ele¨ werden. Nur am Kopf der Queue konnen
¨
mente gelesen, bzw. geloscht
werden.
3.4 Iteratoren
3-13
3.4 Iteratoren
Auf Listenstrukturen hat man ublicherweise
eine Hilfsklasse, welche zum Durchlaufen der Liste dient. Die
¨
zwei wichtigsten Methoden von Iterator Klassen sind hasNext zum Prufen,
ob das Ende der Liste erreicht
¨
¨
ist, sowie die Methode next, welche den Inhalt des nachsten
Elements zuruckgibt.
¨
public interface Iterator<E> {
/**
* Returns true if the iteration has more elements.
*/
boolean hasNext();
/**
* Returns the next element in the iteration.
* @exception NoSuchElementException iteration has no more elements.
*/
E next();
...
}
3-14
3
Datentypen: Listen, Stacks und Queues
¨
3.5 Ubung
3
Fur
¨ die Implementationsaufgabe finden Sie Rahmenprogramme unter
www.sws.bfh.ch/∼amrhein/AlgoData/
1. List Iterator
Entwerfen Sie (ausgehend vom Rahmenprogramm) eine innere Klasse ListIterator, welche als Iterator fur
¨ die LinkedList verwendet werden kann.
Implementieren Sie dazu in der LinkedList Klasse eine innere Klasse ListIterator mit einem Konstruktor ListIterator(int index), welcher ein ListIterator Objekt erzeugt, welches an die Position index zeigt.
Implementieren Sie ausserdem die Methoden E next(), boolean hasNext(), boolean hasPrevious()
und E previous().
2. Queue
¨ dem gegebenen Interface (vgl. Rahmenprogramm).
Implementieren Sie eine Klasse Queue gemass
-
¨ LinkedList, aber ohne die Methoden der
Implementieren Sie die Queue zuerst als Liste (gemass
LinkedList zu benutzen).
-
¨ ArrayList, aber ohne die Methoden
Als zweites implemtieren Sie die Queue als Array (gemass
der ArrayList zu benutzen). In der Array-basierten Queue durfen
Sie annehmen, dass die Queue
¨
¨
nicht mehr als MAX viele Elemente enthalten muss. Uberlegen
Sie sich eine Implementierung,
¨
welche nicht nach jedem Einfugen
oder Loschen
den ganzen Array umkopiert.
¨
3. Das Collection Interface
Zeichnen Sie die Klassenhierarchie der (wichtigsten) Collection Klassen.
Zeichnen Sie die Hierarchie der Interfaces List, Queue, Set und SortedSet, sowie der Klassen ArrayList, HashSet, LinkedHashSet, LinkedList, PriorityQueue, Stack, TreeSet, Vector
4
¨
Datentypen: Baume,
Heaps
¨
Alle bisher betrachteten Strukturen waren linear in dem Sinn, dass jedes Element hochstens
einen Nach¨
folger hat. In einem Baum kann jedes Element keinen, einen oder beliebig viele Nachfolger haben. Baume
sind wichtig als Strukturen in der Informatik, da sie auch oft im Alltag auftauchen: zum Darstellen von
¨
Abhangigkeiten
oder Strukturen, als Organigramme von Firmen, als Familienstammbaum, aber auch
zum Beschleunigen der Suche.
Definition: Ein Graph ist definiert als ein Paar B = (E, K) bestehend aus je einer endlichen Menge E
von Ecken (Knoten, Punkten) und einer Menge von Kanten. Eine Kante wird dargestellt als Zweiermenge
von Ecken {x, y}, den Endpunkten der Kante.
¨
¨
Ein Baum ist ein Graph mit der zusatzliche
Einschrankung,
dass es zwischen zwei Ecken nur eine (direkte oder indirekte) Verbindung gibt1 .
¨
¨
Wir befassen uns hier zuerst vor allem mit einer besonderen Art von Baumen:
den Binarbaumen.
Ein
¨ falls jeder Knoten hochstens
¨
Baum heisst binar,
zwei Nachfolger hat.
¨
1 Ein Baum ist ein zusammenhangender
Graph ohne Zyklen.
4-2
.
¨
4 Datentypen: Baume,
Heaps
.
.
. . . .
..
. ..
¨
Definition: Ein binarer
Baum besteht aus einer Wurzel (Root) und (endlich vielen) weiteren Knoten und
verbindenden Kanten dazwischen. Jeder Knoten hat entweder keine, ein oder zwei Nachfolgerknoten.
¨
Ein Weg in einem Baum ist eine Liste von disjunkten, direkt verbunden Kanten. Ein binarer
Baum ist
¨
¨
¨
vollstandig
(von der Hohe
n), falls alle inneren Knoten zwei Nachfolger haben und die Blatter
maximal
¨
Weglange
n bis zur Wurzel haben.
¨
Jedem Knoten ist eine Ebene (level) im Baum zugeordnet. Die Ebene eines Knotens ist die Lange
des
¨
Pfades von diesem Knoten bis zur Wurzel. Die Hohe
(height) eines Baums ist die maximale Ebene, auf
der sich Knoten befinden.
¨
Ein binarer
Baum besteht also aus Knoten mit einem (Zeiger auf ein) Datenelement data , einem linken
Nachfolgerknoten left und einem rechten Nachfolgerknoten right .
left
public class BinaryTreeNode<T> {
protected T data;
protected BinaryTreeNode<T> leftChild;
protected BinaryTreeNode<T> rightChild;
right
4-3
public BinaryTreeNode(T item){ data=item; }
// tree traversals
public BinaryTreeNode<T> inOrderFind(T item) { . . . }
public BinaryTreeNode<T> postOrderFind(T item) { . . . }
public BinaryTreeNode<T> preOrderFind(T item) { . . .}
// getter and setter methods
. . .
public class BinaryTree<T> {
protected BinaryTreeNode<T> rootTreeNode;
public BinaryTree(BinaryTreeNode<T> root) {
this.rootTreeNode = root;
}
// tree traversals
public BinaryTreeNode<T> inOrderFind(T item) {
return rootTreeNode.inOrderFind(item);
}
public BinaryTreeNode<T> preOrderFind(T item) { ... }
public BinaryTreeNode<T> postOrderFind(T item) { ... }
public BinaryTreeNode<T> postOrderFindStack(T item) { ... }
//getter and setter methods
. . .
4-4
¨
4 Datentypen: Baume,
Heaps
¨
4.1 Baumdurchlaufe
¨
¨
Baume
konnen
auf verschiedene Arten durchlaufen werden. Die bekanntesten Verfahren sind Tiefensuche (depth-first-search, DFS) und Breitensuche (breadth-first-search, BFS). Tiefensuche kann unter¨
¨
schieden werden in die drei Typen praorder,
postorder und inorder, abhangig
von der Reihenfolge der
rekursiven Aufrufe.
4.1.1 Tiefensuche
¨
Praorder
• Betrachte zuerst den Knoten (die Wurzel des Teilbaums),
• durchsuche dann den linken Teilbaum,
• durchsuche zuletzt den rechten Teilbaum.
Inorder
• Durchsuche zuerst den linken Teilbaum,
• betrachte dann den Knoten,
• durchsuche zuletzt den rechten Teilbaum.
Postorder
• Durchsuche zuerst den linken Teilbaum,
• durchsuche dann den rechten Teilbaum,
• betrachte zuletzt den Knoten.
¨
4.1 Baumdurchlaufe
.
. .
..
.
.
.
. . . .
..
. ..
.
4-5
.
. .
. ..
.
.
.
. . . .
..
. ..
¨
Wir betrachten als Beispiel fur
¨ die Tiefensuche den Praorder-Durchlauf.
public BinaryTreeNode<T> preOrderFind(T item) {
if (data.equals(item))
return this;
if (leftChild != null) {
BinaryTreeNode<T> result = leftChild.preOrderFind(item);
if (result != null)
return result;
}
if (rightChild != null) {
BinaryTreeNode<T> result = rightChild.preOrderFind(item);
if (result != null)
return result;
}
return null;
4-6
¨
4 Datentypen: Baume,
Heaps
}
4.1.2 Tiefensuche mit Hilfe eines Stacks
¨
¨
Mit Hilfe eines Stacks konnen
wir die rekursiven Aufrufe in der praorder
Tiefensuche vermeiden. Auf dem
¨
Stack werden die spater
zu behandelnden Baumknoten zwischengespeichert.
public BinaryTreeNode<T> preOrderFindStack(T item) {
Stack<BinaryTreeNode<T>> stack = new Stack<BinaryTreeNode<T>>();
stack.push(this.rootTreeNode);
while (!stack.isEmpty()) {
BinaryTreeNode<T> tmp = stack.pop();
if (tmp.getData().equals(item))
return tmp;
if (tmp.getRightChild() != null)
stack.push(tmp.getRightChild());
if (tmp.getLeftChild() != null)
stack.push(tmp.getLeftChild());
}
return null;
}
¨
4.1 Baumdurchlaufe
4-7
4.1.3 Breitensuche mit Hilfe einer Queue
Bei der Breitensuche besucht man jeweils nacheinander die Knoten der gleichen Ebene:
• Starte bei der Wurzel (Ebene 0).
¨
¨
• Bis die Hohe
des Baumes erreicht ist, setze den Level um eines hoher
und gehe von links nach
rechts durch alle Knoten dieser Ebene.
.
.
.
. . . .
..
. ..
Bei diesem Verfahren geht man nicht zuerst in die Tiefe, sondern betrachtet von der Wurzel aus zuerst
¨
alle Elemente in der naheren
Umgebung. Um mittels Breitensuche (levelorder) durch einen Baum zu
wandern, mussen
wir uns alle Baumknoten einer Ebene merken. Diese Knoten speichern wir in einer
¨
¨
¨
Queue ab, so dass wir spater
darauf zuruckgreifen
konnen.
¨
public BinaryTreeNode<T> levelOrderFind(T item) {
QueueImpl<BinaryTreeNode<T>> queue = new QueueImpl<BinaryTreeNode<T>>();
queue.add(rootTreeNode);
while (!queue.isEmpty()) {
BinaryTreeNode<T> tmp = queue.poll();
if (tmp.getData().equals(item))
return tmp;
if (tmp.getLeftChild() != null)
queue.add(tmp.getLeftChild());
if (tmp.getRightChild() != null)
queue.add(tmp.getRightChild());
}
return null;
}
¨
4 Datentypen: Baume,
Heaps
4-8
¨ Suchbaume
¨
4.2 Binare
¨
¨
Ein binarer
Suchbaum ist ein Baum, welcher folgende zusatzliche
Eigenschaft hat:
Alle Werte des linken Nachfolger-Baumes eines Knotens K sind kleiner, alle Werte des rechten
¨
Nachfolger-Baumes von K sind grosser
als der Wert von K selber.
¨
¨
Der grosse Vorteil von binaren
Suchbaumen
ist, dass wir sowohl beim Einfugen
als auch beim Suchen
¨
von Elementen immer bloss einen der zwei Nachfolger untersuchen mussen.
Falls der gesuchte Wert
¨
kleiner ist als der Wert des Knotens, suchen wir im linken Teilbaum, anderenfalls im rechten Teilbaum
weiter.
¨
Beispiel: Die folgenden zwei Baume
entstehen durch Einfugen
der Zahlen 37, 43, 53, 11, 23, 5, 17, 67,
¨
47 und 41 in einen leeren Baum. Einmal werden die Zahlen von vorne nach hinten eingefugt,
¨ das zweite
Mal von hinten nach vorne.
¨ Suchbaume
¨
4.2 Binare
public class BinarySearchTreeNode <T extends Comparable<T>> {
public void add(T item) {
int compare = data.compareTo(item);
if (compare > 0) {
// (data > item)?
if (leftChild == null)
leftChild = new BinarySearchTreeNode<T>(item);
else
leftChild.add(item);
// left recursion
} else {
// (item >= data)
if (rightChild == null)
rightChild = new BinarySearchTreeNode<T>(item);
else
rightChild.add(item);
// right recursion
}
}
public BinarySearchTreeNode<T> find(T item) {
int compare = data.compareTo(item);
if (compare == 0)
return this;
if (compare > 0 && leftChild != null) // data > item
return leftChild.find(item);
if (compare < 0 && rightChild != null) // data < item
return rightChild.find(item);
return null;
}
. . .
}
4-9
¨
4 Datentypen: Baume,
Heaps
4-10
¨
4.3 B-Baume
¨
¨
Ein B-Baum ist ein stets vollstandig
balancierter und sortierter Baum. Ein Baum ist vollstandig
balanciert,
¨
wenn alle Aste gleich lang sind. In einem B-Baum darf die Anzahl Kindknoten variieren. Ein 3-4-5 BBaum ist zum Beispiel ein Baum, in welchem jeder Knoten maximal 4 Datenelemente speichern und
¨
jeder Knoten (ausser der Wurzel und den Blattern)
minimal 3 und maximal 5 Nachfolger haben darf (der
¨
Wurzelknoten hat 0-4 Nachfolger, Blatter
haben keine Nachfolger).
¨
¨
Durch die flexiblere Anzahl Kindknoten ist das Rebalancing weniger haufig
notig.
Ein Knoten eines B-Baumes speichert:
•
•
•
•
eine variable Anzahl s von aufsteigend sortierten Daten-Elementen k1 , . . . , ks
eine Markierung isLeaf, die angibt, ob es sich bei dem Knoten um ein Blatt handelt.
s + 1 Referenzen auf Kindknoten, falls der Knoten kein Blatt ist.
¨
Jeder Kindknoten ist immer mindestens zur Halfte
gefullt.
¨
Die letzte Bedingung lautet formal: es gibt eine Schranke m, so dass m <= s <= 2m gilt. Das heisst,
¨
jeder Kindknoten hat mindestens m, aber hochstens
2m Daten-Elemente.
Die Werte von k1 , . . . , ks dienen dabei als Splitter. Die Daten-Elemente der Kindknoten ganz links mussen
¨
¨
kleiner sein als k1 , diejenigen ganz rechts grosser
als ks . Dazwischen mussen
die Daten-Elemente des
¨
¨
i-ten Kindes grosser
als ki und kleiner als ki+1 sein.
Das folgende Bild zeigt einen B-Baum mit m gleich 2. Jeder innere Knoten hat also mindestens 2 und
maximal 5 Nachfolger.
¨
4.3 B-Baume
4-11
¨
Operationen in B-Baumen
Suchen
¨
Die Suche nach einem Datenelement e lauft
in folgenden Schritten ab: Beginne bei der Wurzel als aktuellen Suchknoten k.
¨
• Suche in k von links her die Position p des ersten Daten-Elementes x, welches grosser
oder gleich e
ist.
•
•
•
•
Falls alle Daten-Elemente von k kleiner sind als e, fuhre
die Suche im Kindknoten ganz rechts weiter.
¨
Falls x gleich e ist, ist die Suche zu Ende.
Anderfalls wird die Suche beim p-ten Kindelement von k weitergefuhrt.
¨
Falls k ein Blatt ist, kann die Suche abgebrochen werden (fail).
Einfugen
¨
Beim Einfugen
muss jeweils beachtet werden, dass nicht mehr als 2m Daten-Elemente in einem Knoten
¨
¨
untergebracht werden konnen.
¨
Zunachst
wird das Blatt gesucht, in welches das neue Element eingefugt
Dabei kann
¨ werden musste.
¨
gleich wie beim Suchen vorgegegangen werden, ausser dass wir immer bis zur Blatt-Tiefe weitersuchen
(sogar, wenn wir den Wert unterwegs gefunden haben). Falls es in dem gesuchten Blatt einen freien
Platz hat, wird der Wert dort eingefugt.
¨
Einfugen
des Werts 31 in den folgenden Baum:
¨
4-12
¨
4 Datentypen: Baume,
Heaps
Der Wert 31 sollte in das Blatt (30,34,40,44) eingefugt
¨ werden. Dieses ist aber bereits voll, muss also
¨
aufgeteilt werden. Dies fuhrt
dazu, dass der Wert in der Mitte (34) in den VorgangerKnoten verschoben
¨
wird. Da das alte Blatt ganz rechts vom Knoten (20,28) liegt, wird der Wert 34 rechts angefugt
¨ (neuer,
¨
¨ dieser Knoten neu 3 Werte und 4 Nachfolger.
grosster
Wert dieses Knotens). Damit erhalt
Dieser Prozess muss eventuell mehrmals (in Richtung Wurzel) wiederholt werden, falls durch das Hoch¨
¨
schieben des Elements jeweils der Vorganger-Knoten
ebenfalls uberl
auft.
¨
¨
4.3 B-Baume
4-13
¨
Loschen
von Elementen
¨
Beim Loschen
eines Elementes muss umgekehrt beachtet werden, dass jeder Knoten nicht weniger als
m Datenelemente enthalten muss.
¨
Falls das geloschte
Element in einem Blatt liegt, welches mehr als m Datenelemente hat, kann das
¨
¨
Element einfach geloscht
werden. Andernfalls konnen
entweder Elemente vom benachbarte Blatt ver¨
schoben oder (falls zu wenig Elemente vorhanden sind) zwei Blatter
verschmolzen werden.
¨
Verschiebung Aus dem linken B-Baum soll das Element 18 geloscht
werden. Dies wurde
dazu fuhren,
¨
¨
dass das linke Blatt zu wenig Datenelemente hat. Darum wird aus dem rechten Nachbarn das kleinste
¨
Element nach oben, und das Splitter-Element des Vorgangers
in das linke Blatt verschoben.
¨
¨
Analog konnte
(falls vorhanden) aus einem linken Nachbarn das grosste
Element verschoben werden.
¨
Falls ein Element eines inneren Knotens (z.B. das Element 34) geloscht
wird, muss entweder von den
¨
linken Nachfolgern das grosste,
oder von den rechten Nachfolgern das kleinste Element nach oben verschoben werden, damit weiterhin genugend
Elemente (als Splitter) vorhanden sind, und die Ordnung
¨
bewahrt wird.
4-14
¨
4 Datentypen: Baume,
Heaps
¨
Verschmelzung Aus dem linken B-Baum soll das Element 60 geloscht
werden. Dies wurde
dazu fuhren,
¨
¨
dass das mittlere Blatt zu wenig Datenelemente hat. Weder der rechte noch der linke Nachbar hat
¨
genugend
Elemente, um eine Verschiebung durch zu fuhren
- es mussen
zwei Blatter
verschmolzen
¨
¨
¨
werden.
¨ vom mittleren Blatt das Element 55, sowie von der Wurzel das Element 50. Die
Das linke Blatt erhalt
Wurzel muss ebenfalls ein Element abgeben, da nach der Verschmelzung bloss noch 2 Nachfolge-Knoten
¨
existieren. Das rechte Blatt bleibt unverandert.
¨
Mit Hilfe der Verschiebung- und Verschmelzungs-Operation konnen
wir nun beliebige Elemente aus ei¨
nem B-Baum loschen.
Beispiel
¨
Aus dem folgenden Baum loschen
wir zuerst das Element 75, danach das Element 85:
4.4 Priority Queues
4-15
4.4 Priority Queues
¨
In vielen Applikationen will man die verschiedenen Elemente in einer bestimmten Reihenfolge (Prioritat)
¨
¨
abarbeiten. Allerdings will man das (aufwandige!)
Sortieren dieser Elemente nach moglichkeit
vermeiden.
¨
Eine der bekanntesten Anwendungen in diesem Umfeld sind Scheduling-Algorithmen mit Prioritaten.
Alle
¨ ihrer Prioritat
¨ in einer Priority Queue gesammelt, so dass immer das Element
Prozesse werden gemass
¨
¨ verfugbar
mit hochster
Prioritat
ist. Priority Queues haben aber noch weit mehr Anwendungen, zum
¨
Beispiel bei Filekomprimierungs- oder bei Graph-Algorithmen.
¨
Eine elegante Moglichkeit
der Implementierung einer Priority Queue ist mit Hilfe eines Heaps.
4.4.1 Heaps
¨
¨
Ein Heap ist ein (fast) vollstandiger
Baum, in welchem nur in der untersten Ebene von rechts her Blatter
fehlen durfen.
¨
65
56
52
37
25
48
31
18
45
6
3
15
¨
4 Datentypen: Baume,
Heaps
4-16
¨
¨
Definition: [Heap] Ein Heap ist ein vollstandiger
binarer
Baum, dem nur in der untersten Ebene ganz
¨
rechts Blatter
fehlen durfen
mit folgenden Zusatzeigenschaften.
¨
¨ und eventuell noch weitere Daten.
1. Jeder Knoten im Baum besitzt eine Prioritat
¨ eines Knotens ist immer grosser
¨
¨ der Nachkommen.
2. Die Prioritat
als (oder gleich wie) die Prioritat
Diese Bedingung heisst Heapbedingung.
¨
¨
Aus der Definition kann sofort abgelesen werden, dass die Wurzel des Baumes die hochste
Prioritat
¨
¨
¨
besitzt. Weil der Heap im wesentlichen ein vollstandiger
binarer
Baum ist, lasst
er sich einfach als Array2
implementieren. Wir numerieren die Knoten des Baumes von oben nach unten und von links nach rechts.
Die so erhaltene Nummerierung ergibt fur
¨ jeden Knoten seinen Index im Array.
¨
Die dargestellten Werte im Baum sind naturlich
bloss die Prioritaten
der Knoten. Die eigentlichen Daten
¨
lassen wir der Einfachheit halber weg.
public class Heap<T extends Comparable<T>> {
private List<T> heap;
public Heap() { heap = new ArrayList<T>(); }
public T removeMax() { . . . }
public void insert(T data) { . . . }
private
private
private
private
boolean isLeaf(int position) { . . . }
int parent(int position) { . . . }
int leftChild(int position) { . . . }
int rightChild(int position){ . . . }
2 Dies hat den Nachteil, dass die maximale Anzahl Elemente (size ) beim Erzeugen des Heaps bekannt sein muss.
4.4 Priority Queues
4-17
65
56
52
37
25
48
31
18
15
45
6
3
...
Werden die Knoten auf diese Weise in den Array abgelegt, so gelten fur
¨ alle i, 0 ≤ i < length die
folgenden Regeln:
• Der linke Nachfolger des Knotens i befindet sich im Array-Element
Ferner gilt: heap[i]
heap[
]
• Der rechte Nachfolger des Knotens i befindet sich im Array Element
Ferner gilt: heap[i]
heap[
]
• Der direkte Vorfahre eines Knotens i befindet sich im Array-Element
Ferner gilt: heap[i]
heap[
]
Wir sind jetzt in der Lage, die beiden wichtigen Operationen insert und removeMax zu formulieren.
¨
4 Datentypen: Baume,
Heaps
4-18
¨
insert Da ein Element hinzugefugt
wir zuerst length um eins. Das neue Element
¨ werden muss, erhohen
¨
¨
wird dann an der Stelle length-1 eingefugt.
Der Array reprasentiert
immer noch einen vollstandigen
¨
¨
¨
binaren
Baum mit nur rechts unten fehlenden Blattern.
Das neue Element verletzt aber eventuell die
Heapbedingung. Um wieder einen Heap zu erhalten, vertauschen wir das neue Element solange mit
¨
seinen direkten Vorgangern,
bis die Heapbedingung wieder erfullt
¨ ist.
¨ Baum vollstandig
¨
Diese Methode verfolgt einen direkten Weg von einem Blatt zur Wurzel. Da der binare
¨
¨
¨
ist, hat ein solcher Weg hochstens die Lange der Hohe des Baumes. Mit anderen Worten, wir brauchen
¨
hochstens
log2 (n) Vertauschoperationen, um ein Element im Heap einzufugen.
¨
65
52
56
37
25
48
31
18
45
6
3
15
55
public void insert(T data) {
heap.add(data);
int crt = heap.size() - 1;
while ((crt != 0)
// heap[crt] > heap[parent(crt)]
&& (heap.get(crt).compareTo(heap.get(parent(crt))) > 0)) {
Collections.swap(heap, crt, parent(crt));
crt = parent(crt);
}
}
}
4.4 Priority Queues
4-19
¨
¨ befindet sich im Element heap[0] und wird vom
removeMax Das Element mit der hochsten
Prioritat
Heap entfernt. heap[0] wird nun mit heap[length-1]
uberschrieben
und length um eins verringert.
¨
¨
¨
Damit erhalten wir wieder einen fast vollstandigen
binaren
Baum. Das neue Element heap[0] verletzt
nun vermutlich die Heapbedingung.
¨
seiner beiden Nachfolger und fahren so fort, bis die
Wir vertauschen also heap[0] mit dem grosseren
Heapbedingung wieder erfullt
¨ ist.
65
public T removeMax() {
if (heap.isEmpty()) return null;
Collections.swap(heap, 0, heap.size() - 1);
T element = heap.remove(heap.size() - 1);
if (heap.size() > 1)
siftDown(0);
return element;
}
4-20
¨
4 Datentypen: Baume,
Heaps
private void siftDown(int position) {
while (!isLeaf(position)) {
int j = leftChild(position);
if ((j < heap.size() - 1)
// heap[j] < heap[j+1]
&& (heap.get(j).compareTo(heap.get(j + 1)) < 0)) {
j++;
}
// heap[position] >= heap[j]
if (heap.get(position).compareTo(heap.get(j)) >= 0) {
return;
}
Collections.swap(heap, position, j);
position = j;
}
}
¨
4.5 Ubung
4
4-21
¨
4.5 Ubung
4
¨ Suchbaume
¨
Binare
¨ Suchbaume,
¨
Bauen Sie aus der folgenden Zahlenreihe zwei binare
indem Sie die Zahlen einmal von
links nach rechts und einmal von rechts nach links lesen.
39, 40, 50, 10, 25, 5, 19, 55, 35, 38, 12, 16, 45
Heaps
¨
Loschen
Sie aus dem folgenden Heap zuerst drei Elemente, fugen
Sie danach ein neues Element
¨
¨ 42 in den Heap ein.
mit Prioritat
52
43
32
35
17
20
22
12
15
6
8
18
5
3
BTree
¨
Fugen
Sie im folgenden BTree zuerst das Element 42 ein, loschen
Sie dann die Elemente 28 und 45.
¨
4-22
5
Suchen
5.1 Grundlagen
¨
Suchen ist eine der haufigsten
Operationen, die mit dem Computer ausgefuhrt
werden. Normalerweise
¨
geht es darum, in einer Menge von Daten die richtigen Informationen zu finden. Wir kennen zum Beispiel
¨
einen Namen und suchen die zugehorige
Mitgliedernummer. Oder wir geben (mit Hilfe einer EC-Karte)
¨
eine Kontonummer ein und das System sucht das dazugehorige
Konto. Oder wir kennen eine Telefon¨
nummer und suchen den dazugehorigen Abonnenten, usw.
Wenn wir im folgenden jeweils Listen von Zahlen durchsuchen, so tun wir das bloss der Einfachheit
¨
halber. Die gleichen Algorithmen konnen
naturlich
fur
¨
¨ beliebige Objekte angewandt werden. Ein Objekt kann zum Beispiel eine Klasse Adresse mit den Member-Variablen name, vorname, strasse,
wohnort, telefonNummer, kundenNummer
sein. Dann verwenden wir eine der Member-Variablen
als Suchschlussel,
also zum Beispiel Adresse.name .
¨
¨
Da Suchalgorithmen so haufig
verwendet werden, lohnt es sich, diese effizient zu implementieren. An¨
derseits spielt naturlich
die Lange
der zu durchsuchenden Datenmenge eine entscheidende Rolle: Je
¨
¨
grosser
die Datenmenge, desto wichtiger die Effizienz der Suche.
Ausserdem spielt die benutzte Datenstruktur eine entscheidende Rolle. Wir werden hier jeweils annehmen, dass wir auf alle Elemente der Datenfolge schnellen wahlfreien Zugriff haben (wie z. Bsp. in einer
ArrayList). Falls dies nicht der Fall ist, sind gewisse Suchalgorithmen sehr viel weniger effizient.
5-2
5 Suchen
Falls kein wahlfreier Zugriff existiert, kann dies mit Hilfe eines Pointer-Arrays simuliert werden, in welchem die Adressen der Daten-Objekte gespeichert sind. Die richtige Wahl der benutzten Datenstruktur
ist entscheidend, ob ein Algorithmus effizient implementiert werden kann oder nicht.
5.2 Lineare Suche
Wie der Name schon sagt, gehen wir bei der linearen Suche linear durch die Suchstruktur und testen
jedes Element, bis wir das gesuchte finden oder ans Ende gelangen.
/**
* Searches for the first occurence of the given argument.
* @param
elem an object.
* @return the index of the first occurrence of the argument in
*
this list; returns -1 if the object is not found.
*/
public int indexOf(Object elem) {
if (elem == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (elem.equals(elementData[i]))
return i;
}
return -1;
}
¨
Die for -Schleife bricht spatestens
dann ab, wenn das letzte Element der Liste gepruft
¨ ist.
¨ der linearen Suche
Komplexitat
¨
Um die Effizienz der linearan Suche zu bestimmen, bestimmen wir die Anzahl der notigen
Vergleiche in
¨
¨
Abangigkeit
von der Lange
n der Folge.
¨ Suche
5.3 Binare
5-3
¨ Suche
5.3 Binare
¨
¨
¨
¨
Eine Folge, auf die sehr haufig
zugegriffen (und nicht so haufig
verandert)
wird, sollte wenn moglich
¨
sortiert gehalten werden1 . Dies lasst
sich leicht realisieren, indem die neuen Elemente jeweils an der
richtigen Stelle einsortiert werden2 .
Falls die Daten so dargestellt sind, kann das Suchen auf sehr viel schnellere Art und Weise realisiert
¨ Suche. Bei der binaren
¨
werden, zum Beispiel durch binare
Suche wird die Folge in zwei Teile
Elem[0]
|
· · · Elem[p-1]
{z
}
Elem[p+1]
|
· · · Elem[len]
{z
}
geteilt und das Element Elem[p] mit dem zu suchenden Element a verglichen. Falls Elem[p] kleiner als
a ist, suchen wir in der rechten Teilfolge weiter, andernfalls in der linken.
r = size
l=0
a
p = (r+l)/2
¨
Die ersten drei Schritte der binaren
Suche
Da die Folge sortiert ist, ist dieses Vorgehen korrekt.
1 Dies setzt naturlich
voraus, dass sich die Datenelemente sortieren lassen, also eine Ordnungsrelation < auf den Datenschlusseln
existiert.
¨
¨
In Java bedeutet dies, die Elemente mussen
das Comparable Interface erfullen,
d.h. eine compareTo() Methode haben.
¨
¨
¨
¨
2 Im Abschnitt uber
Baume
sind wir bereits der speziell dafur
Baumes begegnet
¨
¨ konzipierten Datenstruktur des binaren
5-4
5 Suchen
¨
Wenn wir die Folge jeweils nicht in der Mitte teilen, sondern p = l oder p = r wahlen,
erhalten wir die
¨
lineare Suche als Spezialfall der binaren
Suche.
/**
* return index where item is found, or -1
*/
public int binarySearch( Comparable<T>[ ] a, T x )
{
int l = 0; int p;
int r = a.length - 1;
while( l <= r )
{
p = ( l + r ) / 2;
if( a[p].compareTo( x ) < 0 )
//
l = p + 1;
else if( a[p].compareTo( x ) > 0 )
r = p - 1;
//
else
return p;
}
return -1
}
¨ der binaren
¨
Komplexitat
Suche
a[p] < x
a[p] > x
5.4 Hashing
5-5
5.4 Hashing
¨
Eine Hash-Tabelle ist eine Ubersetzungstabelle,
mit der man rasch auf jedes gesuchte Element einer
¨ einer zeigerbasierten Liste bietet. Hashtabellen
Liste zugreifen kann, welche aber trotzdem die Flexibilitat
werden zum Beispiel auch bei Compilern benutzt, um die Liste der Variablen (und ev. deren Typen) zu
verwalten.
¨
Ein einfaches Beispiel einer Hash-Tabelle ist ein Array. In einem Array konnen
wir auf jedes Element
direkt zugreifen. Einen Array als Hash-Tabelle zu benutzen ist dann gunstig,
wenn wir genugend
Platz
¨
¨
¨
¨
haben, um einen Array der Lange
Anzahl moglicher
Schlussel
anzulegen.
¨
¨
Beispiel: Falls alle Mitglieder eines Vereins unterschiedliche Initialen haben, konnen
wir dies als
Schlussel
fur
¨
¨ die Hashtabelle benutzen.
Berta Amman
Doris Bucher
Chris Carter
Friedrich Dunner
¨
BA
DB
CC
FD
7→
7
→
7
→
7
→
(2,1)
(4,2)
(3,3)
(6,4)
¨
¨
Mit einem Array der Lange
26×26 konnen
wir auf jedes Mitglied direkt zugreifen.
Hash−Tabelle
(H,R)
1
Menge aller Schlüssel
(D,H)
(U,M)
(C,F)
...
(H,A)
(B,A)
(C,C)
(W,S)
2
3
4
...
Daten(BA)
Daten(DB)
Daten(CC)
...
Benötigte Schlüssel
(D,B)
(F,D)
1
2
3
4
5
6
Daten(FD)
Der Nachteil hierbei ist, dass wir bei dieser Methode offensichtlich viel Speicherplatz verschwenden. Aus¨
serdem konnen
wir nicht sicher sein, dass nicht eines Tages ein neues Mitglied mit bereits existierenden
¨
Initialen in unseren Verein eintreten will. Fur
zu
¨ beide Probleme versuchen wir im folgenden Losungen
finden.
5-6
5 Suchen
5.4.1 Hash-Funktionen
Eine Hash-Funktion ist eine Methode, mit welcher wir mit Hilfe von einfachen arithmetischen Operationen
¨
die Speicherstelle eines Elementes aus seinem Schlussel
berechnen konnen.
Optimalerweise sollte eine
¨
Hash-Funktion jedem Element einer Menge einen anderen Funktionswert zuordnen (vermeiden von Kol¨
lisionen). Dies ist leider in der Regel nicht moglich.
Allerdings gibt es Hash-Funktionen, welche diesen
Anspruch besser, und solche, welcher ihn weniger gut erfullen.
¨
Definition: Eine Hashfunktion sollte mindestens die folgenden Eigenschaften erfullen.
¨
¨
• Der Hashwert eines Objekts muss wahrend
der Ausfuhrung
der Applikation gleich bleiben.
¨
¨
Der Hashwert darf aber bei einer nachsten
Ausfuhrung
der Applikation anders sein.
¨
¨ der equals() Methode, muss der Hashwert der beiden Objekte
• Falls zwei Objekte gleich sind gemass
gleich sein.
¨
• Zwei verschiedene Objekte konnen
den gleichen Hashwert haben.
Allerdings wird die Performance von Applikationen verbessert, wenn unterschiedliche Objekte unterschiedliche Hashwerte haben.
Die folgende Hash-Funktion summiert die ASCII-Werte der Buchstaben eines Strings. Fur
¨ das Einfugen
¨
¨
in die Hashtabelle muss dieser Wert dann modulo der Lange
der Hashtabelle genommen werden.
long Elem_hash(Object key) {
String keyString = key.toString();
int sum = 0;
for (int i = 0; i < keyString.length(); i++)
sum = sum + keyString.charAt(i);
return sum;
}
¨
Wie wir am folgenden Beispiel sehen, funktioniert diese Methode leider nicht allzu gut. Alle Worter,
die
aus den gleichen Buchstaben (in anderer Reihenfolge) bestehen, haben den gleichen Funktionswert.
¨
Aber auch sonst verteilt diese Methode verschiedene Worter
offensichtlich nicht optimal (erzeugt viele
Kollisionen).
5.4 Hashing
5-7
Anna
Jochen
Otto
Gabi
Tina
Kurt
Elem_hash()
Tina
Otto
Jochen
Anna
Kurt
0
1
2
3
4
Gabi
5
6
7
8
9
10
Eine oft benutzte, gut streuende Hash-Funktion fur
¨ Strings ist die ELF hash -Methode.
long ELFhash(String key) {
long h = 0;
for (int i = 0; i < key.length(); i++) {
h = (h << 4) + (int) key.charAt(i);
long g = h & 0xF0000000L; // AND
if (g != 0)
h = h ˆ g >>> 24; // XOR, Shift right
h = h & ˜g;
}
return h;
}
Anna
Jochen
Otto
Gabi
Tina
Kurt
ELF_hash()
Kurt
0
Otto
1
2
Gabi Anna
3
4
5
Tina
6
Jochen
7
8
9
10
5-8
5 Suchen
Naturlich
kann auch die ELF hash -Methode nicht alle Kollisionen vermeiden. Die ELF hash -Methode
¨
¨
¨ zum Beispiel fur
mit Tabellenlange
11 erhalt
¨ “Otto” und “Martin” den gleichen Schlussel.
¨
Die Frage ist darum: wie behandelt man Kollisionen?
5.4.2 Double Hashing
Bei Double Hashing verwenden wir zwei verschiedene Hashfunktionen Hash1 und Hash2 3 . Hash1 dient
dazu, die Hashadresse eines Schlussels
in der Tabelle zu suchen. Hash2 dient dazu, bei einer Kollision
¨
¨
in der Tabelle den nachsten
freien Platz zu suchen.
Hash 1 (k)
0
Hash 2(k)
leeres Feld
besetztes Feld
2
neuer Eintrag
3
4
5
6
7
8
9
10
11
Einfugen
¨
eines Objekts mit Schlussels
¨
k: Zuerst wird die Hashadresse
Hash1 (k) mod N
berechnet. Ist dieser Tabellenplatz noch leer, dann wird das Objekt dort eingetragen. Ist der Tabellenplatz schon besetzt, so wird nacheinander bei den Adressen
(Hash1 (k) + Hash2 (k)) mod N,
(Hash1 (k) + 2 · Hash2 (k)) mod N,
...
¨
¨
3 Im einfachsten Fall wahlen
wir Hash2 (k) = p fur
N der Hashtabelle nicht teilt. Dann
¨ p gleich 1 oder fur
¨ eine Primzahl p, welche die Lange
erhalten wir den sog. Linear Probing Algorithmus. Im Allgemeinen kann Hash2 (k) eine beliebige Funktion sein, welche Werte relativ prim zu
N liefert.
5.4 Hashing
5-9
¨
gesucht, bis ein freier Platz gefunden wird (N = Lange
der Tabelle). An der ersten freien Stelle wird
das Objekt eingetragen.
¨
Damit bei der Suche nach einem freien Platz alle Elemente der Tabelle durchsucht werden konnen,
muss Hash2 (k) fur
¨ alle k relativ prim zu N sein (d.h. Hash2 (k) und N haben keine gemeinsamen
Primfaktoren). Sobald die Hashtabelle den maximalen Fullstand
ubersteigt
(z.B. 60%), muss die
¨
¨
¨
Hashtabelle vergrossert
und ein Rehashing vorgenommen werden (alle Elemente neu zuteilen).
Suchen des Objekts mit Schlussels
¨
k:
Die Hashadresse
Hash1 (k) mod N
wird berechnet. Ist das gesuchte Element mit Schlussel
k an dieser Adresse gespeichert, ist die
¨
Suche erfolgreich. Falls nicht, wird der Wert Hash2 (k) berechnet und das Element in den Tabellen¨
platzen
(Hash1 (k) + Hash2 (k)) mod N,
(Hash1 (k) + 2 · Hash2 (k)) mod N,
...
gesucht. Die Suche wird durch eine der drei folgenden Bedingungen abgebrochen:
• Das Element wird gefunden. Die Suche ist erfolgreich abgeschlossen.
• Ein leerer Tabellenplatz wird gefunden,
• oder wir geraten bei der Suche in einen Zyklus (hypothetischer Fall).
¨
In den beiden letzten Fallen
ist das gesuchte Element nicht in der Tabelle.
Hash1 (k)
Hash2 (k)
leeres Feld
besetztes Feld
gelöschtes Feld
5-10
5 Suchen
¨
¨
Loschen
eines Schlussels:
¨
Das Loschen
von Elementen aus einer Tabelle mit Double Hashing ist etwas heikel. Damit ein in der Tabelle eingetragener Schlussel
in jedem Fall wieder gefunden wird,
¨
¨
durfen
wir die Elemente aus der Tabelle nicht einfach loschen,
da wir nicht wissen, ob sie eventuell
¨
als Zwischenschritt beim Einfugen
von anderen Elementen benutzt wurden.
¨
Jeder Tabelleneintrag muss darum ein Flag besitzen, welches angibt, ob ein Eintrag leer, benutzt
¨
oder geloscht
ist. Die oben beschriebene Suche darf dann nur bei einem leeren Element abgebro¨
¨
chen werden. Beim Loschen
eines Schlussels
aus der Tabelle muss der Tabellenplatz mit geloscht
¨
markiert werden.
Als zweite Hashfunktion genugt
¨ oft eine sehr einfache Funktion, wie zum Beispiel Hash2 (k) = k mod 8 +
¨
1. Um sicherzustellen, dass diese Funktionswerte teilerfremd zur Lange
der Hashtabelle sind, kann man
¨
¨
zum Beispiel als Tabellenlange
eine Primzahl wahlen.
5.4 Hashing
5-11
5.4.3 Bucket Hashing
¨
Eine andere Moglichkeit,
Kollisionen zu behandeln, ist das Aufteilen des Hash-Arrays in verschiedene
¨
¨
buckets. Eine Hashtabelle der Lange
H wird dabei aufgeteilt in H/B Teile (buckets), von der Grosse
B. Es wird nur eine Hashfunktion benutzt, Elemente mit gleichem Hashwert (modulo Hashsize) werden
¨
¨
im selben Bucket abgelegt. Die Hashfunktion sollte die Datenelemente moglichst
gleichmassig
uber
die
¨
verschiedenen Buckets verteilen.
Hashtable
Bucket Hashing,
¨
Buckets der Lange
4
Overflow
0
leer
1
besetzt
2
gelöscht
3
4
5
6
7
8
...
Einfugen:
¨
Die Hashfunktion ordnet das Element dem entsprechenden Bucket zu. Falls der erste Platz
im Bucket bereits besetzt ist, wird das Element im ersten freien Platz des Bucket abgelegt.
¨
Falls ein Bucket voll besetzt ist, kommt der Eintrag in einen Uberlauf
(overflow bucket) von genugen¨
¨
¨
der Lange
am Ende der Tabelle. Alle Buckets teilen sich den selben Uberlauf-Speicher.
¨
¨
Naturlich
soll der Uberlauf-Speicher
moglichst
gar nie verwendet werden. Sobald die Hashtabelle
¨
einen gewissen Fullgrad
erreicht hat, muss ein Rehashing erfolgen, so dass die Zugriffe schnell
¨
bleiben (die Buckets nicht uberlaufen).
¨
Suchen: Um ein Element zu suchen, muss zuerst mittels der Hashfunktion der Bucket gesucht werden,
in welchem das Element liegen sollte. Falls das Element nicht gefunden wurde und im Bucket noch
¨
¨
freie Platze
sind, kann die Suche abgebrochen werden. Falls der Bucket aber keine freien Platze
mehr
hat, muss der Overflow durchsucht werden bis das Element gefunden wurde, oder alle Elemente des
Overflow uberpr
uft
¨
¨ sind.
¨
¨
Loschen:
Auch beim Bucket Hashing mussen
wir beim Loschen
vorsichtig sein. Falls der Bucket noch
¨
¨
¨
freie Platze hat, konnen wir den Tabellen-Platz einfach freigeben. Falls nicht, muss der Platz als
¨
“geloscht”
markiert werden, damit beim Suchen der Elemente auch der Overflow durchsucht wird.
5-12
5 Suchen
¨
¨
Eine Variante des Bucket Hashing ist die folgende: Wir wahlen
wiederum eine Bucket-Grosse
B. Wir
teilen die Hashtabelle aber nicht explizit in Buckets auf, sondern bilden jeweils einen virtuellen Bucket
rund um den Hashwert. Dies hat den Vorteil, dass jeder Tabellenplatz als Ausgangsposition fur
¨ das
¨
Einfugen
benutzt werden kann, was die Anzahl Kollisionen bei gleicher Tabellen-Grosse
vermindert.
¨
Bucket Hashing Variante,
¨
Buckets der Lange
5
Hashtable
Overflow
leer
besetzt
gelöscht
P
neuer Eintrag
P+1
P+2
Einfugen:
¨
Die Hashfunktion berechnet den Platz P in der Hashtabelle. Falls dieser Platz bereits besetzt
ist, werden die Elemente rund um P in der Reihenfolge P + 1, P + 2, . . ., P + B − 2, P + B − 1
durchsucht, bis ein freier Platz gefunden wurde. Falls kein freier Platz in dieser Umgebung gefunden
¨
wird, kommt das Element in den Uberlauf-Speicher.
Suchen: Um ein Element zu suchen, muss zuerst mittels der Hashfunktion der Platz P in der Hashtabelle
bestimmt werden. Falls das Element an der Stelle P nicht gefunden wird, werden die Elemente rund
um P in der selben Reihenfolge wie oben durchsucht.
Falls wir das Element finden, oder auf einen leeren Platz stossen, kann die Suche abgebrochen
werden. Andernfalls muss der Overflow durchsucht werden bis das Element gefunden wurde, oder
der ganze Overflow durchsucht ist.
¨
¨
Loschen:
Das geloschte
Feld muss auch in dieser Variante markiert werden, damit wir beim Suchen
keinen Fehler machen.
5.4 Hashing
5-13
5.4.4 Separate Chaining
Die flexibelste Art, um Kollisionen zu beheben, ist mit Hilfe von Separate Chaining. Bei dieser Methode
hat in jedes Element der Hashtabelle einen next-Zeiger auf eine verkettete Liste.
Einfugen
¨
des Schlussels
¨
k: Zuerst wird die Hashadresse Hash(k) berechnet. Dann wird der neue
Schlussel
am Anfang der verketteten Liste eingefugt.
¨
¨
Suchen des Schlussels
¨
k: Die Hashadresse Hash(k) wird berechnet. Dann wird k mit den Schlusseln
¨
in der entsprechenden Liste verglichen, bis k entweder gefunden wird oder das Ende der Liste erreicht ist.
¨
Loschen
des Schlussels
¨
k: Das Element wird einfach aus der Liste in Hash(k) entfernt.
Hashing mit Separate Chaining
5-14
5 Suchen
¨
5.5 Ubung
5
Linear Probing
Fugen
Sie mit den Hashfunkionen Hash1 (k) = k und Hash2 (k) = 1 die Liste der Zahlen
¨
2, 3, 14, 12, 13, 26, 28, 15
¨
in eine leere Hashtabelle der Lange
11 ein. Wieviele Vergleiche braucht man, um festzustellen, dass
die Zahl 46 nicht in der Hashtabelle ist?
Double Hashing
Fugen
Sie mit den Hashfunkionen Hash1 (k) = k und Hash2 (k) = k mod 8 + 1 die Liste der Zahlen
¨
2, 3, 14, 12, 13, 26, 28, 15
¨
in eine leere Hashtabelle der Lange
11 ein. Wieviele Vergleiche braucht man, um festzustellen, dass
die Zahl 46 nicht in der Hashtabelle vorkommt?
Bucket Hashing
Fugen
Sie mit der Hashfunkion Hash(k) = k die Zahlen 4, 3, 14, 12, 13, 26, 28, 15, 2, 20
¨
¨
¨
leere Hashtabelle der Lange
8 ein mit Bucket Grosse
3 ein.
in eine
Overflow:
Java Hashtabelle
Sie finden unter ∼amrhein/AlgoData/uebung5 eine vereinfachte Version der Klasse Hashtable der
java.util Library. Finden Sie heraus, welche der verschiedenen im Skript vorgestellten Varianten
in der Java Library benutzt werden.
6
Sortieren
¨
¨
Sortierprogramme werden vorallem fur
von Daten benotigt,
wenn die Daten zum
¨ die die Prasentation
¨
¨
Beispiel sortiert nach Zeit, Grosse,
letzten Anderungen,
Wert, ... dargestellt werden sollen.
Wenn die Mengen nicht allzu gross sind (weniger als 500 Elemente), genugt
¨ oft ein einfach zu implemen¨ von O(n2 ), wobei
tierender, langsamer Suchalgorithmus. Diese haben normalerweise eine Komplexitat
der Aufwand bei fast sortierten Mengen geringer sein kann (Bubble Sort). Zum Sortieren von grossen
Datenmengen lohnt es sich allerdings, einen O(n log2 (n)) Algorithmus zu implementieren.
Noch mehr als bei Suchalgorithmen spielen bei Sortieralgorithmen die Datenstrukturen eine entscheidende Rolle. Wir nehmen an, dass die zu sortierende Menge entweder eine Arraystruktur (ein BasisTyp-Array wie float[n] oder eine ArrayList ) oder eine Listenstruktur (wie zum Beispiel LinkedList )
¨
ist. Listen erlauben zwar keinen wahlfreien Zugriff, dafur
Listenelemente durch Umketten (also
¨ konnen
ohne Umkopieren) von einer unsortierten Folge F in eine sortierte Folge S uberf
uhrt
werden. Arrays
¨
¨
¨
erlauben wahlfreien Zugriff. Dafur
und Loschen
(Umsortieren) von Elementen viele
¨ sind beim Einfugen
¨
¨
Kopier-Schritte notig.
Speziell ist zu beachten, dass viele Sortier-Algorithmen auf Array-Strukturen zwar sehr schnell aber nicht
stabil sind.
¨ der Vergleichsfunktion
Definition: Ein Sortier-Algorithmus heisst stabil, falls Elemente, welche gemass
gleich sind, ihre Originalreihenfolge behalten.
6-2
6
Sortieren
6.1 Selection Sort
¨
Beim Sortieren durch Auswahlen
teilen wir die zu sortierende Menge F (scheinbar) in eine unsortierte
Teilmenge U und eine sortierte Menge S. Aus U wird jeweils das kleinste Element gesucht und mit dem
letzten Element von S vertauscht. Wegen dieser Vertausch-Aktionen ist dieser Algorithmus eher geeignet
fur
aber dazu, dass der Algorithmus nicht stabil ist.
¨ Arraystrukturen. Die Vertauschungen fuhren
¨
public void selectionSort(int l, int r) {
int min_pos;
for( int i=l; i<r; i++) {
min_pos = findMin(i);
swap(i, min_pos);
}
}
private int findMin(int n) { // find position with minimal Element
int min = n;
for( int i = n+1; i<array.size(); i++ )
if( array.get(i).compareTo(array.get(min)) < 0 )
min = i;
return min;
}
Die Array-Prozedur find min(i) sucht jeweils das kleinste Element aus dem Rest-Array. find min
betrachtet also nur die Elemente mit Positionen j ≥ i im Array (der unsortieren Teilmenge U ). Das neue
kleinste Element min (an der Position min pos ) wird mit dem Element an der Position i vertauscht (dem
letzten der Teilmenge S).
6.1 Selection Sort
6-3
Beispiel Wir sortieren die Folge F = (21, 5, 12, 1, 27, 3)
21
5 12
1
27
3
Dass der Algorithmus nicht stabil ist, sehen wir auch am folgenden Beispiel. Die Folge Beat Suter, Claudia
Meier, Daniel Suter, Emil Bucher, Fritz Abegg ist bereits sortiert nach Vornamen. Sie soll nun mit Hilfe
¨ den Nachnamen sortiert werden.
von Selection Sort gemass
BeatSuter ClaudiaMeier DanielSuter EmilBucher FritzAbegg
6-4
6
Sortieren
6.2 Insertion Sort
Beim Sortieren durch Einfugen
nimmt man jeweils das erste Element aus der unsortierten Folge F und
¨
fugt
¨ es an der richtigen Stelle in die sortierte Folge S ein. Dieser Algorithmus ist nicht geeignet fur
¨ arraybasierte Folgen, da in jedem Schritt einige Elemente im Array nach hinten verschoben werden mussten.
¨
Wir betrachten nochmals die Folge vom Beispiel vorher:
21
5 12
1
27
3
¨ der Ordnung) gleichen Elemente
Falls das Element beim Ein-Sortieren jeweils am Ende aller (gemass
eingefugt
¨ wird, ist dieser Algorithmus stabil.
public void insertSorted(MSEntry<T> o) {
MSEntry<T> e = header;
// find place for o
while (e.next != null && o.element.compareTo(e.next.element) > 0)
e = e.next;
// while(o.element < e.next.element)
o.next = e.next;
// insert o between e and e.next
e.next = o;
if (e == tail)
tail = o;
size++;
}
6.3 Divide-and-Conquer Sortieren
6-5
public MSList<E> insertSort(MSList<E> list) {
MSList<E> newList = new MSList<E>();
newList.addFirst(list.removeFirst());
while (list.size() > 0)
newList.insertSorted(list.removeFirst());
return newList;
}
6.3 Divide-and-Conquer Sortieren
Das abstrakte Divide-and-Conquer Prinzip (vgl. Kap. 1.6.2) lautet wie folgt:
1. Teile das Problem (divide)
¨ die Teilprobleme (conquer )
2. Lose
¨
3. Kombiniere die Teillosungen
(join)
¨
Beim Sortieren gibt es zwei Auspragungen:
Hard split / Easy join: Dabei wird die gesamte Arbeit beim Teilen des Problems verrichtet und die
Kombination ist trivial, das heisst, F wird so in Teilfolgen F1 und F2 partitioniert, dass zum Schluss
¨
die sortierten Teilfolgen einfach aneinandergereiht werden konnen:
S = S1 S2 . Dieses Prinzip fuhrt
¨
zum Quicksort-Algorithmus.
Easy split / Hard join: Dabei ist die Aufteilung in Teilfolgen F = F1 F2 trivial und die Hauptarbeit liegt
beim Zusammensetzen der sortierten Teilfolgen S1 und S2 zu S. Dieses Prinzip fuhrt
zum Mergesort¨
Algorithmus.
6-6
6
Sortieren
6.4 Quicksort
Einer der schnellsten und am meisten benutzten Sortieralgorithmen auf Arrays ist der Quicksort-Algo¨
rithmus (C.A.R. Hoare, 1960). Seine Hauptvorteile sind, dass er nur wenig zusatzlichen
Speicherplatz
¨
braucht, und dass er im Durchschnitt nur O(n log2 (n)) Rechenschritte benotigt.
¨ 1 . Dann wird der zu sortierende Bereich F
Beim Quicksort wird zuerst ein Pivot-Element q ausgewahlt
so in zwei Teilbereiche Fklein und Fgross partitioniert, dass in Fklein alle Elemente kleiner als q und in Fgross alle
¨
Elemente grosser
gleich q liegen.
Danach wird rekursiv Fklein und Fgross sortiert. Am Ende werden die je sortierten Folgen wieder zusammengefugt.
¨
¨
Die Prozedur partition sucht jeweils von links ein Element g, welches grosser
und von rechts ein Element k, welches kleiner als der Pivot ist. Diese zwei Elemente g und k werden dann vertauscht. Dies wird
so lange fortgesetzt, bis sich die Suche von links und von rechts zusammen trifft. Zuruckgegeben
wird
¨
der Index der Schnittstelle. Anschliessend an partition wird der Pivot an der Schnittstelle eingesetzt
(vertauscht mit dem dortigen Element).
Um zu sehen, was beim Quicksort-Algorithmus passiert, betrachten wir das folgende Tracing:
¨ werden, optimalerweise ist q aber der Median aller zu sortierenden Werte. Da die Berechnung des Medians viel zu
1 q kann beliebig gewahlt
¨
¨
¨ man als Pivot oft einfach das erste Element. Eine bessere Wahl ist das Element an der Stelle length/2 im Array als
aufwandig
ware,
wahlt
Pivot; dies fuhrt
bei beinahe sortierten Folgen zu einem optimalen Algorithmenverlauf.
¨
6.4 Quicksort
54
93
6-7
83
22
7
19
94
48
27
72
39
70
13
28
95
36 100
4
12
6-8
6
Sortieren
private int partition( int l, int r, E pivot )
{
do{
// Move the bounds inward until they meet
while( array.get(++l).compareTo(pivot) < 0 && l < r );
// Move left bound right
while( array.get(--r).compareTo(pivot) > 0 && l < r);
// Move right bound left
if( l < r )
swap( l, r );
// Swap out-of-place values
} while(l < r );
// Stop when they cross
if( array.get(r).compareTo(pivot) < 0 )
return r+1;
return r;
// Return position for pivot
}
¨
Quicksort teilt die zu sortierende Menge so lange auf, bis die Teile eine kritische Lange
(z.B. 4 oder
8) unterschritten haben. Fur
Listen wird der einfachere Selection-Sort Algorithmus benutzt, da
¨ kurzere
¨
dieser fur
¨ kurze Listen einen kleineren Overhead hat.
public void quickSort( int i, int j ) {
int pivotindex = findPivot(i, j);
swap(pivotindex, i);
// stick pivot at i
int k = partition(i, j+1, array.get(i));
// k is the position for the pivot
swap(k, i);
// put pivot in place
if( (k-i) > LIMIT )
// sort left partition
quickSort( i, k-1 );
else
// with selection-sort
selectionSort( i, k-1 );
//
for short lists
if( (j-k) > LIMIT )
quickSort( k+1, j );
// sort right partition
else
selectionSort( k+1, j );
}
6.5 Sortieren durch Mischen (Merge Sort)
6-9
¨ des Quicksort-Algorithmus ist im besten Fall O(n log2 (n). Im schlechtesten Fall (worst
Die Komplexitat
¨ O(n2 ).
case), wenn das Pivot-Element gerade ein Rand-Element ist, dann wird die Komplexitat
¨ des Quicksort Algorithmus. Es
Das Finden eines gunstigen
Pivot Elements ist zentral fur
¨
¨ die Komplexitat
¨
¨
gibt verschiedene Verfahren zum Losen
diesese Problems wie, wahle
als Pivot das erste Element des
¨
¨
¨
Arrays, wahle
das Element in der Mitte des Arrays oder wahle
drei zufallige
Elemente aus dem Array
und nimm daraus das mittlere. Eine Garantie fur
¨ eine gute Wahl des Pivots liefert aber keines dieses
Verfahren. Ausserdem ist der Quicksort Algorithmus nicht stabil.
6.5 Sortieren durch Mischen (Merge Sort)
Der Merge-Sort-Algorithmus ist einer der besten fur
¨ Datenmengen, die als Listen dargestellt sind. Die
¨
Rechenzeit ist in (best case und worst case) O(n log2 (n)). Ausserdem wird kein zusatzlicher
Speicher
¨
benotigt und der Algorithmus ist stabil.
Beim Merge-Sort wird die Folge F mit Hilfe einer Methode divideList()
¨
grosse Halften
geteilt.
private MSList<E>[] divideList(MSList<E> list) {
MSList<E> result[] = (MSList<E>[]) new MSList[2];
result[0] = new MSList<E>();
int length = list.size();
for (int i = 0; i < length / 2; i++)
result[0].addFirst(list.removeFirst());
result[1] = list;
return result;
}
¨
in zwei (moglichst)
gleich
6-10
6
Sortieren
Die (im rekursiven Aufruf) sortierten Folgen werden dann in einem linearen Durchgang zur sortierten
Endfolge zusammen gefugt
¨ (gemischt).
private MSList<E> mergeSort(MSList<E> list) {
if (list.size() < LIMIT)
return insertSort(list);
// divide list into two sublists
MSList<E>[] parts = divideList(list);
MSList<E> leftList = parts[0];
MSList<E> rightList = parts[1];
leftList = mergeSort(leftList);
rightList = mergeSort(rightList);
// left recursion
// right recurstion
return merge(leftList, rightList);
}
¨
Das Mischen geschieht dadurch, dass jeweils das kleinere Kopfelement der beiden Listen ausgewahlt,
¨ und als nachstes
¨
herausgelost
Element an die neue Liste angefugt
¨ wird.
4
7
3
3
13
4
8
28
7
12
8
15
6.5 Sortieren durch Mischen (Merge Sort)
6-11
private MSList<E> merge(MSList<E> left, MSList<E> right) {
MSList<E> newList = new MSList<E>();
while (left.size() > 0 && right.size() > 0) {
if (left.getFirst().compareTo(right.getFirst()) < 0)
newList.addLast(left.removeFirst());
else
newList.addLast(right.removeFirst());
}
for (int i = left.size(); i > 0; i--)
newList.addLast(left.removeFirst());
for (int i = right.size(); i > 0; i--)
newList.addLast(right.removeFirst());
return newList;
}
divide
Beispiel: Wie Mergesort genau funktioniert, wollen wir anhand dieses Beispiel nachvollziehen.
merge
sort
3
2
1
6
3
2
1
6
3
2
1
6
3
4
8
2
2
3
1
6
3
4
2
8
2
3
1
6
3
4
2
8
3
4
3
2
1
4
3
2
3
4
2
8
6
2
1
2
8
2
8
3
6-12
6
Sortieren
¨
6.6 Ubung
6
InsertSort, SelectionSort
Erstellen Sie ein Tracing vom InsertSort-, bzw. vom SelectionSort-Algorithmus auf der Eingabe
F = {42, 3, 24, 17, 13, 5, 10}
Insert Sort
42
3
24
17
Selection Sort
13
5
10
42
3
24
17
13
5
10
Mergesort, Quicksort
Erstellen Sie je ein Tracing vom Mergesort-, bzw. vom Quicksort-Algorithmus (einmal mit dem PivotElement an der Stelle 1, dann an der Stelle (r+l)/2 , r der rechte, l der Linke Rand des Bereichs)
auf der Eingabe
F = {42, 3, 24, 33, 13, 5, 7, 25, 28, 14, 46, 16, 49, 15}
HeapSort
¨
Uberlegen
Sie sich ein Verfahren welches eine gegebene Menge mit Hilfe eines Heaps sortiert.
Welchen Aufwand hat dieses Verfahren im besten / im schlechtesten Fall?
Selbststudium: BucketSort, RadixSort
Lesen Sie die Unterlagen zum Selbststudium uber
die zwei weiteren Suchalgorithmen: Bucket Sort
¨
und Radix Sort.
Sortieren Sie die obige Menge F mit Hilfe von RadixSort mit 5 Buckets.
¨
6.6 Ubung
6
6-13
Quick Sort
42
3
24
33
13
5
7
25
28
14
46
16
49
15
42
3
24
33
13
5
7
25
28
14
46
16
49
15
6-14
7
Pattern Matching
¨
Pattern Matching ist eine Technik, mit welcher ein String aus Text oder Binardaten
nach einer Zeichenfolge durchsucht wird. Die gesuchten Zeichenfolgen werden dabei in Form eines Suchmusters (Pattern)
angegeben.
Solche Algorithmen werden in der Textverarbeitung (Suchen nach einem Zeichenstring in einer Datei) aber
auch von Suchmaschinen auf dem Web verwendet. Das
Hauptproblem beim Pattern-Matching ist: Wie kann entschieden werden, ob ein Text ein gegebenes Muster
erfullt.
¨
¨ Ausdrucke
7.1 Beschreiben von Pattern, Regulare
¨
¨
¨ Ausdrucke)
Zunachst
brauchen wir eine Sprache, mit welcher wir die Pattern (hier regulare
beschreiben
¨
¨
¨
konnen
(um zu sagen, welche Art von Wortern
oder Informationen wir suchen).
Sei E ein Alphabet, d.h. eine endliche Menge von Zeichen. Ein Wort uber
E ist eine endliche Folge von
¨
Zeichen aus E :
w = e1 e2 . . . en ,
ei ∈ E
Das leere Wort, das aus keinem Zeichen besteht, bezeichnen wir mit λ.
¨
Mit E ∗ bezeichnen wir die Menge aller Worter
uber
dem Alphabet E .
¨
7-2
7 Pattern Matching
Beispiel: Fur
¨ E = {A, B} ist E ∗ =
Die Pattern bauen wir mit Hilfe der folgenden drei Regeln zusammen:
¨
Definition: Ein (einfacher) regularer
Ausdruck ist ein Wort (String), welches mit Hilfe der folgenden drei
Operationen zusammengebaut wird:
¨
¨
Konkatenation setzt zwei Worter
zusammen. So wird aus den Wortern
’AB’ und ’BC’ das neue Wort
’ABBC’.
¨
¨
Auswahl erlaubt uns, zwischen einem der Worter
auszuwahlen.
Das heisst AB|B ist entweder das Wort
’AB’ oder das Wort ’B’. Wir nennen solche Terme auch Or-Ausdrucke.
¨
¨
Iteration repetiert das gegebene Wort beliebig oft (auch 0 mal). A(AB)∗ entspricht also den Wortern
’A’,
’AAB’, ’AABAB’, · · ·.
¨
Zu beachten ist in diesem Zusammenhang die Bindungsstarke
der drei Operationen: Die Iteration (∗ )
¨
¨
bindet starker
als die Konkatenation. Am schwachsten
bindet der Oder-Strich (|).
Beispiele
¨
Der Ausdruck (A|BC)∗ AB erzeugt die Worter:
¨
Der Ausdruck AB∗ C|A(BC)∗ erzeugt die Worter:
¨
¨
Wir beschranken
uns hier auf diese wenigen Moglichkeiten
zum Erzeugen von Pattern, obwohl es naturlich
noch viele weitere, sehr elegante Operationen gibt: Zum Beispiel mit dem Zeichen ’.’ fur
¨
¨ einen
¨
beliebigen Buchstaben konnten
lange Or-Ausdrucke
viel kurzer
dargestellt werden. AB(A|B| · · · |Z)∗ G
¨
¨
¨
entspricht dann AB( . )∗ G. Allerdings konnen
wir damit keine neuen Wortmengen definieren. Darum
begenugen
wir uns vorerst mit den obigen Operationen.
¨
¨
Um zu entscheiden, ob ein Wort einen regularen
Ausdruck erfullt,
¨ setzen wir endliche Automaten ein.
7.2 Endliche Automaten
7-3
7.2 Endliche Automaten
¨
¨
Endlichen Automaten begegnen wir im taglichen
Leben in Form von Getrankeautomaten,
Billetautomaten, Bancomaten ... . Allen ist gemeinsam, dass sie ein einfaches endliches Eingabealphabet und ein
¨
einfaches endliches Ausgabealphabet haben und jeweils eine endliche Menge von Zustanden
annehmen
¨
konnen.
Beispiel Ein Bancomat funktioniert wie folgt: Nachdem die EC-Karte eingeschoben wurde, muss der
Benutzer die Geheimzahl (Pincode) eingeben. Falls der Pincode korrekt war, bekommt der Benutzer eine
¨
Auswahl prasentiert
(Geld abheben, Kontostand abfragen, abbrechen). Je nach Verhalten des Benutzers
gibt der Bancomat den gewunschten
Geldbetrag aus und/oder gibt die EC-Karte wieder zuruck.
¨
¨
Geldbetrag
Geld ausgeben
Auswahl Abbruch
Karte ausgeben
Auswahl
Geld
5
korrekter
Pincode
Auswahl
Kontostand
3
Karte
1
einschieben
ok
2
4
falscher Pincode
6
Error
Karte ausgeben
¨
Definition: Ein endlicher (deterministischer) Automat besteht aus einer endlichen Menge von Zustan¨
den, einem Anfangs- (oder Start-) Zustand und einem oder mehreren Endzustanden
(oder akzeptieren¨
den Zustanden).
• Eingabe: Ein Automat wird von aussen bedient, d.h. er wird mit Eingabedaten versorgt. Es gibt also
eine endliche Menge E von Eingabezeichen, die der Automat lesen kann und die eine gewisse Aktion
¨
auslosen.
Die Menge E heisst Eingabealphabet.
• Zustand: Ein deterministischer Automat befindet sich stets in einem bestimmten Zustand. Die endli¨
¨
che Menge Z der moglichen
Zustande
heisst die Zustandsmenge.
7-4
7 Pattern Matching
• Zustandsubergang:
¨
Die Verarbeitung eines einzelnen Eingabezeichens kann durch eine Nachfolgefunktion, ein Zustandsdiagramm oder durch eine Zustandstafel beschrieben werden. Unter der
Einwirkung der Eingabe kann er von einem Zustand in einen andern ubergehen.
¨
• Ausgabe: Im Laufe seiner Arbeit kann der Automat eine Ausgabe produzieren, d.h. er kann Ausgabedaten ausgeben. Die endliche Menge A der produzierten Ausgabezeichen heisst Ausgabealphabet.
Beispiel: Fur
¨ den Bancomat setzen wir
Eingabe:
E = {EC-Karte, Pincode (korrekt/falsch), Auswahl (Geld, Kontostand ...), Geldbetrag, Ok }
¨
Zustande:
Z = {1 (Startzustand), 2 (warten auf Pincode), 3 (warten auf Auswahl), 4 (Kontostand
anzeigen), 5 (warten auf Betrag) }
Ausgabe:
A = {EC-Karte, Kontostand, Geld}.
und definieren die Funktionsweise durch die folgende Zustandstafel:
Zustand
Eingabe
1
2
3
4
5
6 (Error)
Karte (E)
korrekter (K)
Pincode
falscher
Pincode (F)
Auswahl (G)
Geld
Auswahl
Kontostand (S)
Auswahl
Abbruch (A)
Geldbetrag (B)
Ok (O)
¨
¨
Mit Hilfe von endlichen Automaten konnen
wir entscheiden, ob ein gegebenes Wort einem regularen
Ausdruck entspricht, also ob ein gefundenes Wort in unser Schema (das vorgegebene Pattern) passt.
7.2 Endliche Automaten
7-5
¨
Beispiel: Worter,
welche vom obigen Automaten (Bancomaten) akzeptiert werden, sind:
¨
¨
Worter,
welche nicht zum obigen Automaten gehoren
(nicht akzeptiert werden) sind:
Ein endlicher Automat heisst deterministisch, falls jede Eingabe des Eingabealphabetes in jedem Zustand erlaubt ist und zu einem eindeutigen Nachfolgezustand fuhrt.
Ein nichtdeterministischer endlicher
¨
¨
Automat kann fur
haben.
¨ jeden Zustand und jede Eingabe null, einen oder mehrere Nachfolgezustande
Der Automat des vorigen Beispiels ist ein deterministischer endlicher Automat, da alle Eingaben nur
¨
in einen eindeutigen Nachfolgezustand fuhren.
Nichtdeterministische Automaten konnen
aber immer in
¨
¨
deterministische Automaten ubergef
uhrt
werden, indem neue mehrdeutige Zustande
eingefuhrt
werden.
¨
¨
¨
Beispiele Das folgende ist ein deterministischer, endlicher Automat: in jedem Zustand fuhrt
jede Eingabe
¨
zu einem eindeutigen Nachfolgezustand:
A
A
A
1
4
0
B
B
A
C
2
A
B
C
C
B
3
Zustand
B
C
C
Eingabe
A
B
C
0
1
2
3
4
7-6
7 Pattern Matching
Das folgende ist ein nichtdeterministischer, endlicher Automat: Manche Eingaben fuhren
in gewissen
¨
¨
Zustanden
zu keinem oder zu mehr als einem Nachfolgezustand:
A
A
1
C
C
Zustand
4
0
A
Eingabe
A
B
2
1
2
3
4
A
B
B
0
C
A
B
3
C
C
¨
¨
Ein weiteres Konstrukt in nichtdeterministischen Automaten sind sogenannte leere Uberg
ange,
das
¨
¨
¨
¨
¨
¨
heisst Uberg
ange
ohne gelesenes Zeichen. Solche Uberg
ange
heissen auch epsilon-Uberg
ange
und
werden mit ε bezeichnet.
A
A
1
C
C
Zustand
4
0
Eingabe
B
C
B
A
A
ε
ε
2
C
B
3
B
C
0
1
2
3
4
¨
7.3 Automaten zu regularen
Ausdrucken
¨
7-7
¨
7.3 Automaten zu regularen
Ausdrucken
¨
¨
¨
¨
Wir suchen nun einen zu einem regularen
Ausdruck aquivalenten
Automaten. Ausgehend vom regularen
¨
Ausdruck konnen
wir mit den folgenden Regeln den entsprechenden nichtdeterministischen endlichen
Automaten herleiten.
Durch das Symbol
bezeichnet.
wird der Startzustand, durch
der akzeptierende Zustand des Automaten
Konkatenation: E1 E2
E1 :
1
2
E2 :
3
4
Beispiel:
B
A
B
C
A
A
.
7-8
7 Pattern Matching
Auswahl: E1 | E2
E1 :
1
2
E2 :
3
4
.
Beispiel:
B
A
B
C
A
A
Iteration: E∗
E:
1
2
Beispiel:
A
B
C
.
¨
7.3 Automaten zu regularen
Ausdrucken
¨
7-9
¨
Im allgemeinen kann man sehr viel kleinere Automaten konstruieren, welche den gleichen regularen
Ausdruck beschreiben. Es gibt auch einen Algorithmus, welcher aus einem nichtdeterministischen Auto¨
¨
maten einen deterministischen Automaten (ohne leere Uberg
ange)
erzeugt. Diesen Algorithmus wollen
wir hier aber nicht behandeln.
Beispiel 1: Ein endlicher Automat fur
¨ den Ausdruck (AB|C)∗ (A|B)
¨
Beispiel 2: Ein Automat fur
Ausdruck (AB|BC)∗ (C|BC)
¨ den regularen
7-10
7 Pattern Matching
¨
7.4 Ubung
7
¨
1. Finden Sie jeweils einen endlichen Automaten und/oder einen regularen
Ausdruck, welcher die fol¨
gende Menge von Wortern
uber
dem Alphabet {A, B,C} beschreibt:
¨
¨
- Alle Worter,
welche mit A anfangen und mit C enden.
-
¨
Alle Worter,
welche eine gerade Anzahl A enthalten und mit C enden.
-
¨
Alle Worter,
deren Anzahl Buchstaben durch 3 teilbar sind.
-
¨
Alle Worter,
welche eine gerade Anzahl B und eine gerade Anzahl C enthalten.
¨
2. Erzeugen Sie je einen nichtdeterministischen Automaten fur
Ausdrucke:
¨ die regularen
¨
- (A*BC)*|BB
-
((AB|B)(BA|CB))*
- (BB|CAB*|AB)*
Sie mussen
dabei die angegebenen Regeln zur Erzeugung des Automaten nicht strikt befolgen – Sie
¨
¨
¨
konnen
auch versuchen, einen Automaten mit weniger Zustanden
zu finden.
3. Erzeugen Sie eine Zustandsubergangs-Tabelle
fur
¨
¨ den folgenden Automaten.
ε
ε
0
C
B
ε
1
A
A
2
B
4
B
3
A
¨
7.4 Ubung
7
7-11
¨
4. Lesen Sie die Worter
AACBC, ABACA und ACACB mit Hilfe der von Ihnen erzeugten Zustandsuber¨
gangstabelle, indem Sie Schritt fur
¨ Schritt den aktuellen Zustand notieren.
5. Holen Sie sich das Java Programm unter ∼amrhein/AlgoData/uebung7 und beantworten Sie die
folgenden Fragen:
¨
- Erklaren
Sie die Methoden find , group , start , end , split und replaceAll des java.regex
Package.
-
¨
¨
Erganzen
Sie das gegebene Programm: Geben Sie vom Input String alle Worter
aus, welche
weniger als 5 Buchstaben haben.
7-12
7 Pattern Matching
¨
Die wichtigsten regularen
Ausdrucke
¨
Bedeutung
\
.
x
ˆx
[x]
()
|
{x}
{x,}
{x,y}
?
*
+
ˆ
$
Escape, um Instanzen von Zeichen zu finden, welche als Metazeichen benutzt
werden (wie Punkt, Klammer, ... )
Ein beliebiges Zeichen (ausser newline)
Eine Instanz von x
Jedes Zeichen ausser x
Alle Zeichen in diesem Bereich (z. B. [abuv] die Buchstaben a, b, u oder v, [a-z]
alle Kleinbuchstaben)
runde Klammern dienen fur
¨ die Gruppierung
der OR Operator (Auswahl) (a|A)
Der Ausdruck muss genau x mal vorkommen
Der Ausdruck muss mindestens x mal vorkommen
¨
Der Ausdruck kommt mindestens x mal und hochstens
y mal vor
Abkurzung
fur
¨
¨ {0, 1}
Abkurzung
fur
¨
¨ {0, }
Abkurzung
fur
¨
¨ {1, }
Start einer neuen Zeile
Ende der Zeile
Beispiele
[a-zA-Z]*
Beliebig viele Zeichen aus Buchstaben (z. B. ugHrB).
[A-Z0-9]{8}
Acht Zeichen aus A bis Z und 0 bis 9, (z. B. RX6Z45UB).
[A-Z]([a-z])+
Ein Grossbuchstabe gefolgt von mindestens einem Kleinbuchstaben (z.Bsp Stu).
([0-9]-){2}[0-9]}
Drei Zahlen, durch Striche getrennt (z. B. 2-1-8).
[A-G]{2,} Mindestens zwei Grossbuchstaben aus A bis G (z. B. BGA).
[bRxv]{3} Drei Buchstaben aus b, R, x und v (z. B. xxv).
8
Top Down Parser
¨
¨
Hohere
Programmiersprachen wie Java oder C++ konnen
von einem Prozessor nicht direkt verarbeitet
¨
werden. Die in diesen Sprachen geschriebenen Programme mussen
zuerst in eine fur
¨
¨ den gewahlten
Prozessor geeignete Maschinensprache ubersetzt
werden. Programme, die diese Aufgabe ubernehmen,
¨
¨
¨
nennt man Compiler. Ein Compiler hat im Prinzip zwei Aufgaben zu losen:
1. Erkennen der legalen Programme der gegebenen Sprache. Das heisst, der Compiler muss testen,
ob die Syntax des Programms korrekt ist oder nicht. Diese Operation nennt man parsing und das
¨ einen Parser.
Programm, das diese Aufgabe lost,
2. Generieren von Code fur
¨ die Zielmaschine.
Auch wenn wir keinen Compiler schreiben wollen, so kommt es doch oft vor, dass wir komplizierte Benutzereingaben wie zum Beispiel
-
Arithmetische Ausdrucke:
2(a − 3) + b − 21
¨
-
Polynome: a + 3x2 − 4x + 16
-
Boole’sche Ausdrucke
a ⊙ (b ⊙ c) ⊕ b
¨
-
eine Befehlssprache
-
ein Datenubermittlungsprotokoll
¨
-
...
erkennen und verarbeiten mussen.
¨
8-2
8
Top Down Parser
8.1 Kontextfreie Grammatik
Mit Hilfe einer Grammatik beschreiben wir den Aufbau oder die Struktur (-Regeln) einer Sprache. Kontextfreie Grammatiken1 dienen als Notation zur Spezifikation einer Sprachsyntax.
Beispiel: Ein einfacher deutscher Satz kann zum Beispiel eine der folgenden Strukturen annehmen:
S ’ist’ A
S ’ist’ A ’und ’ A
S ’ist’ A ’oder ’ A
wobei S normalerweise die Form
Artikel Nomen
¨ ’gross’, ’schnell’ oder ’lang’.
hat und A ersetzt werden kann durch ein Adjektiv wie ’schon’,
Eine kontextfreie Grammatik (bzw. ein Syntaxdiagramm) fur
¨ dieses Konstrukt sieht dann etwa wie folgt
aus:
Grammatik
(EBNF: Extended Backus-Naur Form)
Satz
S
Artikel
Artikel
Nomen
Nomen
::=
::=
::=
::=
...
::=
::=
S ’ist’ A
A
A
Adjektiv
Adjektiv
::=
::=
::=
::=
::=
...
Satz
S
S
Artikel
A
ist
Artikel Nomen
’der’
Nomen
’ein’
’Baum’
Artikel
der
ein
’Hund’
...
A
Syntax-Diagramm
A
Adjektiv
Adjektiv
Adjektiv ’und’ A
und
Adjektiv ’oder’ A
oder
¨
’schon’
’gross’
Adjektiv
A
...
¨ Chomsky-Hierarchie vier Typen von Grammatiken: Typ 3: lineare Grammatik (entpricht endlichem Automat), Typ 2: kontext1 Es gibt gemass
freie Grammatik (nur Nichtterminale auf der linken Seite), Typ 1: kontextsensitive Grammatik (nichtverkurzend),
Typ 0: allgemeine Grammatik
¨
¨
(ohne Einschrankung).
8.1 Kontextfreie Grammatik
8-3
Eine solche Ersetzungs-Regel heisst eine Produktion.
Ein Satz, welcher dieser Grammatik entspricht, ist:
Ein Satz, welcher nicht dieser Grammatik entspricht, ist:
¨
Eine kontextfreie Grammatik beschreibt eine Sprache (Menge von Wortern
oder Strings).
¨
Definition: Zu einer kontextfreien Grammatik gehoren
vier Komponenten.
1. Eine Menge von Terminalen, d.h. von Symbolen, die am Ende einer Herleitung stehen (hier fettgedruckte Zeichenketten wie ’ist’, ’und’ oder ’oder’).
2. Eine Menge von Nichtterminalen, d.h. von Namen oder Symbolen, die weiter ersetzt werden (hier
kursiv gedruckte Zeichenketten).
3. Eine Menge von Produktionen (l ::= r). Jede Produktion besteht aus einem Nichtterminal, das die linke Seite der Produktion bildet, einem Pfeil und einer Folge von Terminalen und/oder Nichtterminalen,
die die rechte Seite der Produktion darstellen.
4. Ein ausgezeichnetes Nichtterminal dient als Startsymbol. Das Startsymbol ist immer das Nichtterminal auf der linken Seite der ersten Produktion.
Als vereinfachte Schreibweise fassen wir alle rechten Seiten der Produktionen zusammen, welche das
gleiche Nichtterminal als linke Seite haben, wobei die einzelnen Alternativen durch ‘|’ (zu lesen als “oder”)
getrennt werden.
Ziffer
Ziffer
..
.
::= ’0’
::= ’1’
Ziffer ::= ’9’
kann also auch als
Ziffer ::= ’0’ | ’1’ | ’2’ | ’3’ | ’4’ | ’5’ | ’6’ | ’7’ | ’8’ | ’9’
geschrieben werden.
8-4
8
Top Down Parser
Definition: Die Herleitung eines Wortes aus einer Grammatik geschieht wie folgt: Wir beginnen mit dem
Startsymbol und ersetzen dann jeweils in dem hergeleiteten Wort ein Nichtterminal durch die rechte Seite
einer Produktion. Dies wird so lange wiederholt, bis im Ausdruck keine Nichtterminale mehr vorkommen.
¨
Alle Worter,
die aus dem Startsymbol herleitbar sind, bilden zusammen die von der Grammatik definierte
Sprache.
Beispiel:
Die Grammatik G1 besteht aus den Produktionen:
G1 :
S ::= ′ A′ | ′ A′ S | b
b ::= ′ B′ | ′ B′ b
¨
G1 erzeugt die folgende Menge von Wortern:
G2 besteht aus den Produktionen:
G2 :
S ::= ′ A′ | ′ B′ | ′ A′ b | ′ B′ a
a ::= ′ A′ | ′ A′ b
b ::= ′ B′ | ′ B′ a
¨
G2 erzeugt die Worter:
8.1 Kontextfreie Grammatik
8-5
Beispiel Die Menge der arithmetischen Ausdrucke
wird durch die folgende Grammatik erzeugt:
¨
expression
term
factor
number
digit
::= term { ′ + ′ term | ′ − ′ term }
::= factor { ′ ∗ ′ factor }
::=
′ ′
( expression ′ )′ | number
::= digit { digit }
::=
′ ′
0 | ′ 1′ | ′ 2′ | ′ 3′ | ′ 4′ | ′ 5′ | ′ 6′ | ′ 7′ | ′ 8′ | ′ 9′
Geschweifte Klammern bedeuten in der EBNF-Schreibweise null oder beliebig viele Wiederholungen.
Eine Expression ist also ein Term oder eine Summe von Termen. Ein Term ist ein Faktor oder ein Produkt
von Faktoren. Ein Faktor ist ein Klammer-Ausdruck oder eine Zahl. Eine Zahl besteht aus einer oder
mehreren Ziffern.
+
expression
term
*
term
factor
−
Um zu uberpr
ufen,
ob ein Ausdruck ein korrekter arithmetischer Ausdruck ist, beschreiben wir den Her¨
¨
leitungsprozess mit Hilfe eines Herleitungsbaumes (Parsetree):
Der Parsetree von 12 ∗ 4 − 3 ∗ (2 + 14) + 21
8-6
8
Top Down Parser
Beispiel: Suchmaschinen erlauben ublicherweise
die komplexe Suche nach Mustern wie
¨
⊕ (schule
software
⊙ schweiz)
Dafur
uber
Zei¨ braucht man eine Sprache, mit welcher der Benutzer diese (boole’schen) Ausdrucke
¨
¨
chenfolgen eingeben kann. Die Menge der boole’schen Ausdrucke
kann zum Beispiel wie folgt erzeugt
¨
werden:
bexpr
::= bterm [ ′ ⊕ ′ bexpr ]
bterm
::= bfac [ ′ ⊙ ′ bterm ]
bfac
::= [ ′ − ′ ] ′ (′ bexpr ′ )′ | [ ′ − ′ ] string
string
::= letter { letter }
letter
::=
′ ′
a | ′ b′ | . . . | ′ z′
Eckige Klammern bezeichnet in der EBNF Schreibweise ein optionales Vorkommen (null oder einmal).
.. bexpr
....
bterm .............
....
...
....
....
.....
...
.
.
.
.
....
......
+g .
................
bexpr
Wir zeichnen den Parsetree des boole’schen Ausdrucks
( a ⊙ b ) ⊙
ac ⊕ abc
8.2 Top-Down Parser
8-7
8.2 Top-Down Parser
¨
Aus einer solchen Grammatik lasst
sich nun relativ leicht ein (rekursiver) Top-Down Parser herleiten: Auf¨
passen mussen
wir dabei nur, dass wir keine nichtterminierenden Zyklen einbauen. Vermeiden konnen
¨
¨ ein oder zwei Zeichen vorauslesen. Dies kann bei Klammerausdrucken
wir das, indem wir falls notig
oder
¨
¨ sein, bzw. immer dann, wenn es mehrere Moglichkeiten
¨
Operationszeichen notig
gibt.
8.2.1 Parser fur
¨ arithmetische Ausdrucke
¨
Zum Implementieren des Parsers fur
schreiben wir eine Klasse Arithmetic¨ arithmetische Ausdrucke
¨
ExpressionParser . Darin definieren wir die Member-Variablen parseString fur
¨ den zu parsenden
¨
String, position fur
des zu parsenden Strings.
¨ die aktuelle Lese-Position und length die Lange
public class ArithmeticExpressionParser {
private String parseString;
private int position;
private int length;
public void parse(String aExpr) throws ParseException { . . . }
// one method per grammar line
. . .
}
Die Methode parse() ruft die Startfunktion expression()
String gelesen wurde.
auf und testet am Schluss, ob der ganze
public void parse(String aExpr) throws ParseException {
position = 0;
parseString = aExpr;
length = parseString.length();
expression();
if (position < length) throw new ParseException(position, "parse");
}
8-8
8
Top Down Parser
¨
expression()
fangt
(laut Grammatik) immer mit einem Term an, also rufen wir zuerst die Prozedur
term() auf. Falls ein ’+ ’ oder ein ’- ’ im Ausdruck folgt, haben wir weitere Terme und es ist ein Loop
¨
notig.
private void expression() throws ParseException {
term();
while(position < length ) {
if( parseString.charAt(position) == ’+’
|| parseString.charAt(position) == ’-’) {
position++;
term();
}
else
return;
}
}
term()
¨
fangt
(laut Grammatik) mit einem Faktor an. Falls darauf ein ’* ’ folgt, ist ein rekursiver Aufruf
¨
notig.
private void term() throws ParseException {
factor();
while (position < length)
if( parseString.charAt(position) == ’*’) {
position++;
factor();
}
else
return;
}
¨
¨
In factor() ist nun eine Abfrage notig,
um sicherzustellen, dass jede geoffnete
Klammer wieder geschlossen wurde. Falls keine Klammer gelesen wurde, muss an dieser Stelle eine oder mehrere Ziffern
stehen.
8.2 Top-Down Parser
8-9
private void factor() throws ParseException {
try {
if (parseString.charAt(position) == ’(’) {
position++;
expression();
if (parseString.charAt(position) == ’)’)
position++;
else
throw new ParseException(position, "factor");
} else
number();
}
catch (StringIndexOutOfBoundsException e) {
throw new ParseException(position, "factor");
}
}
¨
number() muss mindestens eine Ziffer lesen konnen,
andernfalls ist an dieser Stelle im Ausdruck ein
Fehler.
private void number() throws ParseException {
int n = position;
while (position < length && isDigit(parseString.charAt(position)))
position++;
if (n == position)
throw new ParseException(position, "number: digit expected");
}
8-10
8
Top Down Parser
8.2.2 Parser fur
¨ boole’sche Ausdrucke
¨
¨
Als nachstes
bauen wir einen Parser fur
¨ die von der Grammatik von Seite 8-6 erzeugten boole’schen
Ausdrucke:
¨
bexpr
::= bterm [ ′ ⊕ ′ bexpr ]
bterm
::= bfac [ ′ ⊙ ′ bterm ]
bfac
::= [ ′ − ′ ] ′ (′ bexpr ′ )′ | [ ′ − ′ ] string
string
::= letter { letter }
letter
::=
′ ′
a | ′ b′ | . . . | ′ z ′
Da wir auf der normalen Tastatur die Zeichen
ein + fur
¨ ′ ⊕ ′ und ein ∗ fur
¨ ′ ⊙′ .
′
⊙ ′ und
′
⊕ ′ nicht haben, schreiben wir im Programm
¨
Die Methode parse() konnen
wir (fast) aus dem letzten Abschnitt kopieren.
8.2 Top-Down Parser
public void parse(String aExpr) throws ParseException {
position = 0;
parseString = aExpr;
length = parseString.length();
booleanExpression();
if (position < length)
throw new ParseException(position, "parse");
}
Ein boole’scher Ausdruck ist entweder ein einfacher Term oder ein OR-Ausdruck (bterm + bexpr)2 .
private void booleanExpression( ) throws ParseException {
2 vgl. ∼ amrhein/AlgoData/Parser
8-11
8-12
8
Top Down Parser
Ein bterm ist ein AND-Term, also ein bfac oder ein bfac multipliziert mit einem bterm.
private void booleanTerm( ) throws ParseException {
Ein booleanFactor ist ein String, ein negierter String, ein boole’scher Ausdruck oder ein negierter boole’scher Ausdruck.
private void booleanFactor(
) throws ParseException {
8.2 Top-Down Parser
booleanName liest so lange als Buchstaben im Ausdruck erscheinen.
void booleanName() throws ParseException {
int n = position;
while (position < length && isAlpha(parseString.charAt(position)))
position++;
if (n == position)
throw new ParseException(position, "booleanVariable: letter expected");
}
8-13
8-14
8
Top Down Parser
¨
8.3 Ubung
8
Syntax-Diagramm Gegeben ist das folgende Syntaxdiagramm:
S
T
T
(
+
S
-
S
F
*
T
)
/
F
F
a
b
c
1. Schreiben Sie es in eine Grammatik um.
2. Welche der folgenden Terme sind durch diese Grammatik erzeugt worden?
(a) a*(b-c)
(c) a+(a*b)+c
(e) (a+b)/(a-b)
(b) a/(b*c)
(d) a+b+(c*b)
(f) ((a*b)/(b*c))
Parser Gegeben sei die folgende Grammatik:
r ::=
s ::=
t ::=
′ ′ ′
L +′ s | s
N ′ | ′ N ′ ′ (′ t ′ )′
′ ′ ′ ′
M + r | r
′
1. Welche der folgenden Ausdrucke
sind von dieser Grammatik erzeugt worden?
¨
(a) L + N
(d) L + N (N + M)
(b) N (M + N)
(e) N (L + N (M)))
(c) L + N (L + N)
(f) N (N (L + M))
2. Schreiben Sie einen Parser fur
¨ diese Grammatik.
Grammatiken
1. Geben Sie eine Grammatik fur
dem Alphabet { A,B } an, welche
¨ die Menge aller Strings uber
¨
¨
hochstens
zwei gleiche Buchstaben in Folge haben.
2. Erweitern Sie die Grammatik fur
so, dass auch Exponentiation (∧ ) und
¨ arithmetische Ausdrucke
¨
Division (/) als Operationen erlaubt sind.
9
Kryptologie
Mit der zunehmenden Vernetzung, insbesondere seit das Internet immer mehr Verbreitung findet, sind
Methoden zum Verschlusseln
von Daten immer wichtiger geworden. Kryptologie fand ihren Anfang vor al¨
¨
lem in militarischen
Anwendungen. Seit der Erfindung des elektronischen Geldes findet die Kryptologie1
aber immer mehr Anwendungen im kommerziellen Bereich. Die wichtigsten Stichworte sind hier: Ge¨ Integriat
¨
¨ (Falschungssicherheit).
heimhaltung, Authentifizierung(Nachweisen der eigenen Identitat),
Um zu wissen, wie sicher eine Verschlusselungsmethode
ist, mussen
wir aber auch die andere Seite
¨
¨
¨
kennen. Wahrend
sich die Kryptographie (Lehre des Verschlusselns)
mit den verschiedenen Verschlusse¨
¨
¨
lungs Methoden beschaftigt,
lehrt die Kryptoanalyse, wie man Codes knackt. Nur wenn wir die Methoden
¨
der Code-Knacker kennen, konnen
wir beurteilen, ob eine Verschlusselungsmethode
fur
¨
¨ unser Ansinnen
brauchbar (d.h. genugend
sicher) ist.
¨
Kryptologie = Kryptographie + Kryptoanalyse
In der Regel gilt, je kritischer die Daten sind, desto sicherer muss die Verschlusselung
sein, und desto
¨
¨
¨
aufwandiger
ist das Verschlusselungsverfahren.
Je schwieriger namlich
ein Code zu knacken ist, desto
¨
¨
teurer ist eine Attacke fur
Attacke versuchen,
¨ den Kryptoanalytiker. Er wird also nur dann eine aufwandige
wenn er sich einen entsprechenden Gewinn erhoffen kann. Prinzipiell gilt:
• es gibt keine einfachen Verfahren, die trotzdem einigermassen sicher sind.
• (fast) jeder Code ist knackbar, falls genugend
Zeit und genugend
verschlusselter
Text vorhanden ist.
¨
¨
¨
1 Weitere Informationen zu Kryptographie findet man zum Beispiel unter home.nordwest.net/hgm/krypto
.
9-2
9 Kryptologie
9.1 Grundlagen
¨
Ein klassisches Kryptosystem besteht aus einem Sender, einem Empfanger
und einer Datenleitung, an
¨
welcher ein Horcher2 zuhort.
Horcher
Klartext
Sender
unsichere
Datenleitung
Schlüssel
Empfänger
Klartext
Schlüssel
sichere Übertragung
Der Sender verschlusselt
eine Meldung (den Klartext) und sendet diesen uber
die unsichere Datenlei¨
¨
¨
tung dem Empfanger.
Das Verschlusseln
geschiet entweder Zeichenweise (Stromchiffre) oder der Text
¨
¨
¨
¨
wird erst in Blocke
aufgeteilt und dann werden die Blocke
verschlusselt
(Blockchiffre). Der Emfpanger
¨
entschlusselt
dann die Meldung wieder mit der Umkehrfunktion. Der verwendete Schlussel
muss vorher
¨
¨
auf sicherem Weg (zum Beispiel per Kurier) ubermittelt
werden.
¨
¨
¨
Diese Situation ist heute Normalfall nicht (mehr) gewahrleistet.
So mochte
der Kunde im WWW nicht vorher per Post mit jedem Anbieter geheime Schlussel
austauschen. Trotzdem will er sicher sein, dass kein
¨
¨
Horcher das Bankpasswort, die Bestell/Transaktionsdaten oder die Kreditkartennummer erfahrt.
Ausser¨
dem ist das Verwalten von vielen Schlusseln
sehr aufwandig
(und unsicher).
¨
¨
¨
Das Ziel ist also, ein moglichst
effizientes und doch sicheres System zu haben, welches mit moglichst
wenig Verwaltungsaufwand auskommt.
¨
2 Als Sender/Empfanger
mussen
wir davon ausgehen, dass der Horcher weiss, welches Verschlusselungsverfahren
angewandt wurde.
¨
¨
9.2 Einfache Verschlusselungmethoden
¨
9-3
9.2 Einfache Verschlusselungmethoden
¨
¨
Eine der einfachsten und altesten
Methoden zur Verschlusselung
von Texten wird Kaiser Caesar zuge¨
schrieben. Die Methode heisst darum auch die Caesar Chiffre. Dabei wird jeder Buchstabe des Textes
durch den um k Buchstaben verschobenen Buchstaben ersetzt (Stromchiffre).
Beispiel
Klartext:
A
B
C
D
E
F
G
H
I
Verschlusselt:
¨
D
E
F
G
H
I
J
K
L
···
···
W
X
Z
Y
Z
A
B
C
Der Klartext geheimer Text wird damit zu
Klartext:
G
E
H
Verschlusselt:
¨
J
H
K
E
I
M
E
R
T
E
X
T
Der Schlussel
ist k = 3, da alle Buchstaben um drei verschoben werden.
¨
¨
¨
Dieser Code lasst
sich sehr leicht knacken, auch wenn wir den Schlussel
nicht kennen. Nach hochstens
¨
27 Versuchen haben wir den Code entziffert.
Verschlusselt:
¨
X
Y
W
J
S
L
E
L
J
M
J
N
R
Klartext:
Eine etwas verbesserte Methode ist, ein Schlusselwort
als Additionstabelle zu benutzen (Blockchiffre).
¨
¨
` Chiffre. Dabei andert
Diese Verschlusselungsart
ist bekannt als Vigenere
sich die Anzahl zu verschie¨
benden Buchstaben jeweils in einem Zyklus, welcher gleich lang wie das Schlusselwort
ist.
¨
Beispiel Wir benutzen das Wort geheim als Schlussel.
Die Verschlusselung
geschieht dann nach fol¨
¨
gendem Schema:
Klartext:
D
I
E
S
E
R
Schlussel
¨
G
E
H
E
I
M
Verschlusselt:
¨
G
T
E
X
T
E
H
E
I
I
M
S
T
···
9-4
9 Kryptologie
¨
¨
Eine weitere Moglichkeit
besteht darin, alle Buchstaben zufallig
zu permutieren. Dies fuhrt
zu einer Per¨
mutationschiffre. Der Schlussel
ist dann die auf den Buchstaben benutzte Permutationsfunktion (bzw.
¨
die Umkehrung davon).
Klartext:
A
B
C
D
E
F
G
H
I
J
K
Verschlusselt:
¨
S
H
X
A
K
D
G
R
.
.
.
L
···
¨
Eine leicht verbesserte Version von Permutationschiffren benutzt Permutationen von Blocken,
zum Bei¨
spiel von Zweierblocken:
Klartext:
AA
AB
AC
AD
AE
AF
AG
AH
AI
AJ
Verschlusselt:
¨
RS
HI
WX
CD
JK
RE
HG
UV
.
.
···
Damit mussten
im Prinzip 27! (bzw. 272 !) Variationen getestet werden, um den Text zu entziffern. Leider
¨
ist die Kryptoanalyse auch fur
einfach, sofern ein normaler (deut¨ diese zwei Verschlusselungsmethoden
¨
¨
¨
scher) Text ubermittelt
wird. Um solche Texte zu knacken, arbeitet man mit Haufigkeitsanalysen.
So lasst
¨
sich zum Beispiel die Verschlusselung
des Buchstabens E schnell erraten, da dies mit grossem Abstand
¨
¨
der haufigste
Buchstabe ist.
E
-
N
R
I
S
T
A
D
H
U
L
15.36
15.15
8.84
6.86
6.36
5.39
4.73
4.58
4.39
4.36
3.48
2.93
...
¨
Am zweithaufigsten
ist das Leerzeichen, dann der Buchstabe N usw.. Da Sprache im allgemeinen stark
redundant ist, kann man oft mit Hilfe von ein paar wenigen erkannten Buchstaben bereits den ganzen
Text entziffern.
¨
¨
Auch fur
kurzer Lange)
funktioniert die gleiche Attacke. Fur
¨ Blockpermutationen (mit Blocken
¨ diese
¨
¨
¨
braucht man eine Haufigkeitsanalyse
der Zweier- (Dreier-)Blocke
einer Sprache. Der haufigste
Zweierblock in der deutschen Sprache ist die Silbe en. Der Text muss allerdings genugend
lang sein, d.h.
¨
¨
¨
genugend
viele Blocke
aufweisen, um eine aussagekraftige
Statistik zu erhalten.
¨
¨
¨
` Blockchiffren angewendet werden (sepaSolche Haufigkeitsanalysen
konnen
genau gleich auf Vigenere
¨
rat auf jeden Buchstaben des Schlusselwortes),
falls die Texte viel langer
sind als das Schlusselwort.
¨
¨
9.3 Vernamchiffre, One Time Pad
9-5
9.3 Vernamchiffre, One Time Pad
` Chiffre das Schlusselwort
¨
Falls in der Viginere
aus einer zufalligen
Buchstabenfolge besteht, die ebenso
¨
lang ist wie der verschlusselte
Text, und falls jeder Schlussel
nur einmal verwendet wird, ist das Ver¨
¨
schlusselungs-Verfahren
absolut sicher. Man nennt dieses Verfahren die Vernam-Chiffre oder das One
¨
Time Pad.
¨
Die Vernam-Chiffre ist allerdings sehr umstandlich,
da fur
Zeichen zuvor ein Zei¨ jedes zu ubermittelnde
¨
chen uber
einen sicheren Weg transportiert werden muss. Dennoch wird sie benutzt, wenn absolute,
¨
¨ ist.
beweisbare Sicherheit notig
¨
Statt einer zufalligen
Buchstabenfolge (welche auf einem sicheren Weg ubermittelt
werden muss), wird
¨
¨
als Schlussel
haufig
eine Zufalls-Zahlenreihe benutzt. Diese wird mit Hilfe einer geheimen, vorher aus¨
¨
gemachten Zufallsfunktion generiert. Solange der Horcher diese Funktion nicht durch Sabotage erfahrt,
ist dieses Verfahren sehr effizient und ebenfalls sicher. Oft werden sogar mehrere Zufalls-Funktionen
kombiniert oder abwechlungsweise benutzt.
Beispiel: Eine Funktion, welche eine gute “Zufallsfolge” herstellt, ist zum Beispiel die Funktion f (n) =
⌊100(sin(n) + 1)⌋. Die erzeugte Zahlenreihe ist:
n
:
1
2
3
4
5
6
7
8
9
10
11
12
...
f (n)
:
184
190
114
24
4
72
165
198
141
45
0
46
...
f (n) mod 27 :
22
1
6
24
4
18
3
9
6
18
0
19
...
Mit Hilfe von solchen Funktionen werden sogennante Verschlusselungs/Entschl
usselungs-Maschinen
ge¨
¨
¨
¨
baut, zum Beispiel fur
von grossen Datenmengen notig
¨ Telefone, die typischerweise die Ubertragung
¨ erzeugen jeweils die gleiche Zufallsfolge zum (binaren)
¨
machen. Beide Gerate
Ver- bzw. Entschlusseln
¨
der geheimen Texte. Die im Telefon eingebauten Schlusselerzeuger
sind dann im Prinzip nichts anderes
¨
als gute und effiziente Zufallszahlen-Generatoren.
9-6
9 Kryptologie
9.4 Moderne symmetrische Verfahren
Die meisten der heute eingesetzten symmetrischen Verfahren sind Blockchiffren. Dabei wird der Text in
¨
¨
¨
¨
Blocke
einer gewissen Lange
(haufig
8 oder 16 Zeichen, also 64 oder 128 Bits) zerlegt. Auf diese Blocke
wird dann eine Kombination von verschiedenen einfachen Verschlusselungsverfahren
(z.B. modulare Ad¨
¨
dition, Substitution, Linear-Transformation, Vertauschen von Teilblocken,
...) angewandt.
Das folgende Bild zeigt zum Beispiel die Anordnung fur
¨ DES.
Normalerweise wird ein geheimer, vorher vereinbarter Schlussel
verwendet. Das Entschlusseln
geschieht
¨
¨
indem alle Operationen in umgekehrter Reihenfolge (invers) angewandt werden.
Beispiele von Blockhiffren sind: DES (Data Encryption Standard) oder DEA (Data Encryption Algorithm)
¨
, benutzt einen Schlussel
der Lange
56. Triple-DES: Dreimaliges Anwenden von DES mit zwei oder
¨
drei verschiedenen Schlusseln.
RC4 Rivest Cipher, 1987 von Ronald L. Rivest, eine Stromchiffre IDEA:
¨
¨
arbeitet ahnlich
wie DES, CAST, ... SSL (Secure Socket Layer) verwendet RC4, DES oder Triple-DES.
S-HTTP (Secure HTTP) verwendet DES, Triple-DES oder IDEA. PGP (Pretty Good Privacy) benutzt
verschiedene der Verfahren IDEA, Triple-DES, RSA, ...
9.5 Asymmetrische Verfahren: Public Key Kryptosysteme
9-7
9.5 Asymmetrische Verfahren: Public Key Kryptosysteme
Bei kommerziellen Applikationen wie zum Beispiel Telebanking, beim Benutzen von elektronischem Geld
¨
oder beim Versenden von (geheimer) Email ist es zu aufwandig,
mit jedem Kunden/Partner vorher geheime Schlussel
auszutauschen. Um genugend
Sicherheit zu bieten, mussten
lange Schlussel
verwendet
¨
¨
¨
¨
¨
¨
werden, welche haufig
gewechselt werden. (Die mit den Banken vereinbarten Schlussel/Passw
orter
die¨
nen beim Telebanking in der Regel in erster Linie zur Authentifikation/Identifikation des Kunden.)
Es gibt aber Verfahren, welche ohne die Verteilung von geheimen Schlusseln
auskommen, und die darum
¨
Public Key Kryptosysteme (PKK) genannt werden3 . Wie der Name sagt, benutzen PKKs nicht geheime,
¨
sondern offentliche
Schlussel
zum Verschlusseln
der Texte.
¨
¨
Verschlüsselung
öffentlicher
Schlüssel des Empfängers
Entschlüsselung
Übermittlung
geheimer
Schlüssel des Empfängers
In PKKs werden fur
Funktionen benutzt, welche leicht zu berechnen, aber ohne
¨ die Verschlusselung
¨
¨
¨
¨
zusatzliches
Wissen nicht invertierbar sind. Diese Einwegfunktionen konnen
dann offentlich
bekannt
gegeben werden. Da die Funktionen schwierig zu invertieren sind, ist das Entschlusseln
nicht einfach
¨
¨
moglich.
Es gilt dann also:
¨
• Jeder kann mit dem offentlichen
Schlussel
Meldungen codieren.
¨
• Nur wer den geheimen Schlussel
kennt, kann die Meldung decodieren.
¨
Definition: Eine bijektive Funktion f : X → Y heisst eine Einwegfunktion falls gilt:
• Die Funktion f ist leicht (mit wenig Rechenaufwand) zu berechnen.
3 Da fur
zum Ver- bzw. Entschlusseln
verwendet werden, spricht man auch von asymmetrischen
¨ diese Verfahren zwei verschiedene Schlussel
¨
¨
Verfahren.
9-8
9 Kryptologie
¨
• Die Umkehrfunktion f −1 ist ohne zusatzliche
(geheime) Informationen sehr schwierig (mit grossem
Aufwand) zu berechnen.
Ein einfaches Modell fur
¨ eine Einwegfunktion ist ein Telefonbuch, mit welchem sehr schnell zu jedem
¨
Namen mit Adresse die Telefonnummer gefunden werden kann. Hingegen ist es sehr aufwandig,
mit
¨
Hilfe eines Telefonbuchs zu einer Telefonnummer den zugehorigen
Namen zu finden. Es musste
das
¨
ganze Buch durchsucht werden.
¨
Das Finden von sicheren Einwegfunktionen ist nicht einfach. Wenn wir den Schlussel
kennen, konnen
wir
¨
in allen bisherigen Verfahren sehr leicht die Umkehrfunktion berechnen. Das Suchen von guten, schnellen
und beweisbar sicheren Einwegfunktionen ist ein zentrales Forschungsgebiet der Kryptologie. Heute sind
aber schon einige praktisch verwendbare (nicht beweisbar sichere) Einwegfunktionen bekannt.
Die Idee bei einem Public Key Kryptosystem ist, dass jeder Teilnehmer fur
¨ sich ein Paar von Schlusseln
¨
¨
generiert: einen offentlichen
Schlussel
zum Verschlusseln
der Meldungen, und einen privaten, gehei¨
¨
¨
men Schlussel
zum Entschlusseln.
Der geheime Schlussel
darf aus dem offentlichen
Schlussel
nur mit
¨
¨
¨
¨
¨
riesigem Aufwand oder gar nicht berechnet werden konnen.
Der erste Schlussel
generiert dann eine Einwegfunktion, welche nur mit Kenntnis des zweiten Schlus¨
¨
sels (oder nur mit sehr grossem Aufwand) invertiert werden kann.
¨
Um einem Teilnehmer eine Meldung zu verschicken, verschlusseln
wir die Meldung mit dessen offentli¨
chem Schlussel.
Nur der Adressat, der den geheimen Schlussel
kennt, kann die Einwegfunktion invertie¨
¨
ren, also die verschlusselte
Meldung wieder entschlusseln.
¨
¨
Definition: Fur
die folgenden Bedingungen erfullt
¨ ein PKK mussen
¨
¨ sein:
1. Es gibt genugend
viele Paare (V, E) von Verschlusselungsund Entschlusselungsfunktionen
(bzw.
¨
¨
¨
¨
von offentlichen
und geheimen Schlusseln
(v,e)).
¨
2. Fur
¨ jede Meldung M gilt E(V (M)) = M .
3. V ist eine Einwegfunktion.
4. V und E sind leicht zu berechnen, wenn man den Schlussel
v, bzw. e kennt.
¨
9.5 Asymmetrische Verfahren: Public Key Kryptosysteme
9-9
¨
Das erste PKK ist der Diffie-Hellmann Algorithmus von 1976. Auf einer ahnlichen
Idee basiert der um
1978 von R. Rivest, A. Shamir und L. Adleman gefundene RSA Algorithmus. Die darauf basierenden
Verfahren werden deshalb RSA-Kryptosysteme genannt.
9.5.1 Das RSA Verfahren
Das RSA-Verfahren ist heute das am meisten benutzte PKK. RSA bildet die Grundlage fur
¨ SSL (Secure
Socket Layer), welche vor allem fur
¨ WWW gebraucht werden, fur
¨ SET (Secure Electronic Transactions),
welche im Zusammenhang mit elektronischem Geld wichtig sind, fur
¨ S/Mime, also sichere Email und
vieles mehr (z.B. S-HTTP, SSH, ...).
Die Sicherheit des RSA-Verfahren basiert auf dem Problem, eine grosse Zahl in ihre Primfaktoren zu
zerlegen und aus dem Problem des diskreten Logarithmus. Das Berechnen von M v modm ist relativ
einfach. Fur
¨ das Berechnen der v-ten Wurzel modulo m ist bisher kein schneller Algorithmus bekannt.
Die beiden Schlussel
werden so erzeugt:
¨
¨
• Wahle
zwei verschiedene grosse Primzahlen p und q und berechne deren Produkt: m = p ∗ q. Setze
n = (p − 1) · (q − 1).
¨
• Wahle
einen beliebigen Wert e, der kleiner ist als m und teilerfremd zu n. Zu diesem wird dasjenige
v berechnet, fur
¨ das gilt: e · v = 1 + l · n (Euklid’scher Algorithmus).
˜ = M˜ e mod m.
Es gilt dann fur
V (M) = M v mod m fur
E(M)
¨ das Verschlusseln:
¨
¨ das Entschlusseln:
¨
v
V(M) = M mod m
öffentlicher
Schlüssel des Empfängers
Übermittlung
e
E(M) = M mod m
geheimer
Schlüssel des Empfängers
¨
Es gilt namlich
nach dem Satz von Fermat
E(V (M)) = (V (M))e mod m = M ve mod m = M (1+ln) modm = M
9-10
9 Kryptologie
9.5.2 Authentifikation mit Hilfe von RSA
¨
¨
Mit Hilfe eines RSA-Kryptosystems konnen
wir auch feststellen, ob der Absender einer Meldung tatsachlich derjenige ist, der er zu sein vorgibt. Durch umgekehrtes Anwenden des RSA-Verfahrens kann der
¨
Empfanger
nachprufen,
ob die Meldung vom richtigen Sender stammt.
¨
Wir wissen bereits, dass E(V (M)) = M ve mod m = M gilt. Die Potenzoperation ist aber kommutativ,
¨
so dass auch M ev mod m = M gilt. Wir konnen
also die Operationen Verschlusseln/Entschl
usseln
auch
¨
¨
umdrehen.
Verschlüsselung
Übermittlung
Entschlüsselung
öffentlicher
Schlüssel des Senders
geheimer, privater
Schlüssel des Senders
¨
Nur wenn beim Entschlusseln
eines Textes mit dem offentlichen
Schlussel
des Absenders Klartext ent¨
¨
¨
steht, stammt die Meldung von diesem Absender. Da nur der Absender den zu seinem offentlichen
Schlussel
passenden geheimen Schlussel
kennt, kann nur dieser eine solche Meldung verfassen.
¨
¨
¨
9.5.3 Integritatspr
ufung:
¨
Fingerabdruck, Message Digest
¨
Ein zentrales Problem vor allem von grossen Anbietern (Banken, Online-Verkaufern,
...) ist, den Kunden
¨
zu garantieren, dass eine (unverschlusselte)
Webseite tatsachlich
die richtige Seite ist (und nicht eine
¨
¨
¨ werden.
gefalschte).
Dieses Problem kann mit Hilfe einer Hashfunktion und eines PKKs gelost
Übermittlung
Hash
Funktion
1
0
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
=?
Hash
Funktion
Verschlüsselung
geheimer, privater
Schlüssel des Senders
Entschlüsselung
öffentlicher
Schlüssel des Senders
9.5 Asymmetrische Verfahren: Public Key Kryptosysteme
9-11
¨
Falls beim Entschlusseln
des Hashcodes mit dem offentlichen
Schlussel
des Absenders der gleiche Wert
¨
¨
¨
herauskmmt, sind wir sicher, dass unterwegs niemand die Meldung verandert
hat.
¨
Allerdings funktioniert dies nur, wenn der uns bekannte offentliche
Schlussel
korrekt ist, uns also kein
¨
¨
falscher Schlussel
vorgetauscht
wird. Dies garantieren spezielle Firmen und Institutionen, sogenannte
¨
Trustcenter wie TC TrustCenter, VeriSign oder Thawte.
9.5.4 Kombinierte (Hybride) Verfahren
Eine kombinierte Methode verbindet die Sicherheit von RSA mit der Schnelligkeit von symmetrischen
Verschlusselungsmethoden,
wie zum Beispiel DES.
¨
DES
DES Schlüssel
0110
000000000
111111111
111111111
000000000
111111111
000000000
111111111
000000000
111111111
000000000
Übermittlung
Digital
Envelope
Verschlüsselung
öffentlicher Schlüssel
des Empfängers
Ein Digital Envelope wird erzeugt, indem der Text durch ein schnelles, weniger sicheres Verfahren ver¨
schlusselt
wird (zum Beispiel mit DES), der DES-Schlussel
selber wird mit dem offentlichen
RSA-Schlus¨
¨
¨
¨
sel des Empfangers
verschlusselt.
¨
¨
Auf diese Weise konnen
die Vorteile beider Systeme kombiniert werden. Es mussen
keine geheimen
¨
Schlussel
auf einem (langsamen) sicheren Weg vorher vereinbart werden. Ausserdem kann fur
¨
¨ jede
¨
Ubermittlung ein neuer DES-Schlussel
verwendet werden. Durch einmaliges Verwenden jedes DES¨
Schlussels
wird die Sicherheit des DES-Verfahrens erheblich verbessert.
¨
¨
Nur der Empfanger
kann den DES-Schlussel
lesen, da er dazu seinen privaten RSA-Schlussel
braucht.
¨
¨
Danach kann er mit Hilfe des DES-Schlussels
den Text entziffern.
¨
9-12
9 Kryptologie
¨
9.6 Ubung
9
Fragen / Aufgaben zur Kryptologie
¨ uhrung
¨
Sie finden die Losungen
zu den Fragen entweder im Skript oder unter dem Link Einf
in die Kryp¨
¨
¨ der Ubungsseite.
tologieauf
1. Welches sind heute die zentralen Einsatzgebiete der Kryptologie?
2. Was bedeutet Kryptografie?
3. Was bedeutet Kryptoanalyse?
4. Was ist Steganografie?
¨
¨
5. Wie heisst der Uberbegriff
fur
Zeichenweise ablauft?
¨ die Verfahren, bei welcher die Verschlusselung
¨
¨
6. Gewisse Verfahren teilen den Text zuerst in gleich grosse Blocke
(zum Beispiel 64 Bit) auf: Welches
¨
ist der Uberbegriff
fur
¨ diese Verfahren?
7. Skizzieren Sie ein symmetrisches Kryptosystem.
¨
8. Verschlusseln
Sie mit der Casar
Chiffre und dem Schlussel
B (k=1) Ihren Namen.
¨
¨
` Chiffre und dem Schlussel
9. Verschlusseln
Sie mit der Viginere
BBC Ihren Namen.
¨
¨
10. Welches sind die Vor- und Nachteile des One Time Pad (Vernam Chiffre)?
11. Was waren die Hauptgrunde
dafur,
¨
¨ dass die Enigma im zweiten Weltkrieg geknackt werden konnte?
¨
(Punkte 3, 6 und 7 der Aufzahlung
unter http://www.nwn.de/hgm/krypto/ → Enigma)
12. Welche symmetrischen Kryptoverfahren werden heute (noch) verwendet?
13. Skizzieren Sie ein asymmetrisches Kryptosystem.
14. Welches ist der Hauptunterschied zwischen symmetrischen und asymmtetrischen Verfahren?
15. Wer hat das erste PKK erfunden?
16. Worauf basiert die Sicherheit von RSA?
¨ man mit einem PKK das Authentizitatsproblem?
¨
17. Wie lost
¨ man mit einem PKK das Integritatsproblem?
¨
18. Wie lost
19. Was ist der Vorteil von hybriden Kryptoverfahren?
Literaturverzeichnis
[AHU74] Aho, Hopcroft, and Ullman. The Design and Analysis of Computer Algorithms. Addison Wesley,
Reading, Massachusetts, 1974. ISBN 0-201-00029-6.
[AU95]
A.V. Aho and J.D. Ullman. Foundations Of Computer Science C Edition. Computer Science
Press An Imprint of W.H. Freeman and Company, New York, 1995. ISBN 0-7167-8284-7.
[Bud94] Timothy A. Budd. Classic Data Structures in C++. Addison-Wesley, Reading, MA, 1994.
[Knu73a] D.E. Knuth. The Art of Computer Programing, volume 3 Sorting and Searching. Addison Wesley,
Reading, MA, 1973. ISBN 0-201-03803-X.
[Knu73b] D.E. Knuth. The Art of Computer Programing, volume 1 Fundamental Algorithms. Addison
Wesley, Reading, MA, 1973. ISBN 0-201-03809-9.
[Knu81] D.E. Knuth. The Art of Computer Programing, volume 2 Seminumerical algorithms. Addison
Wesley, Reading, MA, 1981. ISBN 0-201-03822-6.
[Sed92] Robert Sedgewick. Algorithmen in C++. Addison Wesley, Bonn, 1992. ISBN 3-89319-462-2.
[Sed03] Robert Sedgewick. Algorithmen in Java. Grundlagen, Datenstrukturen, Sortieren, Suchen..
Pearson Studium, 2003. ISBN 3-82737-072-8.
[Sha97] Clifford A. Shaffer. A Practical Introduction to Data Structures and Algorithm Analysis. Prentice
Hall, London, 1997. ISBN 0-131-90752-2, C++ Version.
[Sha98] Clifford A. Shaffer. A Practical Introduction to Data Structures and Algorithm Analysis: Java
Edition. Prentice Hall, London, 1998. ISBN 0-136-60911-2.
[Wir86]
N. Wirth. Algorithmen und Datenstrukturen mit Modula-2. Teubner, Stuttgart, 1986. ISBN
0-13-629031-0.
[SaSa04] Gunter Saake, Kay-Uwe Sattler Algorithmen und Datenstrukturen, Eine Einfuhrung
mit Java.
¨
dpunkt, 2004. ISBN 3-89864-255-0.
[Schied05] Reinhard Schiedermeier Programmieren mit Java, Eine methodische Einfuhrung.
Pearson
¨
Studium, 2005 ISBN 3-8273-7116-3.
W-2
Index
abstrakter Datentyp, 1-5, 1-6
Algorithmus, 1-9
asymptotisches Verhalten, 2-5, 2-7
¨ 2-1
Komplexitat,
O-Notation, 2-7
Allgemeine Strukturen, 1-4
Array
Grundtyp, 1-4
Indextyp, 1-4
Selektor, 1-4
Atomare Typen, 1-3
Auswahl, 7-2
Automat, 7-3
¨
ε-Ubergang,
7-6
Ausgabe, 7-4
deterministischer, 7-5
Eingabe, 7-3
Eingabealphabet, 7-3
endlicher, 7-2
¨
leerer Ubergang,
7-6
nichtdeterministischer, 7-5
Zustand, 7-3
Zustandsdiagramm, 7-4
Zustandstafel, 7-4
B-Baum, 4-10
¨
Baume,
4-1
BinNode, 4-2
¨
Baumdurchlaufe,
4-4
Breitensuche, 4-7
Inorder, 4-4
Levelorder, 4-7
Postorder, 4-4
¨
Praorder,
4-4
Tiefensuche, 4-4, 4-6
¨
Binarbaum,
4-1, 4-2
¨ Suchbaume,
¨
Binare
4-8
Caesar Chiffre, 9-3
Compiler, 8-1
Daten, 1-2
Bezeichung, 1-2
Semantik, 1-2
Syntax, 1-2
Wertemenge, 1-2
Datenstruktur, 1-6
Datentyp, 1-2
Konstanten, 1-2
Methoden, 1-2
Operatoren, 1-2
Wertebereich, 1-2
Divide and Conquer, 1-18
EBNF, 8-2
Einwegfunktion, 9-8
Extended Backus-Naur Form, 8-2
Grammatik, 8-2
Herleitung, 8-3
kontextfreie, 8-2
Nichtterminalsymbol, 8-3
Produktion, 8-3
Terminalsymbol, 8-3
Hashing, 5-5
Bucket Hashing, 5-11
Double Hashing, 5-8
Kollision, 5-6
Linear Probing, 5-8
Separate Chaining, 5-13
Heap, 4-15
Heapbedingung, 4-16
Iteration, 1-12, 7-2
Klassen, 1-4
¨ 2-1
Komplexitat,
Best-Case, 2-4
Worst-Case, 2-4
Konkatenation, 7-2
Kryptoanalyse, 9-1
Kryptologie, 9-1
Caesar Chiffre, 9-3
Einwegfunktion, 9-8
Permutationschiffre, 9-4
Public Key Kryptosystem, 9-7, 9-8
RSA-Kryptosystem, 9-9
Vernam-Chiffre, 9-5
` Chiffre, 9-3
Vigenere
Kryptosystem, 9-2
Liste, 3-1
Array Liste, 3-1
doppelt verkettet, 3-5
Node, 3-5
O-Notation, 2-6
Parser, 8-1
Pattern, 7-1
Alphabet, 7-1
Wort, 7-1
Pattern Matching, 7-1
Permutationschiffre, 9-4
Priority Queue, 4-15
Priority Queues, 4-15
Pseudocode, 1-10
Public Key Kryptosystem, 9-7, 9-8
Queue, 3-1, 3-11
¨
Regularer
Ausdruck, 7-2
Rekursion, 1-15
RSA-Kryptosystem, 9-9
Sortieren
Insertion-Sort, 6-4
Merge-Sort, 6-9
Quick-Sort, 6-6
Selection-Sort, 6-2
stabiler Algorithmus, 6-1
Spezifikation, 1-8
Sprache, 8-3
Herleitung, 8-3
Stack, 3-1, 3-11
Suchen, 5-1
¨ Suche, 5-3
Binare
Lineare Suche, 5-2
Top Down Parser, 8-1
Trustcenter, 9-11
Vernam-Chiffre, 9-5
Verschlusselung,
9-1
¨
` Chiffre, 9-3
Vigenere