Algorithmen und Datenstrukturen. Dr. Beatrice Amrhein

Größe: px
Ab Seite anzeigen:

Download "Algorithmen und Datenstrukturen. Dr. Beatrice Amrhein"

Transkript

1 Algorithmen und Datenstrukturen Dr. Beatrice Amrhein 9. Februar 2015

2 ii Zur umfassenden Ausbildung eines Software-Ingenieurs gehören 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 - Komplexität von Algorithmen, - Algorithmen-Schemata: Greedy, Iteration, Rekursion - Wichtige Datenstrukturen: Listen, Stacks, Queues, Bäume, Heaps - Suchen und Sortieren, Hash-Tabellen - Endliche Automaten, reguläre Sprachen, Pattern Matching - Kontextfreie Grammatiken, Parser Lernziele Die Studierenden kennen die wichtigsten Datenstrukturen mit ihren Methoden. Sie kennen die klassischen Algorithmen und können sie anwenden. Ausserdem k nnen sie Komplexitätsabschätzungen von Algorithmen vornehmen. Informationen zum Unterricht Grundlage ist ein Skript, das die wichtigsten Lerninhalte umfasst. Unterrichtssprache: Umfang: Dozentin: Deutsch (Fachliteratur zum Teil in Englisch) 12 halbtägige Blöcke à 4 Lektionen Beatrice Amrhein, Empfohlene Literatur: - Reinhard Schiedermeier Programmieren mit Java, Eine methodische Einführung. Pearson Studium ISBN Robert Sedgewick Algorithms in Java. Addison-Wesley Professional; 2002 ISBN M. T. Goodrich & R. Tamassia Algorithm Design: Foundations, Analysis, and Internet Examples. John Wiley & Sons, Inc. ISBN: Gunter Saake, Kay-Uwe Sattler Algorithmen und Datenstrukturen, Eine Einführung mit Java. dpunkt, ISBN

3 Inhaltsverzeichnis 1 Einführung Die wichtigsten Ziele dieses Kurses Einige Begriffe: Datenstrukturen Einige Begriffe: Algorithmen Algorithmen Schema: Iteration Algorithmen Schema: Greedy (die gierige Methode) Algorithmen Schema: Rekursion Übung Komplexität von Algorithmen Komplexitätstheorie Komplexitätsanalyse Asymptotische Komplexität Übung Datentypen: Listen, Stacks und Queues Array Listen Doppelt verkettete Listen Stacks und Queues Iteratoren Übung Datentypen: Bäume, Heaps Baumdurchläufe Binäre Suchbäume B-Bäume Priority Queues Übung Suchen Grundlagen Lineare Suche Binäre Suche Hashing Übung Sortieren Selection Sort Insertion Sort Divide-and-Conquer Sortieren Quicksort

4 iv Inhaltsverzeichnis 6.5 Sortieren durch Mischen (Merge Sort) Übung Pattern Matching Beschreiben von Pattern, Reguläre Ausdrücke Endliche Automaten Automaten zu regulären Ausdrücken Übung Top Down Parser Kontextfreie Grammatik Top-Down Parser Übung Kryptologie Grundlagen Einfache Verschlüsselungmethoden Vernamchiffre, One Time Pad Moderne symmetrische Verfahren Asymmetrische Verfahren: Public Key Kryptosysteme Übung

5 1 Einführung 1.1 Die wichtigsten Ziele dieses Kurses Die wichtigsten Ziele des Algorithmen und Datenstrukturen Kurses sind: Die Studierenden kennen die wichtigsten Datenstrukturen, können damit arbeiten, und kennen deren Vor- und Nachteile sowie deren Anwendungsgebiete. Die Studierenden erhalten die Grundlagen, um während der Design Phase die richtigen Datenstrukturen auszuwählen und dann richtig einzusetzen. Die Studierenden kennen die wichtigsten Komplexitätsklassen und deren Einfluss auf das Laufzeitverhalten eines Systems. Die Studierenden kennen die klassischen Algorithmen und können diese anwenden. Sie kennen deren Einsatzgebiete (wann soll welcher Algorithmus benutzt werden) und kennen die Komplexität dieser Algorithmen (in Abhängigkeit der darunterliegenden Datenstrukturen). Die Studierenden erhalten einen Überblick über verschiedene Vorgehensweisen bei Problemlösungen und kennen deren Stärken und Schwächen.

6 1-2 1 Einführung 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 erfolgt gemäss klarer Regeln, der sogenannten Syntax. Daten sind darum Informationen mit folgenden Eigenschaften: 1. Die Bezeichnung erklärt den semantischen Teil (die Bedeutung) des Datenobjekts. 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 für Werte ein Objekt dieses Typs annehmen kann. Die Elemente des Wertebereichs bezeichnet man auch als Konstanten des Datentyps. Dazu gehören die Methoden oder Operatoren, welche auf dem Wertebereich definiert sind und somit auf Objekte dieses Typs angewandt werden können. 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 lösen wollen, müssen wir von den Details abstrahieren können. Wir wollen nicht wissen (müssen) wie genau ein Datentyp implementiert ist, sondern bloss, wie wir den Datentyp verwenden können (welche Dienste er anbietet).

7 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 repräsentieren können. 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 repräsentieren. Die Programmiersprachen Smalltalk und Lisp sind typische Repräsentanten dieser Philosophie. In untypisierten Sprachen kann der Compiler keine sogenannten Typentests durchführen. Zur Kompilationszeit sind alle Operationen auf allen Variablen möglich. Es wird also zur Compilationszeit nicht nachgeprüft, ob gewisse Operationen überhaupt erlaubt sind. Unerlaubte Operationen führen zu Laufzeitfehlern. In typisierten Sprachen wird allen Variablen ein Datentyp zugeordnet. Entweder müssen 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 können mit Hilfe von Mengenoperationen (Subtypen, Kartesische Produkte, Listen,...) weitere Typen abgeleitet werden. Welche atomaren Typen zur Verfügung stehen, hängt von der gewählten 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 natürlich nur eine endliche Teilmenge aus dem Bereich der ganzen, bzw. der reellen Zahlen darstellen können.

8 1-4 1 Einführung 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 = DT 1 DT 2 DT 3... DT n Die Datentypen DT 1,...,DT n können atomare oder auch strukturierte Typen sein. Dazu gehört ausserdem die Spezifikation der zugehörigen Operationen oder Methoden auf DT. Beispiel: Wir definieren ein einfaches Interface PushButton als Basis für einen Button auf einer Benutzeroberfläche).

9 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 unabhängig 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 für 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 Möglichkeit, Eigenschaften einer speziellen Implementation auszunutzen. Die Implementation eines Datentyps kann jederzeit verändert 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 sein, wenn ähnliche Probleme gelöst werden müssen. 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.

10 1-6 1 Einführung 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 Repräsentation der Daten und die Implementation von Prozeduren für 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 Verfügung 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 repräsentiert sind. Diese Details bleiben vor dem Benutzer verborgen. Beispiel: Der abstrakte Datentyp Stack wird durch die Menge der angebotenen Dienste definiert: Einfügen eines Elements (push ), entfernen eines Elements (pop ), lesen des obersten Elements (peek ) und prüfen auf leer (empty ). Eine solche Beschreibung berücksichtigt 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 für Voraussetzungen (Einschränkungen, Vorbedingungen) an die Verwendung der Methoden gestellt sind. 1 In Java könnte ein Interface für einen Stack wie folgt aussehen: 1 Optimalerweise steht noch dabei, welchen Aufwand die Methode hat.

11 1.2 Einige Begriffe: Datenstrukturen 1-7 public interface Stack<E> { /** * Pushes an item onto the top of this stack. item the item to be pushed onto this stack. 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. The object at the top of this stack (the last * item of the Vector object). EmptyStackException if this stack is empty. */ E pop(); /** * Looks at the object at the top of this stack without removing * it from the stack. the object at the top of this stack (the last * item of the Vector object). EmptyStackException if this stack is empty. */ E peek(); /** * Tests if this stack is empty. 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.

12 1-8 1 Einführung Die Spezifikation eines Datentyps muss vollständig, präzise und eindeutig sein. Weiter wollen wir keine Beschreibung, die auf der konkreten Implementation des Datentyps basiert, obwohl diese die geforderten Kriterien erfüllen würde. Eine Beschreibung, die auf der Implementation basiert, führt zu einer Überspezifikation des Datentyps. Konkret können 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..

13 1.3 Einige Begriffe: Algorithmen Einige Begriffe: Algorithmen Ein Algorithmus 2 beschreibt das Vorgehen oder eine Methode, mit der eine Aufgabe oder ein Problem gelöst werden kann, bzw. mit der eine Funktion berechnet werden kann. Ein Algorithmus besteht aus einer Folge von einfachen (Rechen-)Schritten (Anweisungen), welche zur Lösung der gestellten Aufgabe führen. Der Algorithmusgedanke ist keine Besonderheit der Informatik. In fast allen Naturwissenschaften aber auch im Alltag werden Arbeitsvorgänge mit Hilfe von Algorithmen beschrieben. Jeder Algorithmus muss die folgenden Eigenschaften erfüllen: 1. Er muss aus einer Reihe von konkret ausführbaren Schritten bestehen. 2. Er muss in einem endlichen Text beschreibbar sein. 3. Er darf nur endlich viele Schritte benötigen (Termination). 4. Er darf zu jedem Zeitpunkt nur endlich viel Speicherplatz benötigen. 5. Er muss bei der gleichen Eingabe immer das selbe Ergebnis liefern. 6. Nach der Ausführung eines Schrittes ist eindeutig festgelegt, welcher Schritt als nächstes auszuführen ist. 7. Der vom Algorithmus berechnete Ausgabewert muss richtig sein (Korrektheit). Bemerkung: Die Forderung nach Eindeutigkeit wird etwa in parallelen oder probabilistischen Algorithmen zum Teil fallengelassen. Nach dem Abschluss eines einzelnen Schrittes ist der nächste Schritt nicht eindeutig bestimmt, sondern es existiert eine endliche Menge von (möglichen) nächsten Schritten. Die Auswahl des nächsten Schrittes aus der gegebenen Menge ist nichtdeterministisch. Der Anspruch, dass alle Algorithmen terminieren müssen, bedeutet, dass nicht alle von uns benutzten Programme Algorithmen sind. Editoren, Shells oder das Betriebssystem sind alles Programme, die nicht (von selber) terminieren. Wir können aber jedes dieser Programme als Sammlung von verschiedenen Algorithmen betrachten, welche in verschiedenen Situationen zur Anwendung kommen. 2 Das Wort Algorithmus stammt vom Persischen Autor Abu Ja far Mohammed ibn Mûsâ al-khowârizmî, welcher ungefähr 825 vor Christus ein Buch über arithmetische Regeln schrieb.

14 Einführung 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 mitbringen könnte. Beispiel: Grösster gemeinsamer Teiler von m und n: Die kleinere der beiden Zahlen wird so lange von der grösseren 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? Nein Verringere m um n Gib m aus Verringere n um m 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 true if n is a prime { return false;

15 1.3 Einige Begriffe: Algorithmen 1-11 Das nächste 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;

16 Einführung 1.4 Algorithmen Schema: Iteration Unter einem Algorithmen-Schema verstehen wir ein Verfahrens-Muster oder eine allgemeine Methode, mit welcher ein Problem gelöst werden kann. Nicht jede Methode ist für jedes Problem gleich gut geeignet. Umso wichtiger ist es also, die verschiedenen Algorithmen-Schemata zu kennen. Ein Problem wird durch Iteration gelöst, falls der zugehörige Algorithmus einen Loop (while- oder for- Schleife) benutzt. Iteration ist zum Beispiel dann sinnvoll, wenn die Daten in einem Array (oder einer Liste) abgelegt sind und wir mit jedem Element des Array die gleichen Schritte durchführen müssen 3. 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 möglichen Lösungen eines Problems die bezüglich einer Bewertungsfunktion f optimale Lösung gesucht wird (Optimierungsprobleme). Die Greedy-Methode arbeitet in Schritten, ohne mehr als einen Schritt voraus- oder zurückzublicken. Bei jedem Schritt wird aus einer Menge von möglichen Wegen derjenige ausgesucht, der den Bedingungen des Problems genügt und lokal optimal ist. 3 Solche Algorithmen lassen sich oft auch sehr einfach parallelisieren.

17 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 möglich zum Gipfel kommen möchte. Eine einfache Greedy-Strategie für dieses Problem ist die folgende: Bewege dich immer entlang der grössten Steigung nach oben bis dies nicht mehr möglich ist, das heisst, bis in allen Richtungen die Wege nur noch nach unten führen. Dieser Ansatz ist in der Abbildung 1.1 dargestellt. Es ist ein typischer Greedy-Ansatz. Man schaut nicht zurück und wählt jeweils die lokal optimale Strategie. 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 führt. 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 Lösung, während ein heuristischer Ansatz 4 mit Greedy nicht immer die beste Lösung liefert, dies aber in polynomialer Zeit. Ähnliche Probleme sind das Finden von kürzesten Wegen, oder besten (Spiel-)Strategien, Verpackungsprobleme (möglichst 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.

18 Einführung Ein weiteres Problem dieser Art ist das Suchen eines minimalen Pfades in einem allgemeinen Graphen. Um eine optimale Lösung zu finden, müssten wir im wesentlichen sämtliche Pfade abgehen und deren Gewichte aufschreiben. Ein Greedy-Algorithmus löst das Problem viel schneller, indem er jeweils lokal den kürzesten (leichtesten) Pfad wählt. Allerdings findet man mit dieser Methode nicht unbedingt den insgesamt kürzesten Pfad. Es existieren aber auch Probleme, bei denen der Greedy-Ansatz zum optimalen Ergebnis führt. Ein Greedy-Algorithmus löst das folgende Problem: Finde ein minimales (maximales) Gerüst in einem gewichteten Graphen. Dabei wählt man jeweils die Kante, die das kleinste (grösste) Gewicht hat und keinen Zyklus verursacht. Der Algorithmus ist fertig, sobald ein zusammenhängender Teilgraph entstanden ist. 9 9 x y x

19 1.6 Algorithmen Schema: Rekursion Algorithmen Schema: Rekursion Rekursion ist ein fundamentales Konzept der Informatik. Eine Prozedur heisst rekursiv, wenn sie sich direkt oder indirekt selber aufruft. Dabei müssen wir darauf achten, dass eine Abbruchbedingung existiert, damit die Prozedur in jedem Fall terminiert. Beispiele: Die rekursive Implementation der Fakultätsfunktion: 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 ) int sub( int c ) { { if( b-a < 5 ) if( c%2 == 0 ) return sub( b ); return c*c; return a * proc(a-1, b/2) 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 existieren, der keinen Aufruf der Prozedur enthält. Diesen Teil der Prozedur nennt man den Rekursionsanfang. 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 ausgeführten rekursiven Aufrufe (also die Rekursionstiefe) vernünftig bleibt, da sonst zu viel Speicher verwendet wird. Beim rekursiven Sortieren von n Elementen sollten zum Beispiel nur O(log(n)) rekursive Aufrufe nötig sein. Rekursion soll dann angewandt werden, wenn die Formulierung der Lösung dadurch klarer und kürzer wird. Auch darf der Aufwand der rekursiven Lösung in der Ordnung nicht grösser werden

20 Einführung als der Aufwand der iterativen Lösung. Insbesondere kann die Rekursion leicht eliminiert werden, wenn die Prozedur nur einen rekursiven Aufruf enthält und dieser Aufruf die letzte Instruktion der 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 führt zu einem exponentiellen Aufwand 5. Auf jeder Stufe sind zwei rekursive Aufrufe nötig, welche jeweils unabhängig voneinander die gleichen Funktionswerte berechnen. Eine bessere Implementation (ohne Rekursion) benötigt nur linearen Aufwand (vgl. Übung) Rekursionselimination Wie bereits vorher erwähnt, soll Rekursion nur dann verwendet werden, wenn dadurch die Programme einfacher lesbar werden und die Komplexität nicht grösser als die der iterativen Lösung ist. Ist ein Problem durch eine (unnötig aufwändige) Rekursion formuliert, stellt sich die Frage, ob und wie sich die Rekursion allenfalls eliminieren lässt. Prinzipiell kann dies durch folgendes Vorgehen versucht werden: 5 Die Prozedur benötigt zum Berechnen von fib(n) in der Grössenordnung von 2 fib(n) rekursive Aufrufe.

21 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 abhängt: long Pascal(int x, int y) { if( x <= 0 y <= 0 ) return 1; return Pascal(x-1, y) + Pascal(x, y-1);

22 Einführung Divide and Conquer Die Divide and Conquer Methode (kurz: DAC) zerlegt das zu lösende Problem in kleinere Teilprobleme (divide) bis die Lösung der einzelnen Teilprobleme (conquer) einfach ist. Anschliessend werden die Teillösungen zur Gesamtlösung vereinigt (merge) 6. Da das Problem in immer kleinere Teilprobleme zerlegt wird, welche alle auf die gleiche Art gelöst werden, ergibt sich normalerweise ein Lösungsansatz mit Rekursion. Ein DAC-Algorithmus hat also folgende allgemeine Form: void DAC( problem P ) { if( Lösung von P sehr einfach ) { return Lösung(P) // conquer else { divide( P, Teil 1,...,Teil n ); return combine( DAC(Teil 1 ),...,DAC(Teil n ) ); DAC-Algorithmen können grob in die beiden folgenden Kategorien unterteilt werden. Das Aufteilen in Teilprobleme (divide) ist einfach, dafür ist das Zusammensetzen der Teillösungen (merge) schwierig. Das Aufteilen in Teilprobleme (divide) ist schwierig, dafür ist das Zusammensetzen der Teillösungen (merge) einfach. Wenn sowohl das Aufteilen in Teilprobleme als auch das Zusammensetzen der Teillösungen 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 Lösen von Problemen.

23 1.6 Algorithmen Schema: Rekursion 1-19 Bekannte Beispiele für Divide and Conquer sind die Sortieralgorithmen Quicksort und Mergesort. Quicksort : (Hard Split Easy Join) Die Elemente werden gemäss einem Pivotelement in verschiedene 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 zurück else { divide( P, Teil 1,...,Teil n ); // Zerteile P in n Teile // Füge die sortierten Mengen zusammen (trivial oder durch Nachsortieren). return merge( Sort(Teil 1 ),...,Sort(Teil n ) );

24 Einführung 1.7 Übung 1 1. Nichtdeterministischer Primzahltest Das folgende Verfahren testet, ob ein Kandidat P eine Primzahl ist: Wählen Sie eine genügend grosse Menge beliebiger (zufälliger) Zahlen z i und versuchen Sie nacheinander, P durch z i zu teilen. Falls keine der Zahlen z i ein Teiler ist, geben Sie true zurück, andernfalls false. Formulieren Sie für 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 für 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: Zählen 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 amrhein/algodata/uebung1

25 2 Komplexität von Algorithmen 2.1 Komplexitätstheorie Nicht alle (mathematischen) Probleme (Funktionen) sind algorithmisch lösbar (berechenbar). Ausserdem sind unter den berechenbaren Funktionen viele, deren Berechnung sehr aufwändig und deshalb undurchführbar ist. In diesem Abschnitt wollen wir nun die prinzipiell berechenbaren Probleme weiter unterteilen: in solche, die mit vernünftigem Aufwand lösbar sind und die restlichen. Alle Funktionen Berechenbare Funktionen Durchführbare Algorithmen Abbildung 2.1: Durchführbare Algorithmen Für lösbare Probleme ist es wichtig zu wissen, wieviele Betriebsmittel (Ressourcen) für ihre Lösung erforderlich sind. Nur solche Algorithmen, die eine vertretbare Menge an Betriebsmitteln benötigen, sind tatsächlich von praktischem Interesse.

26 2-2 2 Komplexität von Algorithmen Die Komplexitätstheorie stellt die Frage nach dem Gebrauch von Betriebsmitteln und versucht diese zu beantworten. Normalerweise werden für einen Algorithmus die Betriebsmittel Zeit- und Speicherbedarf untersucht. Mit Zeitbedarf meint man die Anzahl benötigter Rechenschritte Komplexitätsanalyse Mit Hilfe der Komplexitätsanalyse können wir die Effizienz verschiedener Algorithmen vergleichen, bzw. versuchen zu entscheiden, ob ein Algorithmus das Problem im Allgemeinen innert nützlicher Frist löst. Eine Möglichkeit, die Effizienz verschiedener Algorithmen zu vergleichen wäre, alle Algorithmen zu implementieren und die benötigte Zeit und den Platzverbrauch zu messen. Allerdings ist dieses Verfahren höchst ineffektiv. Es muss unnötig viel programmiert werden. Wir können auch nicht einschätzen, ob nicht ein Algorithmus schlechter programmiert wurde als die anderen oder ob die Testbeispiele eventuell einen Algorithmus begünstigen 2. Auch mit Hilfe einer Komplexitätsanalyse können wir nicht wirklich entscheiden, ob ein Programm schnell laufen wird. Vielleicht kann ein optimierender Compiler den einen Code besser unterstützen als den anderen. Vielleicht sind gewisse Speicherzugriffe übers Netz nötig, die den Code langsam machen. Möglicherweise ist der Algorithmus auch einfach schlecht implementiert. Dennoch kann eine Komplexitätsanalyse einen Hinweis geben, ob ein Algorithmus überhaupt prinzipiell für unser Problem in Frage kommt. Durch das Zählen der Anzahl nötiger Rechenschritte können wir 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 führen mit diesen eine Verarbeitung durch. Die Anzahl Rechenschritte hängt normalerweise von der Länge (Grösse) der Eingabedaten ab. Ein Problem kann durch verschiedene Algorithmen mit verschiedener Komplexität gelöst werden. Für Probleme, welche sehr oft gelöst werden müssen, ist es von grossem Interesse, einen Algorithmus zu finden, welcher möglichst wenig Betriebsmittel erfordert. 1 Die Komplexität eines Algorithmus ist natürlich unabhängig von der Geschwindigkeit des verwendeten Computers. 2 Wir müssten fairerweise alle möglichen Eingaben testen, was natürlich nicht machbar ist.

27 2.2 Komplexitätsanalyse 2-3 Die Komplexität eines Algorithmus hängt von der Grösse der Eingabedaten ab. Je grösser die Dimension n der Matrizen, desto länger wird die Ausführung des Algorithmus dauern. Im Allgemeinen können wir die Komplexität eines Algorithmus als Funktion der Länge der Eingabedaten angeben. Als Vereinfachung betrachten wir normalerweise nicht die (exakte) Länge der Eingabe (zum Beispiel in Anzahl Bytes), sondern grössere, für das Problem natürliche Einheiten. Man spricht dann von der natürlichen Länge des Problems. Will man nur eine Grössenordnung für die Komplexität eines Algorithmus angeben, so zählt man auch nicht alle Operationen, sondern nur diejenigen, welche für die Lösung des Problems am wichtigsten (zeitintensivsten) sind. In der folgenden Tabelle sind Probleme mit ihrer natürlichen Länge und ihren wichtigsten Operationen angegeben. Problem natürliche Einheit Operationen Algorithmen auf ganzen Zahlen Anzahl Ziffern Operationen in (z.b. Primzahlalgorithmen ) Suchalgorithmen Anzahl Elemente Vergleiche Sortieralgorithmen Anzahl Elemente Vergleiche, Vertauschungen Algorithmen auf reellen Zahlen Länge der Eingabe Operationen in IR Matrix Algorithmen Dimension der Matrix Operationen in IR Beispiel: Wir berechnen die Komplexität der folgenden Prozeduren, indem wir die Anzahl Aufrufe von do something() (abhängig vom Input n) zählen. int proc1( int n ) int proc2( int n ) { { int res = 0; int res = 0; for( i = 0; i < n; i++ ) for( i = 0; i < n; i++ ) res = do_something(i); for( j = 0; j < n; j++ ) return res; res = do_something(i, j); return res; Wir verändern die Prozedur etwas und berechnen wiederum die Komplexität.

28 2-4 2 Komplexität von Algorithmen 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 zählen auch hier, wie oft do something() aufgerufen wird Best-Case, Average-Case, Worst-Case Analyse Je nach Input kann die Anzahl benötigter Rechenschritte sehr stark schwanken. Dies geschieht beispielsweise dann, wenn die Prozedur Fallunterscheidungen (if/else) enthält. 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;

29 2.3 Asymptotische Komplexität 2-5 Bei Suchalgorithmen zählen wir die Anzahl nötiger Vergleiche. public int indexof( Object elem ) { // lineare Suche, elem!= null n = size for( int i=0; i < size; i++ ) { if( elem.equals(elementdata[i]) ) return i; return -1; 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 Komplexität Eine Vereinfachung ergibt sich, wenn man nur das asymptotische Verhalten der Komplexität eines Algorithmus betrachtet. Das asymptotische Verhalten eines Polynoms entspricht dessem grössten Term. Konstante Faktoren werden dabei ignoriert. Beispiel: In der Funktion f (n) = 2n 2 10n + 20 fällt für wachsendes n der Ausdruck 10n + 20 gegenüber dem Ausdruck 2n 2 immer weniger ins Gewicht. Der dominierende Ausdruck ist in diesem Fall 2n 2.

30 2-6 2 Komplexität von Algorithmen n n Das asymptotische Verhalten von f ist also n 2. Man schreibt auch f (n) O(n 2 ) um das Wachstumsverhalten einer Funktion zu klassifizieren n 2n 2 10n log(n) 20 n 10 n log(n) Wir sagen, eine Funktion f hat exponentielles Wachstumsverhalten, wenn der dominierende Term von f (n) von der Form kc n ist, f hat polynomiales Wachstum, falls er von der Form kn c 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. Wie schon vorher erwähnt, interessiert uns bei der asymptotischen Komplexität nur das proportionale Verhalten. Die O-Notation gibt uns ein Mittel, dies mathematisch auszudrücken:

31 2.3 Asymptotische Komplexität 2-7 Definition: [O-Notation] Eine Funktion f (n) ist aus O(g(n)), falls es Konstanten c und N gibt, so dass für 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 f (n) gehört zu O(g(n)), falls sie (bis auf eine Konstante) nicht schneller wächst als g(n). Man sagt auch, f hat das gleiche asymptotische Verhalten wie g. So gehören zum Beispiel die Funktionen 300n 2 + 2n 1, 10n + 12 und 5n 3/2 + n alle zu O(n 2 ). Hingegen gehören die Funktionen 2 n oder n 3 nicht zu O(n 2 ). Umgekehrt sagt das Wissen, dass eine Funktion f zu O(g) gehört, nichts über die Konstanten c und N aus. Diese können sehr gross sein, was gleichbedeutend damit ist, dass ein Algorithmus mit dieser (asymptotischen) Komplexität eventuell erst für 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 höchsten Potenz. Für zwei Funktionen f und g gilt: a k n k + a k 1 n k a 1 n + a 0 O(n k ) O( f + g) = max{o( f ),O(g) O( f g) = O( f ) O(g) Die Ordnung der Exponentialfunktion ist grösser als die Ordnung eines beliebigen Polynoms. Für alle c > 1 und k gilt: c n O(n k ) 3 Der FFT-Algorithmus für Langzahlarithmetik ist zum Beispiel erst für Zahlen, die mehrere hundert Stellen lang sind, interessant.

32 2-8 2 Komplexität von Algorithmen Beispiel: Wir berechnen die asymptotische Komplexität der folgenden Prozeduren. 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 zählen wieder, wie oft do something() aufgerufen wird: Eine andere Methode benötigen wir zum Berechnen der Komplexität im folgenden Beispiel. Der Einfachheit halber nehmen wir an, n sei eine Zweierpotenz (n = 2 k ). 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 zählen wiederum, wie oft do something() aufgerufen wird.

33 2.4 Übung Übung 2 1. Komplexiät von einfachen Prozeduren Berechnen Sie die Komplexität der Prozeduren 1 bis 4. Wie oft wird do something() aufgerufen? Überprüfen Sie Ihre Lösungen, indem Sie die Prozeduren in Java implementieren und einen Zähler einbauen. void procedure 1 ( 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 procedure 3 ( int n ) { for(int i=0; i<n; i++) { int j = 0; while( j < 2*n ) { j++; do something(i,j,n); void procedure 2 ( int n ) { for(int i=0; i<n; i++) for(int j=0; j<2*i; j++) do something(i,j,n); void procedure 4 ( int n ) { int j=n; while( j > 0 ) { j = j/2; do something(i,j,n);

34 Komplexität von Algorithmen 2. Komplexität rekursiver Prozeduren Berechnen Sie die Komplexität der folgenden rekursiven Prozeduren. Wie oft wird do something() ausgeführt? Wählen Sie für n eine Zweierpotenz: n = 2 k. void procrec1( int n ) int procrec2( int n, int res ) { { if( n<=1 ) res = do_something(res, n); return; if( n <= 1 ) return res; do_something(n) res = procrec2(n/2, res); procrec1(n/2); res = procrec2(n/2, res); return res; 3. Komplexität verschiedener Java Methoden Bestimmen Sie von den Java Klassen ArrayList und LinkedList die asymptotische Komplexität der Methoden - public boolean contains(object o) - public E get(int index) - public E set(int index, E element) - public boolean add(e o) - public void add(int index, E element) - public E remove int(index) - public boolean remove(object o) Sie müssen dazu die Algorithmen nicht im Detail verstehen. Es genügt, die Iterationen (auch der benötigten Hilfsfunktionen) zu zählen (wir werden diese Algorithmen in einem späteren Kapitel noch genauer betrachten).

35 3 Datentypen: Listen, Stacks und Queues Listen, Stacks und Queues können 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 müssen, wie viele Elemente die Liste maximal enthält. Viele Kopieraktionen sind nötig, wenn der gewählte Bereich zu klein gewählt wurde, oder wenn in der Mitte einer Liste ein Element eingefügt oder gelöscht werden soll. 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 Einfügen oder Löschen von Elementen der Array (in sich) umkopiert werden muss. Ausserdem

36 3-2 3 Datentypen: Listen, Stacks und Queues muss der Array in einen neuen, grösseren Array umkopiert werden, sobald die initiale Anzahl Elemente überschritten 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) {......

37 3.1 Array Listen 3-3 Im Konstruktor wird der elementdata Array mit Länge 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 Einfügen von neuen Elementen in den Array hingegen ist aufwändig, 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++;

38 3-4 3 Datentypen: Listen, Stacks und Queues Das Gleiche gilt für das Löschen von Elementen aus einer ArrayList. Alle Elemente hinter dem gelöschten Element müssen 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);

39 3.2 Doppelt verkettete Listen 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.

40 3-6 3 Datentypen: Listen, Stacks und Queues Eine (doppelt) verkettete Liste entsteht dann durch Zusammenfügen einzelner Node Elemente. Besondere Node Elemente bezeichnen dabei den Listenanfang und das Ende. Die Definition einer Liste sieht dann zum Beispiel wie folgt aus: public class LinkedList<E> { transient Node<E> first; transient Node<E> last; transient 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.

41 3.2 Doppelt verkettete Listen 3-7 */ 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 für das Einfügen und für das Löschen eines Elementes. 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;

42 3-8 3 Datentypen: Listen, Stacks und Queues Einfügen 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++;

43 3.2 Doppelt verkettete Listen 3-9 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 Einfügen, das heisst am Ende oder am Anfang.

44 Datentypen: Listen, Stacks und Queues Löschen Beim Löschen von Elementen muss geprüft werden, ob ev. first und/oder last korrigiert werden müssen. 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;

45 3.3 Stacks und Queues Stacks und Queues Ein Interface für einen Stack hatten wir im Abschnitt 1.2 bereits gesehen. Stacks sind einfache Listenstrukturen, bei denen bloss am Kopf Elemente eingefügt, gelesen, bzw. gelöscht werden dürfen. 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;

46 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 können Elemente nur am Ende angefügt werden. Nur am Kopf der Queue können Elemente gelesen, bzw. gelöscht werden.

47 3.4 Iteratoren Iteratoren Auf Listenstrukturen hat man üblicherweise eine Hilfsklasse, welche zum Durchlaufen der Liste dient. Die zwei wichtigsten Methoden von Iterator Klassen sind hasnext zum Prüfen, ob das Ende der Liste erreicht ist, sowie die Methode next, welche den Inhalt des nächsten Elements zurückgibt. public interface Iterator<E> { /** * Returns true if the iteration has more elements. */ boolean hasnext(); /** * Returns the next element in the iteration. NoSuchElementException iteration has no more elements. */ E next();...

48 Datentypen: Listen, Stacks und Queues 3.5 Übung 3 Für die Implementationsaufgabe finden Sie Rahmenprogramme unter amrhein/algodata/ 1. List Iterator Entwerfen Sie (ausgehend vom Rahmenprogramm) eine innere Klasse ListIterator, welche als Iterator für 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 Implementieren Sie eine Klasse Queue gemäss dem gegebenen Interface (vgl. Rahmenprogramm). - Implementieren Sie die Queue zuerst als Liste (gemäss LinkedList, aber ohne die Methoden der LinkedList zu benutzen). - Als zweites implemtieren Sie die Queue als Array (gemäss ArrayList, aber ohne die Methoden der ArrayList zu benutzen). In der Array-basierten Queue dürfen Sie annehmen, dass die Queue nicht mehr als MAX viele Elemente enthalten muss. Überlegen Sie sich eine Implementierung, welche nicht nach jedem Einfügen oder Löschen 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 Array- List, HashSet, LinkedHashSet, LinkedList, PriorityQueue, Stack, TreeSet, Vector

49 4 Datentypen: Bäume, Heaps Alle bisher betrachteten Strukturen waren linear in dem Sinn, dass jedes Element höchstens einen Nachfolger hat. In einem Baum kann jedes Element keinen, einen oder beliebig viele Nachfolger haben. Bäume sind wichtig als Strukturen in der Informatik, da sie auch oft im Alltag auftauchen: zum Darstellen von Abhängigkeiten 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 zusätzliche Einschränkung, dass es zwischen zwei Ecken nur eine (direkte oder indirekte) Verbindung gibt 1. Wir befassen uns hier zuerst vor allem mit einer besonderen Art von Bäumen: den Binärbaumen. Ein Baum heisst binär, falls jeder Knoten höchstens zwei Nachfolger hat. 1 Ein Baum ist ein zusammenhängender Graph ohne Zyklen.

50 Datentypen: Bäume, Heaps Definition: Ein binärer 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 binärer Baum ist vollständig (von der Höhe n), falls alle inneren Knoten zwei Nachfolger haben und die Blätter maximal Weglänge n bis zur Wurzel haben. Jedem Knoten ist eine Ebene (level) im Baum zugeordnet. Die Ebene eines Knotens ist die Länge des Pfades von diesem Knoten bis zur Wurzel. Die Höhe (height) eines Baums ist die maximale Ebene, auf der sich Knoten befinden. Ein binärer Baum besteht also aus Knoten mit einem (Zeiger auf ein) Datenelement data, einem linken Nachfolgerknoten left und einem rechten Nachfolgerknoten right. left right public class BinaryTreeNode<T> { protected T data; protected BinaryTreeNode<T> leftchild; protected BinaryTreeNode<T> rightchild;

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

52 4-4 4 Datentypen: Bäume, Heaps 4.1 Baumdurchläufe Bäume können auf verschiedene Arten durchlaufen werden. Die bekanntesten Verfahren sind Tiefensuche (depth-first-search, DFS) und Breitensuche (breadth-first-search, BFS). Tiefensuche kann unterschieden werden in die drei Typen präorder, postorder und inorder, abhängig von der Reihenfolge der rekursiven Aufrufe Tiefensuche Präorder 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.

53 4.1 Baumdurchläufe Wir betrachten als Beispiel für die Tiefensuche den Präorder-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;

54 4-6 4 Datentypen: Bäume, Heaps Tiefensuche mit Hilfe eines Stacks Mit Hilfe eines Stacks können wir die rekursiven Aufrufe in der präorder Tiefensuche vermeiden. Auf dem Stack werden die später 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;

55 4.1 Baumdurchläufe 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 Höhe des Baumes erreicht ist, setze den Level um eines höher 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 näheren Umgebung. Um mittels Breitensuche (levelorder) durch einen Baum zu wandern, müssen wir uns alle Baumknoten einer Ebene merken. Diese Knoten speichern wir in einer Queue ab, so dass wir später darauf zurückgreifen können. 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;

56 4-8 4 Datentypen: Bäume, Heaps 4.2 Binäre Suchbäume Ein binärer Suchbaum ist ein Baum, welcher folgende zusätzliche Eigenschaft hat: Alle Werte des linken Nachfolger-Baumes eines Knotens K sind kleiner, alle Werte des rechten Nachfolger-Baumes von K sind grösser als der Wert von K selber. Der grosse Vorteil von binären Suchbäumen ist, dass wir sowohl beim Einfügen als auch beim Suchen von Elementen immer bloss einen der zwei Nachfolger untersuchen müssen. 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 Bäume entstehen durch Einfügen 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 eingefügt, das zweite Mal von hinten nach vorne.

57 4.2 Binäre Suchbäume 4-9 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;...

58 Datentypen: Bäume, Heaps 4.3 B-Bäume Ein B-Baum ist ein stets vollständig balancierter und sortierter Baum. Ein Baum ist vollständig balanciert, wenn alle Äste gleich lang sind. In einem B-Baum darf die Anzahl Kindknoten variieren. Ein B- Baum ist zum Beispiel ein Baum, in welchem jeder Knoten maximal 4 Datenelemente speichern und jeder Knoten (ausser der Wurzel und den Blättern) minimal 3 und maximal 5 Nachfolger haben darf (der Wurzelknoten hat 0-4 Nachfolger, Blätter haben keine Nachfolger). Durch die flexiblere Anzahl Kindknoten ist das Rebalancing weniger häufig nötig. Ein Knoten eines B-Baumes speichert: eine variable Anzahl s von aufsteigend sortierten Daten-Elementen k 1,...,k s 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 Hälfte gefüllt. Die letzte Bedingung lautet formal: es gibt eine Schranke m, so dass m <= s <= 2m gilt. Das heisst, jeder Kindknoten hat mindestens m, aber höchstens 2m Daten-Elemente. Die Werte von k 1,...,k s dienen dabei als Splitter. Die Daten-Elemente der Kindknoten ganz links müssen kleiner sein als k 1, diejenigen ganz rechts grösser als k s. Dazwischen müssen die Daten-Elemente des i-ten Kindes grösser als k i und kleiner als k i+1 sein. Das folgende Bild zeigt einen B-Baum mit m gleich 2. Jeder innere Knoten hat also mindestens 2 und maximal 5 Nachfolger.

59 4.3 B-Bäume 4-11 Operationen in B-Bäumen Suchen Die Suche nach einem Datenelement e läuft 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 grösser oder gleich e ist. Falls alle Daten-Elemente von k kleiner sind als e, führe 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 weitergeführt. Falls k ein Blatt ist, kann die Suche abgebrochen werden (fail). Einfügen Beim Einfügen muss jeweils beachtet werden, dass nicht mehr als 2m Daten-Elemente in einem Knoten untergebracht werden können. Zunächst wird das Blatt gesucht, in welches das neue Element eingefügt werden müsste. Dabei kann 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 eingefügt. Einfügen des Werts 31 in den folgenden Baum:

60 Datentypen: Bäume, Heaps Der Wert 31 sollte in das Blatt (30,34,40,44) eingefügt werden. Dieses ist aber bereits voll, muss also aufgeteilt werden. Dies führt dazu, dass der Wert in der Mitte (34) in den Vorgänger- Knoten verschoben wird. Da das alte Blatt ganz rechts vom Knoten (20,28) liegt, wird der Wert 34 rechts angefügt (neuer, grösster Wert dieses Knotens). Damit erhält dieser Knoten neu 3 Werte und 4 Nachfolger. Dieser Prozess muss eventuell mehrmals (in Richtung Wurzel) wiederholt werden, falls durch das Hochschieben des Elements jeweils der Vorgänger-Knoten ebenfalls überläuft.

61 4.3 B-Bäume 4-13 Löschen von Elementen Beim Löschen eines Elementes muss umgekehrt beachtet werden, dass jeder Knoten nicht weniger als m Datenelemente enthalten muss. Falls das gelöschte Element in einem Blatt liegt, welches mehr als m Datenelemente hat, kann das Element einfach gelöscht werden. Andernfalls können entweder Elemente vom benachbarte Blatt verschoben oder (falls zu wenig Elemente vorhanden sind) zwei Blätter verschmolzen werden. Verschiebung Aus dem linken B-Baum soll das Element 18 gelöscht werden. Dies würde dazu führen, dass das linke Blatt zu wenig Datenelemente hat. Darum wird aus dem rechten Nachbarn das kleinste Element nach oben, und das Splitter-Element des Vorgängers in das linke Blatt verschoben. Analog könnte (falls vorhanden) aus einem linken Nachbarn das grösste Element verschoben werden. Falls ein Element eines inneren Knotens (z.b. das Element 34) gelöscht wird, muss entweder von den linken Nachfolgern das grösste, oder von den rechten Nachfolgern das kleinste Element nach oben verschoben werden, damit weiterhin genügend Elemente (als Splitter) vorhanden sind, und die Ordnung bewahrt wird.

62 Datentypen: Bäume, Heaps Verschmelzung Aus dem linken B-Baum soll das Element 60 gelöscht werden. Dies würde dazu führen, dass das mittlere Blatt zu wenig Datenelemente hat. Weder der rechte noch der linke Nachbar hat genügend Elemente, um eine Verschiebung durch zu führen - es müssen zwei Blätter verschmolzen werden. Das linke Blatt erhält vom mittleren Blatt das Element 55, sowie von der Wurzel das Element 50. Die Wurzel muss ebenfalls ein Element abgeben, da nach der Verschmelzung bloss noch 2 Nachfolge-Knoten existieren. Das rechte Blatt bleibt unverändert. Mit Hilfe der Verschiebung- und Verschmelzungs-Operation können wir nun beliebige Elemente aus einem B-Baum löschen. Beispiel Aus dem folgenden Baum löschen wir zuerst das Element 75, danach das Element 85:

63 4.4 Priority Queues Priority Queues In vielen Applikationen will man die verschiedenen Elemente in einer bestimmten Reihenfolge (Priorität) abarbeiten. Allerdings will man das (aufwändige!) Sortieren dieser Elemente nach möglichkeit vermeiden. Eine der bekanntesten Anwendungen in diesem Umfeld sind Scheduling-Algorithmen mit Prioritäten. Alle Prozesse werden gemäss ihrer Priorität in einer Priority Queue gesammelt, so dass immer das Element mit höchster Priorität verfügbar ist. Priority Queues haben aber noch weit mehr Anwendungen, zum Beispiel bei Filekomprimierungs- oder bei Graph-Algorithmen. Eine elegante Möglichkeit der Implementierung einer Priority Queue ist mit Hilfe eines Heaps Heaps Ein Heap ist ein (fast) vollständiger Baum, in welchem nur in der untersten Ebene von rechts her Blätter fehlen dürfen

64 Datentypen: Bäume, Heaps Definition: [Heap] Ein Heap ist ein vollständiger binärer Baum, dem nur in der untersten Ebene ganz rechts Blätter fehlen dürfen mit folgenden Zusatzeigenschaften. 1. Jeder Knoten im Baum besitzt eine Priorität und eventuell noch weitere Daten. 2. Die Priorität eines Knotens ist immer grösser als (oder gleich wie) die Priorität der Nachkommen. Diese Bedingung heisst Heapbedingung. Aus der Definition kann sofort abgelesen werden, dass die Wurzel des Baumes die höchste Priorität besitzt. Weil der Heap im wesentlichen ein vollständiger binärer Baum ist, lässt er sich einfach als Array 2 implementieren. Wir numerieren die Knoten des Baumes von oben nach unten und von links nach rechts. Die so erhaltene Nummerierung ergibt für jeden Knoten seinen Index im Array. Die dargestellten Werte im Baum sind natürlich bloss die Prioritäten 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 boolean isleaf(int position) {... private int parent(int position) {... private int leftchild(int position) {... private int rightchild(int position){... 2 Dies hat den Nachteil, dass die maximale Anzahl Elemente (size ) beim Erzeugen des Heaps bekannt sein muss.

65 4.4 Priority Queues Werden die Knoten auf diese Weise in den Array abgelegt, so gelten für alle i, 0 i < length folgenden Regeln: die 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.

66 Datentypen: Bäume, Heaps insert Da ein Element hinzugefügt werden muss, erhöhen wir zuerst length um eins. Das neue Element wird dann an der Stelle length-1 eingefügt. Der Array repräsentiert immer noch einen vollständigen binären Baum mit nur rechts unten fehlenden Blättern. Das neue Element verletzt aber eventuell die Heapbedingung. Um wieder einen Heap zu erhalten, vertauschen wir das neue Element solange mit seinen direkten Vorgängern, bis die Heapbedingung wieder erfüllt ist. Diese Methode verfolgt einen direkten Weg von einem Blatt zur Wurzel. Da der binäre Baum vollständig ist, hat ein solcher Weg höchstens die Länge der Höhe des Baumes. Mit anderen Worten, wir brauchen höchstens log 2 (n) Vertauschoperationen, um ein Element im Heap einzufügen 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);

67 4.4 Priority Queues 4-19 removemax Das Element mit der höchsten Priorität befindet sich im Element heap[0] und wird vom Heap entfernt. heap[0] wird nun mit heap[length-1] überschrieben und length um eins verringert. Damit erhalten wir wieder einen fast vollständigen binären Baum. Das neue Element heap[0] verletzt nun vermutlich die Heapbedingung. Wir vertauschen also heap[0] mit dem grösseren seiner beiden Nachfolger und fahren so fort, bis die Heapbedingung wieder erfüllt 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;

68 Datentypen: Bäume, 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;

69 4.5 Übung Übung 4 Binäre Suchbäume Bauen Sie aus der folgenden Zahlenreihe zwei binäre Suchbäume, 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 Löschen Sie aus dem folgenden Heap zuerst drei Elemente, fügen Sie danach ein neues Element mit Priorität 42 in den Heap ein BTree Fügen Sie im folgenden BTree zuerst das Element 42 ein, löschen Sie dann die Elemente 28 und 45.

70 4-22

71 5 Suchen 5.1 Grundlagen Suchen ist eine der häufigsten Operationen, die mit dem Computer ausgeführt werden. Normalerweise geht es darum, in einer Menge von Daten die richtigen Informationen zu finden. Wir kennen zum Beispiel einen Namen und suchen die zugehörige Mitgliedernummer. Oder wir geben (mit Hilfe einer EC-Karte) eine Kontonummer ein und das System sucht das dazugehörige Konto. Oder wir kennen eine Telefonnummer und suchen den dazugehörigen Abonnenten, usw. Wenn wir im folgenden jeweils Listen von Zahlen durchsuchen, so tun wir das bloss der Einfachheit halber. Die gleichen Algorithmen können natürlich für 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 Suchschlüssel, also zum Beispiel Adresse.name. Da Suchalgorithmen so häufig verwendet werden, lohnt es sich, diese effizient zu implementieren. Anderseits spielt natürlich die Länge der zu durchsuchenden Datenmenge eine entscheidende Rolle: Je grösser 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.

72 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. elem an object. 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 spätestens dann ab, wenn das letzte Element der Liste geprüft ist. Komplexität der linearen Suche Um die Effizienz der linearan Suche zu bestimmen, bestimmen wir die Anzahl der nötigen Vergleiche in Abängigkeit von der Länge n der Folge.

73 5.3 Binäre Suche Binäre Suche Eine Folge, auf die sehr häufig zugegriffen (und nicht so häufig verändert) wird, sollte wenn möglich sortiert gehalten werden 1. Dies lässt sich leicht realisieren, indem die neuen Elemente jeweils an der richtigen Stelle einsortiert werden 2. Falls die Daten so dargestellt sind, kann das Suchen auf sehr viel schnellere Art und Weise realisiert werden, zum Beispiel durch binäre Suche. Bei der binären Suche wird die Folge in zwei Teile Elem[0] Elem[p-1] Elem[p+1] Elem[len] {{ {{ 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. l = 0 a r = size p = (r+l)/2 Die ersten drei Schritte der binären Suche Da die Folge sortiert ist, ist dieses Vorgehen korrekt. 1 Dies setzt natürlich voraus, dass sich die Datenelemente sortieren lassen, also eine Ordnungsrelation < auf den Datenschlüsseln existiert. In Java bedeutet dies, die Elemente müssen das Comparable Interface erfüllen, d.h. eine compareto() Methode haben. 2 Im Abschnitt über Bäume sind wir bereits der speziell dafür konzipierten Datenstruktur des binären Baumes begegnet

74 5-4 5 Suchen Wenn wir die Folge jeweils nicht in der Mitte teilen, sondern p = l oder p = r wählen, erhalten wir die lineare Suche als Spezialfall der binären 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 ) // a[p] < x l = p + 1; else if( a[p].compareto( x ) > 0 ) r = p - 1; // a[p] > x else return p; return -1 Komplexität der binären Suche

75 5.4 Hashing Hashing Eine Hash-Tabelle ist eine Übersetzungstabelle, mit der man rasch auf jedes gesuchte Element einer Liste zugreifen kann, welche aber trotzdem die Flexibilität einer zeigerbasierten Liste bietet. Hashtabellen 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 können wir auf jedes Element direkt zugreifen. Einen Array als Hash-Tabelle zu benutzen ist dann günstig, wenn wir genügend Platz haben, um einen Array der Länge Anzahl möglicher Schlüssel anzulegen. Beispiel: Falls alle Mitglieder eines Vereins unterschiedliche Initialen haben, können wir dies als Schlüssel für die Hashtabelle benutzen. Berta Amman BA (2,1) Doris Bucher DB (4,2) Chris Carter CC (3,3) Friedrich Dünner FD (6,4) Mit einem Array der Länge können wir auf jedes Mitglied direkt zugreifen. (H,R) Hash Tabelle Menge aller Schlüssel (D,H) (U,M) (C,F)... (H,A) (B,A) (C,C) Benötigte Schlüssel (D,B) (F,D) (W,S) Daten(BA) Daten(DB) Daten(CC) Daten(FD) Der Nachteil hierbei ist, dass wir bei dieser Methode offensichtlich viel Speicherplatz verschwenden. Ausserdem können wir nicht sicher sein, dass nicht eines Tages ein neues Mitglied mit bereits existierenden Initialen in unseren Verein eintreten will. Für beide Probleme versuchen wir im folgenden Lösungen zu finden.

76 5-6 5 Suchen Hash-Funktionen Eine Hash-Funktion ist eine Methode, mit welcher wir mit Hilfe von einfachen arithmetischen Operationen die Speicherstelle eines Elementes aus seinem Schlüssel berechnen können. Optimalerweise sollte eine Hash-Funktion jedem Element einer Menge einen anderen Funktionswert zuordnen (vermeiden von Kollisionen). Dies ist leider in der Regel nicht möglich. Allerdings gibt es Hash-Funktionen, welche diesen Anspruch besser, und solche, welcher ihn weniger gut erfüllen. Definition: Eine Hashfunktion sollte mindestens die folgenden Eigenschaften erfüllen. Der Hashwert eines Objekts muss während der Ausführung der Applikation gleich bleiben. Der Hashwert darf aber bei einer nächsten Ausführung der Applikation anders sein. Falls zwei Objekte gleich sind gemäss der equals() Methode, muss der Hashwert der beiden Objekte gleich sein. Zwei verschiedene Objekte können 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. Für das Einfügen in die Hashtabelle muss dieser Wert dann modulo der Länge 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 Wörter, die aus den gleichen Buchstaben (in anderer Reihenfolge) bestehen, haben den gleichen Funktionswert. Aber auch sonst verteilt diese Methode verschiedene Wörter offensichtlich nicht optimal (erzeugt viele Kollisionen).

77 5.4 Hashing 5-7 Anna Jochen Otto Gabi Tina Kurt Elem_hash() Tina Otto Kurt Jochen Anna Gabi Eine oft benutzte, gut streuende Hash-Funktion für 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 & 0xF L; // 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 Otto Gabi Anna Tina Jochen

78 5-8 5 Suchen Natürlich kann auch die ELF hash -Methode nicht alle Kollisionen vermeiden. Die ELF hash -Methode mit Tabellenlänge 11 erhält zum Beispiel für Otto und Martin den gleichen Schlüssel. Die Frage ist darum: wie behandelt man Kollisionen? Double Hashing Bei Double Hashing verwenden wir zwei verschiedene Hashfunktionen Hash 1 und Hash 2 3. Hash 1 dient dazu, die Hashadresse eines Schlüssels in der Tabelle zu suchen. Hash 2 dient dazu, bei einer Kollision in der Tabelle den nächsten freien Platz zu suchen. Hash (k) Hash (k) 2 leeres Feld besetztes Feld neuer Eintrag Einfügen eines Objekts mit Schlüssels k: Zuerst wird die Hashadresse Hash 1 (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 (Hash 1 (k) + Hash 2 (k)) mod N, (Hash 1 (k) + 2 Hash 2 (k)) mod N,... 3 Im einfachsten Fall wählen wir Hash 2 (k) = p für p gleich 1 oder für eine Primzahl p, welche die Länge N der Hashtabelle nicht teilt. Dann erhalten wir den sog. Linear Probing Algorithmus. Im Allgemeinen kann Hash 2 (k) eine beliebige Funktion sein, welche Werte relativ prim zu N liefert.

79 5.4 Hashing 5-9 gesucht, bis ein freier Platz gefunden wird (N = Länge 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 können, muss Hash 2 (k) für alle k relativ prim zu N sein (d.h. Hash 2 (k) und N haben keine gemeinsamen Primfaktoren). Sobald die Hashtabelle den maximalen Füllstand übersteigt (z.b. 60%), muss die Hashtabelle vergrössert und ein Rehashing vorgenommen werden (alle Elemente neu zuteilen). Suchen des Objekts mit Schlüssels k: Die Hashadresse Hash 1 (k) mod N wird berechnet. Ist das gesuchte Element mit Schlüssel k an dieser Adresse gespeichert, ist die Suche erfolgreich. Falls nicht, wird der Wert Hash 2 (k) berechnet und das Element in den Tabellenplätzen (Hash 1 (k) + Hash 2 (k)) mod N, (Hash 1 (k) + 2 Hash 2 (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 Fällen ist das gesuchte Element nicht in der Tabelle. Hash (k) 1 Hash (k) 2 leeres Feld besetztes Feld gelöschtes Feld

80 Suchen Löschen eines Schlüssels: Das Löschen von Elementen aus einer Tabelle mit Double Hashing ist etwas heikel. Damit ein in der Tabelle eingetragener Schlüssel in jedem Fall wieder gefunden wird, dürfen wir die Elemente aus der Tabelle nicht einfach löschen, da wir nicht wissen, ob sie eventuell als Zwischenschritt beim Einfügen von anderen Elementen benutzt wurden. Jeder Tabelleneintrag muss darum ein Flag besitzen, welches angibt, ob ein Eintrag leer, benutzt oder gelöscht ist. Die oben beschriebene Suche darf dann nur bei einem leeren Element abgebrochen werden. Beim Löschen eines Schlüssels aus der Tabelle muss der Tabellenplatz mit gelöscht markiert werden. Als zweite Hashfunktion genügt oft eine sehr einfache Funktion, wie zum Beispiel Hash 2 (k) = k mod Um sicherzustellen, dass diese Funktionswerte teilerfremd zur Länge der Hashtabelle sind, kann man zum Beispiel als Tabellenlänge eine Primzahl wählen.

81 5.4 Hashing Bucket Hashing Eine andere Möglichkeit, Kollisionen zu behandeln, ist das Aufteilen des Hash-Arrays in verschiedene buckets. Eine Hashtabelle der Länge H wird dabei aufgeteilt in H/B Teile (buckets), von der Grösse B. Es wird nur eine Hashfunktion benutzt, Elemente mit gleichem Hashwert (modulo Hashsize) werden im selben Bucket abgelegt. Die Hashfunktion sollte die Datenelemente möglichst gleichmässig über die verschiedenen Buckets verteilen. Bucket Hashing, 0 Hashtable Overflow leer Buckets der Länge besetzt gelöscht Einfügen: 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 Überlauf (overflow bucket) von genügender Länge am Ende der Tabelle. Alle Buckets teilen sich den selben Überlauf-Speicher. Natürlich soll der Überlauf-Speicher möglichst gar nie verwendet werden. Sobald die Hashtabelle einen gewissen Füllgrad erreicht hat, muss ein Rehashing erfolgen, so dass die Zugriffe schnell bleiben (die Buckets nicht überlaufen). 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 Plätze sind, kann die Suche abgebrochen werden. Falls der Bucket aber keine freien Plätze mehr hat, muss der Overflow durchsucht werden bis das Element gefunden wurde, oder alle Elemente des Overflow überprüft sind. Löschen: Auch beim Bucket Hashing müssen wir beim Löschen vorsichtig sein. Falls der Bucket noch freie Plätze hat, können wir den Tabellen-Platz einfach freigeben. Falls nicht, muss der Platz als gelöscht markiert werden, damit beim Suchen der Elemente auch der Overflow durchsucht wird.

82 Suchen Eine Variante des Bucket Hashing ist die folgende: Wir wählen wiederum eine Bucket-Grösse 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 für das Einfügen benutzt werden kann, was die Anzahl Kollisionen bei gleicher Tabellen-Grösse vermindert. Bucket Hashing Variante, Buckets der Länge 5 Hashtable Overflow leer besetzt gelöscht P neuer Eintrag P+1 P+2 Einfügen: 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 Überlauf-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. Löschen: Das gelöschte Feld muss auch in dieser Variante markiert werden, damit wir beim Suchen keinen Fehler machen.

83 5.4 Hashing 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. Einfügen des Schlüssels k: Zuerst wird die Hashadresse Hash(k) berechnet. Dann wird der neue Schlüssel am Anfang der verketteten Liste eingefügt. Suchen des Schlüssels k: Die Hashadresse Hash(k) wird berechnet. Dann wird k mit den Schlüsseln in der entsprechenden Liste verglichen, bis k entweder gefunden wird oder das Ende der Liste erreicht ist. Löschen des Schlüssels k: Das Element wird einfach aus der Liste in Hash(k) entfernt. Hashing mit Separate Chaining

84 Suchen 5.5 Übung 5 Linear Probing Fügen Sie mit den Hashfunkionen Hash 1 (k) = k und Hash 2 (k) = 1 die Liste der Zahlen 2, 3, 14, 12, 13, 26, 28, 15 in eine leere Hashtabelle der Länge 11 ein. Wieviele Vergleiche braucht man, um festzustellen, dass die Zahl 46 nicht in der Hashtabelle ist? Double Hashing Fügen Sie mit den Hashfunkionen Hash 1 (k) = k und Hash 2 (k) = k mod die Liste der Zahlen 2, 3, 14, 12, 13, 26, 28, 15 in eine leere Hashtabelle der Länge 11 ein. Wieviele Vergleiche braucht man, um festzustellen, dass die Zahl 46 nicht in der Hashtabelle vorkommt? Bucket Hashing Fügen Sie mit der Hashfunkion Hash(k) = k die Zahlen 4, 3, 14, 12, 13, 26, 28, 15, 2, 20 in eine leere Hashtabelle der Länge 8 ein mit Bucket Grösse 3 ein. 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.

85 6 Sortieren Sortierprogramme werden vorallem für die die Präsentation von Daten benötigt, wenn die Daten zum Beispiel sortiert nach Zeit, Grösse, letzten Änderungen, Wert,... dargestellt werden sollen. Wenn die Mengen nicht allzu gross sind (weniger als 500 Elemente), genügt oft ein einfach zu implementierender, langsamer Suchalgorithmus. Diese haben normalerweise eine Komplexität von O(n 2 ), wobei der Aufwand bei fast sortierten Mengen geringer sein kann (Bubble Sort). Zum Sortieren von grossen Datenmengen lohnt es sich allerdings, einen O(nlog 2 (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 Basis- Typ-Array wie float[n] oder eine ArrayList ) oder eine Listenstruktur (wie zum Beispiel LinkedList ) ist. Listen erlauben zwar keinen wahlfreien Zugriff, dafür können Listenelemente durch Umketten (also ohne Umkopieren) von einer unsortierten Folge F in eine sortierte Folge S überführt werden. Arrays erlauben wahlfreien Zugriff. Dafür sind beim Einfügen und Löschen (Umsortieren) von Elementen viele Kopier-Schritte nötig. Speziell ist zu beachten, dass viele Sortier-Algorithmen auf Array-Strukturen zwar sehr schnell aber nicht stabil sind. Definition: Ein Sortier-Algorithmus heisst stabil, falls Elemente, welche gemäss der Vergleichsfunktion gleich sind, ihre Originalreihenfolge behalten.

86 6-2 6 Sortieren 6.1 Selection Sort Beim Sortieren durch Auswählen 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 für Arraystrukturen. Die Vertauschungen führen aber dazu, dass der Algorithmus nicht stabil ist. 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).

87 6.1 Selection Sort 6-3 Beispiel Wir sortieren die Folge F = (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 von Selection Sort gemäss den Nachnamen sortiert werden. BeatSuter ClaudiaMeier DanielSuter EmilBucher FritzAbegg

88 6-4 6 Sortieren 6.2 Insertion Sort Beim Sortieren durch Einfügen nimmt man jeweils das erste Element aus der unsortierten Folge F und fügt es an der richtigen Stelle in die sortierte Folge S ein. Dieser Algorithmus ist nicht geeignet für arraybasierte Folgen, da in jedem Schritt einige Elemente im Array nach hinten verschoben werden müssten. Wir betrachten nochmals die Folge vom Beispiel vorher: Falls das Element beim Ein-Sortieren jeweils am Ende aller (gemäss der Ordnung) gleichen Elemente eingefügt 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++;

89 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 ) lautet wie folgt: 1. Teile das Problem (divide) 2. Löse die Teilprobleme (conquer) 3. Kombiniere die Teillösungen (join) Beim Sortieren gibt es zwei Ausprägungen: 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 F 1 und F 2 partitioniert, dass zum Schluss die sortierten Teilfolgen einfach aneinandergereiht werden können: S = S 1 S 2. Dieses Prinzip führt zum Quicksort-Algorithmus. Easy split / Hard join: Dabei ist die Aufteilung in Teilfolgen F = F 1 F 2 trivial und die Hauptarbeit liegt beim Zusammensetzen der sortierten Teilfolgen S 1 und S 2 zu S. Dieses Prinzip führt zum Mergesort- Algorithmus.

90 6-6 6 Sortieren 6.4 Quicksort Einer der schnellsten und am meisten benutzten Sortieralgorithmen auf Arrays ist der Quicksort-Algorithmus (C.A.R. Hoare, 1960). Seine Hauptvorteile sind, dass er nur wenig zusätzlichen Speicherplatz braucht, und dass er im Durchschnitt nur O(nlog 2 (n)) Rechenschritte benötigt. Beim Quicksort wird zuerst ein Pivot-Element q ausgewählt 1. Dann wird der zu sortierende Bereich F so in zwei Teilbereiche F klein und F gross partitioniert, dass in F klein alle Elemente kleiner als q und in F gross alle Elemente grösser gleich q liegen. Danach wird rekursiv F klein und F gross sortiert. Am Ende werden die je sortierten Folgen wieder zusammengefügt. Die Prozedur partition sucht jeweils von links ein Element g, welches grösser 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. Zurückgegeben 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: 1 q kann beliebig gewählt werden, optimalerweise ist q aber der Median aller zu sortierenden Werte. Da die Berechnung des Medians viel zu aufwändig wäre, wählt man als Pivot oft einfach das erste Element. Eine bessere Wahl ist das Element an der Stelle length/2 im Array als Pivot; dies führt bei beinahe sortierten Folgen zu einem optimalen Algorithmenverlauf.

91 6.4 Quicksort

92 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 Länge (z.b. 4 oder 8) unterschritten haben. Für kürzere Listen wird der einfachere Selection-Sort Algorithmus benutzt, da dieser für 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 );

93 6.5 Sortieren durch Mischen (Merge Sort) 6-9 Die Komplexität des Quicksort-Algorithmus ist im besten Fall O(nlog 2 (n). Im schlechtesten Fall (worst case), wenn das Pivot-Element gerade ein Rand-Element ist, dann wird die Komplexität O(n 2 ). Das Finden eines günstigen Pivot Elements ist zentral für die Komplexität des Quicksort Algorithmus. Es gibt verschiedene Verfahren zum Lösen diesese Problems wie, wähle als Pivot das erste Element des Arrays, wähle das Element in der Mitte des Arrays oder wähle drei zufällige Elemente aus dem Array und nimm daraus das mittlere. Eine Garantie für 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 für Datenmengen, die als Listen dargestellt sind. Die Rechenzeit ist in (best case und worst case) O(nlog 2 (n)). Ausserdem wird kein zusätzlicher Speicher benötigt und der Algorithmus ist stabil. Beim Merge-Sort wird die Folge F mit Hilfe einer Methode dividelist() grosse Hälften geteilt. in zwei (möglichst) gleich 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;

94 Sortieren Die (im rekursiven Aufruf) sortierten Folgen werden dann in einem linearen Durchgang zur sortierten Endfolge zusammen gefügt (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 ausgewählt, herausgelöst und als nächstes Element an die neue Liste angefügt wird

95 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; Beispiel: Wie Mergesort genau funktioniert, wollen wir anhand dieses Beispiel nachvollziehen. divide sort merge

96 Sortieren 6.6 Übung 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 Selection Sort Mergesort, Quicksort Erstellen Sie je ein Tracing vom Mergesort-, bzw. vom Quicksort-Algorithmus (einmal mit dem Pivot- Element 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 Überlegen 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 über die zwei weiteren Suchalgorithmen: Bucket Sort und Radix Sort. Sortieren Sie die obige Menge F mit Hilfe von RadixSort mit 5 Buckets.

97 6.6 Übung Quick Sort

98 6-14

99 7 Pattern Matching Pattern Matching ist eine Technik, mit welcher ein String aus Text oder Binärdaten 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 erfüllt. 7.1 Beschreiben von Pattern, Reguläre Ausdrücke Zunächst brauchen wir eine Sprache, mit welcher wir die Pattern (hier reguläre Ausdrücke) beschreiben können (um zu sagen, welche Art von Wörtern oder Informationen wir suchen). Sei E ein Alphabet, d.h. eine endliche Menge von Zeichen. Ein Wort über E ist eine endliche Folge von Zeichen aus E: w = e 1 e 2...e n, e i E Das leere Wort, das aus keinem Zeichen besteht, bezeichnen wir mit λ. Mit E bezeichnen wir die Menge aller Wörter über dem Alphabet E.

Programmierkurs Java

Programmierkurs Java Programmierkurs Java Dr. Dietrich Boles Aufgaben zu UE16-Rekursion (Stand 09.12.2011) Aufgabe 1: Implementieren Sie in Java ein Programm, das solange einzelne Zeichen vom Terminal einliest, bis ein #-Zeichen

Mehr

Grundlagen der Informatik. Prof. Dr. Stefan Enderle NTA Isny

Grundlagen der Informatik. Prof. Dr. Stefan Enderle NTA Isny Grundlagen der Informatik Prof. Dr. Stefan Enderle NTA Isny 2 Datenstrukturen 2.1 Einführung Syntax: Definition einer formalen Grammatik, um Regeln einer formalen Sprache (Programmiersprache) festzulegen.

Mehr

Objektorientierte Programmierung

Objektorientierte Programmierung Objektorientierte Programmierung 1 Geschichte Dahl, Nygaard: Simula 67 (Algol 60 + Objektorientierung) Kay et al.: Smalltalk (erste rein-objektorientierte Sprache) Object Pascal, Objective C, C++ (wiederum

Mehr

Einführung in die Programmierung

Einführung in die Programmierung Technische Universität München WS 2003/2004 Institut für Informatik Prof. Dr. Christoph Zenger Testklausur Einführung in die Programmierung Probeklausur Java (Lösungsvorschlag) 1 Die Klasse ArrayList In

Mehr

Überblick. Lineares Suchen

Überblick. Lineares Suchen Komplexität Was ist das? Die Komplexität eines Algorithmus sei hierbei die Abschätzung des Aufwandes seiner Realisierung bzw. Berechnung auf einem Computer. Sie wird daher auch rechnerische Komplexität

Mehr

Kapiteltests zum Leitprogramm Binäre Suchbäume

Kapiteltests zum Leitprogramm Binäre Suchbäume Kapiteltests zum Leitprogramm Binäre Suchbäume Björn Steffen Timur Erdag überarbeitet von Christina Class Binäre Suchbäume Kapiteltests für das ETH-Leitprogramm Adressaten und Institutionen Das Leitprogramm

Mehr

5 DATEN. 5.1. Variablen. Variablen können beliebige Werte zugewiesen und im Gegensatz zu

5 DATEN. 5.1. Variablen. Variablen können beliebige Werte zugewiesen und im Gegensatz zu Daten Makro + VBA effektiv 5 DATEN 5.1. Variablen Variablen können beliebige Werte zugewiesen und im Gegensatz zu Konstanten jederzeit im Programm verändert werden. Als Variablen können beliebige Zeichenketten

Mehr

Kapitel 6. Komplexität von Algorithmen. Xiaoyi Jiang Informatik I Grundlagen der Programmierung

Kapitel 6. Komplexität von Algorithmen. Xiaoyi Jiang Informatik I Grundlagen der Programmierung Kapitel 6 Komplexität von Algorithmen 1 6.1 Beurteilung von Algorithmen I.d.R. existieren viele Algorithmen, um dieselbe Funktion zu realisieren. Welche Algorithmen sind die besseren? Betrachtung nicht-funktionaler

Mehr

SEP 114. Design by Contract

SEP 114. Design by Contract Design by Contract SEP 114 Design by Contract Teile das zu entwickelnde Programm in kleine Einheiten (Klassen, Methoden), die unabhängig voneinander entwickelt und überprüft werden können. Einheiten mit

Mehr

Folge 19 - Bäume. 19.1 Binärbäume - Allgemeines. Grundlagen: Ulrich Helmich: Informatik 2 mit BlueJ - Ein Kurs für die Stufe 12

Folge 19 - Bäume. 19.1 Binärbäume - Allgemeines. Grundlagen: Ulrich Helmich: Informatik 2 mit BlueJ - Ein Kurs für die Stufe 12 Grundlagen: Folge 19 - Bäume 19.1 Binärbäume - Allgemeines Unter Bäumen versteht man in der Informatik Datenstrukturen, bei denen jedes Element mindestens zwei Nachfolger hat. Bereits in der Folge 17 haben

Mehr

Abschnitt: Algorithmendesign und Laufzeitanalyse

Abschnitt: Algorithmendesign und Laufzeitanalyse Abschnitt: Algorithmendesign und Laufzeitanalyse Definition Divide-and-Conquer Paradigma Divide-and-Conquer Algorithmen verwenden die Strategien 1 Divide: Teile das Problem rekursiv in Subproblem gleicher

Mehr

VBA-Programmierung: Zusammenfassung

VBA-Programmierung: Zusammenfassung VBA-Programmierung: Zusammenfassung Programmiersprachen (Definition, Einordnung VBA) Softwareentwicklung-Phasen: 1. Spezifikation 2. Entwurf 3. Implementierung Datentypen (einfach, zusammengesetzt) Programmablaufsteuerung

Mehr

1 Mathematische Grundlagen

1 Mathematische Grundlagen Mathematische Grundlagen - 1-1 Mathematische Grundlagen Der Begriff der Menge ist einer der grundlegenden Begriffe in der Mathematik. Mengen dienen dazu, Dinge oder Objekte zu einer Einheit zusammenzufassen.

Mehr

Der Aufruf von DM_in_Euro 1.40 sollte die Ausgabe 1.40 DM = 0.51129 Euro ergeben.

Der Aufruf von DM_in_Euro 1.40 sollte die Ausgabe 1.40 DM = 0.51129 Euro ergeben. Aufgabe 1.30 : Schreibe ein Programm DM_in_Euro.java zur Umrechnung eines DM-Betrags in Euro unter Verwendung einer Konstanten für den Umrechnungsfaktor. Das Programm soll den DM-Betrag als Parameter verarbeiten.

Mehr

Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen

Binäre Bäume. 1. Allgemeines. 2. Funktionsweise. 2.1 Eintragen Binäre Bäume 1. Allgemeines Binäre Bäume werden grundsätzlich verwendet, um Zahlen der Größe nach, oder Wörter dem Alphabet nach zu sortieren. Dem einfacheren Verständnis zu Liebe werde ich mich hier besonders

Mehr

Algorithmen & Datenstrukturen 1. Klausur

Algorithmen & Datenstrukturen 1. Klausur Algorithmen & Datenstrukturen 1. Klausur 7. Juli 2010 Name Matrikelnummer Aufgabe mögliche Punkte erreichte Punkte 1 35 2 30 3 30 4 15 5 40 6 30 Gesamt 180 1 Seite 2 von 14 Aufgabe 1) Programm Analyse

Mehr

Primzahlen und RSA-Verschlüsselung

Primzahlen und RSA-Verschlüsselung Primzahlen und RSA-Verschlüsselung Michael Fütterer und Jonathan Zachhuber 1 Einiges zu Primzahlen Ein paar Definitionen: Wir bezeichnen mit Z die Menge der positiven und negativen ganzen Zahlen, also

Mehr

Über Arrays und verkettete Listen Listen in Delphi

Über Arrays und verkettete Listen Listen in Delphi Über Arrays und verkettete Listen Listen in Delphi Michael Puff mail@michael-puff.de 2010-03-26 Inhaltsverzeichnis Inhaltsverzeichnis 1 Einführung 3 2 Arrays 4 3 Einfach verkettete Listen 7 4 Doppelt verkettete

Mehr

Einführung in die Java- Programmierung

Einführung in die Java- Programmierung Einführung in die Java- Programmierung Dr. Volker Riediger Tassilo Horn riediger horn@uni-koblenz.de WiSe 2012/13 1 Wichtig... Mittags keine Pommes... Praktikum A 230 C 207 (Madeleine + Esma) F 112 F 113

Mehr

II. Grundlagen der Programmierung. 9. Datenstrukturen. Daten zusammenfassen. In Java (Forts.): In Java:

II. Grundlagen der Programmierung. 9. Datenstrukturen. Daten zusammenfassen. In Java (Forts.): In Java: Technische Informatik für Ingenieure (TIfI) WS 2005/2006, Vorlesung 9 II. Grundlagen der Programmierung Ekkart Kindler Funktionen und Prozeduren Datenstrukturen 9. Datenstrukturen Daten zusammenfassen

Mehr

1 topologisches Sortieren

1 topologisches Sortieren Wolfgang Hönig / Andreas Ecke WS 09/0 topologisches Sortieren. Überblick. Solange noch Knoten vorhanden: a) Suche Knoten v, zu dem keine Kante führt (Falls nicht vorhanden keine topologische Sortierung

Mehr

Objektorientierte Programmierung

Objektorientierte Programmierung Universität der Bundeswehr Fakultät für Informatik Institut 2 Priv.-Doz. Dr. Lothar Schmitz FT 2006 Zusatzaufgaben Lösungsvorschlag Objektorientierte Programmierung Lösung 22 (Java und UML-Klassendiagramm)

Mehr

Objektorientierte Programmierung. Kapitel 12: Interfaces

Objektorientierte Programmierung. Kapitel 12: Interfaces 12. Interfaces 1/14 Objektorientierte Programmierung Kapitel 12: Interfaces Stefan Brass Martin-Luther-Universität Halle-Wittenberg Wintersemester 2012/13 http://www.informatik.uni-halle.de/ brass/oop12/

Mehr

Suchbäume. Annabelle Klarl. Einführung in die Informatik Programmierung und Softwareentwicklung

Suchbäume. Annabelle Klarl. Einführung in die Informatik Programmierung und Softwareentwicklung Suchbäume Annabelle Klarl Zentralübung zur Vorlesung Einführung in die Informatik: http://www.pst.ifi.lmu.de/lehre/wise-13-14/infoeinf WS13/14 Action required now 1. Smartphone: installiere die App "socrative

Mehr

Modellierung und Programmierung 1

Modellierung und Programmierung 1 Modellierung und Programmierung 1 Prof. Dr. Sonja Prohaska Computational EvoDevo Group Institut für Informatik Universität Leipzig 19. November 2015 Gültigkeitsbereich (Scope) von Variablen { int m; {

Mehr

Institut für Programmierung und Reaktive Systeme 25. August 2014. Programmier-Labor. 04. + 05. Übungsblatt. int binarysearch(int[] a, int x),

Institut für Programmierung und Reaktive Systeme 25. August 2014. Programmier-Labor. 04. + 05. Übungsblatt. int binarysearch(int[] a, int x), Technische Universität Braunschweig Dr. Werner Struckmann Institut für Programmierung und Reaktive Systeme 25. August 2014 Programmier-Labor 04. + 05. Übungsblatt Aufgabe 21: a) Schreiben Sie eine Methode

Mehr

Programmieren I. Kapitel 7. Sortieren und Suchen

Programmieren I. Kapitel 7. Sortieren und Suchen Programmieren I Kapitel 7. Sortieren und Suchen Kapitel 7: Sortieren und Suchen Ziel: Varianten der häufigsten Anwendung kennenlernen Ordnung Suchen lineares Suchen Binärsuche oder Bisektionssuche Sortieren

Mehr

Java Kurs für Anfänger Einheit 5 Methoden

Java Kurs für Anfänger Einheit 5 Methoden Java Kurs für Anfänger Einheit 5 Methoden Ludwig-Maximilians-Universität München (Institut für Informatik: Programmierung und Softwaretechnik von Prof.Wirsing) 22. Juni 2009 Inhaltsverzeichnis Methoden

Mehr

Erwin Grüner 09.02.2006

Erwin Grüner 09.02.2006 FB Psychologie Uni Marburg 09.02.2006 Themenübersicht Folgende Befehle stehen in R zur Verfügung: {}: Anweisungsblock if: Bedingte Anweisung switch: Fallunterscheidung repeat-schleife while-schleife for-schleife

Mehr

Lösungsvorschläge. zu den Aufgaben im Kapitel 4

Lösungsvorschläge. zu den Aufgaben im Kapitel 4 Lösungsvorschläge zu den Aufgaben im Kapitel 4 Aufgabe 4.1: Der KNP-Algorithmus kann verbessert werden, wenn in der Funktion nexttabelle die Zuweisung next[tabindex] = ruecksprung; auf die etwas differenziertere

Mehr

Software Engineering Klassendiagramme Assoziationen

Software Engineering Klassendiagramme Assoziationen Software Engineering Klassendiagramme Assoziationen Prof. Adrian A. Müller, PMP, PSM 1, CSM Fachbereich Informatik und Mikrosystemtechnik 1 Lesen von Multiplizitäten (1) Multiplizitäten werden folgendermaßen

Mehr

Formale Sprachen und Grammatiken

Formale Sprachen und Grammatiken Formale Sprachen und Grammatiken Jede Sprache besitzt die Aspekte Semantik (Bedeutung) und Syntax (formaler Aufbau). Die zulässige und korrekte Form der Wörter und Sätze einer Sprache wird durch die Syntax

Mehr

Java Einführung Collections

Java Einführung Collections Java Einführung Collections Inhalt dieser Einheit Behälterklassen, die in der Java API bereitgestellt werden Wiederholung Array Collections (Vector, List, Set) Map 2 Wiederholung Array a[0] a[1] a[2] a[3]...

Mehr

AVL-Bäume Analyse. Theorem Ein AVL-Baum der Höhe h besitzt zwischen F h und 2 h 1 viele Knoten. Definition Wir definieren die nte Fibonaccizahl:

AVL-Bäume Analyse. Theorem Ein AVL-Baum der Höhe h besitzt zwischen F h und 2 h 1 viele Knoten. Definition Wir definieren die nte Fibonaccizahl: AVL-Bäume Analyse (Folie 85, Seite 39 im Skript) Theorem Ein AVL-Baum der Höhe h besitzt zwischen F h und 2 h 1 viele Knoten. Definition Wir definieren die nte Fibonaccizahl: 0 falls n = 0 F n = 1 falls

Mehr

Programmiertechnik II

Programmiertechnik II Analyse von Algorithmen Algorithmenentwurf Algorithmen sind oft Teil einer größeren Anwendung operieren auf Daten der Anwendung, sollen aber unabhängig von konkreten Typen sein Darstellung der Algorithmen

Mehr

Einführung in die Informatik für Naturwissenschaftler und Ingenieure (alias Einführung in die Programmierung)

Einführung in die Informatik für Naturwissenschaftler und Ingenieure (alias Einführung in die Programmierung) Wintersemester 2007/08 Einführung in die Informatik für Naturwissenschaftler und Ingenieure (alias Einführung in die Programmierung) (Vorlesung) Prof. Dr. Günter Rudolph Fakultät für Informatik Lehrstuhl

Mehr

M. Graefenhan 2000-12-07. Übungen zu C. Blatt 3. Musterlösung

M. Graefenhan 2000-12-07. Übungen zu C. Blatt 3. Musterlösung M. Graefenhan 2000-12-07 Aufgabe Lösungsweg Übungen zu C Blatt 3 Musterlösung Schreiben Sie ein Programm, das die Häufigkeit von Zeichen in einem eingelesenen String feststellt. Benutzen Sie dazu ein zweidimensionales

Mehr

Unterrichtsmaterialien in digitaler und in gedruckter Form. Auszug aus: Übungsbuch für den Grundkurs mit Tipps und Lösungen: Analysis

Unterrichtsmaterialien in digitaler und in gedruckter Form. Auszug aus: Übungsbuch für den Grundkurs mit Tipps und Lösungen: Analysis Unterrichtsmaterialien in digitaler und in gedruckter Form Auszug aus: Übungsbuch für den Grundkurs mit Tipps und Lösungen: Analysis Das komplette Material finden Sie hier: Download bei School-Scout.de

Mehr

4. Jeder Knoten hat höchstens zwei Kinder, ein linkes und ein rechtes.

4. Jeder Knoten hat höchstens zwei Kinder, ein linkes und ein rechtes. Binäre Bäume Definition: Ein binärer Baum T besteht aus einer Menge von Knoten, die durch eine Vater-Kind-Beziehung wie folgt strukturiert ist: 1. Es gibt genau einen hervorgehobenen Knoten r T, die Wurzel

Mehr

Programmieren in C. Rekursive Funktionen. Prof. Dr. Nikolaus Wulff

Programmieren in C. Rekursive Funktionen. Prof. Dr. Nikolaus Wulff Programmieren in C Rekursive Funktionen Prof. Dr. Nikolaus Wulff Rekursive Funktionen Jede C Funktion besitzt ihren eigenen lokalen Satz an Variablen. Dies bietet ganze neue Möglichkeiten Funktionen zu

Mehr

Datenstrukturen & Algorithmen

Datenstrukturen & Algorithmen Datenstrukturen & Algorithmen Matthias Zwicker Universität Bern Frühling 2010 Übersicht Binäre Suchbäume Einführung und Begriffe Binäre Suchbäume 2 Binäre Suchbäume Datenstruktur für dynamische Mengen

Mehr

Übersicht. Datenstrukturen und Algorithmen. Übersicht. Divide-and-Conquer. Vorlesung 9: Quicksort (K7)

Übersicht. Datenstrukturen und Algorithmen. Übersicht. Divide-and-Conquer. Vorlesung 9: Quicksort (K7) Datenstrukturen und Algorithmen Vorlesung 9: (K7) Joost-Pieter Katoen Lehrstuhl für Informatik 2 Software Modeling and Verification Group http://www-i2.rwth-aachen.de/i2/dsal0/ Algorithmus 8. Mai 200 Joost-Pieter

Mehr

Grundbegriffe der Informatik

Grundbegriffe der Informatik Grundbegriffe der Informatik Einheit 15: Reguläre Ausdrücke und rechtslineare Grammatiken Thomas Worsch Universität Karlsruhe, Fakultät für Informatik Wintersemester 2008/2009 1/25 Was kann man mit endlichen

Mehr

Zeichen bei Zahlen entschlüsseln

Zeichen bei Zahlen entschlüsseln Zeichen bei Zahlen entschlüsseln In diesem Kapitel... Verwendung des Zahlenstrahls Absolut richtige Bestimmung von absoluten Werten Operationen bei Zahlen mit Vorzeichen: Addieren, Subtrahieren, Multiplizieren

Mehr

Einführung. Vorlesungen zur Komplexitätstheorie: Reduktion und Vollständigkeit (3) Vorlesungen zur Komplexitätstheorie. K-Vollständigkeit (1/5)

Einführung. Vorlesungen zur Komplexitätstheorie: Reduktion und Vollständigkeit (3) Vorlesungen zur Komplexitätstheorie. K-Vollständigkeit (1/5) Einführung 3 Vorlesungen zur Komplexitätstheorie: Reduktion und Vollständigkeit (3) Univ.-Prof. Dr. Christoph Meinel Hasso-Plattner-Institut Universität Potsdam, Deutschland Hatten den Reduktionsbegriff

Mehr

Algorithmen und Datenstrukturen

Algorithmen und Datenstrukturen Algorithmen und Datenstrukturen Dipl. Inform. Andreas Wilkens 1 Organisatorisches Freitag, 05. Mai 2006: keine Vorlesung! aber Praktikum von 08.00 11.30 Uhr (Gruppen E, F, G, H; Vortestat für Prototyp)

Mehr

Das Typsystem von Scala. L. Piepmeyer: Funktionale Programmierung - Das Typsystem von Scala

Das Typsystem von Scala. L. Piepmeyer: Funktionale Programmierung - Das Typsystem von Scala Das Typsystem von Scala 1 Eigenschaften Das Typsystem von Scala ist statisch, implizit und sicher 2 Nichts Primitives Alles ist ein Objekt, es gibt keine primitiven Datentypen scala> 42.hashCode() res0:

Mehr

Grundlagen der Theoretischen Informatik, SoSe 2008

Grundlagen der Theoretischen Informatik, SoSe 2008 1. Aufgabenblatt zur Vorlesung Grundlagen der Theoretischen Informatik, SoSe 2008 (Dr. Frank Hoffmann) Lösung von Manuel Jain und Benjamin Bortfeldt Aufgabe 2 Zustandsdiagramme (6 Punkte, wird korrigiert)

Mehr

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren

Lineargleichungssysteme: Additions-/ Subtraktionsverfahren Lineargleichungssysteme: Additions-/ Subtraktionsverfahren W. Kippels 22. Februar 2014 Inhaltsverzeichnis 1 Einleitung 2 2 Lineargleichungssysteme zweiten Grades 2 3 Lineargleichungssysteme höheren als

Mehr

Übungen zu Einführung in die Informatik: Programmierung und Software-Entwicklung: Lösungsvorschlag

Übungen zu Einführung in die Informatik: Programmierung und Software-Entwicklung: Lösungsvorschlag Ludwig-Maximilians-Universität München WS 2015/16 Institut für Informatik Übungsblatt 9 Prof. Dr. R. Hennicker, A. Klarl Übungen zu Einführung in die Informatik: Programmierung und Software-Entwicklung:

Mehr

Java Kurs für Anfänger Einheit 4 Klassen und Objekte

Java Kurs für Anfänger Einheit 4 Klassen und Objekte Java Kurs für Anfänger Einheit 4 Klassen und Ludwig-Maximilians-Universität München (Institut für Informatik: Programmierung und Softwaretechnik von Prof.Wirsing) 13. Juni 2009 Inhaltsverzeichnis klasse

Mehr

368 4 Algorithmen und Datenstrukturen

368 4 Algorithmen und Datenstrukturen Kap04.fm Seite 368 Dienstag, 7. September 2010 1:51 13 368 4 Algorithmen und Datenstrukturen Java-Klassen Die ist die Klasse Object, ein Pfeil von Klasse A nach Klasse B bedeutet Bextends A, d.h. B ist

Mehr

Theoretische Grundlagen der Informatik

Theoretische Grundlagen der Informatik Theoretische Grundlagen der Informatik Vorlesung am 12.01.2012 INSTITUT FÜR THEORETISCHE 0 KIT 12.01.2012 Universität des Dorothea Landes Baden-Württemberg Wagner - Theoretische und Grundlagen der Informatik

Mehr

Gleichungen Lösen. Ein graphischer Blick auf Gleichungen

Gleichungen Lösen. Ein graphischer Blick auf Gleichungen Gleichungen Lösen Was bedeutet es, eine Gleichung zu lösen? Was ist überhaupt eine Gleichung? Eine Gleichung ist, grundsätzlich eine Aussage über zwei mathematische Terme, dass sie gleich sind. Ein Term

Mehr

Sortierverfahren für Felder (Listen)

Sortierverfahren für Felder (Listen) Sortierverfahren für Felder (Listen) Generell geht es um die Sortierung von Daten nach einem bestimmten Sortierschlüssel. Es ist auch möglich, daß verschiedene Daten denselben Sortierschlüssel haben. Es

Mehr

Klausur WS 2006/07 Programmiersprache Java Objektorientierte Programmierung II 15. März 2007

Klausur WS 2006/07 Programmiersprache Java Objektorientierte Programmierung II 15. März 2007 Fachhochschule Bonn-Rhein-Sieg University of Applied Sciences Fachbereich Informatik Prof. Dr. Peter Becker Klausur WS 2006/07 Programmiersprache Java Objektorientierte Programmierung II 15. März 2007

Mehr

Grundlagen von Python

Grundlagen von Python Einführung in Python Grundlagen von Python Felix Döring, Felix Wittwer November 17, 2015 Scriptcharakter Programmierparadigmen Imperatives Programmieren Das Scoping Problem Objektorientiertes Programmieren

Mehr

Kostenmaße. F3 03/04 p.188/395

Kostenmaße. F3 03/04 p.188/395 Kostenmaße Bei der TM nur ein Kostenmaß: Ein Schritt (Konfigurationsübergang) kostet eine Zeiteinheit; eine Bandzelle kostet eine Platzeinheit. Bei der RAM zwei Kostenmaße: uniformes Kostenmaß: (wie oben);

Mehr

Theoretische Informatik SS 04 Übung 1

Theoretische Informatik SS 04 Übung 1 Theoretische Informatik SS 04 Übung 1 Aufgabe 1 Es gibt verschiedene Möglichkeiten, eine natürliche Zahl n zu codieren. In der unären Codierung hat man nur ein Alphabet mit einem Zeichen - sagen wir die

Mehr

Entscheidungsbäume. Definition Entscheidungsbaum. Frage: Gibt es einen Sortieralgorithmus mit o(n log n) Vergleichen?

Entscheidungsbäume. Definition Entscheidungsbaum. Frage: Gibt es einen Sortieralgorithmus mit o(n log n) Vergleichen? Entscheidungsbäume Frage: Gibt es einen Sortieralgorithmus mit o(n log n) Vergleichen? Definition Entscheidungsbaum Sei T ein Binärbaum und A = {a 1,..., a n } eine zu sortierenden Menge. T ist ein Entscheidungsbaum

Mehr

Programmiersprachen und Übersetzer

Programmiersprachen und Übersetzer Programmiersprachen und Übersetzer Sommersemester 2010 19. April 2010 Theoretische Grundlagen Problem Wie kann man eine unendliche Menge von (syntaktisch) korrekten Programmen definieren? Lösung Wie auch

Mehr

Einführung in die Programmierung

Einführung in die Programmierung : Inhalt Einführung in die Programmierung Wintersemester 2008/09 Prof. Dr. Günter Rudolph Lehrstuhl für Algorithm Engineering Fakultät für Informatik TU Dortmund - mit / ohne Parameter - mit / ohne Rückgabewerte

Mehr

1.4.12 Sin-Funktion vgl. Cos-Funktion

1.4.12 Sin-Funktion vgl. Cos-Funktion .4. Sgn-Funktion Informatik. Semester 36 36.4.2 Sin-Funktion vgl. Cos-Funktion Informatik. Semester 37 37 .4.3 Sqr-Funktion Informatik. Semester 38 38.4.4 Tan-Funktion Informatik. Semester 39 39 .5 Konstanten

Mehr

Konzepte der Informatik

Konzepte der Informatik Konzepte der Informatik Vorkurs Informatik zum WS 2011/2012 26.09. - 30.09.2011 17.10. - 21.10.2011 Dr. Werner Struckmann / Christoph Peltz Stark angelehnt an Kapitel 1 aus "Abenteuer Informatik" von Jens

Mehr

Vorkurs Informatik WiSe 15/16

Vorkurs Informatik WiSe 15/16 Konzepte der Informatik Dr. Werner Struckmann / Stephan Mielke, Jakob Garbe, 16.10.2015 Technische Universität Braunschweig, IPS Inhaltsverzeichnis Suchen Binärsuche Binäre Suchbäume 16.10.2015 Dr. Werner

Mehr

Vorkurs C++ Programmierung

Vorkurs C++ Programmierung Vorkurs C++ Programmierung Klassen Letzte Stunde Speicherverwaltung automatische Speicherverwaltung auf dem Stack dynamische Speicherverwaltung auf dem Heap new/new[] und delete/delete[] Speicherklassen:

Mehr

Tutorium 5 - Programmieren

Tutorium 5 - Programmieren Tutorium 5 - Programmieren Grischa Liebel Uni Karlsruhe (TH) Tutorium 11 1 Einleitung 2 Abschlussaufgaben 3 Vorlesungsstoff 4 Ergänzungen zum Vorlesungsstoff Grischa Liebel (Uni Karlsruhe (TH)) c 2008

Mehr

50. Mathematik-Olympiade 2. Stufe (Regionalrunde) Klasse 11 13. 501322 Lösung 10 Punkte

50. Mathematik-Olympiade 2. Stufe (Regionalrunde) Klasse 11 13. 501322 Lösung 10 Punkte 50. Mathematik-Olympiade. Stufe (Regionalrunde) Klasse 3 Lösungen c 00 Aufgabenausschuss des Mathematik-Olympiaden e.v. www.mathematik-olympiaden.de. Alle Rechte vorbehalten. 503 Lösung 0 Punkte Es seien

Mehr

Kontrollstrukturen - Universität Köln

Kontrollstrukturen - Universität Köln Kontrollstrukturen - Universität Köln Mario Manno Kontrollstrukturen - Universität Köln p. 1 Was sind Sprachen Auszeichnungssprachen HTML, XML Programmiersprachen ASM, Basic, C, C++, Haskell, Java, Pascal,

Mehr

Binäre Bäume Darstellung und Traversierung

Binäre Bäume Darstellung und Traversierung Binäre Bäume Darstellung und Traversierung Name Frank Bollwig Matrikel-Nr. 2770085 E-Mail fb641378@inf.tu-dresden.de Datum 15. November 2001 0. Vorbemerkungen... 3 1. Terminologie binärer Bäume... 4 2.

Mehr

Funktionale Programmierung mit Haskell

Funktionale Programmierung mit Haskell Funktionale Programmierung mit Haskell Dr. Michael Savorić Hohenstaufen-Gymnasium (HSG) Kaiserslautern Version 20120622 Überblick Wichtige Eigenschaften Einführungsbeispiele Listenerzeugung und Beispiel

Mehr

Lineare Gleichungssysteme

Lineare Gleichungssysteme Lineare Gleichungssysteme 1 Zwei Gleichungen mit zwei Unbekannten Es kommt häufig vor, dass man nicht mit einer Variablen alleine auskommt, um ein Problem zu lösen. Das folgende Beispiel soll dies verdeutlichen

Mehr

1 Vom Problem zum Programm

1 Vom Problem zum Programm Hintergrundinformationen zur Vorlesung GRUNDLAGEN DER INFORMATIK I Studiengang Elektrotechnik WS 02/03 AG Betriebssysteme FB3 Kirsten Berkenkötter 1 Vom Problem zum Programm Aufgabenstellung analysieren

Mehr

Übung 9 - Lösungsvorschlag

Übung 9 - Lösungsvorschlag Universität Innsbruck - Institut für Informatik Datenbanken und Informationssysteme Prof. Günther Specht, Eva Zangerle Besprechung: 15.12.2008 Einführung in die Informatik Übung 9 - Lösungsvorschlag Aufgabe

Mehr

Einfache Arrays. Annabelle Klarl. Einführung in die Informatik Programmierung und Softwareentwicklung

Einfache Arrays. Annabelle Klarl. Einführung in die Informatik Programmierung und Softwareentwicklung Annabelle Klarl Zentralübung zur Vorlesung Einführung in die Informatik: http://www.pst.ifi.lmu.de/lehre/wise-13-14/infoeinf WS13/14 Action required now 1. Smartphone: installiere die App "socrative student"

Mehr

Assoziation und Aggregation

Assoziation und Aggregation Assoziation und Aggregation Martin Wirsing in Zusammenarbeit mit Matthias Hölzl, Nora Koch 05/03 2 Ziele Verstehen der Begriffe Assoziation und Aggregation Implementierung von Assoziationen in Java schreiben

Mehr

Reihungen. Martin Wirsing. in Zusammenarbeit mit Matthias Hölzl und Nora Koch 11/03

Reihungen. Martin Wirsing. in Zusammenarbeit mit Matthias Hölzl und Nora Koch 11/03 Reihungen Martin Wirsing in Zusammenarbeit mit Matthias Hölzl und Nora Koch 11/03 2 Ziele Die Datenstruktur der Reihungen verstehen: mathematisch und im Speicher Grundlegende Algorithmen auf Reihungen

Mehr

IT-Basics 2. DI Gerhard Fließ

IT-Basics 2. DI Gerhard Fließ IT-Basics 2 DI Gerhard Fließ Wer bin ich? DI Gerhard Fließ Telematik Studium an der TU Graz Softwareentwickler XiTrust www.xitrust.com www.tugraz.at Worum geht es? Objektorientierte Programmierung Konzepte

Mehr

Vorname:... Matrikel-Nr.:... Unterschrift:...

Vorname:... Matrikel-Nr.:... Unterschrift:... Fachhochschule Mannheim Hochschule für Technik und Gestaltung Fachbereich Informatik Studiengang Bachelor of Computer Science Algorithmen und Datenstrukturen Wintersemester 2003 / 2004 Name:... Vorname:...

Mehr

Übung Grundlagen der Programmierung. Übung 03: Schleifen. Testplan Testergebnisse

Übung Grundlagen der Programmierung. Übung 03: Schleifen. Testplan Testergebnisse Übung 03: Schleifen Abgabetermin: xx.xx.xxxx Name: Matrikelnummer: Gruppe: G1 (Prähofer) G2 (Prähofer) G3 (Wolfinger) Aufgabe Punkte gelöst abzugeben schriftlich abzugeben elektronisch Aufgabe 03.1 12

Mehr

3.1 Konstruktion von minimalen Spannbäumen Es gibt zwei Prinzipien für die Konstruktion von minimalen Spannbäumen (Tarjan): blaue Regel rote Regel

3.1 Konstruktion von minimalen Spannbäumen Es gibt zwei Prinzipien für die Konstruktion von minimalen Spannbäumen (Tarjan): blaue Regel rote Regel 3.1 Konstruktion von minimalen Spannbäumen Es gibt zwei Prinzipien für die Konstruktion von minimalen Spannbäumen (Tarjan): blaue Regel rote Regel EADS 3.1 Konstruktion von minimalen Spannbäumen 16/36

Mehr

1. Einfach verkettete Liste unsortiert 2. Einfach verkettete Liste sortiert 3. Doppelt verkettete Liste sortiert

1. Einfach verkettete Liste unsortiert 2. Einfach verkettete Liste sortiert 3. Doppelt verkettete Liste sortiert Inhalt Einführung 1. Arrays 1. Array unsortiert 2. Array sortiert 3. Heap 2. Listen 1. Einfach verkettete Liste unsortiert 2. Einfach verkettete Liste sortiert 3. Doppelt verkettete Liste sortiert 3. Bäume

Mehr

SOI 2013. Die Schweizer Informatikolympiade

SOI 2013. Die Schweizer Informatikolympiade SOI Die Schweizer Informatikolympiade Lösung SOI Wie schreibe ich eine gute Lösung? Bevor wir die Aufgaben präsentieren, möchten wir dir einige Tipps geben, wie eine gute Lösung für die theoretischen

Mehr

Computeranwendung und Programmierung (CuP)

Computeranwendung und Programmierung (CuP) Computeranwendung und Programmierung (CuP) VO: Peter Auer (Informationstechnologie) UE: Norbert Seifter (Angewandet Mathematik) Organisatorisches (Vorlesung) Vorlesungszeiten Montag 11:15 12:45 Freitag

Mehr

Grammatiken. Einführung

Grammatiken. Einführung Einführung Beispiel: Die arithmetischen Ausdrücke über der Variablen a und den Operationen + und können wie folgt definiert werden: a, a + a und a a sind arithmetische Ausdrücke Wenn A und B arithmetische

Mehr

Objektorientierte Programmierung für Anfänger am Beispiel PHP

Objektorientierte Programmierung für Anfänger am Beispiel PHP Objektorientierte Programmierung für Anfänger am Beispiel PHP Johannes Mittendorfer http://jmittendorfer.hostingsociety.com 19. August 2012 Abstract Dieses Dokument soll die Vorteile der objektorientierten

Mehr

Informationsblatt Induktionsbeweis

Informationsblatt Induktionsbeweis Sommer 015 Informationsblatt Induktionsbeweis 31. März 015 Motivation Die vollständige Induktion ist ein wichtiges Beweisverfahren in der Informatik. Sie wird häufig dazu gebraucht, um mathematische Formeln

Mehr

Institut für Programmierung und Reaktive Systeme 26. April 2013. Programmieren II. 10. Übungsblatt

Institut für Programmierung und Reaktive Systeme 26. April 2013. Programmieren II. 10. Übungsblatt Technische Universität Braunschweig Dr. Werner Struckmann Institut für Programmierung und Reaktive Systeme 26. April 2013 Programmieren II 10. Übungsblatt Hinweis: Insgesamt werden in diesem Semester fünf

Mehr

Einführung in die Java- Programmierung

Einführung in die Java- Programmierung Einführung in die Java- Programmierung Dr. Volker Riediger Tassilo Horn riediger horn@uni-koblenz.de WiSe 2012/13 1 Wichtig... Mittags Pommes... Praktikum A 230 C 207 (Madeleine) F 112 F 113 (Kevin) E

Mehr

Eine Baumstruktur sei folgendermaßen definiert. Eine Baumstruktur mit Grundtyp Element ist entweder

Eine Baumstruktur sei folgendermaßen definiert. Eine Baumstruktur mit Grundtyp Element ist entweder Programmieren in PASCAL Bäume 1 1. Baumstrukturen Eine Baumstruktur sei folgendermaßen definiert. Eine Baumstruktur mit Grundtyp Element ist entweder 1. die leere Struktur oder 2. ein Knoten vom Typ Element

Mehr

Große Übung Praktische Informatik 1

Große Übung Praktische Informatik 1 Große Übung Praktische Informatik 1 2005-12-08 fuessler@informatik.uni-mannheim.de http://www.informatik.uni-mannheim.de/pi4/people/fuessler 1: Announcements / Orga Weihnachtsklausur zählt als Übungsblatt,

Mehr

3.2 Binäre Suche. Usr/local/www/ifi/fk/menschen/schmid/folien/infovk.ppt 1

3.2 Binäre Suche. Usr/local/www/ifi/fk/menschen/schmid/folien/infovk.ppt 1 3.2 Binäre Suche Beispiel 6.5.1: Intervallschachtelung (oder binäre Suche) (Hier ist n die Anzahl der Elemente im Feld!) Ein Feld A: array (1..n) of Integer sei gegeben. Das Feld sei sortiert, d.h.: A(i)

Mehr

Praktikum zu Einführung in die Informatik für LogWiIngs und WiMas Wintersemester 2015/16. Vorbereitende Aufgaben

Praktikum zu Einführung in die Informatik für LogWiIngs und WiMas Wintersemester 2015/16. Vorbereitende Aufgaben Praktikum zu Einführung in die Informatik für LogWiIngs und WiMas Wintersemester 2015/16 Fakultät für Informatik Lehrstuhl 14 Lars Hildebrand, Marcel Preuß, Iman Kamehkhosh, Marc Bury, Diana Howey Übungsblatt

Mehr

Folge 18 - Vererbung

Folge 18 - Vererbung Workshop Folge 18 - Vererbung 18.1 Ein einfacher Fall der Vererbung Schritt 1 - Vorbereitungen Besorgen Sie sich - vielleicht aus einer der Übungen der Folge 17 - ein fertiges und lauffähiges Listenprojekt,

Mehr

Klausur zur Einführung in die objektorientierte Programmierung mit Java

Klausur zur Einführung in die objektorientierte Programmierung mit Java Klausur zur Einführung in die objektorientierte Programmierung mit Java im Studiengang Informationswissenschaft Prof. Dr. Christian Wolff Professur für Medieninformatik Institut für Medien-, Informations-

Mehr

Kapitel 2: Formale Sprachen Kontextfreie Sprachen. reguläre Grammatiken/Sprachen. kontextfreie Grammatiken/Sprachen

Kapitel 2: Formale Sprachen Kontextfreie Sprachen. reguläre Grammatiken/Sprachen. kontextfreie Grammatiken/Sprachen reguläre Grammatiken/prachen Beschreibung für Bezeichner in Programmiersprachen Beschreibung für wild cards in kriptsprachen (/* reguläre Ausdrücke */)?; [a-z]; * kontextfreie Grammatiken/prachen Beschreibung

Mehr

Kapitel MK:IV. IV. Modellieren mit Constraints

Kapitel MK:IV. IV. Modellieren mit Constraints Kapitel MK:IV IV. Modellieren mit Constraints Einführung und frühe Systeme Konsistenz I Binarization Generate-and-Test Backtracking-basierte Verfahren Konsistenz II Konsistenzanalyse Weitere Analyseverfahren

Mehr

Übungen 19.01.2012 Programmieren 1 Felix Rohrer. Übungen

Übungen 19.01.2012 Programmieren 1 Felix Rohrer. Übungen Übungen if / else / else if... 2... 2 Aufgabe 2:... 2 Aufgabe 3:... 2 Aufgabe 4:... 2 Aufgabe 5:... 2 Aufgabe 6:... 2 Aufgabe 7:... 3 Aufgabe 8:... 3 Aufgabe 9:... 3 Aufgabe 10:... 3 switch... 4... 4 Aufgabe

Mehr

Übersicht. Schleifen. Schleifeninvarianten. Referenztypen, Wrapperklassen und API. 9. November 2009 CoMa I WS 08/09 1/15

Übersicht. Schleifen. Schleifeninvarianten. Referenztypen, Wrapperklassen und API. 9. November 2009 CoMa I WS 08/09 1/15 Übersicht Schleifen Schleifeninvarianten Referenztypen, Wrapperklassen und API CoMa I WS 08/09 1/15 CoMa I Programmierziele Linux bedienen Code umschreiben strukturierte Datentypen Anweisungen und Kontrollstrukturen

Mehr

t r Lineare Codierung von Binärbbäumen (Wörter über dem Alphabet {, }) Beispiel code( ) = code(, t l, t r ) = code(t l ) code(t r )

t r Lineare Codierung von Binärbbäumen (Wörter über dem Alphabet {, }) Beispiel code( ) = code(, t l, t r ) = code(t l ) code(t r ) Definition B : Menge der binären Bäume, rekursiv definiert durch die Regeln: ist ein binärer Baum sind t l, t r binäre Bäume, so ist auch t =, t l, t r ein binärer Baum nur das, was durch die beiden vorigen

Mehr