Vorlesung Programmieren 7. Listen und Abstrakte Datentypen Prof. Dr. Ralf H. Reussner Version 1.0 LEHRSTUHL FÜR SOFTWARE-DESIGN UND QUALITÄT (SDQ) INSTITUT FÜR PROGRAMMSTRUKTUREN UND DATENORGANISATION (IPD), FAKULTÄT FÜR INFORMATIK sdq.ipd.kit.edu KIT Universität des Landes Baden-Württemberg und nationales Forschungszentrum in der Helmholtz-Gemeinschaft www.kit.edu
Rekursive Datentypen Wir haben bisher Arrays verwendet, um Folgen von Elementen (Kollektionen) zu implementieren. Mit uns bereits bekannten Mitteln lassen sich Kollektionen auch auf andere Weise implementieren: als rekursiver Datentyp Rekursiver Datentyp: Objekte einer Klasse enthalten Verweise auf Objekte derselben Klasse. Beispiel: class Recursive { private int element; private Recursive next;... class Tree { private int element; private Tree leftsubtree; private Tree rightsubtree;... 2
Einfach verkettete Listen Idee Eine (einfach) verkettete Liste besteht aus Zellen, wobei jede Zelle 1. das eigentliche Listenelement (den Inhalt der Zelle) und 2. eine Referenz auf die Zelle des nächsten Listenelements (Verkettung) enthält. Die Verkettung der letzten Listenzelle verweist auf null, um das Ende der Liste zu kennzeichnen. Grafische Darstellung: 5 10 28-3 78 Inhalt Verkettung null Listenende 3
Einfach verkettete Listen in Java Wir betrachten zunächst Listen von Vector2D-Objekten. Listenzellen in Java: class ListCell { private Vector2D content; private ListCell next; public ListCell(Vector2D p, ListCell n) { this.content = p; this.next = n; Listen in Java: class List { private ListCell head; public List() {... head = null; // erzeugt leere Liste 4
Erstellung einer Liste Um nun eine Liste aufzubauen, müssen die ListCell-Objekte erzeugt werden: p q r Vector2D p = new Vector2D(), q = new Vector2D(), r = new Vector2D(); ListCell cell3 = new ListCell(r, null); //r ist letzter Vector2D der Liste ListCell cell2 = new ListCell(q, cell3);//q vor r in der Liste ListCell cell1 = new ListCell(p, cell2);//p ist erstes Element, vor q Falls nur die Referenz auf das erste Element direkt benötigt wird: ListCell c = new ListCell(p, new ListCell(q, new ListCell(r, null))); Mit der Referenz auf die erste Listenzelle hat man Zugriff auf die gesamte Liste (über das next-attribut der Klasse ListCell) 5
Operationen auf Listen Einfügen von Listenelementen am Beginn der Liste am Ende der Liste Löschen von Listenelementen Suche nach Elementen class Vector2DList { private class ListCell { Vector2D content; ListCell next; ListCell(Vector2D p, ListCell n) { this.content = p; this.next = n; private ListCell head; public Vector2DList() { this.head = null; // empty list public void addfirst(vector2d p) {... public void addlast(vector2d p) {... public void remove(vector2d p) {... public boolean contains(vector2d p) {... 6
Einfügen von Listenelementen: addfirst Beispiel: vorher: Implementierung: q head r public void addfirst(vector2d p) { ListCell newhead = new ListCell(p, this.head); this.head = newhead; nach addfirst(p): head p q r 7
Einfügen von Listenelementen: addlast Beispiel: vorher: head q r nach addlast(p): head q r p Implementierung: public Zum Listenende void addlast(vector2d gehen: p) { // add to empty list if ListCell (this.head c = this.head; == null) { while // addfirst(p) (c.next!= null) { this.head c = c.next; = new ListCell(p, null); return; // now c.next == null, and we // add can to insert non-empty a new list cell ListCell c = this.head; while c.next (c.next = new ListCell(p,!= null) { null); c = c.next; // now c.next == null Problem: c.next = leere new ListCell(p, Liste null); 8
Löschen von Listenelementen: remove Beispiel: vorher: head q r p nach remove(r): head q p Korrekte Implementierung: Implementierung: // remove(vector2d r) removes // all list elements, whose // content is the same object // reference as r. // Zu remove(vector2d klären: r) removes // all list elements, whose 1. Was soll passieren, wenn r // content is the same object ListCell mehrfach c = this.head; in der Liste vorkommt? // reference as r. 2. // remove Wann ist head element(s) r gleich einem public while (c Listenelement? void!= null remove(vector2d && c.content == r) Bei Gleichheit r) { { // this.head empty = list c = c.next; der Objektreferenzen oder bei if (this.head == null) { gleichen Objektattributen? // empty return; list? // nothing to do if (c == null) { return; // nothing more to do ListCell c = this.head; Spezifikation: while (c.next!= null) { // traverse void if remove(vector2d (c.next.content list r) == r) { while (c.next!= null) { löscht if (c.next.content // aus found! der Liste now, == r) remove sämtliche { it Listenelemente, // c.next found! now, = c.next.next; remove deren it Inhalt c.next dieselbe = c.next.next; else { Objektreferenz ist wie r. = c.next; c = c.next; public void remove(vector2d r) { Was passiert, wenn wir das erste oder letzte Listenelement löschen? 9 9
Suche nach Listenelementen: contains Beispiel: q head p Implementierung: // contains(vector2d p) returns // true if and only if the list // contains an element, whose // content is the same object // reference as p. Vector2DList l =... (Liste wie oben)... boolean b = l.contains(r); // b ist false boolean c = l.contains(p); // c ist true public boolean contains(vector2d p) { for (ListCell c = head; c!= null; c = c.next) { if (c.content == p) { return true; return false; 10
Listen als abstrakter Datentyp (ADT) Prinzip Geheimnisprinzip Wenn wir eine Liste nur als solche verwenden wollen, interessiert uns der interne Aufbau der Liste eigentlich gar nicht Deswegen: Erstelle eine abstrakte Sicht auf die Liste, welche die Implementierungsdetails komplett vor dem Nutzer verbirgt Realisierung Gewähre den Zugriff auf den Inhalt der Liste nur über eine definierte Schnittstelle Ein Abstrakter Datentyp (ADT) ist eine Menge von Typen eine Menge von Signaturen (d.h. Funktionen mit Rückgabewertstyp und Parametertypen eine Menge von Axiomen (d.h. Einschränkungen auf den Funktionen) 11
Listen als ADT Realisierung class Vector2DList { public Vector2DList()... // Konstruktor (leere Liste) public void addfirst(vector2d p)... // Element am Anfang einfügen public void addlast(vector2d p)... // Element am Ende einfügen public void remove(vector2d p)... // Element löschen public boolean contains(vector2d p)... // Element suchen public int size()... public boolean isempty()...... // Länge der Liste // Liste leer? 12
Iteratoren Die Listen-Schnittstelle bietet bisher nur wenige Möglichkeiten, mit der Liste zu arbeiten Typisches Gebrauchsmuster : Gehe über die Liste und führe für jedes Element eine Arbeit durch Beispiel Arrays: for (int i = 0; i < a.length; i++) { dowork(a[i]); Wie lässt sich dies verallgemeinern? Abstraktes Konzept Iterator Definition Iterator: Ein Iterator bezeichnet einen Zeiger (cursor), mit dem über die Elemente einer Kollektion gewandert (iteriert) werden kann. 13
Iteratoren Am Beispiel der Klasse Vector2DList Schnittstelle: class Vector2DList { public class Iterator { public boolean hasnext(); public Vector2D next(); public Iterator iterator(); // Gibt es ein weiteres Element? // Gebe aktuelles Element zurück und // bewege Zeiger auf das nächste weiter // Erzeugt neuen Iterator der Liste Iterator implementiert als geschachtelte Klasse (nested class; mehr dazu später in der Vorlesung) Verwendung: Vector2DList l = createlist(); Vector2DList.Iterator it = l.iterator(); while (it.hasnext()) { Vector2D v = it.next(); dowork(v); 14
Implementierung Iterator für Vector2DList class Vector2DList {... public class Iterator { // geschachtelte Klasse innerhalb // der Vector2DList-Klasse private ListCell cursor; // cursor, Zeiger private Iterator(ListCell start) { // privater Konstruktor; kann nur this.cursor = start; // von Iterator- und Vector2DList- // Klasse aus aufgerufen werden public boolean hasnext() { return (this.cursor!= null); public Vector2D next() { Vector2D currentcontent = this.cursor.content; this.cursor = this.cursor.next; return currentcontent; public Iterator iterator() { return new Iterator(this.head);... 15
Exkurs: Command / Query-Separation A methodcan either inspect / query the component state (getters), or transform the state of the component but not both Reason: several calls to a method should return the same result. Taken from Bertrand Meyer, Object Oriented Software Construction, 2 nd Ed. 1992 16 Ralf Reussner - CBSE: Design of Components
Exkurs: Command / Query-Separation (2) Drawback: more method invocations Violations: the iterator-pattern implementations in java and C# Java while (i.hasnext()) { Object ele = i.next(); boolean hasnext(): query Object next(): query and command C# while (i.movenext()) { Object ele = i.current(); boolean MoveNext(): query and command Object Current(): query Note that iterator (IEnumerator) points to invalid element after creation. 17 Ralf Reussner - CBSE: Design of Components
Exkurs: Command / Query-Separation (3) An elegant iterator(?) New Java while (i.iscurrentvalid()) { Object ele = i.current(); // no switch to next element i.movenext(); boolean iscurrentvalid(): query Object current(): query void movenext(): command (what is in case of last element?) 18 Ralf Reussner - CBSE: Design of Components
Gesamtstruktur Listenimplementierung in Java 19 class Vector2DList { private class ListCell {... // Detail der Implementierung, // vor dem Benutzer verborgen private ListCell head; // Attribut: Start der Liste public Vector2DList()... // Konstruktor (leere Liste) public void addfirst(vector2d p)... // Element am Anfang einfügen public void addlast(vector2d p)... // Element am Ende einfügen public void remove(vector2d p)... // Element löschen public boolean contains(vector2d p)... // Element suchen public int size()... public boolean isempty()...... public Iterator iterator()... public class Iterator { public boolean hasnext()... public Vector2D next()... // Länge der Liste // Liste leer? // Iterator, um über Listen- // elemente zu laufen
Verwendung der Listenimplementierung Beispiel: Generierung einer Liste mit 3 Elementen und Ausgabe der Liste class Vector2DListTest { public static void main(string[] args) { Vector2D p = new Vector(1.0, 2.0); Vector2D q = new Vector(0.0, -1.0); Vector2D r = new Vector(5.5, 7.7); Vector2DList list = new Vector2DList(); list.addfirst(r); list.addfirst(q); list.addfirst(p); Vector2DList.Iterator it = list.iterator(); while (it.hasnext()) { Vector2D v = it.next(); System.out.println(v); 20
Vergleich Listen Arrays Vorteile Arrays: Einfacher Zugriff auf die Elemente Handhabung und Syntax sind relativ intuitiv Unterstützung durch Java-API Nachteile Arrays: Die Länge eines Arrays muss bei der Initialisierung angegeben werden und kann später nicht mehr verändert werden Dynamisch wachsende oder schrumpfende Listen nur schwer mit Arrays zu implementieren Umstrukturierung des Arrays (z.b. Einfügen oder Löschen von Elementen) ist immer mit großem Kopieraufwand verbunden Oft ist ein direkter Zugriff auf die inneren Elemente gar nicht nötig (z.b. Warteschlange) 21
Mehr Flexibilität: doppelt verkettete Listen Einfach verkettete Listen haben einen Nachteil: man kann sie nur von vorne nach hinten durchlaufen, nicht aber rückwärts gehen Doppelt verkettete Listen Doppelt verkettete Listen (doubly linked lists): neben der Vorwärtsverkettung gibt es noch eine Rückwärtsverkettung Grafische Darstellung: first last 5 10 28-3 -3 78 78 Vorwärtsverkettung Rückwertsverkettung 22 27.10.2014
Implementierung doppelt verkettete Liste class Vector2DDoublyLinkedList { private class ListCell { Vector2D content; ListCell prev; ListCell next; // Vorgänger // Nachfolger ListCell(Vector2D v, ListCell p, ListCell n) { this.content = v; this.prev = p; this.next = n; private ListCell first, last; public Vector2DDoublyLinkedList() { this.first = this.last = null; public void addfirst(vector2d p) {... public void addlast(vector2d p) {... public void remove(vector2d p) {... public boolean contains(vector2d p) {... // leere Liste 23
Einfügen von Listenelementen: addlast Implementierung: public void addlast(vector2d v) { ListCell newlistcell = new ListCell(v, last, null); if (first == null) { // add to empty list first = newlistcell; else { last.next = newlistcell; last = newlistcell; last last -3 78 v 24
Verwendung von Listen: Gerichtete Graphen Graph: 0 1 2 3 4 5 Adjazenzlisten: 0: 1 3 3: 1: 2 3 4: 5 class Vertex { private int content; private VertexList successors;... class VertexList { // singly/doubly linked list of vertices... class DirectedGraph { private VertexList vertices;... 2: 5: 4 25
Zusammenfassung Listen bestehen aus verketteten Listenelementen und sind ein rekursiver Datentyp Die Länge einer Liste ist nicht im Voraus festgelegt, sondern kann sich dynamisch ändern Implementierung in Java: Listenzellen (unser Beispiel: ListCell) enthalten Attribute für Inhalt (content) und Verkettung (next / prev) Die nach außen sichtbare Listenklasse (unser Beispiel: Vector2DList) enthält eine Referenz auf den Listenstart (bei doppelt verketteten Listen auch das Listenende) und Methoden zur Erstellung und Modifikation von Listen Die Implementierung der Listenzellen ist nach außen nicht sichtbar! Die Listenklasse kann als abstrakter Datentyp aufgefasst werden Iteratoren sind abstrakte Zeiger auf Listenelemente und erlauben über die Listenelemente zu wandern 26