Algorithmen und Datenstrukturen CS1017 Th. Letschert TH Mittelhessen Gießen University of Applied Sciences Datenstrukturen I: Lineare Sequenzen / Listen – Datenstrukturen für Sequenzen – Sequenzen in der Java-API: Listen und Ströme – Iteratoren – Algorithmen auf Sequenzen Lineare Sequenzen / Listen Funktionaler Datentyp Liste Eine Liste ist eine endliche Kollektion von Werten, bei der jedem Wert eine eindeutige Position aus dem Bereich der natürlichen Zahlen zugeordnet ist. (a, b, c) ~ 0 → a, 1 → b, 2 → c Listen-Werte: Alle Sequenzen (endliche total geordnete Folgen) von Element-Werten Operationen: – Den Wert an (Index-) Position i feststellen – Feststellen der Länge (Anzahl der Elemente) Seite 2 Lineare Sequenzen / Listen Imperativer Datentyp Liste Eine imperative (veränderliche) Liste ist eine endliche Kollektion von Variablen, bei der jeder Variablen eine eindeutige Position aus dem Bereich der natürlichen Zahlen zugeordnet ist. 0 → a , ( a , b , c ) ~ 1 → b , 2 → c Listen-Variable: Alle veränderlichen Sequenzen (endliche total geordnete Folgen) von Variablen Operationen: – Den Wert an (Index-) Position i feststellen – Feststellen der Länge (Anzahl der Elemente) – Ein Element an einer bestimmten Position einfügen – Eventuell weitere abgeleitete Operationen (Vorgänger / Nachfolger, etc.) – Eventuell Beschränkungen bei den Einfüge- und Zugriffs-Operationen (Nur am Anfang / Ende der Sequenz) – ... Seite 3 Lineare Sequenzen / Listen Im Zusammenhang mit Listen werden viele Begriffe gebraucht die eine völlig unterschiedliche, ähnliche oder gleiche Bedeutung haben. Es ist wichtig zu wissen, was genau gemeint ist, bzw. gemeint sein könnte und selbst eine klare Ausdrucksweise zu benutzen. Liste als (Daten-) Typ Alles mit einer von aussen beobachtbaren linearen Ordnung* Folge, Sequenz, lineare Sequenz, Vektor, Strom, … sind weitere Bezeichnungen Unterscheidung: – funktional (= unveränderlich) „ist unveränderlich“ ~ „ist ein Wert“ – imperativ (= veränderlich) Die Liste und eventuell aber nicht unbedingt auch ihre Elemente können verändert werden „ist veränderlich“ ~ „ist eine Variable“ *siehe Veranstaltung Diskrete Mathematik oder https://de.wikipedia.org/wiki/Ordnungsrelation Seite 4 Lineare Sequenzen / Listen Liste als (Daten-) Struktur Alles was tatsächlich in einer linearen Ordnung angeordnet ist Weitere Bezeichnungen: Folge, Sequenz, lineare Sequenz, Vektor, Strom, … Auch hier kann zwischen veränderlich und unveränderlich unterschieden werden. Datenstrukturen werden aber in der Regel als veränderliche Beziehungen realisiert, auch wenn sie von aussen unveränderlich zu sein scheinen Seite 5 Imperative Listen (lineare Sequenzen) in der Java-API Imperative Listen / Sequenzen in der Java-API Imperative (Veränderliche) Listen werden in unterschiedlichen Varianten von der Java-API zur Verfügung gestellt Interfaces (Varianten in den möglichen Operationen) – java.util Interface List<E> Einfügen und Entnehmen an beliebigen Positionen möglich – java.util Interface Queue<E> Einfügen nur an einem Ende, Entnehmen nur am anderen Ende möglich – java.util Interface Deque<E> Einfügen und Entnehmen nur an den beiden Enden möglich – … Klassen (ADTs, Varianten in der Implementierung der Interfaces) – java.util Class ArrayList<E> Array-basierte Listenimplementierung – java.util Class Vector<E> Array-basierte Listenimplementierung mit synchronisierten Operationen – java.util Class ArrayDeque<E> Array-basierte Listenimplementierung mit Einfügen und Entnehmen nur an den beiden Enden möglich – … Seite 6 Funktionale Listen (lineare Sequenzen) in der Java-API Funktionale Listen / Sequenzen in der Java-API: Streams Die Java-API unterstützt ab Java-8 auch die funktionale Programmierung Dazu gehören funktionale / unveränderliche Listen mit speziellen Operationen Streams java.util.stream.Stream ist ein Interface – es bietet eine funktionale Sicht auf nicht-funktionale Listen / Sequenzen – Beispiel: import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class Streams_App { public static void main(String[] args) { List<Integer> lst = Arrays.asList(new Integer[]{1,2,3,4,5,6,7,8,9,10}); int sum1 = 0; for (int i: lst) { sum1 = sum1+i; } System.out.println(sum1); Klassisch imperativ auf imperativer Liste: Summe mit Iterator Imperative Liste bekommt funktionales Gesicht und wird funktional summiert Stream<Integer> strm = lst.stream(); int sum2 = strm.reduce(0, (a, x) -> a+x ); } System.out.println(sum2); } Seite 7 Datenstrukturen für Listen Listen auf Basis von Arrays Listen können mit Hilfe von Arrays implementiert werden Datentyp Liste Vorteil Datenstruktur Array Effiziente Zugriffsoperationen Nachteil Aufwendiges Einfügen und Entfernen im Inneren Aufwendiges Vergrößern Eventuell Verschwendung von Speicherplatz Array-basierte Liste Implementierungen in der Java-API – ArrayList<E> – Vector<E> veraltet – Stack<E> veraltet Verwenden Sie die Klasse ArrayList, es sei denn Sie wissen, dass und warum diese nicht geeignet ist. Verwenden Sie dann eine andere Klasse aus der API. Gibt es nichts Geeignetes dann implementieren Sie eine Klasse auf Basis von java.util.AbstractList. Seite 8 Datenstrukturen für Listen Eigene Listenimplementierung auf Basis von Arrays Listen auf Basis von Arrays sind leicht zu implementieren public class MyArrayList<E> { private int topIndex = private int arraySize = Aufgabe: 0; // index of first empty position 10; // actual array size Ist dies eine Implementierung von java.util.List<E> ? @SuppressWarnings("unchecked") private E[] array = (E[]) new Object[arraySize]; @SuppressWarnings("unchecked") public boolean add(E element) { if (topIndex == arraySize) { E[] arrayTemp = (E[]) new Object[arraySize+10]; System.arraycopy(array, 0, arrayTemp, 0, arraySize); array = arrayTemp; arraySize = arraySize+10; } array[topIndex++] = element; public static void main(String[] args) { return true; MyArrayList<String> l = new MyArrayList(); } for (int i=0; i<15; i++) { l.add("blub "+i); } public E get(int index) { return array[index]; } } System.out.println(l.size()); public int size() { return topIndex; } } for (int i=0; i<l.size(); i++) { System.out.println(l.get(i)); } Seite 9 Datenstrukturen für Listen Eigene Listenimplementierung auf Basis von Arrays Listen auf Basis von Arrays sind leicht zu implementieren Allerdings erfüllt eine einfache Implementierung nicht das Interface java.util.List<E> public class MyArrayList<E> implements List<E> { … Es fehlen sehr (!) viele Methoden die im Interface deklariert sind! } Seite 10 Datenstrukturen für Listen Eigene Listenimplementierung auf Basis von Arrays Die Java-API bietet eine Basisklasse zur Erleichterung der Implementierung eigener Array-basierter Listen: java.util Class AbstractList<E> Implementierungsskelett für eigene Listenimplementierungen durch Ableitung import java.util.AbstractList; import java.util.List; public class MyArrayList<E> extends AbstractList<E> implements List<E> { private int topIndex = private int arraySize = 0; // index of first empty position 10; // actual array size @SuppressWarnings("unchecked") private E[] array = (E[]) new Object[arraySize]; } @SuppressWarnings("unchecked") @Override public boolean add(E element) { if (topIndex == arraySize) { E[] arrayTemp = (E[]) new Object[arraySize+10]; System.arraycopy(array, 0, arrayTemp, 0, arraySize); array = arrayTemp; arraySize = arraySize+10; } array[topIndex++] = element; return true; } @Override public E get(int index) { return array[index]; } @Override public int size() { return topIndex; } Seite 11 Die fehlenden Methoden werden von AbstractList geliefert! Datenstrukturen für Listen Verwendungs-Beispiel der selbst definierten Liste: public static void main(String[] args) { Fragen: List<String> l = new MyArrayList<>(); 1. Warum gibt add true zurück? for (int i=0; i<15; i++) { l.add("blub "+i); } 2. Funktionieren beide (überladenen) Varianten der Methode add? 3. Funktionieren die Schleifen? l.add(5, "Hi"); 4. Wie nennt man die Form der dritten Schleife? System.out.println(l.size()); 5. Implementiert die Klasse das Interface Iterable<E>? for (int i=0; i<l.size(); i++) { System.out.println(l.get(i)); } } for (String s : l) { System.out.println(s); } Seite 12 Datenstrukturen für Listen Verwendungs-Beispiel der selbst definierten Liste: public static void main(String[] args) { Fragen: List<String> l = new MyArrayList<>(); 1. Warum gibt add true zurück? Das Interface List verlangt es so for (int i=0; i<15; i++) { l.add("blub "+i); } 2. Funktionieren beide (überladenen) Varianten der Methode add? Nein, nur die erste, siehe API zu AbstractList l.add(5, "Hi"); 3. Funktioniert die Schleifen? Ja! System.out.println(l.size()); 4. Funktioniert die zweite Schleife? Ja! for (int i=0; i<l.size(); i++) { System.out.println(l.get(i)); } } 5. Wie nennt man die Form der dritten Schleife? for each Schleife for (String s : l) { System.out.println(s); } 6. Implementiert die Klasse das Interface Iterable<E>? Ja: MyArrayList ~extends~> AbstractList ~implements~> List ~extends~> Iterable Seite 13 Iteratoren for-Each-Schleife ~> Iterator Elegantes Durchlaufen einer iterierbaren Kollektion Basiert auf einem Iterator. Iterator: Mechanismus zum Durchlaufen einer Kollektion ohne deren innere (Daten-) Strukturen kennen zu müssen. Iterator<Integer> iter = c.iterator(); Eine ordentliche Kollektion ist iterierbar !! Sie implementiert das Interface Iterable Sie hat eine Methode Iterator diese Methode liefert einen Iterator. while ( iter.hasNext() ) { System.out.println( iter.next() ); } automatische Umwandlung durch den Compiler for (int i : c ) System.out.println(i); Die Maus (der Iterator) agiert im Hintergrund Seite 14 Iteratoren Kollektion Iterable und Iterator Iterable – Die Kollektion Iterator – Der Mechanismus um die Werte der Kollektion zu durchlaufen Ein Iterator kann das jeweils nächste Element zur Verfügung stellen und testen, ob alle Elemente durchlaufen wurden. Iterator Sinn – Die Kollektion kann durchlaufen werden ohne den Aufbau und die Organisation der Kollektion kennen zu müssen Geheimnisprinzip: Ich will nicht jeden in meinen Keller lassen der bei mir eine Flasche nach der anderen trinkt! java.lang Iterable<T> java.lang Iterable<T> Iterator<T> iterator(); Iterator<T> iterator(); iterator java.util List<T> java.util List<T> java.lang Iterator<T> java.lang Iterator<T> next():T next():T hasNext():boolean hasNext():boolean java.util AbstractList<T> java.util AbstractList<T> MyArrayList<T> MyArrayList<T> Seite 15 iterator Iteratoren Iterator Mechanismus zum Durchlaufen einer Kollektion – deren inneren Aufbau man nicht kennt – und der nicht offen gelegt werden soll. Die Kollektion stellt ihren Nutzern einen Iterator zur Verfügung Sie ist damit iterierbar (erfüllt java.lang Interface Iterable<T>) Ein Iterator kann das jeweils nächste Element zur Verfügung stellen und testen, ob alle Elemente durchlaufen wurden. Jede Kollektion, die Sie schreiben, muss iterierbar sein – es sei denn Sie haben wirklich gute Gründe, die dagegen sprechen. Seite 16 Iteratoren Beispiel: Array-basierte Liste mit eigenem Iterator import java.util.Iterator; public class SimpleIterableList<E> implements Iterable<E> { private int topIndex = private int arraySize; private E[] element; 0; // index of first empty position // maximal size public SimpleIterableList(int maxSize) { this.arraySize = maxSize; element = (E[]) new Object[this.arraySize]; } public void add(E e) { if (topIndex == arraySize) throw new IllegalStateException(); element[topIndex++] = e; } @Override @Override public Iterator<E> iterator() { public Iterator<E> iterator() { return new Iterator<E>() { return new Iterator<E>() { private int pos = 0; private int pos = 0; @Override @Override public boolean hasNext() { public boolean { return pos hasNext() < topIndex; return pos < topIndex; } } public E get(int index) { if (index < 0 || index >= topIndex) throw new IllegalArgumentException(); return element[index]; } @Override @Override public E next() { public E next() { return element[pos++]; return element[pos++]; } } } @Override @Override public void remove() { public void remove() { throw throw new UnsupportedOperationException(); new UnsupportedOperationException(); } } }; }; Seite } 17 } Datenstrukturen für Listen Listen auf Basis von verketteten Zellen (verkettete Listen) Listen können mit Hilfe von verketteten Zellen implementiert werden Vorteil Datentyp Liste Schnelles Einfügen / Entfernen Datenstruktur Nachteil Langsamer Indexzugriff Implementierungen in der Java-API Liste mit Verkettung – LinkedList<E> Verwenden Sie die Klasse LinkedList, wenn Einfügeund Entnahme-Operationen häufig vorkommen. Seite 18 Datenstrukturen für Listen Listen auf Basis von verketteten Zellen (verkettete Listen) Die Java-API bietet eine Basisklasse zur Erleichterung der Implementierung eigener verketteter Listen java.util Class AbstractSequentialList<E> Implementierungsskelett für eigene Listenimplementierungen durch Ableitung Komplexe Aufgabe da ein ListIterator implementiert werden muss. Seite 19 Datenstrukturen für Listen Beispiel verkettete Liste: Klassisch mit null-Zeiger Zur Demonstration des Umgangs mit Verkettungen hier ein einfaches Beispiel einer verketteten Liste. public class MyLinkedList<E> { private static class Node<E>{ E data; Node<E> next; } Node(E data) { this.data = data; } ... private int nodeCount = 0; private Node<E> first; first public int size() { return nodeCount; } Knoten speichern die Informationen und einen Verwies auf den nächsten Knoten } Seite 20 Datenstrukturen für Listen Beispiel verkettete Liste: Klassisch mit null-Zeiger Anhängen, Abfragen und Löschen eines Elements public void add(E element) { if (first == null) { first = new Node<>(element); } else { Node<E> l = first; while (l.next != null) { l = l.next; } l.next = new Node<>(element); } nodeCount++; } public E get(int i) throws NoSuchElementException { Node<E> l = first; int j = 0; while (i != j) { if (l == null) { throw new NoSuchElementException(); } j++; l = l.next; } if (l == null) { throw new NoSuchElementException(); } return l.data; } public void delete (int i) throws NoSuchElementException { if (first == null) { throw new NoSuchElementException(); } int j = 0; Node<E> act = first; // points to the j-th element Node<E> prev = null; while (j < i && act != null) { prev = act; act = prev.next; j++; } if (act == null) { throw new NoSuchElementException(); } else { nodeCount--; if (prev != null) { prev.next = act.next; } else { first = act.next; } } } Seite 21 Datenstrukturen für Listen Beispiel verkettete Liste: Klassisch mit null-Zeiger Die Liste soll iterierbar sein. Dazu muss sie das Interface java.util.Iterable implementieren import java.util.Iterator; import java.util.NoSuchElementException; public class MyLinkedList<E> implements Iterable<E> { private static class Node<E>{ E data; Node<E> next; } Node(E data) { this.data = data; } private int nodeCount = 0; private Node<E> first; . . . } @Override public Iterator<E> iterator() { // TODO Auto-generated method stub return null; } Was muss diese Methode liefern? Seite 22 Datenstrukturen für Listen Beispiel verkettete Liste: Klassisch mit null-Zeiger Die Liste soll iterierbar sein. Dazu muss sie das Interface java.util.Iterable implementieren @Override public Iterator<E> iterator() { return new Iterator<E>() { @Override public boolean hasNext() { // TODO Auto-generated method stub return false; } @Override public E next() { // TODO Auto-generated method stub return null; } @Override public void remove() { // TODO Auto-generated method stub } } }; Seite 23 Datenstrukturen für Listen Beispiel verkettete Liste: Klassisch mit null-Zeiger Die Liste soll iterierbar sein. Dazu muss sie das Interface java.util.Iterable implementieren @Override public Iterator<E> iterator() { return new Iterator<E>() { Node<E> actNode = first; @Override public boolean hasNext() { return actNode != null; } @Override public E next() { if (actNode == null) { throw new NoSuchElementException(); } E value = actNode.data; actNode = actNode.next; return value; } @Override public void remove() { throw new UnsupportedOperationException(); } } }; Seite 24 Vergleichen Sie die Funktionalität dieser Methoden mit der der entsprechenden Methoden von java.util.Iterator<E> Algorithmen, Datentypen und Datenstrukturen Algorithmen auf Listen Algorithmen können „auf der Schnittstelle“ oder im „Inneren einer Liste“ agieren. – Algorithmen auf dem Datentyp Liste Beispiel Suchen: Die Suche verwendet die öffentlichen Methoden der Liste – Algorithmen auf sequentiellen Datenstrukturen (Array / Verkettete-Liste) Beispiel Suchen: Die Suche operiert auf den Datenstrukturen der Listen-Implementierung public <T> boolean seach(List<T> l, T element) { for (int i= 0; i<l.size(); i++) { if (l.get(i).equals(element)) { return true; } } } Suche in einer Liste: Laufzeit: O(n) da schlimmstenfalls auf alle n Elemente zugegriffen werden muss. return false; Angenommen die Liste ist als verkette Liste implementiert – so wie MyLinkedList oben – welche Komplexität hat der Algorithmus in dieser Implementierung dann? Seite 25 Algorithmen, Datentypen und Datenstrukturen Algorithmen auf Listen Algorithmen können „auf der Schnittstelle“ oder im „Inneren einer Liste“ agieren. public <T> boolean seach(List<T> l, T element) { for (int i= 0; i<l.size(); i++) { if (l.get(i).equals(element)) { return true; } } } return false; Suche in einer Liste: Laufzeit dieses Algorithmus': O(n): Für den Zugriff l.get(i) wird konstante Laufzeit angenommen. Wenn die Liste als verkette Liste implementiert wurde, dann ist die Annahme O(1) für l.get(i) falsch und damit die Annahme O(n) für die Suche ebenfalls. Die sequentielle Suche als solche hat aber trotzdem die Komplexität O(n)! Sie ist definiert unter der Voraussetzung, dass der Zugriff auf ein Element der Liste O(1) ist. – Eine Annahmen, die sinnvollerweise implizit bei allen Komplexitäts-Betrachtungen von Algorithmen auf Listen vorausgesetzt wird. Seite 26 Algorithmen, Datentypen und Datenstrukturen Algorithmen auf Listen Algorithmen operieren auf Datentypen, hinter denen dann die Datenstrukturen der Implementierung dieser Datentypen liegen Beispiel Suchen: public <T> boolean seach(MyLinkedList<T> l, T element) { for (int i= 0; i<l.size(); i++) { if (l.get(i).equals(element)) { return true; } } } O(n) Operationen auf der Liste (get) return false; public class MyLinkedList<E> { . . . public E get(int i) throws NoSuchElementException { Node<E> l = first; int j = 0; while (i != j) { if (l == null) { throw new NoSuchElementException(); } j++; l = l.next; } if (l == null) { throw new NoSuchElementException(); } return l.data; } . . . } Seite 27 Jede get-Operation ist O(n) Diese Suche ist O(n2). Das liegt aber nicht an am schlechten Suchalgorithmus, sondern an einer für dieses Einsatz ungeeigneten, schlechten Datenstruktur. Algorithmen, Datentypen und Datenstrukturen Algorithmen und Datentypen / Datenstrukturen Für Algorithmen sind Datenstrukturen interessant. Der Aufruf einer Methode eines Datentyps ist „nichts weiter als“ ein Methodenaufruf. (Die Veranstaltung heißt darum auch „Datenstrukturen und Algorithmen“) Trotzdem spielen Datentypen eine wichtige Rolle : Datentypen – erfordern die Implementierung einer bestimmten Funktionalität. – Diese wird durch Algorithmen realisiert, – Deren Effizienz massiv davon beeinflusst wird, mit welchen Datenstrukturen der Datentyp implementiert wurde. Sequenz / Sequentielle Datenstruktur – Datentypen und Datenstrukturen werden oft verwechselt oder identifiziert Das ist teilweise berechtigt, verwirrt aber gelegentlich. – Liste ist für uns ein Datentyp, bei dem unterschiedliche Datenstrukturen zur Implementierung verwendet werden können – Sequenz oder Sequenzielle Datenstruktur Ein Datentyp mit der Eigenschaft dass Alle Elemente können über einen Index zugegriffen werden können der Zugriff auf jedes beliebige Element ist O(1) Seite 28 Algorithmen auf Sequenzen Sequenz / Sequenzielle Datenstruktur – Datenstruktur (elementarer Datentyp) – mit Listen-Operationen und – mit Zugriffsoperationen der Komplexität O(1) Algorithmen auf Sequenzen – Suchen finde ein Element (mit einer bestimmten Eigenschaft) in einer Sequenz – Sortieren erzeuge eine Permutation der Elemente, derart, dass ein Ordnungskriterium erfüllt ist. – Muster erkennen z.B. finde die längste gemeinsame Teilsequenz von zwei Sequenzen ... Seite 29