Die Präsentation wird geladen. Bitte warten

Die Präsentation wird geladen. Bitte warten

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

Ähnliche Präsentationen


Präsentation zum Thema: "1 Algorithmen & Datenstrukturen Prof. Dr. Peter Kneisel Sommersemester 2009."—  Präsentation transkript:

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

2 2 Didaktik: Durchführung Diese Vorlesung enthält Übungen Die Übungen werden je nach Bedarf durchgeführt. Zur Vorbereitung werden Übungsblätter, je nach Vorlesungsverlauf zusammengestellt. Weitere Übungen sind im Foliensatz vorhanden und sollten selbständig und vollständig bearbeitet werden. Vorsicht ! Kommen Sie in alle Veranstaltungen - machen Sie die Übungen … auch wenn vieles auf JAVA zugeschnitten ist, so sind die Konzepte verallgemeinbar und vielseitig zu verwenden – insb. seien mir syntaktische Ungenauigkeiten verziehen und sogar zusätzclicher Ansporn für eigene konstruktive Verbesserungsvorschläge ;-)

3 3 Didaktik: Folien Der Vorlesungsstoff wird anhand von Folien dargelegt Die Folien bilden nur einen Rahmen für die Inhalte. Die Folien sollten daher mit Hilfe eigener Vorlesungsskizzen ergänzt werden - am besten in Form einer Vorlesungsnachbereitung max. 3 Tage nach der Vorlesung Zusätzlich zu den Folien werden Beispiele an der Tafel oder am Rechner gezeigt. Diese sollten Sie vollständig mitskizzieren. Zur vollständigen Nachbereitung, z.B. als Klausurvorbereitung, sind die Folien einheitlich strukturiert Es gibt genau drei Gliederungsebenen: Kapitel, Unterkapitel, Abschnitte Die Inhalte jedes Kapitels und jedes Unterkapitels werden jeweils motiviert und sind verbal beschrieben. Zusätzlich gibt es jeweils ein stichwortartiges Inhaltsverzeichnis der Unterkapitel, bzw. Abschnitte Die Vorlesung wird ständig überarbeitet, so dass sich die Foliensätze ändern können (und werden) Laden Sie sich zur endgültigen vollständigen Klausurvorbereitung nochmals zusätzlich den kompletten Foliensatz herunter.

4

5 5 Inhalt In Grundlagen der Informatik haben wir uns mit zwei grundlegenden Aspekte der Informatik befasst: Was ist Information und wie kann man diese auf höheren semantischen Ebenen strukturieren. Aus welchen einfachen Elementen ist ein (imperativer) Algorithmus aufgebaut Algorithmen & Datenstrukturen nimmt diese Zweiteilung auf: Zunächst werden wir die semantische Leiter nach oben steigen und komplexere semantische Strukturen kennenlernen, die grundlegend für Lösungen vieler typischer Problemstellungen sind. Anschließend werden wir die wichtigsten Algorithmen kennenlernen, die auf diesen Strukturen arbeiten. Inhalt 1.Abstrakte Datentypen (ADTs) 2.Suchen:Grundlagen, Algorithmus, Analyse 3.SortierenGrundlagen, Algorithmus, Analyse

6 6 Überblick und Einordnung Elemente OOP Information RAGDI Strukturierung Statik, Struktur Codes Zeichen Zahlen Datenstrukturen Verifikation Komplexität A&D Dynamik, Algorithmik ADTsSuchen Sortieren PIS

7 7 Kapitel 1Abstrakte Datentypen (ADTs) In Grundlagen der Informatik haben wir elementare Strukturen kennengelernt und gesehen, wie daraus mit komplexeren Strukturierungsverfahren komplexere Strukturen aufgebaut werden können. Wir haben uns dabei genau auf die Strukturen beschränkt, die den meisten imperativen Programmiersprachen gemeinsam sind. In diesem Kapitel gehen wir nun in semantisch höhere Ebenen und erläutern Strukturen, die häufig verwendet werden, aber nicht im Sprachumfang der meisten Programmiersprachen liegen (sehr wohl aber in Klassenbibliotheken) Inhalt 1.Wiederholung 2.Was sind ADTs 3.Stacks (Kellerspeicher, Stapel) 4.Queues (Warteschlangen) 5.Einfach verkettete Listen 6.Zweifach verkettete Listen 7.Hashlisten 8.Bäume 9.Graphen 10.Frameworks

8 1.1Wiederholung Wir haben bereits in Grundlagen der Informatik einiges über die Beziehung von Datentypen erfahren. Was, wird hier kurz zusammengefasst 1.Datenstrukturen 2.Datentypen 3.KLassifikation von Datentypen 8

9 Datenstrukturen In der Informatik werden Objekte der realen oder abstrakten Welt erfasst Bei der Erfassung beschränkt man sich möglichst auf die für den weiteren Transport / Speicherung/Verarbeitung/Umsetzung notwendige Information Zur internen Repräsentation werden diese Objekte abstrahiert Zur Abstraktion gehört die Erkennung von Strukturen - zunächst im Sinne einer Aggregation. Also Aus welchen Teilobjekten bestehen Objekte ? In welchem Verhältnis stehen die Teilobjekte zueinander ? Welches sind die atomaren Teilobjekte ? es existieren noch weitere strukturelle Beziehungen (z.B. Vererbung) Anschließend werden diese Objekte typisiert. Typisierung ist die Einteilung von abstrakten internen Objekten in Gruppen mit gleichen oder ähnlichen Eigenschaften.

10 Datentypen Typen sind also nicht die intern repräsentierten Objekte, sondern beschreiben die Eigenschaft einer Gruppe von Objekten. Zu diesen Eigenschaften gehören: Struktur Wertebereich anwendbare Operatoren, Funktionen, Relationen Beziehungen zu anderen Typen interne Repräsentationsweise … Einige Anmerkungen:: Der Begriff Datentyp ist weitergehend als der Begriff Datenstruktur In der Objektorientierten Programmierung wird statt Datentyp auch der Begriff Klasse verwendet (Klassen beschreiben mehr Eigenschaften) Konkrete Repräsentanten eines Datentyps werden (u.a) Variable oder - bei OO-Sprachen - Instanz genannt Beispiel: Imaginäre Zahlen

11 Klassifikation der Datentypen Datentypen IdealisierteAbstrakteKonkrete EinfacheStrukturiertePointer(Zeiger) Boolean (Wahrheitswert) Integer (Ganzzahl) Char (Zeichen) Enumeration (Aufzählung) OrdinaleReal (Fließkomma) Array (Feld) Record (Verbund) Union (Variantenverb.)...

12 Erläuterung der Klassifikation Idealisierte Datentypen aus der Mathematik bekannte Datentypen: R, N, Z,... Variablen dieser Typen sind oft nicht endlich darstellbar (Bsp: 2) In einem Computer-Algebra-System symbolisch darstellbar (Bsp: 2^( 1/2)) Konkrete Datentypen in einem Rechner von Hard- oder Software bereitgestellte Datentypen entweder vordefiniert oder durch den Benutzer definierbar Abstrakte Datentypen verbergen ihren inneren Aufbau vor dem Benutzer bestehen aus beliebigen Strukturen über konkrete/idealisierte Datentypen, sowie aus Zugriffsfunktionen bzw. Prozeduren Beispiel:Baum insert (Element) delete (Element) search (Element)

13 1.2Was sind ADTs Ein abstrakter Datentyp fasst die wesentlichen Eigenschaften und Operationen einer Datenstruktur zusammen, ohne auf deren eigentlichen Realisierung im Rechner einzugehen Konkrete Datentypen werden aus ordinalen (Basis-) Datentypen konstruiert und sind somit direkt in einer Implementierung einsetzbar. 1.Grundsätze 2.Algebren 3.Signaturen 4.Axiome 5.Beispiel einer ADT-Schnittstelle 6.Anwendung: Tabelle 13

14 1.2.1Grundsätze Kapselung: Ein abstrakter Datentyp darf nur über seine Schnittstellen benutzt werden. Das bedeutet insbesondere, dass interne Strukturen von außen nicht direkt zugreifbar sind dass interne Strukturen, die nicht über Operationen der Schnittselle zugreifbar sind, gar nicht von außen zugegriffen werden können. Geheimnisprinzip: Die interne Realisierung eines abstrakten Datentyps ist verborgen. Das bedeutet insbesondere, dass konkrete Umsetzungen von ADTs sehr stark von der verwendeten Programmiersprache und der geplanten Verwendung abhängen. Diese Prinzipen der Kapselung und des Geheimnisprinzips wurden schon in frühen rein prozeduralen imperativen Programmiersprachen gefordert, aber erst mit der Einführung objektorientierter imperativer Programmiersprachen ducrh Sprachkonstrukte mehr oder weniger erzwungen. In Pascal konnte man Teilstrukturen eines abstrakten Datentyps jederzeit auch von außen zugreifen. Die möglichen Operation waren sprachlich nicht mit den Strukturen verknüpft. In Java werden Datenstrukturen als private vor Zugriffen von außen geschützt und Operationen in Methoden geheim realisiert. 14

15 1.2.2Algebren Datentypen (auch abstrakte) lassen sich mathematisch als Algebren betrachten ( Vorlesung Diskrete Strukturen) Eine Algebra ist definiert durch Wertemengen und die Operatoren, die man darauf anwenden kann. Bsp: Betrachten Sie die natürlichen Zahlen. darauf lassen sich (zunächst) die Operatoren: +, -, x und % (ganzahliges Teilen) anwenden, als Ergebnis bekommen Sie Werte aus der Wertemenge der natürlichen Zahlen Sie können aber auch Vergleichsoperatoren: >, <, ==, != anwenden, dann bekommen Sie als Ergebnis Werte einer anderen Wertemenge, die der boolsche Zahlen: true, false, Sie können nun auf die Wertemenge der boolschen Werte auch boolsche Operatoren anwenden:,, als Ergebnis bekommen Sie wieder boolsche Werte. Ihre gesamte Algebra verwendet also zwei Sorten von Datenstrukturen (mehrsortige Algebra): natürliche Zahlen und boolsche Werte und kann darauf unterschiedliche Operatoren anwenden: +, -, x, %, >, <, ==, !=,,, wobei nicht jeder Operator auf jeden Wert (oder Wertepaar) anwendbar ist. Eine Algebra ist also definiert durch ihre Sorten, die Operationen und die Art, wie diese Operationen auf Werte der Sorten anwendbar sind. 15

16 1.2.3Signaturen Die Schnittstellen eines (A)DTs - also die Art, wie man den (A)DT verwendet - lassen sich durch seine Signatur beschreiben. Bsp: betrachten Sie den Datentyp integer : integer unterstützt/erzeugt zwei Sorten: integer und bool integer unterstützt die Operatoren: const : integer // nullstelliger Operator: Konstante successor : integer integer // einstellige Operation +, -, x, % : integer integer integer // zweistellige Operation >, <, ==, != : integer integer bool // zweistellige Operation, : bool bool bool // zweistellige Operation : bool bool // einstellige Operation Diese Formalisierung einer Algebra beschreibt die Strukturen und die Operationen eines (abstrakten) Datentyps und wird Signatur des Datentyps genannt. Aus der Signatur eines (A)DTs geht also insbesonder hervor: Dessen Wertebereiche in den unterschiedlichen Sorten Die Operatoren und deren Stelligkeit Die Wertebereiche der bei den Operationen verwendeten Operanten 16

17 1.2.4Axiome Selbst wenn Sie die Signatur eines (A)DT kennen, wissen Sie zwar welche Operatoren auf welche Wertebereiche (Sorten) anzuwenden sind, Sie wissen aber immer noch nicht wie die Werte durch die Operatoren verändert werden: Das beschrieben Sie mit Axiomen. Bsp.: Betrachten Sie die natürlichen Zahlen, so gilt z.B. für die Addition folgendes Axiom: + (i,0) = i + (i,successor (j)) = succesor (+ (i,j)) Entsprechend lassen sich für alle Operatoren Axiome aufstellen. Damit ergibt sich als Spezifikation für den ADT integer : (in Pseudo-Notation) type: integer // implizit auch verwendbare Sorte import: boolean // Sorten, die zusätzlich verwendet werden operators: +, -, x, % : integer integer integer... axioms: i,j : integer + (i,0) = i + (i,successor (j)) = succesor (+ (i,j))... 17

18 1.2.5Beispiel einer ADT-Schnittstelle type: list(T) // T ist die Wertemenge der Elemente // T ist ein sog. Sortenparameter import: integer operators: [] : list _ : _ : T x list list // erweitert Liste // _ : _ ist Infix-Operator head : list T // Kopf der Liste tail : list list // Liste ohne Kopf length : list integer // Anzahl Listenelemente axioms: l : list, x : T head ( x : l ) = x tail ( x : l ) = l lenght ( [] ) = 0 // [] ist leere Liste length ( x : l ) = successor ( length (l) ) 18

19 1.2.6Anwendung: Tabellen Listen repräsentieren oft Tabellen: Definition: Eine Tabelle o der Größe n ist eine Folge (z.B. Liste) von n Elementen gleichen Typs o = (o 1, o 2, …, o n ) Oft sind die Elemente einer Tabelle nochmals in zwei Teile unterstruktiert: Schlüssel-Daten (key) Die Schlüsseldaten bezeichnen (oft eindeutig) das Element einer Liste. Der Key kann nochmals unterstrukturiert sein. Informations-Daten (info) Die Informations-Daten geben für das durch den key bezeichnete Element zusätzliche Informationen an. Auch info kann nochmals unterstrukturiert sein. 19 key 1 info 1 key 2 info 2 key n info n … Anmerkung: Da die Indizierung von Listen in vielen Programmiersprachen mit 0 beginnt, man aber in der realen Welt mit 1 zu zählen beginnt, wird das 0-te Element oft als Dummy- Element mit einem Dummy-Wert versehen und ignoriert.

20 1.3.Stacks (Kellerspeicher, Stapel) Stacks (Kellerspeicher, Stapel) sind einfache Abstraktionen von Strukturen, die in vielen Bereichen der Informatik, insbesondere aber in den systemnahen Bereichen verwendet werden. Stacks bezeichnet man manchmal auch als LIFO (Last in – First Out)- Schlangen 1.Spezifikation 2.Implementierung 3.Die Java-Klasse stack 20

21 1.3.1Spezifikation type: stack(T) // T ist die Wertemenge der Elemente import: boolean operators: empty : stack // erzeugt leeren Stack push : stack x T stack // Legt Element auf Stack pop : stack stack // nimmt Element von Stack top : stack T // zeigt oberstes Element an is_empty : stack boolean // ist Stack leer ? axions: s : stack, x : T pop (push (s,x)) = s top (push (s,x)) = x is_empty (empty) = true // empty ist Wert des Stack is_empty (push (s,x)) = false 21

22 1.3.2Implementierung eines Stacks public class ArrayStack implements Stack { private Object elements[] = null; // Elemente private int num = 0; // aktuelle Anzahl // Stack mit vorgegebener Größe erzeugen public ArrayStack(int size) { elements = new Object[size]; } // Abfrage auf leeren Stack public boolean isEmpty() { return num == 0; } public void push(Object obj) throws StackException { if (num == elements.length) // KapazitŠt erschöpft throw new StackException(); elements[num++] = obj; } public Object pop() throws StackException { if (isEmpty()) // Stack ist leer throw new StackException(); Object o = elements[--num]; elements[num] = null; return o; } public Object top() throws StackException { if (isEmpty()) // Stack ist leer throw new StackException(); return elements[num - 1]; } } 22

23 1.3.3Die Java-Klasse stack import java.util.*; public class StackExample { public static void main(String[] args) { Stack s = new Stack(); // ohne Parameter s.push("Erstes Element"); // Rückgabewert: eingefügtes Element... s.push("Zweites Element"); //... wird ignoriert s.push("Drittes Element"); while (true) { try { System.out.println(s.pop()); // ? peek() würde Element entfernen } catch (EmptyStackException e) { // wird beim Lesezugriff auf... break; //... leeren Stack geworfen } 23

24 1.4.Queues Queues (Warteschlangen) sind lineare Listen, deren Elemente nach dem FIFO- Prinzip (First in–First Out) ein- bzw. ausgefügt werden Auch Queues kommen in systemnahen Bereichen vor, insbesondere bei Betriebssystemen. 1.Spezifikation 2.Implementierung einer Queue 3.Die Java-Klasse queue 24

25 1.4.1Spezifikation type: queue(T) // T ist die Wertemenge der Elemente import: boolean operators: empty : queue // erzeugt leere Queue enter : queue x T queue // stellt Element ans Ende der Queue leave : queue queue // nimmt erstes Element von Queue front : queue T // zeigt erstes Element der Queue is_empty : queue boolean // Ist Queue leer ? axions: q : queue, x : T // empty ist der Wert einer leeren queue leave (enter (empty,x)) = empty // (x) ohne Kopf = empty leave (enter (enter(q,x),y)) = enter (leave (enter (q,x)), y) // (q,x,y) ohne Kopf = (q,x) ohne Kopf + y -> ((q,x) ohne Kopf,y) front (enter (empty,x)) = x // Kopf von (x) = x front (enter (enter(q,x), y)) = front (enter (q,x)) // Kopf von (q,x,y) = Kopf von (q,x) is_empty (empty) = true // is_empty von empty ist true is_empty (enter(q,x)) = false // is_empty von (q,x) ist falsch 25

26 1.4.2Implementierung einer Queue public class ArrayQueue implements Queue { private Object[] elements; // Elemente private int l = 0; // lower Zeiger private int u = 0; // upper Zeiger // in der Queue sind max. size-1 Elemente // Queue mit vorgegebener Länge erzeugen public ArrayQueue (int size) { elements = new Object[size]; } public boolean isEmpty () { return l == u; } // Zeige das lower Element public Object front () throws QueueException { if (isEmpty ()) throw new QueueException (); return elements[l]; } // Einfügen eines Elementes public void enter (Object obj) throws QueueException { if ((elements.length - l + u) % elements.length == elements.length - 1) // Kapazität ist erschöpft (= size-1) throw new QueueException (); elements[u] = obj; // oberen Zeiger aktualisieren u = (u + 1) % elements.length; // Modulo, da array zyklisch verwendet. } // Herausnehmen des lower-Elementes public Object leave () throws QueueException { if (isEmpty ()) throw new QueueException (); Object obj = elements[l]; elements[l] = null; // unteren Zeiger aktualisieren l = (l + 1) % elements.length; return obj; } 26

27 1.4.3Die Java-Klasse queue import java.util.*; public class QueueExample { public static void main(String[] args) { Queue queue = new LinkedList (); // gibt den Typ // von Elementen an queue.offer( "Fischers" ); queue.offer( "Fritze" ); queue.offer( "fischt" ); queue.offer( "frische" ); queue.offer( "Fische" ); queue.poll(); queue.offer( "Nein, es war Paul!" ); while ( !queue.isEmpty() ) System.out.println( queue.poll() ); } // und es gibt noch einige weitere Queues in java.util.* 27

28 1.5Einfach verkettete Liste Listen sind (ziemlich) simple Datentypen, die sich statisch durch den konkreten strukturierten Datentyp array (Feld) darstellen lässt und damit in den meisten Programmiersprachen implizit vorhanden ist. In der nicht-imperativen Programmiersprache LISP ist Liste zudem der einzige strukturierte Datentyp. Möchte man die Länge einer Liste jedoch zur Laufzeit eines Programmes dynamisch verändern so muss man auf eigenen Umsetzungen mithilfe eines ADTs zurückgreifen. 1.class 2.main 3.Methoden 4.Implementierung als Liste 28

29 1.5.1class public class List { static class Node { Object obj; Node next; public Node(Object o, Node n) { obj = o; next = n; } public Node() { obj = null; next = null; } public void setElement(Object o) { obj = o; } public Object getElement() { return obj; } public void setNext(Node n) { next = n; } public Node getNext() { return next; } } private Node head = null; public List() {} public void addFirst(Object o) {} public void addLast(Object o) {} public Object getFirst() throws ListEmptyException {} public Object getLast() throws ListEmptyException {} public Object removeFirst() throws ListEmptyException {} public Object removeLast() throws ListEmptyException {} public int size() {} public boolean isEmpty() {} } 29

30 1.5.2main public static void main(String args[]) { List lst = new List(); lst.addFirst("Drei"); lst.addFirst("Zwei"); lst.addFirst("Eins"); lst.addLast("Vier"); lst.addLast("Fünf"); lst.addLast("Sechs"); while (! lst.isEmpty()) { System.out.println((String) lst.removeFirst()); } 30

31 1.5.3Methoden public List() { head = new Node(); } public void addFirst(Object o) { Node n = new Node(o, head.getNext()); head.setNext(n); } public Object getFirst() throws ListEmptyException { if (isEmpty()) throw new ListEmptyException(); return head.getNext().getElement(); } public void addLast(Object o) { Node l = head; while (l.getNext() != null) l = l.getNext(); Node n = new Node(o, null); l.setNext(n); } public Object removeFirst() throws ListEmptyException { if (isEmpty()) throw new ListEmptyException(); Object o = head.getNext().getElement(); head.setNext(head.getNext().getNext()); return o; } public Object removeLast() throws ListEmptyException { if (isEmpty()) throw new ListEmptyException(); Node l = head; while (l.getNext().getNext() != null) l = l.getNext(); Object o = l.getNext().getElement(); l.setNext(null); return o; } 31

32 1.5.4Implementierung als Liste public class ListStack implements Stack { private List list; // Liste zur Verwaltung der Elemente public ListStack () { list = new List (); } public void push (Object obj) { // Element vorn anfŸgen list.addFirst (obj); } public Object pop () throws StackException { if (isEmpty ()) throw new StackException (); // Element von vorn entfernen return list.removeFirst (); } public Object top () throws StackException { if (isEmpty ()) throw new StackException (); return list.getFirst (); } public boolean isEmpty () { return list.isEmpty (); } 32

33 1.6Zweifach verkettete Liste Aus bestimmten Gründen – vor allem Laufzeit-Effizienz – verwendet man oft Listen, deren einzelne Elemente nicht nur den jeweiligen Nachfolger, sondern auch den jeweiligen Vorgänger kennen. Diese Listen nennt man das Zweifach bzw. Doppelt verkettete Listen 1.class 2.iterator 3.main 4.Methoden 33

34 1.6.1class public class DList { static class Node { Object obj; Node prev, next; public Node (Object o, Node p, Node n) { obj = o; prev = p; next = n; } public Node () { obj = null; prev = next = null; }... // Setter und Getter-Methoden public void setElement (Object o) { obj = o; } public Object getElement () { return obj; } public void setNext (Node n) { next = n; } public Node getNext () { return next; } public void setPrevious (Node p) { prev = p; } public Node getPrevious () { return prev; } } private Node head = null; private Node tail = null;... public java.util.Iterator iterator () {} } 34

35 1.6.2iterator class ListIterator implements java.util.Iterator { private Node node = null; public ListIterator () { node = head.getNext(); } public boolean hasNext () { return node.getNext () != tail; } public void remove () { throw new UnsupportedOperationException (); } public Object next () { if (! hasNext ()) throw new java.util.NoSuchElementException (); Object o = node.getElement (); node = node.getNext (); return o; } 35

36 1.6.3main public static void main (String args[]) { DList lst = new DList (); java.util.Iterator it = lst.iterator (); while (it.hasNext ()) { System.out.println ((String) it.next ()); } lst.addFirst ("Drei"); lst.addFirst ("Zwei"); lst.addFirst ("Eins"); lst.addLast ("Vier"); lst.addLast ("Fünf"); lst.addLast ("Sechs"); it = lst.iterator (); while (it.hasNext ()) { System.out.println ((String) it.next ()); } 36

37 1.6.4Methoden public DList () { head = new Node (); // dieser Knoten existiert immer, auch bei leerer Liste tail = new Node (); // dieser Knoten existiert immer, auch bei leerer Liste head.setNext(tail); // head und tail werden initial miteinander verlinkt tail.setPrevious(head); tail.setNext(tail); // tail.next zeigt auf sich selbst } public void addFirst (Object o) { Node n = new Node (o, head, head.getNext()); head.getNext ().setPrevious (n); head.setNext (n); } public Object getFirst () throws ListEmptyException { if (isEmpty ()) throw new ListEmptyException (); return head.getNext ().getElement (); } public void addLast (Object o) { Node l = tail.getPrevious (); Node n = new Node (o, l, tail); l.setNext (n); tail.setPrevious (n); } public Object removeFirst () throws ListEmptyException { if (isEmpty ()) throw new ListEmptyException (); Object o = head.getNext ().getElement (); head.setNext (head.getNext ().getNext ()); head.getNext ().setPrevious (head); return o; } public Object removeLast () throws ListEmptyException { if (isEmpty ()) throw new ListEmptyException (); Node n = tail.getPrevious (); n.getPrevious ().setNext (tail); tail.setPrevious (n.getPrevious ()); return n.getElement (); } 37

38 1.7Hashlisten Hashlisten sind Listenstrukturen, manchmal erweitert durch weitere Strukturen, die sich sehr gut für das Suchen eignen ( Kapitel 2). Hier seien die grundlegenden Ideen des Hashens dargestellt. 1.Grundprinzip des Hashens 2.Die Hashfunktion 3.Behandlung von Kollisionen 4.Implementierung einer Hashliste 38

39 1.7.1Grundprinzipien des Hashens Das Hashen basiert auf drei Grundprinzipien: Die Speicherung der Datensätze erfolgt in einem Feld mit Indexwerten von 0 bis n-1. wobei die einzelnen Positionen als Buckets (Eimer) bezeichnet werden. Eine Hashfunktion h bestimmt für ein zu speicherndes Element e dessen Position h(e) im Feld Diese Hashfunktion h sorgt für eine gute – im besten Fall kollisionsfreie, d.h. injektive (meist aber Nur kollisionsarme) Abbildung d.h. Verteilung der zu speichernden Elemente. Da normalerweise der Wertebereich der möglicherweise zu speichernden Element größer ist als die Anzahl der Elemente in der Hashliste kann die Funktion h (meist) nicht für alle Werte n eindeutige Hashwerte h(n) liefern. Das führt zu Kollisionen, deren Behandlung die Qualität eines Hashverfahrens ausmacht. Ist die Hashfunktion ungeschickt gewählt, kann das Verfahren entarten, was zu teilweise dramatischen Geschwindigkeitsverlusten führen kann. 39

40 1.7.2Die Hashfunktion Die Auswahl der Hashfunktion h hängt natürlich vom zu speichernden Datentyp (bzw. dessen Wertebereich) und der Auftrittswahrscheinlichkeit der Werte ab. Für Integerwerte i wird oft die Modulofunktion verwendet: h(i) = i mod n (wobei n die größe der Hashliste ist) Diese Funktion funktioniert in der Regel nur für große primzahlige n gut (inbesondere ist n = 2 x nicht gut !) Beispiel: h(i) = i mod 7 Index Element (danach führt jedes Element zu Kollision) Für andere Datentypen kann eine Abbildung auf Integerwerte erfolgen: Bei Fließkommazahlen kann man z.B. Mantisse und Exponent addieren Bei Strings kann man den ASCII oder Unicode der einzelnen Buchstaben, eventuell mit einem Faktor gewichtet, miteinander addieren. Meist ist eine Gleichverteilung der Bildbereiches der Hashfunktion wünschenswert, so dass man sich bestimmte Eigenschaften (z.B. ungleichgewichtige Verteilungen) des Urbildes zu Nutze machen kann und sollte. Andererseits geht die Komplexität der Hashfunktion h multiplikativ in die Gesamtkomplexität ein und sollte daher einfach gehalten werden. 40

41 1.7.3Behandlung von Kollisionen Führt die Hashfunktion für unterschiedlich Werte des Urbildes auf gleiche Hashwerte, so spricht man von Kollision, die man z.B. mit folgenden Verfahren behandeln kann: Verkettung der Überläufer: Man erweitert die eindimensionale Listenstruktur der Hashliste um eine zweite Dimension (z.B. durch eine einfach verkettete Liste), in die man die kollidierenden Werte ablegt Sondieren: Man legt den kollidierenden Wert an ein andere Stelle in der Hashliste ab, die sich durch die Berechnung eines Offsets ergeben: beim linearen Sondieren wird die nächste freie Position verwendet. (also als Offset die Werte 1,2,3,4, …) beim quadratischen Sondieren ergibt sich der mögliche Offset durch die Quadratzahlen (also 1,4,9,16,25, …). Dadurch wir d die Klumpenbildung, zu der das lineare Sondieren neigt, vermieden. 41

42 1.7.4Implementierung einer Hashliste 42 public class HashTable { Object[] table; public HashTable (int size) { table = new Object [size]; } // fügt Element in Hashliste public void add (Object o) { int idx, oidx; // berechnen Hashfunktion oidx = idx = (o.hashCode () & 0x7fffffff) % table.length; // falls Kollision -> suche nächstes Freies while (table[idx] != null) { idx = ++idx % table.length; // fall Suche erfolglos -> Fehler if (idx == oidx) throw new HashTableOverflowException (); } // trage Wert ein table[idx] = o; } // sucht Element in Hashliste public boolean contains (Object o) { int idx, oidx; oidx = idx = (o.hashCode () & 0x7fffffff) % table.length; while (table[idx] != null) { if (o.equals (table[idx])) return true; idx = ++idx % table.length; if (idx == oidx) break; } return false; } public static void main (String[] args) { HashTable tbl = new HashTable (20); tbl.add (Au"); tbl.add (Oh"); tbl.add (Ah"); System.out.println (tbl.contains (Ah")); System.out.println (tbl.contains (Be")); }

43 1.8Bäume Bäume sind (zumindest) zweidimensionale Strukturen, die viele reale Strukturen abzubilden Vermögen und zudem sehr gut zum Durchsuchen geeignet sind. Es gibt daher sehr viele spezielle Arten von Bäumen, von denen hier stellvertretend vor allem die binären Bäume behandelt werden sollen. 1.Definitionen & Beispiele 2.Spezifikation 3.Datentypen 4.Traversierung 5.Weitere Bäume 43

44 1.8.1.Definitionen & Beispiele Ein Baum ist eine Menge von Knoten und (gerichteten) Kanten mit folgenden Eigenschaften: Ein ausgezeichneter Knoten wird als Wurzel bezeichnet Jeder Knoten (außer der Wurzel) ist durch genau eine Kante mit seinem Vorgängerknoten verbunden (Vaterknoten, Elternknoten). Dieser Knoten wird dann auch als Kind (Sohn, Nachfolger) bezeichnet. Ein Knoten ohne Kinder heißt Blatt Knoten mit Kindern heißen innere Knoten 44 Wirbeltiere (Unterstamm) Kiefermünder (Oberklasse) Vögel (Klasse) … (Ordnungen) Säugetiere (Klasse) Primaten (Ordnung) Kieferlose (Oberklasse) … (Klassen) Wirbeltiere (Unterstamm) Kiefermünder (Oberklasse) Vögel… Säugetiere (Klasse) Primaten (Ordnung) Kieferlose… … nich so praktisch … wie sich der Informatiker einen Baum vorstellt

45 1.8.1.Definitionen & Beispiele Ein Pfad in einem Baum ist eine Folge von unterschiedlichen Knoten, in der die aufeinanderfolgenden Knoten durch Kanten verbunden sind Zwischen jedem Knoten und der Wurzel gibt es genau einen Pfad Dies bedeutet, dass ein Baum zusammenhängend ist und keine Zyklen besitzt Unter dem der Niveau (der Tiefe) eines Knotens versteht man die Länge dessen Pfades zu der Wurzel Die Höhe (Tiefe) eines Baumes entspricht dem maximalen Niveau eines Blattes + 1 (+1 da die Wurzel mitzählt) Je nach Art und Anzahl von Kindern unterscheidet man zwischen n-ären Bäumen, wenn die maximale Anzahl von Kindern gleich n ist (also z.B. binärer Baum, wenn die maximale Anzahl der Kinder gleich 2 ist) geordneten Bäumen, wenn die Kinder entsprechend einer Ordnungsrelation (z.B. von links nach rechts) angeordnet sind 45 +* Tiefe 0 Tiefe 1 Tiefe 2 Tiefe 3 ((1+2)*3)+(2+5)

46 1.8.2.Binäre Bäume: Spezifikation type: tree (T) // T ist die Wertemenge der Elemente import: boolean operators: empty : tree // erzeugt leeren Baum // verbindet zwei Bäume über neue Wurzel T bin : tree x T x tree tree left : tree tree // liefert den linken Teilbaum right : tree tree // liefert den rechten Teilbaum value : tree T // liefert die Wurzel is_empty : tree boolean // ist Baum leer ? axions: s : stack, x : T left (bin (x,b,y)) = x // linker Teilbaum right (bin (x,b,y)) = y // rechter Teilbaum value (bin (x,b,y)) = b // Wurzel is_empty (empty) = true // empty ist Wert des Baums is_empty (bin (x,b,y)) = false 46

47 1.8.3Binäre Bäume: Datentypen 47 static class TreeNode { Object key; // Wert des Knotens TreeNode left = null; // Referenz auf linken Teilbaum TreeNode right = null; // Referenz auf rechten Teilbaum // Konstruktor public TreeNode (Object e) { key = e; } // getter Methoden public TreeNode getLeft () { return left; } public TreeNode getRight () { return right; } public Object getKey () { return key; } // setter Methoden public void setLeft (TreeNode n) { left = n; } public void setRight (TreeNode n) { right = n; } } static class BinaryTree { protected TreeNode root = null; public BinaryTree () { } public BinaryTree (TreeNode n) { root = n; } } +* TreeNode e1 = new TreeNode(+); e1.setleft (new TreeNode(1)); e1.setright (new TreeNode(2)); TreeNode e2 = new TreeNode(*); e2.setleft (e1); e2.setright (new TreeNode(3)); TreeNode e3 = new TreeNode(+); e3.setleft (new TreeNode(2)); e3.setright (new TreeNode(5)); TreeNode e = new TreeNode(+); e.setleft (e2); e.setright (e3); Bäume baut man von unten nach oben auf

48 1.8.4Binäre Bäume: Traversierung Je nach Reihenfolge unterschiedet man beim Baumdurchlauf folgende Traversierungsarten. Inorder: Hier wird zuerst rekursiv der linke Teilbaum, danach der Knoten selbst, und schließlich der rechte Teilbaum durchlaufen. Preorder: Hier wird zuerst der Knoten, danach zunächst rekursiv der linke Teilbaum und schließlich rekursiv der rechte Teilbaum durchlaufen. Postorder: Hier wird zuerst rekursiv der linke Teilbaum, danach rekursiv der rechte Teilbaum, schließlich der Knoten durchlaufen. Diese Traversierungsarten gehen also für jeden Knoten rekursiv in die Tiefen der beiden Teilbäume und können daher auch Tiefentraversierung genannt werden. Daneben gibt es noch eine Traversierungsart, die auf jedem Niveau alle Knoten berücksicht. Diese Breitentraversierung nennt man: Levelorder: erst werden alle Knoten eines Niveaus durchlaufen, danach rekursiv die beiden Teilbäume 48 +* Inorder:1 + 2 * Preoder: + * Postorder: * ( UPN) Levelorder:+ *

49 1.8.4Binäre Bäume: Traversierung 49 private void printPreorder (TreeNode n) { if (n != nullNode) { System.out.println (n.toString ()); printPreorder (n.getLeft ()); printPreorder (n.getRight ()); } private void printPostorder (TreeNode n) { if (n != nullNode) { printPostorder (n.getLeft ()); printPostorder (n.getRight ()); System.out.println (n.toString ()); } protected void printInorder (TreeNode n) { if (n != nullNode) { printInorder (n.getLeft ()); System.out.println (n.toString ()); printInorder (n.getRight ()); } private void printLevelorder (Queue q) { while (! q.isEmpty ()) { TreeNode n = (TreeNode) q.leave (); if (n.getLeft () != nullNode) q.enter (n.getLeft ()); if (n.getRight () != nullNode) q.enter (n.getRight ()); System.out.println (n.toString ()); }... // zur Zwischenspeicherung der Knoten ->1.4.2 Queue queue = new ArrayQueue (); // Initialisierung queue.enter (root); // Aufruf printLevelorder (queue);

50 1.8.5Weitere Bäume Für spezielle Anwendungen des Suchens und Sortierend werden bestimmte Spezialformen von Bäumen verwendet Ausgeglichene (balanced) Bäume: Hier wird beim Auf- und Abbau des Baumes versucht,die Tiefen der Teilbäume möglichst ähnlich oder sogar gleich zu halten: AVL-Bäume sind binäre Bäume und beschränken die Niveaudifferenz aller Teilbäume auf 1. Sie werden vor allem zum Suchen verwendet. B-Bäume (b steht für balanciert, buschig, breit) sind n-äre Bäume, bei denen alle Teilbäume gleichtief sind. Diese sind also meist nicht binär. Sie werden oft bei Datenbanksystemen zur Indexierung verwendet. Digitale Bäume: Das sind n-äre Bäume die eine feste Anzahl von Verzweigungen (Nachfolgenknoten) unabhängig von den Werten im Baum haben. Tries (retrieval): sind n-äre Bäume bei denen die n Werte (z.B. 127 ASCII- Werte) des Knotens als Index für die Nachfolgeknoten verwendet werden. Sie werden zum Suchen von Worten in Texten verwendet. ( Patricia-Bäume (Practical Algorithm to Retrieve Information Coded in Alphanumeric): Spezielle Form von Tries, bei denen Knoten mit nur einem Nachfolger übersprungen werden können. Auch Sie werden zum Suchen von Worten in Texten (oder von Gensequenzen in einem Genom) verwendet. Kapitel 2 50

51 1.9.Graphen Graphen sind (oft) die komplexesten Grundstrukturen, mit denen man es bei abstrakten Datentypen zu tun hat.,,, und tatsächlich sind die im vorherigen Unterkapitel behandelten Bäume Spezialfälle von Graphen. 1.Arten 2.Umsetzung 3.Implementierung eines Graphen 51

52 1.9.1Arten Es gibt (neben anderen) drei wichtige Arten von Graphen ungerichtete Graphen: Hier sind Knoten mit ungerichteten Kanten verbunden, d.h. es gibt kein Nachfolge- oder Vorgänger-Beziehung und auch kein Einschränkungen bezüglich Anzahl von Kanten pro Knoten. Anwendungen findet man bei der Modellierung von Straßenverbindungen (ohne Einbahnstraßen), der Nachbarschaft von Gegenständen oder eines Telefonnetzes. gerichtete Graphen: Hier sind Knoten durch gerichtete Kanten verbunden, es kann also zwischen zwei Knoten bis zu zwei Kanten geben (eine hin, eine zurück). Anwendungen sind Modelle von Förderanlagen, der Kontrollfluss von Programmre gerichtete azyklische Graphen (DAG directed acyclic graphs): dieser Spezialfall von gerichteten Graphen erlaubt keine Zyklen im Graph, d.h. es darf keinen Pfad von einem Knoten zu sich selbst geben. Zusätzlich können Kanten von Graphen noch gewichtet sein (gewichtete Graphen)

53 1.9.2Umsetzung Die interne Darstellung von Graphen erfolgt (historisch) in vier Varianten: Knotenliste:,, ( :=, ) 6, 8, 1,2, 1,4, 3,2, 3,5, 4,5, 4,6, 5,2, 6,3 Kantenliste:,, ( :=, ) 6, 8, 2,2,4, 0, 2,2,5, 2,5,6, 1,2, 1,3 Adjazenzmatrix dynamische Adjazenzliste

54 1.9.3Implementierung eines Graphen 54 public class Graph { static class Edge { int dest, cost; public Edge(int d, int c) { dest = d; // Nachfolgeknoten cost = c; // Gewicht } private ArrayList nodes; public Graph() { nodes = new ArrayList(); } public void addNode(String label) {... } public void addEdge(String src, String dest, int cost) {... } public Iterator getEdges(int node) {... } }

55 1.10.Frameworks Aufgrund des häufigen Einsatzes dieser ADTs gibt es praktisch für jede Programmiersprache entsprechende Bibliotheken. 1.ADTs in Programmiersprachen 2.Bibliotheken in Java 55

56 1.10.1ADTs in Programmiersprachen ADTs werden in vielen Programmiersprachen unterstützt: Diese Bibliotheken sind zwar teilweise standardmäßig in den Entwicklungsumgebungen enthalten, sind aber (meist) nicht Teil des Sprachumfangs Manche Programmiersprachen besitzen ADTs als Teil des Sprachumfangs. (z.B. good ol Pascal: sets) Beispiele für C++ und Java: C++: Standard Template Library (Vorsicht: nicht standardisiert !) (z.B. :Java Collection Framework (http://java.sun.com/docs/books/tutorial/ collections/index.html) 56

57 1.10.2Bibliotheken in Java In Java sind diverse Klassen definiert, die die hier beschriebenen ADTs implementieren: Vector funktioniert wie ein array, das bei Bedarf dynamisch wachsen kann. Nur für Integerwerte. Generische Variante: ArrayList Stack ferweiterert Vector zu eimem LIFO-Stack. LinkedList Doppelt verkettete Liste, kann auch als Queue (Warteschlange) eingesetzt werden. HashMap Hashliste. TreeMap kann auch für gehashten (assoziativen) Zugriff verwendet werden, ist intern als Baum aufgebaut und etwas langsamer – dafür sind die Schlüssel alle sortiert. TreeSet Balancierter Binärbaum. Die Elemente im Baum sind sortiert Diese Klassen befinden sich im Paket: java.util.* und können mit import java.util.* eingebunden werden. 57

58 1.11Zusammenfassung Ein abstrakter Datentyp fasst die wesentlichen Eigenschaften und Operationen einer Datenstruktur zusammen, ohne auf deren eigentlichen Realisierung im Rechner einzugehen Stacks (Kellerspeicher, Stapel) sind einfache Abstraktionen von Strukturen, die in vielen Bereichen der Informatik, insbesondere aber in den systemnahen Bereichen verwendet werden. Stacks bezeichnet man manchmal auch als LIFO (Last in – First Out)-Schlangen Queues (Warteschlangen) sind lineare Listen, deren Elemente nach dem FIFO-Prinzip (First in–First Out) ein- bzw. ausgefügt werden Auch Queues kommen in systemnahen Bereichen vor, insbesondere bei Betriebssystemen. Listen sind (ziemlich) simple Datentypen, die sich statisch durch den konkreten strukturierten Datentyp array (Feld) darstellen lässt und damit in den meisten Programmiersprachen implizit vorhanden ist. In der nicht-imperativen Programmiersprache LISP ist Liste zudem der einzige strukturierte Datentyp. Möchte man die Länge einer Liste jedoch zur Laufzeit eines Programmes dynamisch verändern so muss man auf eigenen Umsetzungen mithilfe eines ADTs zurückgreifen. Aus bestimmten Gründen – vor allem Laufzeit-Effizienz – verwendet man oft Listen, deren einzelne Elemente nicht nur den jeweiligen Nachfolger, sondern auch den jeweiligen Vorgänger kennen. Diese Listen nennt man das Zweifach bzw. Doppelt verkettete Listen Bäume sind (zumindest) zweidimensionale Strukturen, die viele reale Strukturen abzubilden vermögen und zudem sehr gut zum Durchsuchen geeignet sind. Es gibt daher sehr viele spezielle Arten von Bäumen, von denen hier stellvertretend vor allem die binären Bäume behandelt werden sollen. Graphen sind (oft) die komplexesten Grundstrukturen, mit denen man es bei abstrakten Datentypen zu tun hat (Tatsächlich sind die im vorherigen Unterkapitel behandelten Bäume Spezialfälle von Graphen) Aufgrund des häufigen Einsatzes dieser ADTs gibt es praktisch für jede Programmiersprache entsprechende Bibliotheken. 58

59 Übung 1 1.Implementieren Sie einen stack 1.Fügen Sie 10 Elemente ein. 2.Entnehmen Sie die Elemente wieder und geben Sie sie dabei aus. 2.Implementieren Sie eine queue. 1.Fügen Sie 10 Elemente ein. 2.Entnehmen Sie die Elemente wieder und geben Sie sie dabei aus. 3.Implementieren Sie eine Hashliste (der Länge 41) für deutsche Worte mit quadratischem Sondieren zur Auflösung von Kollisionen 1.Fügen Sie 30 Worte ein. 2.Geben Sie Ihre Hashfunktion an und die Hashwerte für Ihre eingetragen Worte 3.Suchen Sie nach 5 vorhandenen und 5 nicht vorhandenen Worten, geben Sie dabei jeweils auch den Hashwert an 4.Implementieren Sie einen binären Baum und fügen Sie 30 Element ein 1.Traversieren Sie den Baum Inorder, Preorder, Postorder und inline, Geben Sie die Elemente dabei jeweils aus. 59

60 2.Sortieren Suchen und Sortieren sind grundlegende Operationen in der Informatik. Man schätzt, dass über 50% der Rechenzeiten auf diese Operationen zurückzuführen sind. Für diese beiden Operationen gibt es zwar völlig unterschiedliche Umsetzungen, doch sind beide Operationen mitteinander verwandt, denn oft basiert ein Suche auf sortierten Strukturen. Das ist auch der Grund, weshalb das (eher etwas kniffeligere) Sortieren vor dem Suchen behandelt wird. 1.Wiederholung: Komplexität 2.Grundlagen 3.Elementare Sortieralgorithmen 4.Fortgeschrittene Sortieralgorithmen 60

61 61 2.1Wiederholung: Komplexität In GDI haben wir den Begriff Komplexität diskutiert und definiert. Komplexität, insbesomdere Zeitkomplexität (Aufwand) ist nun ein entscheidendes Kriterium für und wider den Einsatz der im folgenden behandelten Algorithmen und soll daher hier nochmals kurz wiederholt werden. Inhalt 1.Wie gut ist ein Algorithmus 2.Die O-Notation 3.Häufige O-Ausdrücke 4.Einige Regeln 5.Quantitatives 6.Platzbedarf

62 Qualität eines Algorithmus Die Abarbeitung eines Algorithmus benötigt Ressourcen, vor allem: ZeitLaufzeit des Algorithmus PlatzSpeicherplatzbedarf des Algorithmus Problem bei der Ressourcenermittlung - der Ressourcenbedarf ist Abhängig von: der Problemgröße (z.B. Multiplikation einer 10x10 bzw. 100x100 Matrix) der Eingabewerte (z.B. Sortieren einer bereits sortierten Menge) der Fragestellung (bester, mittlerer, schlechtester Fall) der Güte der Implementierung (z.B. (un-)geschickte Typwahl) der Hard- und Software (z.B. Schneller Rechner, optimierter Compiler) Es gibt auch Qualitätsmerkmale eines Algorithmus, der sich nicht am Ressourcenbedarf festmachen (aber das ist eine andere Geschichte...) Wartbarkeit, Wartungsintensität Robustheit Eleganz...

63 Die O-Notation: Definition Definition: Eine Funktion g(n) wird O(f(n)) genannt (Die Laufzeit, der Aufwand, die Zeitkomplexität von g(n) ist O(f(n))), falls es Konstanten c und n 0 gibt, so dass: g(n) c f(n), für fast alle n n o ist f(n) ist damit eine obere Schranke für die Laufzeit des Algorithmus (allerdings nur zusammen mit einem festen c und ab bestimmten n 0 ) ! Die Problemgröße kann der Umfang der Eingabemenge sein, die Größe des zu verarbeitenden Objektes (z.B. der Zahl), … LaufzeitLaufzeit Problemgröße g(n) f(n) c f(n) nono g(n) c f(n), für alle n n o

64 Die O-Notation: Beispiel Beispiel: Bei der Analyse eines Algorith- mus hat sich herausgestellt, dass die Laufzeit: g(n) = 3n 2 + 7n – 1 ist. Behauptung: Die Laufzeit von g(n) ist O(n 2 ), also f(n)=n 2, Beweis: Es muss Konstanten c und n 0 geben, so dass gilt: 3n 2 +7n-1 c n 2, für alle n n 0 setze n 0 =7 und c=4, dann gilt: 3n 2 +7n-1 3n 2 +7n 3n 2 +n 2 = 4n 2 Allgemein: g(n) = a m n m + a m-1 n m-1 + … + a 0 n 0 a m n m + a m-1 n m + … + a 0 n m = n m (a m + a m-1 + … + a 0 ) also: g(n) c n m mit c = a m + a m-1 + … + a 0 LaufzeitLaufzeit Problemgröße g(n) f(n)=n 2 c f(n) = 4 n 2 nono g(n) c f(n), für fast alle n n o

65 Die O-Notation: Schranken Die Notation gibt nur eine obere Schranke der Komplexität, das muss nicht notwendigerweise die beste Schranke sein. Beispiel: Eine weitere obere Schranke für g(n) = 3n 2 + 7n - 1 ist auch O(n 3 ), welche sicher nicht die beste ist. Bei der Suche nach der Größenordnung von f(n) wird man versuchen, das kleinste f(n) zu finden, für das g(n) c. f(n) Dieses ist dann eine kleinste, obere Schranke für den Aufwand Zur Bestimmung des tatsächlichen asymptotischen Aufwands wird man also noch eine größte, untere Schranke h(n) = ( g(n)) suchen für die gilt: lim n h(n)/f(n) = 1 Eine untere Schranke ist die Zeit, die jeder Algorithmus (ab einem n>n 0 ) benötigt Das ist im Allgemeinen viel schwieriger !

66 Die O-Notation: Achtung Achtung ! Die Konstanten c und n 0 werden üblicherweise nicht angegeben und können sehr groß sein Beispiel: Algorithmus A habe eine Laufzeit von O(n 2 ) Algorithmus B für das gleiche Problem eine Laufzeit von O(1,5 n ) Welcher Algorithmus ist besser ? schnelle Antwort: A (das stimmt auch für große n) bessere Antwort: Wie groß ist n ? Wie groß sind die Konstanten ? z.B. für c A =1000 und c B =0,001 nc A n 2 c B 1,5 n , , ,3 502, , , Bis hier ist B besser als A

67 Übung 2.1: 1.Erstellen Sie ein Graphik (mit Excel) in der Sie die Laufzeiten der wichtigsten Komplexitätsklassen sinnvoll darstellen. 67

68 2.2..Grundlagen … bevor es losgeht: 1.Definitionen 2.Beispiele 3.Framework für Implementierungen 68

69 2.2.1Definitionen Beim Sortieren werden Elemente entsprechend der Werte ihrer Schlüssel entsprechend einer Ordnungsrelation angeordnet Elemente sind Datenstrukturen, die aus mehreren Unterstrukturen bestehen können, d.h. Element müssen nicht elementar (Int, Real, Char, etc). sein. Sortieren ist eine generische Operation, d.h. Elemente unterschiedlichsten Typs können sortiert werden, sofern eine sinnvolle Ordnungsrelation existiert, Liegen die Elemente vollständig im Hauptspeichers vor, sprechen wir von internem Sortieren, ansonsten von externem Sortieren. Dabei ist der wesentliche Unterschied, dass beim internen Sortieren leicht auf beliebige Elemente zugegriffen werden kann. Bein externen Sortieren kann das nur sequenziell oder allenfalls blockweise geschehen. Eine oder mehrere Element-Unterstrukturen definieren den (nicht notwendigerweise eindeutigen) Schlüssel, der einen eindeutigen Wert besitzt. Ist der Schlüssel nicht eindeutig, so kann es mehrere auch unterschiedliche Elemente mit gleichem Schlüssel geben. Sortierverfahren die die ursprüngliche Reihenfolge von Elementen gleichen Schlüssels beibehalten heißen stabil. Auf dem Wertebereich des Schlüsselwertes muss eine Ordnungsrelation definiert sein, die die Reihenfolge der Schlüsselwerte festlegt. 69

70 2.2.2Beispiele Kartenspiel Element = Schlüssel unterschiedliche Ordnungsrelationen (Für Skat, Doppelkopf, …) Telefonbuch: Name, Vorname, Telefonnr Element > Schlüssel Alphabet als Ordnungsrelation … Tafel 70

71 2.2.3Framework für Implementierungen interface ITEM { boolean less(ITEM v); } class Sort { static boolean less(ITEM v, ITEM w) { return v.less(w); } static void exch(ITEM[] a, int i, int j) { ITEM t = a[i]; a[i] = a[j]; a[j] = t; } static void compExch(ITEM[] a, int i, int j) { if (less(a[j], a[i])) exch (a, i, j); } static void sort(ITEM[] a, int l, int r) { example(a, l, r); } static void example(ITEM[] a, int l, int r) { for (int i = l+1; i <= r; i++) for (int j = i; j > l; j--) compExch(a, j-1, j); } }s class myItem implements ITEM // Key ist int { private int key; public boolean less(ITEM w) { return key < ((myItem) w).key; } void read() { key = In.getInt(); } void rand() { key = (int) (1000 * Math.random()); } public String toString() { return key + ""; } } class myItem implements ITEM // Key ist string { String key; public boolean less(ITEM w) { return key.compareTo(((myItem) w).key)<0; } void read() { key = In.getString(); } void rand() { int a = (int)('a'); key = ""; for (int i = 0; i < 1+9*Math.random(); i++) key += (char) (a + 26*Math.random()); } public String toString() { return key; } } 71

72 Übung 2.2: 1.Implementieren Sie dieses Framework und wenden Sie es in einem einfachen Fall an. 72

73 2.3.Elementare Sortieralgorithmen … da Sortieren eine so grundlegende Operation in der Informatik ist, gibt es schon seit einigen Jahrzehnten eingeführte Algorithmen, die teilweise optimiert wurden und immer noch Einsatz finden: 1.Selection Sort (Sortieren durch Auswählen) 2.Insertion Sort (Sortieren durch Einfügen) 3.Shellsort 4.Bubblesort 5.Vergleich sorting-algorithms.com 73

74 2.3.1Selection Sort (Sortieren durch Auswählen) Idee: Suche das kleinste Element (z.B. einer Liste) und tausche es mit dem Element an der ersten Position. Betrachte dann den Rest der Liste und gehe ebenso vor Beispiel instabil: 3 kommt hinter

75 2.3.1Selection Sort: Implementierung 1 // Sorts array a starting from index l up to index r static void selection(ITEM[] a, int l, int r) { // iterates through list for (int i = l; i < r; i++) { int min = i; // initialize index to minimum // iterate through unsorted part of list for (int j = i+1; j <= r; j++) { if (less(a[j], a[min])) { min = j; // index to minimum has changed } exch(a, i, min); // swap first element with minimum // even if i=min, i.e. minimum is already // in front } 75

76 2.3.1Selection Sort: Diskussion 1 Eigenschaften: Nicht stabil (gleiche Keys können umgeordnet werden) Nicht adaptiv, d.h. Algorithmus profitiert nicht von günstigen Vorgaben:, z.B. von einer vorhandenen Sortierung. Aufwand: im Beispiel: …+1 Vergleiche = (n*(n+1)) / 2 O(n 2 ) Im Beispiel: 11 Umordnungen (Einsortierungen) = n O(n) Best Case = Worst Case = Average Case = O(n 2 ) O(1) Platzkomplexität Selection Sort wird (trotz schlechten Aufwandes) eingesetzt für das Sortieren von Daten mit großen Elementen mit jeweils kleinen Schlüsseln: … bei diesen Daten sind die Kosten für den Vergleich sehr viel kleiner als die Kosten für die Umordnung. Der Aufwand für die Umordnungen ist mit O(n) kleiner als in den meisten anderen Verfahren. 76

77 2.3.1Selection Sort: Implementierung 2 (stabil) // Sorts a linked list, by removing it from in-list (h.next) and // inserting max in front of the out-list (out) // (head of list is dummy) // find node previous to minimum in linked list private static Node findMin(Node h) { for (Node t = h; t.next != null; t = t.next) if (t.next.item < h.next.item) h = t; return h; } // iterate through in-list and move max to head of out-list static Node selection(Node h) { Node head = new Node(-1, h), out = null; while (head.next != null) { Node min = findMin(head); Node t = max.next; min.next = t.next; // remove from in-list t.next = out; out = t; // put in front of out-list } return out; } 77

78 2.3.1Selection Sort: Diskussion 2 Eigenschaften: stabil Nicht adaptiv, d.h. Algorithmus profitiert nicht von günstigen Vorgaben:, z.B. von einer vorhandenen Sortierung. Aufwand: wie bei Implementierung 1 schlechtere O(n) Platzkomplexität Selection Sort wird (trotz schlechten Aufwandes) eingesetzt für das Sortieren von Daten mit großen Elementen mit jeweils kleinen Schlüsseln: … bei diesen Daten sind die Kosten für den Vergleich sehr viel kleiner als die Kosten für die Umordnung. Der Aufwand für die Umordnungen ist mit O(n) kleiner als in den meisten anderen Verfahren. 78

79 2.3.2Insertion Sort (Sortieren durch Einfügen) Idee: Wie beim Sortieren eines Kartenblattes auf der Hand eines Spielers werden neue (rechts neben den bereits sortierten) Karten in das bereits sortierte Kartenblatt an der richtigen Stelle eingefügt. Angewandt auf eine Liste existiert also immer eine bereits sortierte Teilliste (am Anfang der Liste), die bei jeder Iteration um ein weiteres korrekt einsortiertes Element erweitert wird.. Beispiel: // swapping of 1 is exhausting

80 2.3.2Insertion Sort: Implementierung Variante 1 // sort array ITEM[] between indexes l and r static void example(ITEM[] a, int l, int r) { // iterate through list (starting with second position) from ltr for (int i = l+1; i <= r; i++) { // consider first element after already sorted list. // Iterate from rtl through already sorted list // and swap elements if considered one is smaller for ( int j = i; j > l; j-- ) { compExch(a, j-1, j); } 80

81 2.3.2Insertion Sort: Implementierung Variante 2 Bringt zunächst das kleinste Element nach vorn, so dass der sortierte Teil nicht mehr vollständig verschoben werden muss, wenn immer wieder kleinste Elemente einzusortieren sind. Die innere Schleife beinhaltest keine Vertauschungen ( compExch = drei Zuweisungen) sondern nur eine Zuweisung ( a[j] = a[j-1] ) Die innere Schleife terminiert, sobald die richtige Position gefunden ist. 81 // sort array ITEM[] between indexes l and r static void insertion(ITEM[] a, int l, int r) { int i; // initially bring smallest element to front for (i = r; i > l; i--) compExch(a, i-1, i); // iterate through list starting with second position from left to right for (i = l+2; i <= r; i++) { int j = i; ITEM v = a[i]; // remember element to be inserted // Iterate from right to left through already sorted list // and shift elements to right... while (less(v, a[j-1])) //... stop on correct position { a[j] = a[j-1]; j--; } // insert element to its proper position a[j] = v; }

82 2.3.2Insertion Sort: Diskussion Eigenschaften Stabil Adaptiv: O(n) Zeitkomplexität, wenn die Daten stark vorsortiert sind kleiner overhead (kompakter Code) Aufwand: Vergleiche :min: O(n), max: O(n 2 ), average O(n 2 ) Bewegungen:min: O(n), max: O(n 2 ), average O(n 2 ) also O(n) für stark vorsortierte oder sortierte Daten. O(1) Platzkomplexität Der Insertion Sort wird eingesetzt, wenn es auf einen stabilen Algorithmus ankommt … … und die Daten stark vorsortiert sind (da er adaptiv ist) … oder die Problemgröße klein ist (da er kompakt ist, also wenig Overhead hat) 82

83 2.3.3Shellsort (Donald L. Shell, 1959) Motivation: Der Insertion-Sort ist langsam, da nur benachbarte Element getauscht werden. Insbesondere sehr kleine Elemente müssen dabei häufig vertauscht werden, um vom Ende an den Anfang zu rutschen Idee: Bei den bislang behandelten Algorithmen ist der linke Teil der Liste jeweils sortiert, als jedes Element links. Beim Shellsort werden Teillisten, bestehend aus den jeweils h-ten Elementen mit dem Insertion-Sort sortiert. Man verkleinert h bis es zu 1 wird. Die Schrittweite des Vertauschens ist anfangs also groß, so dass Elemente recht schnell grob vorsortiert werden. Beispiel (h-Folge: 4,3,1) h = 4h = mit h=1 wird hier abschließend nochmals Insertion-sortiert

84 2.3.3Shellsort: Implementierung // sort array ITEM[] between indexes l and r static void shell(ITEM[] a, int l, int r) { int h; // compute initial value of h depending on lebgth of array (r-l) for (h = 1; h <= (r-l)/9; h = 3*h+1); // dicrease h – by dividing by 3 -> h =...,364,121,40,13,4,1 for ( ; h > 0; h /= 3) { // apply insertion sort - increment not 1 but h for (int i = l+h; i <= r; i++) { int j = i; ITEM v = a[i]; while (j >= l+h && less(v, a[j-h])) { a[j] = a[j-h]; j -= h; } a[j] = v; } 84

85 2.3.3Shellsort: Diskussion Eigenschaften Nicht Stabil Adaptiv: O(n log(n)) Zeitkomplexität, wenn die Daten stark vorsortiert sind kleiner overhead (kompakter Code) Aufwand Vergleiche = Bewegungen : min = max = average O(n 1,2 ) für die Gonnet-Folge: Gonnet-Folge (1984)h 1 = * n, h n = * h n-1 mit = 0,45454 weitere Folgen (mit leicht schlechterem Aufwand): Hibbard-Folge (1969)2 i -1 1,3,7,15,31, … <= h 1 mit n/4 < h 1 < n/2 Knuth-Folge (1973)(3 i -1)/2 1,4,13,40,121, … <= h 1 mit n/4 < h 1 < n/2 O(1) Platzkomplexität Der Shell Sort ist adaptiv, einfach zu implementieren und hat ein besseres Komplexitätsverhalten als O(n 2 ). Daher wird er bei nicht zu umfangreichen Daten eingesetzt. Der Shellsort war zwischen 1959 und 1991 ein Jahr lang der schnellste bekannte Sortieralgorithmus. 85

86 2.3.4.Bubblesort Idee: Durchlaufe die Datei und vertausche die Elemente solange bis alle Elemente am richtigen Ort sind Dadurch bubbeln kleine Elemente nach oben (links), solange bis sie auf noch kleinere stoßen, diese bubbeln dann weiter. Mit jedem Durchgang wird das kleinste nach oben gebubbeld, gleichzeitig werden dabei auch noch andere kleine mitgerissen Beispiel: // stoppt bei Gleichheit

87 2.3.4Bubblesort: Implementierung // sort array ITEM[] between indexes l and r static void bubble(ITEM[] a, int l, int r) { for (int i = l; i < r; i++) for (int j = r; j > i; j--) compExch(a, j-1, j); } // stoppt bei Gleichheit // ab hier kein Bubbeln mehr -> stoppen

88 2.3.4Bubblesort: Diskussion Der Bubblesort ist zwar sehr einfach zu implementieren und stabil, ist aber i.A. langsamer als Selection- und Insertion-Sort (und daher diesen nicht vorzuziehen) Aufwand: Vergleiche : min: O(n), max: O(n 2 ), average O(n 2 ) Bewegungen: min: O(n), max: O(n 2 ), average O(n 2 ) Platzkomplexität: O(1) Der Bubblesort ist sehr ähnlich der Variante 1 des Insertion Sort. Dort wird in der inneren Schleife allerdings der sortierte linke Teil durchlaufen, beim Bubblesort der unsortierte rechte. Der Bubblesort lässt sich noch etwas optimieren, indem die äußere Schleife abgebrochen wird, sobald in der inneren keine Vertauschung mehr stattfindet, denn dann ist die Folge bereits sortiert. Dadurch wird er aber auch nicht weniger aufwändig als Selection- oder Insertionsort. 88

89 2.3.5Indexsort (Schlüsselindizierendes Sortieren) Idee: Gibt es für die N zu sortierenden Elemente eine Hashfunktion, die auf c*N Werte abbildet und die Ordnungsrelation einhält, so kann man innerhalb eines c*N großen arrays die Elemente direkt sortiert ablegen. Dabei wird für jeden unterschiedlichen Hashwert ein Block in der Liste belegt – die Werte in den Blöcken sind also gleich, die Blöcke untereinander sind sortiert. Beispiel: (Index) … 89 Anzahlliste #0=6, #1=4, #2=2, #3=3, Anzahlsummenliste #<0=0, #<1=6 #<2=10, #<3=12

90 2.3.5Indexsort: Implementierung // sort array a[] between indexes l and r // assuming: hash-function h is identical function, i.e. h(x)=x // M: max. number of different keys static void distCount(int a[], int l, int r) { int i // run-variables int cnt[] = new int[M]; // Anzahlliste/Anzahlsummenliste int b[] = new int[a.length]; // help-list for copying // initialize Anzahlliste for (i = 0; i < M; i++) cnt[i] = 0; // compute values of Anzahlliste, iterate from l to r for (i = l; i <= r; i++) cnt[a[i]+1]++; // a[i] starts // compute values of Anzahlliste by summing up previous elements for (i = 1; i < M; i++) cnt[i] += cnt[i-1]; // move numbers to block (and increment within block) for (i = l; i <= r; i++) b[cnt[a[i]]++] = a[i]; // copy helplist b[] back to original list a[] for (i = l; i <= r; i++) a[i] = b[i-l]; } 90

91 2.3.3Indexsort: Diskussion Eigenschaften Stabil Nicht Adaptiv (ist aber egal, da der Aufwand eh klein genug ist) Nicht In-situ: Indexsort benötigt eine Hilfsliste, In-situ-Variante ist nicht stabil Aufwand Vergleiche = Bewegungen : min = max = average O(n) O(n) Platzkomplexität (für Hilfsliste) Der Indexsort ist der schnellste Sortieralgorithmus. Der Indexsort nur auf solche Daten anwendbar, bei denen der Bereich unterscheidbarer Schlüsselwerte innerhalb eines konstanten Faktors der Datengröße bleibt. Ist die Hashfunktion nichttrivial, so wird der Vorteil des Verfahrens durch einen hohen konstanten Multiplikationsfaktor im Aufwand, selbst für große n, aufgefressen. Bei großen n ist die Platzkomplexität ein Problem. 91

92 2.3.5Vergleich SelectionInsertionShellBubbleIndexsort minO(n 2 )O(n)O(n 1,2 ) O(n) O(n) maxO(n 2 )O(n 2 )O(n 1,2 )O(n 2 )O(n) averageO(n 2 )O(n 2 )O(n 1,2 )O(n 2 )O(n) Platz (In-situ)O(1)O(1)O(1)O(1)O(n) Stabilneinjaneinjaja Adaptivneinjajajanein Schnellster Algorithmus ist der Indexsort – der braucht aber eine effiziente Hashfunktion und ist zudem nicht In-situ. Der Shellsort ist ein schneller universeller in-situ Algorithmus, allerdings nicht stabil. Bubble und Insertion-Sort sind sehr vergleichbar – meist ist der Insertion Sort schneller. Der Selection Sort ist der schlechteste Algorithmus, allerdings benötigt er nur O(n) Umordnungen – kommt also bei teuren Umordnungsaktionen in Betracht. 92

93 Übung 2.3: 1.Implementieren Sie einen einfachen Sortieralgorithmus verwenden Sie dabei Java-arrays aus java.util.array – aber zunächst nicht deren sort-methode hash(IhrNachname) = Summe(Ascii(Buchstaben)) mod 5 hash(IhrNachname) = 0 Selection, 1 Insertion, 2 Shell, 3 Bubbke, 4 Index 2.Generieren Sie zu sortierende Daten: zufällig, sortiert, umgekehrt sortiert 3.Bestimmen Sie für diese Daten jeweils: (Stellen Sie das Ergebnis in einer Excel-Graphik dar) Anzahl der Vergleiche Inkrementieren Sie dazu eine globale Variable V Anzahl der Zuweisungen Inkrementieren Sie dazu eine globale Variable Z Laufzeit entfernen Sie vorher die Inkrementierungen von V und Z 4.Vergleichen Sie Ihre Laufzeiten mit der array.sort –Methode aus java.util.array (Erweitern Sie dazu Ihre Excel-Graphik) 5.In welchen Situationen bzw. für welche Daten würden Sie Ihre Algorithmen verwenden ? 93

94 2.4.Fortgeschrittene Sortieralgorithmen 94

95 2.4.1Mergesort Idee: Die Mergesort-Verfahren sortieren (mischen) bereits sortierte Teildatenmengen (beginnend mit ein-elementigen Mengen) zusammen zu immer größeren Datenmengen. Dabei werden die zu sortierenden Mengen rekursiv geteilt und nach erfolgter Sortierung der Teilmengen zu Größeren sortiert zusammengemischt. Beispiel: // sorting 1-element lists 3 4 // sorting 1-element lists // merging 2-element lists 3 9 // sorting 1-element lists 8 1 // sorting 1-element lists // merging 2-element lists // merging 4-element lists 5 7 // sorting 1-element lists 2 5 // sorting 1-element lists // merging 2-element lists // final merging 95

96 2.4.1Mergesort: Mischen (Version 1) Idee: vergleiche paarweise die (sortierten) Listen a und b und kopiere den jeweils kleinsten Wert nach c. Falls eine Liste (a oder b) abgearbeitet ist, können die noch verbleibenden Elemente der anderen Liste uinbesehen ans Ende von c kopiert werden // merge array a[] (from indexes al to ar) and b[] (from index bl to br) into // array c[] (starting from index cl) static void mergeAB(ITEM[] c, int cl, ITEM[] a, int al, int ar, ITEM[] b, int bl, int br ) { int i = al, j = bl; for (int k = cl; k < cl + ar-al + br-bl + 1; k++) { // copy all element of b into c, if a has been finished if (i > ar) { c[k] = b[j++]; continue; } // index i larger as right index // copy all element of a into c, if b has been finished if (j > br) { c[k] = a[i++]; continue; } // index j larger bs right index // move bigger element of a or b into c and increment correspondent index c[k] = less(a[i], b[j]) ? a[i++] : b[j++]; } 96

97 2.4.1Mergesort: Mischen (Version 2) Idee: Die Vergleiche der ersten Version, ob a bzw. b schon abgearbeitet ist sind aufwändig. Das kann man vermeidenindem man b in umgekehrter Reihenfolge an a angehängt und dann jeweils das erste mit dem letzten Element vergleicht, Beispiel: Mische in : invertiere und hänge es an > > > > 7 5 -> // merge two blocks (l to m and m to r) of array a into array aux static void merge(ITEM[] a, int l, int m, int r) { int i, j; // copy first block (l to m) to aux for (i = m+1; i > l; i--) aux[i-1] = a[i-1]; // i at beginning // reverse copy second block (m to r) to aux for (j = m; j < r; j++) aux[r+m-j] = a[j+1]; // j at end // iterate through elements and compare pairwise first and last element for (int k = l; k <= r; k++) { // copy smallest element to aux if (less(aux[j], aux[i])) a[k] = aux[j--]; else a[k] = aux[i++]; } } 97

98 2.4.1Mergesort: Mischen (Version 3: linked list) Der Mergesort eignet sich hervorragend für das Sortieren verketteter Listen. Diese können wie folgt gemischt werden: // merge two blocks referenced by a and b, return link to sorted list static Node merge(Node a, Node b) { Node dummy = new Node(); Node head = dummy, // head is link to head of list, which is a dummy element Node c = head; // c is a running pointer, initialized to head // iterate through both lists until one of them is empty while ((a != null) && (b != null)) { // link c.next to minimim (a or b), link c to this element (end of list), // increase pointer in list where miniumum has been taken from if (less(a.item, b.item)) { c.next = a; c = a; a = a.next; } else { c.next = b; c = b; b = b.next; } } // add rest of list that is not empty to result c.next = (a == null) ? b : a; // do not return 1st dummy element but 1st content element return head.next; } 98

99 2.4.1Mergesort: Top-Down Sortieren (Rekursiv) Idee des Top Down Sortierens (Wie eingangs beschrieben) Der Algorithmus zerlegt die zu sortierende Datenmenge in zwei Teile und sortiert diese durch rekursive Aufrufe unabhängig voneinander. Die Ergebnisse werden dann zusammengemischt // sort a from index l to r static void mergesort(ITEM[] a, int l, int r) { // stop rekursion if only one element is to be considered if (r <= l) return; // split list in two int m = (r+l)/2; // recursively call mergesort for first half mergesort(a, l, m); // recursively call mergesort for second half mergesort(a, m+1, r); // merge the two halfs into one sorted list (by which merge ever) merge(a, l, m, r); } 99

100 2.4.1Mergesort: Bottom-Up Sortieren (nicht rekursiv) Idee des Bottom-Up Sortierens : Der Algorithmus führt berechnet zunächst alle Sortierungen/Mischungen kleiner Datenmengen durch, danach werden die Datenmengen verdoppelt, bis die Gesamtliste sortiert ist. Das ganz funktioniert iterativ: Beispiel: // sorting 1-element lists 3 4 // sorting 1-element lists 3 9 // sorting 1-element lists 8 1 // sorting 1-element lists 5 7 // sorting 1-element lists 2 5 // sorting 1-element lists // merging 2-element lists // merging 2-element lists // merging 2-element lists // merging 4-element lists // final merging 100

101 2.4.1Mergesort: Bottom-Up Sortieren (nicht rekursiv) // procedure to get minimal element of A and B static int min(int A, int B) { return (A < B) ? A : B; } // sort a from index l to r static void mergesort(ITEM[] a, int l, int r) { // need not sort if list is empty if (r <= l) return; // global auxiliary list needed for merging in merge aux = new ITEM[a.length]; // iterate through list by blocks that are doubled each time for (int m = 1; m <= r-l; m = m+m) // iterate through each block for (int i = l; i <= r-m; i += m+m) // merge left half of block with right half merge(a, i, i+m-1, min(i+m+m-1, r)); // do not use i+m+m-1 if r is smaller // important if r-l <> 2 x } 101

102 2.4.1Mergesort: Top-Down Sortieren (linked list) // sorts linked list referenced by c static Node mergesort(Node c) { // need not sort empty list if (c == null || c.next == null) return c; Node a = c; // a is link to 1 st element Node b = c.next; // b is link to 2 nd element // iterate through list until b is at the end (and c in the middle) while ((b != null) && (b.next != null)) { c = c.next; // increase c by one b = (b.next).next; // increase b by two } // c point to the element in the middle // a still point to 1 st element b = c.next; // b points to the element right from the middle (c) c.next = null; // cut of list, right from element in the middle (c) // recursively call mergesort for a and b and merge it return merge(mergesort(a), mergesort(b)); } 102

103 2.4.1Mergesort: Diskussion Eigenschaften Stabil (wenn merge stabil) Nicht Adaptiv kleiner overhead (kompakter Code) Aufwand Vergleiche = Bewegungen : min = max = average O(n log n) O(n) Platzkomplexität (aux-Liste oder Links der verketteten Liste) Der Mergesort ist stabil, hat ein besseres Komplexitätsverhalten als O(n 2 ). und garantiert auch im schlechtesten Fall eine Zeitkomplexität von O(n log n). wegen dieser Garantie wird der Mergesort in vielen Bibliotheken verwendet. Auch Java.util.array verwendet für sort auf object den Mergesort (für andere Typen, z.B. int wird ein – wahrscheinlich – 3-Wege- Quicksort verwendet). Dabei setzt garantiert die Java Runtime Environment nicht den verwendeten Algorithmus, sondern nur die Stabilität des Sortieralgorithmus. 103

104 2.4.1Mergesort: Verbesserungen Beschränkt man die Rekursionstiefe, so dass dieser nicht bei ein-elementigen Mengen stoppt, sondern bereits bei mehr-elementigen, so sind die resultierenden Mengen nicht vollständig sortiert, sondern nur vorsortiert. Auf dieses Resultat kann man nun den Insertion-Sort anwenden, der für diese Fälle ein sehr gutes Laufzeitverhalten O(n) hat. Dadurch erspart man sich die sehr häufig durchlaufenen kleinen Fälle. Dieses Verfahren führt zu einer Leistungssteigerung um 10-15% Beim Mergen wird immer wieder in eine Hilfsliste (aux) umkopiert. Das kann man bei mehrmaligem Aufruf (z.B. in der Rekursion) dadurch vermeiden, dass man die Rolle er Hilfsliste und der zu mischenden Teillisten jeweils vertauscht Damit gewinnt man nochmals ca. 40% Auch der Mergesort für verkettete Listen lässt sich in einer Bottom-Up Strategie realisieren. Dazu kann man z.B. Warteschlangen zur Zwischenspeicherung der immer größer werdenden Teillisten verwenden. Diese Realisierung eignet sich, um bereits vorsortierte Datenmengen zu sortieren. ( Sedgewick, Kapitel 8.7) 104

105 2.4.2Heapsort Idee (erster Ansatz): Beim Heapsort werden Elemente so in eine Struktur eingehängt, dass sie schnell und in sortierter Reihenfolge ausgelesen werden können. Damit das effizient funktioniert wird beim Einfügen die Heap-Eigenschaft eingehalten, das bedeutet, dass die Daten logisch wie ein binärer Baum strukturiert sind (aber als z.B. Liste), wobei kein Nachfolgeknoten kleiner als sein Vorgängerknoten ist. Damit befindet sich in der Wurzel das kleinste Element. Diese Liste nennt man dann Prioritätswarteschlange (Priority-Queue) Beispiel für eine Priority Queue für : (Index) Priority Queue (Heap) (not neccessarily completely sorted) 105

106 2.4.2Heapsort: Implementierung einer PQ Dieser Prototyp implementiert einige grundlegende Teile eines abstrakten Datentyps Priority queue. Insbesondere wir bei Insert das neue Element als Blatt ganz unten (= ganz hinten) an den Heap angefügt. Danach muss nur noch die Heap-Eigenschaft sichergestellt werden. class PQ { static boolean less(ITEM v, ITEM w) { return v.less(w); } static void exch(ITEM[] a, int i, int j) { ITEM t=a[i]; a[i]=a[j]; a[j]=t; } private ITEM[] pq; // array holding priority queue private int N; // counter, how many elements are in queue PQ(int maxN) { pq = new ITEM[maxN]; N = 0; } // constructor boolean empty() { return N == 0; } // is queue empty // insert at end of array void insert(ITEM item) { pq[N++] = item; // add element as last element, increase counter... // heap-property has to be ensured here ! } // get minimal element from head ITEM getmin() {... // heap-property has to be ensured here ! return pq[1]; // returm minimal element, which is lovated at head } }; 106

107 2.4.2Heapsort: Einhaltung der Heap-Eigenschaft Bottom-Up Verfahren: Bei diesem Verfahren wird ein Element (auf Position k) solange mit seinem übergeordneten Element (auf Position k/2) vertauscht, bis dieses kleiner ist private void swim(int k) { while (k > 1 && less(k, k/2)) { exch(k, k/2); k = k/2; } } Top-Down Verfahren: Bei diesem Verfahren wird ein Element (auf Position k) solange mit dem kleineren seiner beiden untergeordneten Element e (auf den Position 2k und 2k+1) vertauscht, bis diese beide nicht mehr kleiner sind private void sink(int k, int N) { while (2*k <= N) { int j = 2*k; if (j < N && less(j, j+1)) j++; if (!less(k, j)) break; exch(k, j); k = j; } 107

108 2.4.2Heapsort: Ein- und Ausfügen Mit swim und sink können jetzt die einfügen und ausfüge-Operationen (des Minimums) implemementiert werden: class PQ {... void insert(ITEM v) { pq[++N] = v; // new element is added at end of list... swim(N); // and swims to top until smaller element is found } ITEM getmin() { exch(1, N); // biggest is swapped to top by swapping with smallest sink(1, N-1); // this big element on top sinks until it reaches a bigger return pq[N--]; // return the smallest element, which has been swapped to // end of list } }; 108

109 2.4.2Heapsort: alternatives heapify Statt den Heap über aufeinanderfolgende Einfügungen aufzubauen ist es effizienter, den Heap zu erstellen, indem man ihn rückwärts durchläuft und dabei kleine Teil-Heaps von der untersten Ebene nach oben hin aufbaut. Dabei kann man Element der untersten Ebene ignorieren Beispiel : (Index) |

110 2.4.2Heapsort: Implementierung Sortieren kann man nun durch heapify der Liste und anschließendes sequenzielles Ausfügen., wobei das Ausfügen durch Tausch an das Ende der Liste (also In-situ) erfolgt. private void sink(int k, int N) { while (2*k <= N) { int j = 2*k; if (j < N && less(j, j+1)) j++; if (!less(k, j)) break; exch(k, j); k = j; } // heapify the list, starting from N/2 with N=2 x with N >= length(list) for (int k = N/2; k >= 1; k--) sink(k, N); // iterate through heap while (N > 1) { exch(1, N); // exchange big with smallest (which is at top) sink(1, --N); // decrease size and heapify heap with big element at top } 110

111 2.4.2Heapsort: Diskussion Eigenschaften Nicht Stabil Nicht (wirklich) Adaptiv Aufwand Vergleiche = Bewegungen : min = max = average O(n log n) (genauer: < 2n ld n) O(1) Platzkomplexität (In-situ) Der Heapsort ist gut geeignet die k-kleinsten Elemente zu finden, denn dann kann man die Ausleseschleife nach k bereits verlassen. Die Entscheidung zwischen Merge- und Heapsort ist eine Entscheidung zwischen einem stabilen nicht-In-situ (Mergesort) und einem nicht-stabilen In- situ-Algorithmus. Wir werden aber sehen, dass es noch einen schnelleren nicht-stabilen In-situ- Algorithmus gibt. 111

112 2.4.3Quicksort the quick brown fox jumps over the lazy dog abcdeeefghhijklmnoooopqrrsttuuvwxyz 112

113 2.4.4Vergleich MergeHeapQuick minc*O(n log n)c*O(n log n)O(n log n) maxc*O(n log n)c*O(n log n)O(n 2 ) averagec*O(n log n)c*O(n log n)O(n log n) Platz (In-situ)O(n)O(1)O(1) Stabiljaneinnein Adaptivneinneinnein Schnellster Algorithmus ist der Quicksort, der hat aber ein garstiges maximales Laufzeitverhalten. Der Heapsort ist in-Situ aber instabil und langsamer als der Quicksort, garantiert aber ein maximales Laufzeitverhalten O (n log n) Der Mergesort ist stabil aber nicht In-situ und ebenfalls langsamer als der Quicksort, garantiert aber ein maximales Laufzeitverhalten O (n log n) In java.util.array werden sowohl der Mergesort (für object ) als auch der Quicksort (für alle anderen Typen) verwendet. 113

114 Übung 2.4: 1.Implementieren Sie einen fortgeschrittenen Sortieralgorithmus verwenden Sie dabei Java-arrays aus java.util.array – aber zunächst nicht deren sort-methode hash(IhrNachname) = Summe(Ascii(Buchstaben)) mod 3 hash(IhrNachname) = 0 Mergesort, 1 Heapsort, 2 Quicksort 2.Generieren Sie zu sortierende Daten: zufällig, sortiert, umgekehrt sortiert 3.Bestimmen für diese Daten jeweils: (Stellen Sie das Ergebnis in einer Excel-Graphik dar) Anzahl der Vergleiche Inkrementieren Sie dazu eine globale Variable V Anzahl der Zuweisungen Inkrementieren Sie dazu eine globale Variable Z Laufzeit entfernen Sie vorher die Inkrementierungen von V und Z 4.Vergleichen Sie Ihre Laufzeiten mit der array.sort –Methode aus java.util.array (Erweitern Sie dazu Ihre Excel-Graphik) 5.In welchen Situationen bzw. für welche Daten würden Sie Ihren Algorithmus verwenden ? 114


Herunterladen ppt "1 Algorithmen & Datenstrukturen Prof. Dr. Peter Kneisel Sommersemester 2009."

Ähnliche Präsentationen


Google-Anzeigen