13. Verkettete Listen Algorithmen und Datenstrukturen 13. Verkettete Listen Prof. Dr. Christoph Pflaum Department Informatik • Martensstraße 3 • 91058 Erlangen 13.1 13.2 13.3 13.4 13.5 13.6 13.7 13.8 13.9 13.10 Generische Listen und Iteratoren Verkettete Listen Exkurs: Geschachtelte Klassen in Java Mengen ohne Sortierung Mengen mit Sortierung Sortieren durch Auswählen Sortieren durch Einfügen Fächersortierung Radix-Sortierung Sortieren durch Mischen („merge sort“) Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-2 Zur Erinnerung: aus Abschnitt „12.3 Liste“ 13.1 Generische Listen und Iteratoren ADT Liste In Java ist die Liste Spezialfall einer Objektmenge beliebiger Größe: Typ-Parameter Signatur Liste(T) □ create: □ append: T x Liste □ head: Liste □ tail: Liste □ length: Liste Axiome □ A1: □ A2: □ A3: □ A4: Æ Liste Æ Liste ÆT Æ Liste Æù Namen im folgenden Code head(append(x,l)) = x tail(append(x,l)) = l length(create) = 0 length(append(x,l))=1+length(l) add entry(idx) remove(idx) size head(create) und tail(create) sind nicht spezifiziert. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-3 © Michael Philippsen java.util.Collection Eine Collection ist eine Ansammlung von Objekten, zunächst noch ohne bestimmte Struktur. java.util.List List spezifiziert genauer: Sammlung von Objekten mit folgenden Eigenschaften: „Reihenfolge wichtig“ und „Duplikate möglich“. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-4 1 13.1 Generische Listen und Iteratoren 13.1 Generische Listen und Iteratoren Imperative Lösung mit Java Die ADT-Modellierung hat eine „Basis-Liste“ modelliert. Hier nun Implementierung in Java mit mehr Operationen: Java-Lösung: Das interface java.util.List realisiert die abstrakte Idee einer Liste. □ □ □ □ □ □ □ Ein Interface implementiert noch keine konkreten Methoden. □ Es zeigt die Grundidee einer Liste auf. □ Die konkrete Implementierung erfolgt erst später (Æ z.B als java.util.LinkedList). Erstellen einer neuen Liste. Einfügen eines neuen Elements an beliebiger Stelle der Liste. Löschen eines Elements an beliebiger Stelle der Liste. Überprüfen, ob ein Element in einer Liste enthalten ist. Überprüfen, ob eine Liste leer ist. Die Länge einer Liste bestimmen. Eigenschaften □ Duplikate sind möglich. □ Kontrollierte Reihenfolge der Elemente (über Parameter index). Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-5 13.1 Generische Listen und Iteratoren Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-6 13.1 Generische Listen und Iteratoren Einfache Java-Schnittstelle für „Liste“ Allgemeine Object-Menge Einfache Java-Schnittstelle für „Liste“ // Entfernt das Element an der Stelle index Object remove(int index); public interface List extends Collection { // Element elem an der Stelle index einfuegen void add(int index, Object elem); // Element elem an die Liste anhaengen boolean add(Object elem); // Alle Elemente aus der Liste entfernen void clear(); 0 = erstes Listenelement // Entfernt das erste Vorkommnis des Objekts o boolean remove(Object o); true, falls Liste geändert wurde. // Prüfen, ob die Liste leer ist boolean isEmpty(); // Liefert die Anzahl der Elemente der Liste int size(); // Ueberprueft, ob Objekt o enthalten ist boolean contains(Object o); // Liefert das Element an der Stelle index Object get(int index); ... ... } Problem: Typinformation geht verloren, denn get liefert in jedem Fall Object. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-7 © Michael Philippsen false, wenn o nicht vorkommt. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-8 2 13.1 Generische Listen und Iteratoren 13.1 Generische Listen und Iteratoren „Obst-Beispiel“: einfache „Liste“ Mithilfe von generischen Klassenparametern können Klassen spezialisiert werden. ... List aepfel = new ListImpl(); // Liste von Aepfeln aepfel.add(new Apfel("Boskoop")); get liefert Object, aepfel.add(new Apfel("Granny Smith")); aepfel.get(0); Apfel erster = (Apfel) aepfel.get(0); Typkonvertierung notwendig. List birnen = new ListImpl(); // Liste von Birnen birnen.add(new Birne("Williams Christ")); Kein Übersetzungsbirnen.add(new Apfel("Boskoop")); Birne zweite = (Birne) birnen.get(1); ... Fehler zur Laufzeit! Programm erwartet eine Birne, in der Liste steht aber ein Apfel. java.util.Collection<E> java.util.List<E> fehler! Programmierer muss aufpassen, das er nur richtige Objekte in der Liste speichert. Die Collection ist eine Ansammlung von Objekten, zunächst noch ohne bestimmte Struktur. Der Elementtyp ist als später zu konkretisierender Parameter E realisiert. Man spricht von einem parametrisierten Typ. List spezifiziert genauer: Sammlung von Objekten (vom Typ E) mit folgenden Eigenschaften: „Reihenfolge wichtig“ und „Duplikate möglich“. Ist es möglich Typ-Prüfung zur Laufzeit zu vermeiden? Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-9 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-10 13.1 Generische Listen und Iteratoren 13.1 Generische Listen und Iteratoren Als generische Klassenparameter können Klassen und Interfaces angewendet werden. Es können keine speziellen Methoden der generischen Klasse aufgerufen werden: Generische Java-Schnittstelle für „Liste“ class MySpecialClass<E> { E f; // o.k., da hier nur Referenz ... public void do(E h) { f = h; // o.k., da hier nur Referenz MySpecialClass<E> g = new MySpecialClass<E>(); // o.k., // E kann als Parameter verwendet werden public interface List<E> extends Collection<E> { // Element elem an der Stelle index einfuegen void add(int index, E elem); // Element elem an die Liste anhaengen boolean add(E elem); // Alle Elemente aus der Liste entfernen void clear(); E g = new E(); // Konstruktion eines Objektes nicht // erlaubt, da Laufzeittyp unbekannt f.Print(); // Aufruf dieser Methode nicht erlaubt, // da Laufzeittyp unbekannt f.toString(); // o.k., da Object die Methode garantiert } Typ-Parameter // Ueberprueft, ob Objekt o enthalten ist boolean contains(Object o); Als Platzhalter für Klasse verwendet. Nur Elemente v. Typ E anhängbar. Auch für Objekte, die nicht vom Typ E sind nutzbar. // Liefert das Element an der Stelle index E get(int index); ... } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-11 © Michael Philippsen Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-12 3 13.1 Generische Listen und Iteratoren „Obst-Beispiel“: einfache „Liste“ 13.1 Generische Listen und Iteratoren Liste nur für Objekte vom Typ Apfel. Generische Java-Schnittstelle für „Liste“ // Entfernt das Element an der Stelle index E remove(int index); ... List<Apfel> aepfel = new ListImpl<Apfel>(); aepfel.add(new Apfel("Boskoop")); get liefert Apfel, aepfel.add(new Apfel("Granny Smith")); // Entfernt das erste Vorkommnis des Objekts o boolean remove(Object o); false, wenn o Typkonvertierung nicht notwendig Apfel erster = aepfel.get(0); // Prüfen, ob die Liste leer ist boolean isEmpty(); List<Birne> birnen = new ListImpl<Birne>(); birnen.add(new Birne("Williams Christ")); birnen.add(new Apfel("Boskoop")); Übersetzungsfehler! Birne zweite = (Birne) birnen.get(1); ... // Liefert die Anzahl der Elemente der Liste int size(); birnen.add(..) ist nur für Birne deklariert. Niemals Typfehler zur Laufzeit! Übersetzer prüft Typ schon beim Einfügen. nicht vorkommt. // Liefert einen Iterator für die Liste, // der Elemente vom Typ E liefert. Iterator<E> iterator(); ... } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-13 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-14 13.1 Generische Listen und Iteratoren 13.1 Generische Listen und Iteratoren Iterator: Methoden der Klasse java.util.Iterator<E>: = Möglichkeit zur Navigation durch eine beliebige Datenstruktur. □ Das Besuchen der Datenstruktur wird unabhängig von der konkreten Implementierung der Datenstruktur Æ Geheimnisprinzip. □ Falls eine Datenstruktur gleichzeitig mehrmals durchlaufen (traversiert) werden soll, muss der Zustand der Traversierung unabhängig von der Datenstruktur sein. public interface Iterator<E> { // Gibt es noch ein nächstes Objekt? boolean hasNext(); // Liefert das nächste Element (vom Typ E) E next(); Ein Iterator ist ein Objekt für genau diesen Zweck. // entfernt das Objekt, das der letzte Aufruf von next() // geliefert hat. (Pro next()-Aufruf ist nur ein // remove()-Aufruf zugelassen.) // Achtung: unspezifiziertes Verhalten, wenn die zugrunde // liegende Datenstruktur auf andere Weise modifiziert // wird void remove(); □ Es ist eine Art fortschaltbarer Zeiger, der immer das nächste Element der Datenstruktur liefert: next() Element 0 next() Element 1 next() Element 2 ... Element n next() Iterator für Reihung: implementierungsunabhängiges Fortschreiten Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-15 © Michael Philippsen } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-16 4 13.1 Generische Listen und Iteratoren 13.1 Generische Listen und Iteratoren Anmerkungen zu den folgenden Code-Beispielen: Beispiel für die Anwendung eines Iterators: □ Unsere Implementierung orientiert sich weitgehend an den Klassen aus java.util.* □ Einige Sonderfälle und selten genutzte Funktionen werden nicht betrachtet. □ Ausnahme-Behandlung wird weitgehend ausgeklammert. Vorteil: □ Kenntnis der Java-Klassenbibliothek wird vertieft. □ Späterer Praxiseinsatz problemlos möglich. public class Bla { void do() { ... }; } die Klasse LinkedList<E> wird später erklärt public class Test { LinkedList<Bla> list; ... Bla e; for (Iterator<Bla> iter = list.iterator(); iter.hasNext(); e = iter.next()){ e.do(); } } Kürzere Schreibweise der Schleife: Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-17 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-18 13.2 Verkettete Liste 13.2 Verkettete Liste Die Implementierung basiert auf dem Prinzip der CodeWiederverwendung und Spezialisierung durch Unterklassen: Wurzel der gesamten Objekthierarchie. Verkettete Liste java.lang.Object Veranschaulichung: header java.util.Collection<E> for (Bla e : list) { e.do(); } java.util. AbstractCollection<E> Index 0 Index 1 Index size-1 Index 2 null ... element next element next element next element next element next Schnittstellen java.util.List<E> Eine grobe Listenimplementierung, bei der die Zugriffsmethoden über den Iterator implementiert sind. Listenimplementierung als verkettete Liste. java.util.AbstractList<E> java.util. AbstractSequentialList<E> java.util.LinkedList<E> Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-19 © Michael Philippsen Klassen Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-20 5 13.2 Verkettete Liste 13.2 Verkettete Liste Die folgende Implementierung orientiert sich weitgehend an der Klasse java.util.LinkedList<E>: // // // // public class LinkedList<E> extends AbstractSequentialList<E> implements List<E> { Klasse für Einträge (innere Klasse von LinkedList<E>). Damit ist die generische Klasse E bekannt! Jeweils mit einem Elementverweis und einem Verweis auf das nachfolgende Element. private final class Entry { E element; Entry next; // Kopfelement erzeugen, Größe auf 0 private Entry header = new Entry(null, null); private int size = 0; // Konstruktor der Klasse public LinkedList() { // kein Typ-Parameter! header.next = header; } O(1) ... Entry(E elem, Entry next) { this.element = elem; this.next = next; } Leere Liste: Ein Wächterelement vereinfacht die Implementierung der Listenoperationen. header } null element next Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-21 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-22 13.2 Verkettete Liste 13.2 Verkettete Liste // Hilfsfkt. zum Einfügen von elem nach Eintrag e // Hilfsmethode, die den Eintrag an der Stelle O(1) private Entry addAfter(E elem, Entry e) { O(n) // index liefert assert (elem!=null) && (e!=null); Entry newEntry = new Entry(elem, e.next); e.next = newEntry; size++; return newEntry; private Entry entry(int index) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException(); Entry e = header; for (int i=0; i <= index; i++) { e = e.next; } return e; } } Entry e Entry x element next element next Entry e element next Entry x header element next null element next Entry e Entry x element next element next Entry newEntry element next Entry newEntry element next Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-23 © Michael Philippsen Index 0 Index 1 Index size-1 Index 2 ... element next element next element next element next O(n) // Liefert das Element an der Stelle index public E get(int index) { return entry(index).element; O(n) } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-24 6 13.2 Verkettete Liste 13.2 Verkettete Liste // Ein Element am Anfang in die Liste einfügen public boolean add(E elem) { addAfter(elem, header); O(1) return true; } O(1) // Element elem an der Stelle index einfügen public void add(int index, E elem) { O(n) addAfter(elem, (index == 0 ? header : entry(index 1))); O(1) } O(n) index gibt an, wie viele Schritte man braucht, um das Element zu finden, nach dem einzufügen ist. Im schlimmsten Fall sind dies n. Daher O(n). // Überprüft, ob Objekt o enthalten ist O(n) public boolean contains(Object o) { // o == null wird nicht behandelt for (Entry e = header.next; e != header; e = e.next) { if (o.equals(e.element)) return true; } return false; } Man spricht von sequentieller Suche, weil die Liste Element für Element durchlaufen wird. Man kann schneller • Bester Fall: O(1) suchen. Liste ist daher • Schlechtester Fall: O(n) nicht die ideale • Durchschnitt (erfolgreich): O(n/2) Datenstruktur, wenn • Durchschnitt (erfolglos): O(n) viel gesucht wird. (boolean-Return-wert von add wird bei Mengen verständlich.) Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-25 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-26 13.2 Verkettete Liste 13.2 Verkettete Liste // Hilfsmethode zum Entfernen eines Listenelements private void remove(Entry e) { Entry p; for (p = header; p.next != e; p = p.next) { } p.next = e.next; size--; } Entry y Entry x element next element next O(n) Beachte: selbst wenn man schon die Referenz auf das zu löschende Objekt hat, muss man die Liste erneut durchlaufen, um den Vorgänger zu korrigieren. Entry e Entry y Entry x element next element next element next Entry e // Entfernt das Element an der Stelle index public E remove(int index) { Entry e = entry(index); O(n) remove(e); O(n) return e.element; } // Entfernt das erste Vorkommen des Objekts o public boolean remove(Object o) { ... } O(n) O(n) Einfügen, Löschen und Suchen sind langsam, weil es nicht möglich ist, gezielt auf ein Listenelement zuzugreifen. Die Liste muss stets linear durchlaufen werden. Der Aufwand ist also im schlimmsten Fall in O(n). element next Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-27 © Michael Philippsen Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-28 7 13.2 Verkettete Liste // Liefert die Anzahl der Elemente in der Liste public int size() { return size; } // Überprüft, ob Liste leer ist public boolean isEmpty() { return size==0; } // Alle Elemente aus der Liste entfernen public void clear() { header.next = header; size = 0; } // Konkrete Implementierung des Iterators in // LinkedList. private class ListItr implements Iterator<E> { ... „Innere Klasse“ } // Liefert einen Iterator für die Liste public Iterator<E> iterator() { return new ListItr(); } ... O(1) O(1) O(1) 13.2 Verkettete Liste Die gezeigte Implementierung nutzte ein Wächterelement header. □ Die leere Liste besteht aus diesem Wächterelement. □ Das letzte Listenelement verweist auf das header-Element, wodurch die Liste zu einem geschlossenen Zyklus wird. Übung: □ Implementieren Sie eine Liste ohne ein solches Wächterelement. □ Überzeugen Sie sich davon, dass die Behandlung der Sonderfälle leere Liste Einfügen am Listenende Löschen am Listenende … aufwändiger zu implementieren ist. } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-29 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-30 13.3 Exkurs: Geschachtelte Klassen in Java 13.3 Exkurs: Geschachtelte Klassen in Java In Java kann man neben Variablen und Methoden auch Klassen als Bestandteil anderer Klassen definieren: Innere Klassen in Java class EnclosingClass { ... class ANestedClass { ... } } Enthaltene Klassen können ebenso unbehindert auf andere Bestandteile der enthaltenden Klasse zugreifen wie Methoden. Genau wie Methoden können auch Klassen statische Bestandteile der enthaltenden Klasse sein. Genau wie statische Methoden können statische enthaltene Klassen nicht auf Instanzvariablen der enthaltenden Klasse zugreifen. Eine enthaltene Klasse heißt innere Klasse, wenn sie nicht statisch ist. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-31 © Michael Philippsen Eine innere Klasse I ist zur Laufzeit assoziiert: □ mit einer Instanz der inneren Klasse (explizit: I.this) □ mit einer Instanz der sie umgebenden Klasse O (O.this) Der Code einer inneren Klasse kann transparent (unmittelbar) auf die Instanzvariablen (und Methoden) der Instanz der umgebenden Klasse(n) zugreifen. Instanz der enthaltenden Klasse Instanz der inneren Klasse (mehrere möglich) Der Benutzer eines Objekts einer inneren Klasse kann dadurch indirekt auf den Zustand des umgebenden Objekts zugreifen, ohne dessen Implementierung zu kennen. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-32 8 13.3 Exkurs: Geschachtelte Klassen in Java 13.3 Exkurs: Geschachtelte Klassen in Java Iterator einer verketteten Liste Trennung statischer und nicht-statischer Kontexte Instanzvariablen private class ListItr implements Iterator<E> { int currentIdx = 0; des IteratorEntry currentElement = header; // initial Objekts speipublic boolean hasNext() { chern Zustand. return (currentIdx < size); Mehrere Itera} toren möglich. public E next() { currentElement = currentElement.next; if (currentIdx < size) { Iterator-Code nutzt currentIdx++; Instanzvariablen d. return currentElement.element; } else { umschließenden return null; Klasse. } } public void remove() { ... } } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-33 Geschachtelte Klassen aus statischen Kontexten können nicht auf Instanzvariablen der sie enthaltenden Klasse zugreifen. Innere Klassen können auch in einem Block deklariert werden (die Namen sind ab dem Deklarationspunkt sichtbar). □ Solche innere Klassen können auf die lokalen Variablen, formalen Parameter, … des deklarierenden Blocks zugreifen, wenn diese als final deklariert wurden (und bereits initialisiert sind). Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-34 13.3 Exkurs: Geschachtelte Klassen in Java 13.3 Exkurs: Geschachtelte Klassen in Java Kontext-Trennung am Beispiel Anonyme (innere) Klassen in Java statischer Kontext class Outer { int i = 100; keine „innere Klasse“, static void classMethod() { sondern „enthaltene Klasse“ final int l = 200; class LocalInStaticContext { int k = i; // Fehler (i nicht statisch) int m = l; // ok (l final und initialisiert) } } nicht-statischer Kontext void foo() { int l = 200; Innere Klasse class Local { im Block int k = i; // ok (i Instanzvariable) int m = l; // Fehler (l außen nicht final) } } } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-35 © Michael Philippsen Anstatt eine innere Klasse zu definieren, dieser Klasse damit einen Bezeichner zu geben, und dann Objekte dieses Typs per „new“-Operator zu erzeugen, kann man auf die Namensvergabe verzichten: new BaseClassOrInterface() {... class-def ...} Sinnvoll, wenn Objekte der inneren Klasse nur an einer Stelle erzeugt werden und ein Bezeichner eigentlich nicht nötig ist. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-36 9 13.3 Exkurs: Geschachtelte Klassen in Java 13.4 Menge ohne Sortierung Statt expliziter Klasse ListItr hier anonyme Klasse, die public Iterator<E> iterator() { Iterator<E> implementiert. return new Iterator<E>() { Das return gibt ein Objekt int currentIdx = 0; dieser Klasse zurück. Entry currentElement = header; public boolean hasNext() { return (currentIdx < size); } public E next() { currentElement = currentElement.next; if (currentIdx < size) { currentIdx++; return currentElement.element; } else { return null; } } public void remove() { ... } }; Jetzt allgemeiner Fall: Menge von Elementen □ Duplikate sind nicht mehr erlaubt. □ Keine feste Reihenfolge der Elemente. Wie in der mathematischen Mengenlehre können … □ einzelne Werte hinzugefügt oder entnommen werden, □ Schnitt-, Vereinigungs- oder Subtraktionsmengen gebildet werden oder □ Mengen bzw. Elemente miteinander verglichen werden. Beliebter Fehler: return-Anweisung muss mit „;“ abgeschlossen werden. } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-37 13.4 Menge ohne Sortierung Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-38 13.4 Menge ohne Sortierung ADT Menge public interface Set<E> extends Collection<E> { Signatur Menge(T), für T muss Gleichheitsoperator definiert sein create: add: isIn: del: empty: T x Menge T x Menge T x Menge Menge single: union: intersect: diff: equals: containsAll: T Æ Menge Menge x Menge Æ Menge Menge x Menge Æ Menge Menge x Menge Æ Menge Menge x Menge Æ Boolean Menge x Menge Æ Boolean Æ Menge Æ Menge Æ Boolean Æ Menge Æ Boolean Namen in folgendem Code add contains remove isEmpty Übung: Axiome erstellen. addAll retainAll removeAll equals containsAll Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-39 © Michael Philippsen // Liefert die Kardinalität der Menge int size(); // Überprüft, ob die Menge leer ist boolean isEmpty(); // Überprüft, ob die Menge das Objekt o enthält boolean contains(Object o); // Liefert einen Iterator für die Menge Iterator<E> iterator(); // Element elem zur Menge hinzufügen boolean add(E elem); true, wenn elem wirklich eingefügt wurde und nicht schon vorhanden war. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-40 10 13.4 Menge ohne Sortierung 13.4 Menge ohne Sortierung // Entfernt das Objekt o aus der Menge boolean remove(Object o); Beliebiger Parametertyp möglich. // Behält die Elemente der Menge, die auch in c // enthalten sind (entspricht Schnittmenge) boolean retainAll(Collection<?> c); // Überprüft, ob die Menge alle Elemente aus c enthält // (entspricht Teilmengenbeziehung) boolean containsAll(Collection<?> c); // Fügt alle Elemente von c zur Menge hinzu // (entspricht Vereinigung der Mengen) boolean addAll(Collection<? extends E> c); Nur E und Untertypen von E möglich. // Leert die Menge void clear(); // Überprüft die Gleichheit zweier Mengen boolean equals(Object o); } // Entfernt alle Elemente von c aus der Menge, sofern // enthalten (entspricht Differenz der Mengen) boolean removeAll(Collection<?> c); Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-41 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-42 13.4 Menge ohne Sortierung 13.4 Menge ohne Sortierung Implementierung von Set<E> mithilfe von LinkedList<E> Implementierung von Set<E> mithilfe von LinkedList<E> universell einsetzbar für alle Mengenklassen. besonders flexibel für Mengen unbeschränkter Größe. Beachte: Eingabedaten Set<E> Transformation Eingabedaten Ausgabedaten Transformation LinkedList <E> Ausgabedaten Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-43 © Michael Philippsen Einfügen eines Elements in die Liste hatte den Aufwand O(1). Einfügen eines Elements in eine Menge, die mit einer Liste implementiert wird, ist aufwändiger, weil beim Einfügen überprüft werden muss, ob das Element schon in der Menge (der Liste) ist. Das erfordert einen sequentiellen Listendurchlauf Æ O(n). Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-44 11 13.4 Menge ohne Sortierung 13.4 Menge ohne Sortierung // Liefert einen Iterator für die Menge public Iterator<E> iterator() { return list.iterator(); } public class SetAsList<E> extends AbstractSet<E> implements Set<E> { // Zugrunde liegende Liste protected List<E> list; // Leere Liste wird erzeugt public SetAsList() { list = new LinkedList<E>(); } // Liefert die Kardinalität der Menge public int size() { O(1) return list.size(); } O(1) // Überprüft, ob die Menge leer ist public boolean isEmpty() { O(1) return list.isEmpty(); } O(1) Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-45 // Fügt alle Elemente von c zur Menge hinzu // (Vereinigungsmenge) public boolean addAll(Collection<? extends E> c) { Iterator<? extends E> i = c.iterator(); boolean setChanged = false; while (i.hasNext()) { n-mal setChanged |= add(i.next()); O(n) } return setChanged; } O(n) // Fügt das angegebene Element zur Menge hinzu public boolean add(E elem) { if (contains(elem)) O(n) return false; O(1) return list.add(elem); } O(n) // Entfernt Objekt o aus der Menge public boolean remove(Object o) { return list.remove(o); O(n) } O(n) 13.4 Menge ohne Sortierung O(n2) n-mal O(n2) Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-47 © Michael Philippsen // Überprüft, ob die Menge das Objekt o enthält public boolean contains(Object o) { return list.contains(o); O(n) } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-46 13.4 Menge ohne Sortierung // Überprüft, ob die Menge alle Elemente aus c // enthält (Teilmengenbeziehung) public boolean containsAll(Collection<?> c) { Iterator<?> i = c.iterator(); while (i.hasNext()) { if (!contains(i.next())) return false; O(n) } return true; } O(1) // Entfernt alle Elemente von c aus der Menge (Differenz) public boolean removeAll(Collection<?> c) { O(n2) Iterator<?> i = c.iterator(); boolean setChanged = false; while (i.hasNext()) { setChanged |= remove(i.next()); n-mal O(n) } return setChanged; } Mögliche alternative Formulierung der Schleife (seit Java 1.5): for (Object o : c) { setChanged |= remove(o); } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-48 12 13.4 Menge ohne Sortierung 13.4 Menge ohne Sortierung // Behält die Elemente der Menge, die auch in c // enthalten sind - Schnitt public boolean retainAll(Collection<?> c) { Iterator<?> i = iterator(); boolean setChanged = false; while (i.hasNext()) { E a = i.next(); if (!c.contains(a)) { O(n) n-mal i.remove(); O(n) setChanged = true; } } return setChanged; } // Leert die Menge public void clear(){ list.clear(); } O(1) // Überprüft die Gleichheit zweier Mengen public boolean equals(Object o) { if (o == this) return true; 2 if (!(o instanceof Set)) return false; O(n ) Collection c = (Collection) o; if (c.size() != size()) return false; Prinzip: Preiswerte Tests return containsAll(c); zuerst durchführen. } O(n2) } // Zur Aufwandsanalyse betrachten wir nur: public boolean retainAll(SetAsList<E> c) { ... } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-49 13.4 Mengen ohne Sortierung O(1) O(1) Um Enthaltensein zu prüfen, muss man die ganze Liste durchlaufen. boolean contains(Object o) boolean add(E elem) O(n) 1 Element: O(n) n Elemente: O(n2) boolean remove(Object o) O(n) Verkettete Liste Für weitere Set-Methoden: hatte nur O(1) O(n²) boolean equals(Object o) bzw. O(n). boolean addAll(Collection<? extends E> c) O(n²) O(n²) boolean retainAll(SetAsList<E> c) O(n²) boolean removeAll(Collection<?> c) Aufwand Für jedes Element von c muss this komplett durchlaufen werden, da keine Duplikate erlaubt sind reduzierbar? Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-51 © Michael Philippsen Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-50 13.5 Menge mit Sortierung Aufwände für SetAsList<E> boolean isEmpty () int size() Einseitiger Test reicht bei Mengen, da es keine Duplikate gibt. Wenn die Elemente in der Menge aufsteigend sortiert sind, … … dann muss man nicht die ganze Liste durchlaufen, um nach Duplikaten zu suchen. □ contains endet im Durchschnitt schneller □ add endet im Durchschnitt schneller (weil contains benutzt wird) □ remove endet im Durchschnitt schneller … dann sind die Mengenoperationen schneller (Aufwand in O(n) statt in O(n2)), weil ein Reißverschlussverfahren verwendet werden kann. Beispiel: addAll 3 7 10 3 7 10 3 7 10 2 4 5 2 4 5 2 4 5 Ergebnis: 2 2 3 O(n) 2 3 4 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-52 13 13.5 Menge mit Sortierung 13.5 Menge mit Sortierung Sortieren Klassen von Sortierverfahren □ Internes Sortierverfahren: zum Sortieren von Datensätzen im Hauptspeicher. Es wird direkter Zugriff auf alle Elemente benötigt. □ Externes Verfahren: Sortieren von Massendaten, die auf externen Speichermedien gehalten werden. Der Zugriff ist auf einen Ausschnitt der Datenelemente beschränkt. Gegeben: Grundmenge U, totale Ordnung ≤ hierauf, Mehrfachmenge M mit Elementen e ∈ U. Gesucht: Anordnung der Elemente aus M gemäß ≤. Formulierung: □ Gegeben: Liste M = [e0, e1,..., en-1]. □ Gesucht: Liste L = [ej0, ej1, ... , ejn-1] mit Nachbedingung ej0 ≤ ej1 ≤ ... ≤ ejn-1 ∧ perm(M, L). Eine Sortierung heißt stabil, wenn gleiche Werte ihre relative Reihenfolge nicht ändern. Formal: Wenn k<j und ek = ej dann ik=perm(k) < perm(j)=jk und eik = ejk Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-53 Kosten-/Nutzen-Analyse: □ Sei m: Zahl der Suchvorgänge über die Lebensdauer der Menge □ Sortierung ist vorteilhaft, falls Ts + m⋅T’ < m⋅T Sortieraufwand, mit Ts: T’: Suchaufwand sortiert, T: Suchaufwand unsortiert. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-54 13.5 Menge mit Sortierung 13.5 Menge mit Sortierung Objektvergleiche zur Sortierung in Java addAll mit Reißverschlussprinzip Das Interface java.lang.Comparable<T> definiert eine einzige Methode: public int compareTo(T o); Alle Implementierungen dieser Schnittstelle sollten Hinweise: Wir verwenden eine LinkedList<E extends Comparable<E>>. addAll erzeugt nicht eine neue Liste, sondern fügt c zu this hinzu. □ einen negativen Wert zurückgeben, wenn gemäß der gegebenen Ordnung dieses Objekt „kleiner“ als das übergebene Objekt ist: this < o □ 0 zurückgeben, wenn die beiden Objekte „gleich groß“ sind (oder die relative Sortierung gleichgültig ist): this = o □ einen positiven Wert zurückgeben, wenn dieses Objekt „größer“ als das übergebene Objekt ist: this > o Wir gehen im Folgenden davon aus, dass Elemente von sortierten Listen dieses Interface implementieren. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-55 © Michael Philippsen public boolean addAll(LinkedList<E> c) { boolean setChanged = false; Die Liste c wird mithilfe einer for-Schleife/Iterator abgelaufen. Die Liste this wird elementweise durchlaufen, bis der header wieder erreicht ist. Ein Schleppzeiger zeigt auf das „vorletzte“ Element von this. Nach diesem wird mithilfe von addAfter das nächste Element von c eingefügt. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-56 14 13.5 Menge mit Sortierung 13.5 Menge mit Sortierung Entry currentElement = header.next; //initial Entry schlepp = header; Konstruktion zum Durchlaufen der Liste for (E cElem : c) { while (currentElement != header && currentElement.element.compareTo(cElem) <= 0) { schlepp = currentElement; currentElement = currentElement.next; } schlepp = addAfter(cElem, schlepp); bedingt Stabilität setChanged = true; } return setChanged; Sortieren verketteter Listen (Abschnitte 13.6 – 13.10) Betrachtung von Sortierverfahren, für die sich die verkettete Liste als Datenstruktur besonders eignet. Dies ist der Fall, wenn das Verfahren die Elemente der Liste entlang ihrer Anordnung aufgreifen will. } Jedes Listenelement von this und c wird einmal angefasst. Daher Aufwand O(n). Aber Aufwand des Sortierens? Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-57 13.6 Sortieren durch Auswählen Sortieren durch Auswählen („selection sort“) Grundidee: aufsteigend sortieren: Lösche nacheinander die Maxima aus einer Liste M und füge sie vorne an eine anfangs leere Ergebnisliste L an. absteigend sortieren: Lösche nacheinander die Minima aus einer Liste und füge sie vorne an eine anfangs leere Ergebnisliste an. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-58 13.6 Sortieren durch Auswählen Beispiel 10 L 10 © Michael Philippsen 2 3 5 8 1 7 5 8 1 4 7 3 2 5 1 4 7 3 2 5 1 4 3 2 1 4 3 2 1 2 1 10 7 8 10 5 7 8 10 4 5 7 8 10 3 4 5 7 8 10 2 3 4 5 7 8 2 4 2 8 1 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-59 3 3 4 5 7 10 1 8 10 M Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-60 15 13.6 Sortieren durch Auswählen 13.6 Sortieren durch Auswählen Imperative Lösung Äußere Methode Problemlösung durch Induktion: □ Induktionsanfang: Leere Liste L und einelementige Liste L sind sortiert. □ Induktionsschritt: Sei (n-1)-elementige Liste L sortiert. Nächstes Element aus M ist kleiner als alle Elemente in L und wird als erstes Element in L eingefügt. Daher ist n-elementige Liste L sortiert. □ Induktionsende: Wenn M leer, befinden sich alle Elemente in L. Formulierung als rekursive Methode der Klasse LinkedList<E>. Wir wollen nicht mit jedem Rekursionsschritt eine neue (Teil-) Liste erstellen, daher brauchen wir eine äußere Methode zur Einbettung der rekursiven Methode, in der insbesondere die Liste L vereinbart wird. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-61 13.6 Sortieren durch Auswählen Innere Methode (überladene Methode selsortRec) LinkedList<E> selsortRec(LinkedList<E> M, LinkedList<E> L) { if (!M.isEmpty()) { Übung: Implementieren Sie die E m = M.maximum(); Methode maximum(). M.remove(m); L.add(m); return selsortRec(M, L); //{I: ∀ v∈M, v'∈L: v≤v' ∧ M∪L = M0 ∧ L sortiert} } else return L; } public LinkedList<E> selsortRec() { // Kopie der Ausgangsliste M0 bezeichnet die LinkedList<E> M = new LinkedList<E>(); zu sortierende Liste M.addAll(this); // Erzeuge leere Ergebnisliste L LinkedList<E> L = new LinkedList<E>(); //{P: ∀ v∈M, v'∈L: v≤v' ∧ M∪L = M0 ∧ L sortiert} // Aufruf der Rekursion return selsortRec(M, L); //{Q: ∀ v∈M, v'∈L: v≤v' ∧ M∪L = M0 ∧ L sortiert ∧ M leer} } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-62 13.6 Sortieren durch Auswählen Wiederholung: Konstruktion einer iterativen Lösung Umformulierung der rekursiven Formulierung in folgendes Schema Bedingung Initialisierung T' p(T x) { A; if (B) { C; x = f(x); return p(x); } else D; } Abschluss (Schleifen)invariante Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-63 © Michael Philippsen ¬b P Auf dem Weg zur Schleifeninvariante Veränderung der Rekursionsvariablen x Rekursion Diese Form der Rekursion heißt Rechtsrekursion. Von geringerer Bedeutung sind Linksrekursionen, bei denen der rekursive Aufruf vor dem C-Teil liegt. Diese lassen sich nicht so leicht in eine iterative Form überführen. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-64 16 13.6 Sortieren durch Auswählen 13.6 Sortieren durch Auswählen Wiederholung: Konstruktion einer iterativen Lösung Innere Methode ist Rechtsrekursion Rekursive Version: T' p(T x) { A; if (B) { C; x = f(x); return p(x); } else D; } Äquivalente iterative Version: T' p(T x) { A; while (B) { C; x = f(x); A } D; } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-65 Init. A leer Bedingung B LinkedList<E> selsortRec(LinkedList<E> M, LinkedList<E> L) { if (!M.isEmpty()) { C ist hier leer! E m = M.maximum(); M.remove(m); Anweisung mit Veränderung L.add(m); return selsortRec(M, L); der Rekursionsvariablen M,L Rekursion//{I: ∀ v∈M, v'∈L: v≤v' ∧ M∪L = M0 ∧ L sortiert} } else return L; } Abschluss D Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-66 13.6 Sortieren durch Auswählen 13.6 Sortieren durch Auswählen Innere Methode umgewandelt in Iteration Einbettung der iterativen Lösung LinkedList<E> selsortIt(LinkedList<E> M, LinkedList<E> L) { //{P: ∀ v∈M, v'∈L: v≤v' ∧ M∪L = M0 ∧ L sortiert} while (!M.isEmpty()) { E m = M.maximum(); M.remove(m); L.add(m); } //{Q: ∀ v∈M, v'∈L: v≤v' ∧ M∪L = M0 ∧ L sortiert ∧ M leer} return L; } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-67 © Michael Philippsen public LinkedList<E> selsort() { // Kopie der Ausgangsliste this LinkedList<E> M = new LinkedList<E>(); „Außenmethode“ M.addAll(this); // Erzeuge leere Ergebnisliste L LinkedList<E> L = new LinkedList<E>(); //{P: ∀ v∈M, v'∈L: v≤v' ∧ M∪L = M0 ∧ L sortiert} while (!M.isEmpty()) { statt MethodenE m = M.maximum(); aufruf: Rumpf M.remove(m); von selsortIt L.add(m); offen eingebaut } //{Q: ∀ v∈M, v'∈L: v≤v' ∧ M∪L = M0 ∧ L sortiert ∧ M leer} return L; } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-68 17 13.6 Sortieren durch Auswählen public LinkedList<E> selsort() { LinkedList<E> M = new LinkedList<E>(); M.addAll(this); LinkedList<E> L = new LinkedList<E>(); //{P: ∀ v∈M,v'∈L: v≤v' ∧ M∪L=M0 // ∧ L sortiert} while (!M.isEmpty()) { E m = M.maximum(); M.remove(m); L.add(m); } //{Q: ∀ v∈M,v'∈L: v≤v' ∧ M∪L=M0 // ∧ L sortiert ∧ M leer} return L; } 13.6 Sortieren durch Auswählen selsort ist korrekt Q=I ∧ M leer I gilt für die leere Liste L. Bei Verlassen der Schleife gilt I mit M = Ø: L = M0 ∧ L sortiert (”=” steht für Gleichheit von Mehrfachmengen). I ist Schleifeninvariante: Wiederherstellen von I durch remove() und add() Terminierung, da M monoton verkürzt wird. selsort ist stabil Aufwandsabschätzung sofern maximum() das in der Reihenfolge von M letzte Maximum nimmt und remove() auch dieses Element löscht (das ist in unserer Beispielimplementierung nicht der Fall, dort wird das erste genommen!). 13.7 Sortieren durch Einfügen Sortieren durch Einfügen („insertion sort“) Grundidee: Beispiel Angenommen wir können n-1 Werte sortieren. Dann können wir den n-ten Wert einsortieren, indem wir seinen Platz in der sortierten Liste finden und die restlichen Elemente nach hinten verschieben. Nimm das nächste Element aus der Menge M und füge es an der richtigen Stelle in die (anfangs leere) Menge L ein. L 10 © Michael Philippsen 3 10 2 3 5 8 1 7 5 8 1 4 7 2 5 8 1 4 7 5 8 1 4 7 8 1 4 7 1 4 7 4 7 10 2 3 10 2 3 5 10 2 3 5 8 10 1 2 3 5 8 10 1 2 3 4 5 8 2 4 2 3 1 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-71 durch Merken der Maximum-Position: O(1) Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-70 13.7 Sortieren durch Einfügen □ Starte mit der ersten Karte einen neuen Stapel. □ Nimm jeweils die nächste Karte des Originalstapels und füge diese an der richtigen Stelle in den neuen Stapel ein. Idee: Nutze Halde, um Maximum schnell zu entfernen. Später mehr! Schlecht! Lohnt sich nur, wenn über die Lebensdauer oft gesucht wird. Daher Tselsort = Θ(n2). Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-69 Typisches Verfahren beim Sortieren von Spielkarten: im Mittel sind in M n/2 Elemente public LinkedList<E> selsort() { LinkedList<E> M = new LinkedList<E>(); M.addAll(this); LinkedList<E> L = new LinkedList<E>(); //{P: ∀ v∈M,v'∈L: v≤v' ∧ M∪L=M0 // ∧ L sortiert} while (!M.isEmpty()) { O(1) int m = M.maximum(); n-mal O(n) M.remove(m); O(n) L.add(m); O(1) } //{Q: ∀ v∈M,v'∈L: v≤v' ∧ M∪L=M0 // ∧ L sortiert ∧ M leer} return L; } 3 4 5 7 10 7 8 10 M Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-72 18 13.7 Sortieren durch Einfügen 13.7 Sortieren durch Einfügen Pseudocode mit Einbettung der iterativen Lösung Aufwandsabschätzung konstanter Aufwand c0 public LinkedList<E> insort() { LinkedList<E> M = new LinkedList<E>(); M.addAll(this); Schleifeninvariante LinkedList<E> L = new LinkedList<E>(); //{P: M∪L = M0 ∧ L sortiert ∧ L leer } while (!M.isEmpty()) { E m = M.get(0); M.remove(m); //suche in L m', m'' aufeinanderfolgend, //m' ≤ m < m''; //füge in L m zwischen m', m'' ein Ergänzung von } LinkedList<E> //{Q: M∪L = M0 ∧ L sortiert ∧ M leer } um weitere return L; Operatoren } while (!M.isEmpty()) { E m = M.get(0); M.remove(m); Sequentielle Suche: //suche in L m', m'' aufeinanderfolgend, mittlerer Aufwand O(k/2) //m' ≤ m < m''; schlimmster Fall O(k) //füge in L m zwischen m', m'' ein wenn k Länge von L ist. } Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-73 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-74 erforderlich! Damit Gesamtaufwand über alle Durchläufe der äußeren Schleife: n-1 n-1 k=1 k=1 Tinsort(n) = Σ O(k) ≤ Σ c·k = c·n(n-1)/2 0 O(n2) Günstigster Aufwand, falls M bereits entgegengesetzt geordnet ist. Dann verursacht die innere Schleife einen konstanten Aufwand c1. Dann ist der Gesamtaufwand im besten Fall in O(n). 13.8 Fächersortierung („bucket sort“) 13.8 Fächersortierung („bucket sort“) Poststellen arbeiten häufig mit Fächersortierung. Stabil, wenn die Briefe in einem Postfach „gestapelt“ werden, wenn also zur Implementierung des Fachs eine Schlange verwendet wird. □ Pro Person wird ein Postfach angelegt, insgesamt m Fächer, also endliche Zahl möglicher sogenannter Sortierschlüssel. □ Jedes Element der eingehenden Post (Menge mit n Elementen) wird in das Fach des Adressaten gelegt. □ Also ist eine bijektive Funktion nötig, die jedem Sortierschlüssel eine Fachnummer zuordnet. Aufwand in O(n + m) □ Jeder der n Briefe ist anzufassen, seine Fachnummer ist zu bestimmen, dann ist er in dieses Fach zu legen. □ Am Schluss müssen die Fächer geleert werden. Bei m 0 O(n) hat dieses Sortierverfahren linearen Aufwand! Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-75 © Michael Philippsen Sortieren einer Reihung erfolgt analog zum „Counting Sort“: □ Erst zählen, wie viele Daten in jedes Fach fallen (Histogramm). □ Dann aus diesen Daten pro möglichem Sortierschlüssel einen Offset in der Ziel-Reihung berechnen. □ Dann elementweise einsortieren und dabei den zum Sortierschlüssel gehörenden Offset erhöhen. (Das entspricht dem Schlangenverhalten.) Leider nicht praktikabel bei großem m oder wenn m>>n, wenn es also viel mehr mögliche Werte/Sortierschlüssel gibt, als in der zu sortierenden Menge tatsächlich vorkommen. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-76 19 13.9 Radix-Sortierung Radix-Sortierung (Verallg. der Fächersortierung) Statt für jeden infrage kommenden Schlüssel ein Fach anzulegen, wird der Schlüssel in Segmente aufgeteilt. Pro Schlüsselteil gibt es ein Fach. □ Z.B. die einzelnen Ziffern der Postleitzahl, die Buchstaben eines Worts fester Länge, ... 13.9 Radix-Sortierung Radix-Sortierung (Segmente v.r.n.l.) am Beispiel: 503 087 512 061 908 170 897 275 653 426 154 509 612 677 765 703 170 061 512 612 503 653 703 154 275 765 426 087 897 677 908 509 Fach für letzte Stelle = 3 Die Elemente der Menge werden gemäß ihres Schlüsselteils per Fächersortierung sortiert. □ Wesentlich weniger Fächer (10, 26, ...) Für die Sortierung nach den restlichen Schlüsselsegmenten wird die Fächersortierung rekursiv angewendet. Achtung: Segmentbetrachtung von rechts nach links! 503 703 908 509 512 612 426 653 154 061 765 170 275 677 087 897 061 087 154 170 275 426 503 509 512 612 653 677 703 765 897 908 Beachte: • Nach jeder Partitionierung werden die Daten wieder gesammelt. • Essentiell ist die Stabilität des Sortierens. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-77 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-78 13.9 Radix-Sortierung 13.9 Radix-Sortierung Herleitung durch Induktion Aufwandsabschätzung Induktionshypothese: Schlüssel mit weniger als k Segmenten, die jeweils maximal d mögliche Werte einnehmen können, können sortiert werden. Induktionsanfang: 1 Segment/vorderstes Segment: Fächersortierung mit d Fächern erzeugt korrekte Sortierung. Induktionsschluss: Pro Segment der Radix-Sortierung werden alle n Werte untersucht. Bei k Segmenten ergibt sich ein Gesamtaufwand von O(n·k). □ 1. Fall: zwei Elemente unterscheiden sich im k-ten Segment. Dann wird der k-te Sortierschritt nach Induktionsvoraussetzung zur korrekten Sortierung der Elemente führen. □ 2. Fall: zwei Elemente sind im k-ten Segment gleich. Dann sind sie nach Induktionsvoraussetzung in den k-1 Segmenten rechts davon korrekt sortiert. Ein stabiles Sortierverfahren behält die relative Ordnung der beiden Elemente bei. Damit sind sie auch noch korrekt sortiert, wenn nach dem k-ten Segment sortiert worden ist. Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-79 © Michael Philippsen Bei Betrachtung der Binärdarstellung gibt es k=log2n Segmente (die jeweils 2 Werte 0 oder 1 haben können). Damit ist der Gesamtaufwand in O(n·log2n) □ Das ist besser als der quadratische Aufwand der bisher gezeigten Sortierverfahren. (Verbesserungsmöglichkeit: Betrachtung von Segmenten, die mehr als 1 Bit umfassen.) □ Allerdings ist vorausgesetzt, dass ein Sortierschlüssel in Segmente aufgeteilt werden kann, also eine feste Struktur hat. □ java.lang.Comparable<T> ist allgemeiner und setzt eine beliebige totale Ordnung voraus (lexikografisch (Stichwort Umlaute), Gleitpunktzahlen float und double, Graphen, Farben, …) Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-80 20 13.10 Sortieren durch Mischen („merge sort“) 13.10 Sortieren durch Mischen („merge sort“) Sortieren durch Mischen („merge sort“) Grundprinzip 1 Rekursion: Grundidee: Teile-und-Herrsche Wir können eine Liste der Länge n=2m, m>0, mit geringem Aufwand Θ(n/2) in zwei gleich große Teillisten der Länge n/2=2m-1 zerlegen und mit gleichem Aufwand die Ergebnisse zusammenfügen (Reißverschluss). Leere oder einelementige Mengen sind sortiert. Dann gibt es statt (n-1) Rekursionen nur noch m = log2n n Lösung 1. Teil Gesamtlösung Mischen: 5 4 2 10 7 3 takeAndDrop 3 3 2 5 2 5 8 1 4 8 1 7 4 7 10 3 2 5 8 1 4 7 10 3 2 5 8 1 4 7 3 10 2 5 1 8 4 7 3 10 2 5 1 8 4 7 3 5 10 2 1 2 3 4 1 5 4 7 7 8 5 4 10 7 5 4 ≤ ≤ 2 3 2 … 13.10 Sortieren durch Mischen („merge sort“) Beispiel 10 10 7 3 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-82 13.10 Sortieren durch Mischen („merge sort“) 10 Lösung 2. Teil Rekursion ≤ Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-81 n Basisfälle protected LinkedList<E> mergesortRec(LinkedList<E> m) { if (m.size() < 2) return m; // Zerlege M in 2 ungefähr gleich lange Teillisten: // hier durch takeAndDrop, das die zweite Hälfte von M // in eine neue Liste packt und die erste Hälfte in M // belässt Zerlegungsphase LinkedList<E> o = takeAndDrop(m); // Sortiere die beiden Teillisten und füge sie zusammen: return merge(mergesortRec(m), mergesortRec(o)); } 8 10 public LinkedList<E> mergesort() { // Äußere Methode LinkedList<E> m = new LinkedList<E>(); m.addAll(this); return mergesortRec(m); } merge Mischphase Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-83 © Michael Philippsen Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-84 21 LinkedList<E> merge(LinkedList<E> l1, LinkedList<E> l2) { // Implementierung der Funktion addAll wie bei // Vereinigung von 2 sortierten Mengen (Abschn. 13.5) l1.addAll(l2); return l1; } 10 10 3 3 2 5 2 5 Pro Zeile 2k·n/2k = n Insgesamt 2·log2n Zeilen Aufwand in O(n·log2n) 8 1 4 8 1 7 4 7 1 21=2 4 2 2 5 8 1 4 7 2 10 3 2 5 8 1 4 7 3 23=8 1 3 10 2 5 1 8 4 7 3 23=8 1 3 10 2 5 1 8 4 7 2 22=4 2 3 5 10 1 21=2 4 0 20=1 8 2 1 2 3 4 1 5 4 7 7 8 8 10 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-86 Aufwandsschätzung (n sei die Länge der Liste) Wichtige Eigenschaften mergesort() ist stabil. © Michael Philippsen 8 3 13.10 Sortieren durch Mischen („merge sort“) Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-87 20=1 10 13.10 Sortieren durch Mischen („merge sort“) Wir benennen die Ebenen von oben kommend mit 0, 1, … Gesamtaufwand ist Summe der Aufrufe aller Knoten im Rekursionsbaum. Aufwand eines Knotens auf Ebene k ist jeweils in O(n/2k) für das Zerlegen und Mischen der sortierten Hälften (Länge ca. n/2k). Anzahl der Knoten/Listen auf Ebene k ist 2k, also in O(2k). Summe der Aufwände auf Ebene k ist demnach O(2k)·O(n/2k) = O(n), unabhängig von k. Anzahl der Ebenen ist in O(log2n). Also Gesamtaufwand im schlechtesten Fall: O(n·log2n), besser als alle allgemein verwendbaren Verfahren bisher! 0 22=4 Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-85 Rekursive Aufrufe lassen sich als Baum darstellen, dessen Knoten Aufrufe von mergesortRec repräsentieren. Länge n/2k Aufwand im Beispiel Listen 2k LinkedList<E> takeAndDrop(LinkedList<E> m) { // Belässt die zweite Hälfte der Elemente von M, // entfernt den Rest und liefert ihn zurück LinkedList<E> o = new LinkedList<E>(); Iterator<E> i = m.iterator(); int n = m.size(); for (int j = 1; j <= n/2; j++) { o.add(i.next()); i.remove(); Übung: vervollständigen Sie } obigen Iterator, damit remove und return o; next korrekt zusammenwirken. } 13.10 Sortieren durch Mischen („merge sort“) Ebene k 13.10 Sortieren durch Mischen („merge sort“) Schnellstes Verfahren auf verketteten Listen wegen der geringsten Zahl an Vergleichen. Sequentieller Zugriff auf Teillisten Æ externes Sortierverfahren. Daher verbreitetes Sortierverfahren für große Listen, die nicht mehr in den Hauptspeicher passen: □ □ □ □ Unterteile Datei in Abschnitte. Sortiere jeden der Abschnitte für sich im Hauptspeicher. Speichere sortierten Abschnitt in Hilfsdatei. Verschmelze die Hilfsdateien mit Reißverschlussverfahren. Wichtig ist dabei, dass die Hilfsdateien sequentiell von links nach rechts verarbeitet werden. (Das geht schnell und ohne wahlfreien Zugriff.) Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-88 22 13.10 Sortierung durch Mischen („merge sort“) Aufwände für SortedSet<E> boolean isEmpty () int size() O(1) O(1) boolean contains(Object o) boolean add(E elem) boolean remove(Object o) O(n) 1 Element: O(n) n Elemente: O(n·log2n) O(n) Für weitere Set-Methoden: boolean equals(Object o) boolean addAll(SortedSet<E> c) boolean retainAll(SortedSet<E> c) boolean removeAll(SortedSet<E> c) O(n) O(n) O(n) O(n) Verbesserung bei sortierten Mengen Algorithmen und Datenstrukturen • Philippsen/Stamminger/Pflaum • WS 2008/09 • Folie 13-89 © Michael Philippsen 23