Stack und Queue. Thomas Schwotzer

Ähnliche Dokumente
ALP II Dynamische Datenmengen Datenabstraktion

Schnittstellen, Stack und Queue

Problem: Was ist, wenn der Stapel voll ist? Idee: Erzeuge dynamisch ein grösseres Array und kopiere um. Dynamische Anpassung der Größe

Faulheit professionell: Fertige Datenbehälter. Das Java-Collections-Framework Typsicherheit Generische Klassen

Programmieren 2 15 Abstrakte Datentypen

Einfache Liste: Ein Stapel (Stack) Ansatz. Schaubild. Vorlesung 1. Handout S. 2. Die einfachste Form einer Liste ist ein Stapel (stack).

Mehrdimensionale Arrays

12.3 Ein Datenmodell für Listen

ALP II Dynamische Datenmengen Datenabstraktion (Teil 2)

Exceptions. Prof. Dr.-Ing. Thomas Schwotzer 21. November 2017

1. Die rekursive Datenstruktur Liste

Algorithmen und Programmierung III

Einführung in die Objektorientierte Programmierung Vorlesung 18: Lineare Datenstrukturen. Sebastian Küpper

Übung: Algorithmen und Datenstrukturen SS 2007

Übung Algorithmen und Datenstrukturen

Nachtrag: Vergleich der Implementierungen von Stack

12 Abstrakte Klassen, finale Klassen und Interfaces

Gliederung. 5. Compiler. 6. Sortieren und Suchen. 7. Graphen

Informatik II Übung 5 Gruppe 3

Interface. So werden Interfaces gemacht

7. Verkettete Strukturen: Listen

Schwerpunkte. Verkettete Listen. Verkettete Listen: 7. Verkettete Strukturen: Listen. Überblick und Grundprinzip. Vergleich: Arrays verkettete Listen

Aufgabenblatt 4. Aufgabe 3. Aufgabe 1. Aufgabe 2. Prof. Dr. Th. Letschert Algorithmen und Datenstrukturen

55 Ring-Queue. size. push. clear. get. Reinhard Schiedermeier / Klaus Köhler, Das Java-Praktikum, dpunkt.verlag, ISBN

Programmieren 2 16 Java Collections und Generizität

Prüfung Algorithmen und Datenstrukturen I

Einstieg in die Informatik mit Java

Konkatenation zweier Listen mit concat

Einführung in die Informatik: Programmierung und Software-Entwicklung, WS 16/17. Kapitel 13. Listen. Listen 1

Einführung in die Informatik: Programmierung und Software-Entwicklung, WS 15/16. Kapitel 12. Listen. Listen 1

Programmieren 2 Übung Semesterwoche 2

16. Dynamische Datenstrukturen

Informatik II Übung 6

Algorithmen und Programmierung III

Ordnung im Materiallager: Datenstrukturen II. Suchen und Sortieren im Array Verkettete Listen Rekursion

Objektorientierung III

SS10 Algorithmen und Datenstrukturen 2. Kapitel Fundamentale Datentypen und Datenstrukturen

Einführung in die Informatik: Programmierung und Software-Entwicklung, WS 11/12 1. Kapitel 11. Listen. Listen

Schein-/Bachelorklausur Teil 2 am Zulassung: Mindestens 14 Punkte in Teilklausur 1 und 50% der Übungspunkte aus dem 2. Übungsblock.

Stacks, Queues & Bags. Datenstrukturen. Pushdown/Popup Stack. Ferd van Odenhoven. 19. September 2012

Programmieren I. Kapitel 13. Listen

II.4.4 Exceptions - 1 -

Sichtbarkeiten, Klassenmember und -methoden

Polymorphie und UML Klassendiagramme

Fallstudie: Online-Statistik

Informatik II Übung 06. Benjamin Hepp 5 April 2017

Einführung in die Informatik

Einführung in die Programmierung

Die abstrakte Klasse Expression:

Javakurs für Anfänger

Organisatorisches. drei Gruppen Gruppe 1: 10:10-11:40, Gruppe 2: 11:45-13:15 Gruppe 3: 13:20-14:50

1 Abstrakte Datentypen

Fragen zur OOP in Java

Software Entwicklung 1

Spezielle Datenstrukturen

Stack. Queue. pop() liefert zuletzt auf den Stack gelegtes Element und löscht es push( X ) legt ein Element X auf den Stack

Kapitel 3: Datentyp Keller und Schlange

Datenstrukturen. Ziele

Wiederholung: Zusammenfassung Felder. Algorithmen und Datenstrukturen (für ET/IT) Definition Abstrakter Datentyp. Programm heute

Datenstrukturen (66) Programmieren 2 - H.Neuendorf

Verkettete Datenstrukturen: Listen

Übungsblatt 13. Abgabe / Besprechung in Absprache mit dem Tutor

Listen. Prof. Dr. Christian Böhm. in Zusammenarbeit mit Gefei Zhang. WS 07/08

Organisatorisches. Neue Übungsblätter: Nur mehr elektronisch? Abgabe Di, , 14 Uhr bis Do, , 8Uhr

Einführung in die Programmierung

3. Übungsbesprechung Programmkonstruktion

EINFÜHRUNG IN DIE PROGRAMMIERUNG

Informatik II Übung 5

Einfügen immer nur am Kopf der Liste Löschen auch nur an einem Ende (2 Möglichkeiten!)

Abstrakte Datentypen und deren Implementierung in Python

Algorithmen und Datenstrukturen 10

Javakurs für Anfänger

public interface Stack<E> { public void push(e e); public E pop();

Informatik II Übung 7 Gruppe 7

Programmierung für Mathematik HS11

Programmiermethodik 2. Klausur Lösung

EINFÜHRUNG IN DIE PROGRAMMIERUNG

Prüfung Softwareentwicklung I (IB)

5.3 Doppelt verkettete Listen

Punkte. Teil 1. Teil 2. Summe. 1. Zeigen Sie, dass der untenstehende Suchbaum die AVL-Bedingung verletzt und überführen Sie ihn in einen AVL-Baum.

1 Abstrakte Klassen, finale Klassen und Interfaces

6. Verkettete Strukturen: Listen

13. Dynamische Datenstrukturen

Tutoraufgabe 1 (Implementierung eines ADTs):

Wie kann man es verhindern das Rad immer wieder erneut erfinden zu müssen?

Ordnung im Materiallager: Datenstrukturen

Datenstrukturen. Mariano Zelke. Sommersemester 2012

EINFÜHRUNG IN DIE PROGRAMMIERUNG

Johannes Unterstein - TINF16 - Java - Sommersemester 2017 JAVA. Weiterführende Spracheigenschaften

Prüfung Softwareentwicklung II (IB)

Kapitel 4: Datentyp Keller und Schlange

Informatik II. Übungsstunde 6. Distributed Systems Group, ETH Zürich

Stapel (Stack, Keller)

Testgetriebene Entwicklung

Javakurs für Anfänger

Grundlagen der Informatik

Programmierkurs Java

Abschnitt 10: Datenstrukturen

Organisatorisches. Folien (u.a.) auf der Lva-Homepage Skriptum über MU Online

Transkript:

Stack und Queue Thomas Schwotzer 1 Einführung Wir kennen eine Reihe von Java-Strukturen zur Verwaltung von Daten. Wir wollen aus Gründen der Übung zwei Datenstrukturen implementieren, die sicherlich bereits aus dem Modul Algorithmen und Datenstrukturen bekannt sind. Man kann damit den Umgang mit Generics und einigen Datenstrukturen sehr gut üben. Im SU werden wir die Implementierung daher auch auf Arrays ansetzen, was wirklich keine gute Idee ist. In der Übung werden wir aber die Arrays ersetzen durch ArrayList was deutlich effektiver ist. 2 Stack Der Stack ist uns seit unseren ersten Schritten im imperativen Programmieren wohl bekannt wenn nicht explizit, dann implizit. Jeder Methodenaufruf erzeugt Operationen auf dem Stack 1. Der Stack wird im Deutschen auch als Stapelspeicher bezeichnet. Der Stack erlaubt zwei Operationen Push Ein Datum wird auf den Stack gelegt. Pop Das Datum, das oben auf dem Stack liegt wird entnommen. Ein Stack kann leer sein. In dem Fall wird die Operation pop scheitern. Bei manchen Implementierungen kann ein Stack ein maximale Größe haben. In dem Fall kann auch ein push scheitern. In manchen Implementierungen ist ein peek implementiert. Diese Operation ähnelt pop, allerdings wird das oberste Element nicht entnommen 2 Der Stack ist ein LIFO-Speicher (last-in first-out). 2.1 Interface Stack In jeder Stack-Implementierung aber gibt es wenigstens einen Fehlerfall: Der Stack ist leer und es wird versucht ein Datum zu entnehmen. Solche Fehler 1 siehe dazu auch Programmieren 1 (LN Methoden und Stack) 2 siehe https://docs.oracle.com/javase/10/docs/api/java/util/stack.html 1

Abbildung 1: Stack werden mit Exceptions dokumentiert. Bevor wir also unser Interface definieren, implementieren wir uns eine Exceptionklasse für unser Projekt: public class StackException extends Exception { public StackException() { super(); public StackException(String s) { super(s); Nun ist das Interface unseres Stack schnell notiert: interface Stack<T> { void push(t newdata) throws StackException; T pop() throws StackException; Wir haben Generics genutzt. Das ist sinnvoll. Der Stack arbeitet unabhängig davon, welche Daten wir darin speichern. Durch die Generics geben wir aber Nutzer*innen unserer Klasse die Möglichkeit, Typsicherheit herzustellen. Das ist gut! 2.2 Test Wir schreiben uns auch gleich einen kleinen ersten unsortierten Test. public class StackAndQueueTests throws StackException { @Test public void stacktests() { Stack<String> ourstack = new OurStack(40); ourstack.push("hallo"); 2

String retval = ourstack.pop(); assertequals("hallo", retval); // test on Exception try { // stack is empty! ourstack.pop(); // we are not supposed to reach that point fail(); catch(stackexception e) { // ok! Wir testen, ob das Ablegen und Entnehmen eines Datums funktioniert. Dann testen wir, ob eine Exception geworfen wird, wenn wir versuchen, eine Element aus einem leeren Stack zu entnehmen. 2.3 Implementierung Wir implementieren nun den Stack mit einem Array. Das ist eine wirklich schlechte Idee, weil wir dadurch gezwungen sind, a) die maximale Größe des Stacks von Anfang an zu kennen. Außerdem b) belegen wir den maximalen Speicherplatz des Stack unabhängig davon, wie viele Elemente tatsächlich darin liegen. Eine ArrayList wäre deutlich sinnvoller. Das ist unsere Aufgabe für die Übung. Hier das Skelett einer möglichen Implementierung. Die Punkte repräsentieren die Stelle an der wir weiter unten die beiden Methoden einfügen werden. Betrachten wir aber zunächst die Member. class OurStack<T> implements Stack<T> { private final T[] dataarray; private final int len; private int nextindex = 0; OurStack(int len) { this.dataarray = (T[]) new Object[len]; this.len = len; //... 3

Wir deklarieren das Array, in dem die Daten gespeichert werden. Der Datentyp ist generisch. Wir merken uns außerdem die Länge des Arrays. Damit vermeiden wir, über das Array hinaus Daten speichern zu wollen. Beide Variablen werden bei der Erzeugung des Objektes unveränderlich instantiiert und können daher final sein. Wir merken uns außerdem an welcher Stelle (nextindex) des Arrays wir uns befinden. Diesen Wert initialisieren wir mit 0: die nächste Stelle in die wir schreiben werden, ist der Index 0. Betrachten wir nun den Konstruktor. Das Arbeiten mit Generics ist ein wenig tricky, wenn wir mit Arrays arbeiten (das ist wirklich keine gute Idee!). Das folgende wäre logisch, ist aber syntaktisch in Java nicht möglich: this.dataarray = new T[len]; Gründe diskutieren wir in der SU. Wir benötigen also einen Umweg. Ein möglicher ist in der Implementierung aufgezeigt. Wir erzeugen ein Array von Object. Object ist die allgemeinste Klasse in Java. Wir casten außerdem das erzeugte Array herunter auf den Generic T. Das geht in jedem Fall, da T als Java-Klasse von Object ableiten muss. Das Vorgehen verstößt auch nicht gegen die Typsicherheit jedenfalls nicht von außen. Nutzer*innen dieser Implementierungen werden lediglich die Methoden push oder pop nutzen können. Diese sind typsicher, danke der Generics. Auf das Array kann von außen nicht zugegriffen werden, da es private ist. Das kann man so machen. Schauen wir uns nun eine Implementierung von push an: @Override public void push(t newdata) throws StackException { if(this.nextindex < len) { this.dataarray[nextindex++] = newdata; else { throw new StackException("stack is already full"); Wir prüfen eingangs ob der nächste Index noch immer kleiner ist als die Länge des Arrays. Wir erinnern uns: Der maximale Index eines Array ist die Länge des Array - 1. Ist das der Fall, dann legen wir das neue Datum an diese Stelle im Array und erhöhen den Index um 1. Beim nächsten Aufruf der Methode würde das Datum auf der folgende Stelle abgelegt werden daher heißt die Variable auch nextindex. Wenn nextindex aber bereits den Wert len hat, dann ist das Array voll. Wir dokumentieren den Fall, indem wir eine Exception werfen. Schauen wir uns pop an: @Override 4

Abbildung 2: Queue public T pop() throws StackException { if(this.nextindex > 0) { this.nextindex--; T data = this.dataarray[this.nextindex]; return data; else { throw new StackException("stack is empty"); Wir prüfen eingangs, ob der Stack nicht leer ist. Das ist der Fall, wenn der nächste zu beschreibende Index größer ist als 0. Wäre der nächste zu beschreibende Index 0, dann wäre der Stack leer. War der Test positiv, so dekrementieren wir den index. Dieser hat nun den Wert des Index des zuletzt geschriebenen Datum. Dieses Datum geben wir nun heraus. Ein folgender Aufruf von push würde den alten Wert im Array überschreiben. Eine Exception wird geworfen, wenn der Stack leer ist. Wir lassen den Test laufen uns sehen, ob es klappt. 3 Queue Die Queue ähnelt dem Stack sehr. Im Gegensatz zum Stack ist die Queue allerdings ein FIFO (first-in first-out) Speicher. Zwei Methoden sind notwendig für die Arbeit mit einer Queue. Die Methode add fügt ein Datum hinzu, die Methode remove entfernt ein Datum von der Queue, siehe Abbildung 2 Es gelten ähnliche Überlegungen wie beim Stack. Es ist ein Fehler, wenn man von einer leeren Queue ein Datum entfernen will. Bei einer Implementierung mit einer maximalen Länge ist es ein Fehler, wenn man in eine volle Queue weitere Daten schreiben will. Wir gehen analog zur Implementierung des Stack vor. 3.1 Interface Queue Wir benötigen für die Fehlerfälle eine Exception. public class QueueException extends Exception { 5

public QueueException() { super(); public QueueException(String s) { super(s); Das Interface ist nun schnell deklariert: interface Queue<T> { void add(t newdata) throws QueueException; T remove() throws QueueException; 3.2 Test Schreiben wir einen Test. @Test public void queuetests() throws QueueException { Queue<String> ourqueue = new OurQueue(40); String teststring = "hallo"; ourqueue.add(teststring); String retval = ourqueue.remove(); assertequals(teststring, retval); // test on Exception try { // stack is empty! ourqueue.remove(); // we are not supposed to reach that point fail(); catch(queueexception e) { // ok! Wir testen, ob das Hinzufügen und Entnehmen eines Datums funktioniert. Dann testen wir, ob eine Exception geworfen wird, wenn wir versuchen, eine Element aus einer leeren Queue zu entnehmen. 3.3 Implementierung Eine Queue mit eine Array zu implementieren ist komplizierter als eine Stack. Ein Array (mit fixer Größe) soll eine Queue beinhalten. Was bedeutet das? Wir brauchen zwei Indizes. Einer enthält den Index, in den wir das nächste Element 6

Abbildung 3: Queueimplementierung mit Array schreiben, nennen wir ihn nextin. Der zweite Index ist der aus dem wir das nächste Element lesen; nennen wir ihn nextout. In einem Stack fallen beide Werte in eins, siehe oben. In einer Queue aber schreiben wir einen Wert in einen Index. Die folgenden Werte werden dahinter geschrieben. Werte werden vorn aus dem Array entnommen, d.h. der nextout Index wandert langsam nach hinten, genau wie der nextin Index. Abbildung 3 illustiert die übliche Situation in der ersten Zeile. Bei der Initialisierung einer Queue ist diese leer. Das bedeutet, dass es kein nächstes Element für die Ausgabe gibt. Anders gesagt, zeigt der nextout Index ins Leere. Der nextin Index zeigt auf das ersten Element des Array (Index 0). Das ist in der zweiten Zeile in Abbildung 3 dargestellt. Offensichtlich wandern die gültigen Elemente der Queue schrittweise ans Ende des Array. Es kann geschehen (und ist sehr wahrscheinlich), dass der nextin das Ende des Arrays erreicht hat. Es können am Ende keine neuen Werte mehr eingefügt werden. Wurden zuvor aber Werte entnommen, so ist am Anfang des Arrays noch Platz. Dieser Zustand wird in Abbildung 3 als full bezeichnet. Die Situation ist nicht kritisch wir müssen die Werte im Array nach vorn schieben. Die letzte Situation ist aber kritisch. Das Array ist voll und es können keine weiteren Werte hinzu gefügt werden. Dieser Fall kann nur durch eine Exception dokumentiert werden. Schauen wir uns eine mögliche Implementierung an und beginnen bei den Konstruktoren und Membern. public class OurQueue<T> implements Queue<T> { private final T[] dataarray; 7

private final int len; private int nextin = 0; private int nextout = -1; public OurQueue(int len) { this.dataarray = (T[]) new Object[len]; this.len = len; //... Den Trick mit der Array-Erzeugung haben wir bereits oben diskutiert. Der Unterschied zum Stack besteht in der Initialisierung der beiden Indizes. Der nächste Wert wird bei einer leeren Queue in den Index 0 geschrieben. Der nächste entnehmbare Wert existiert nicht wir symbolisieren das indem wir nextout auf -1 setzen einen Index den es nicht gibt. Schauen wir uns zunächst das Entnehmen eines Datums (add) an. Die Methode ist einfacher als das Hinzufügen. @Override public T remove() throws QueueException { if(this.nextout >= 0 && this.nextout < this.nextin) { T data = this.dataarray[this.nextout]; this.nextout++; return data; else { throw new QueueException("Queue empty"); Wir prüfen, ob wir überhaupt gültige Werte in der Queue haben. Das ist der Fall, wenn nextout auf einen gültigen Index ( 0) verweist. Wir müssen aber auch sicher stellen, dass die mehrfache Ausführung von remove nicht hinter die gültigen Daten im Array läuft. Das prüfen wir, indem wir mit nextin vergleichen. Wir entnehmen das Datum und erhöhen den index. Im anderen Fall wird eine Exception geworfen. Nun schauen wir uns add an. @Override public void add(t newdata) throws QueueException { if(this.nextin < len) { this.dataarray[this.nextin++] = newdata; if(this.nextout < 0) this.nextout = 0; else { // reached end of array if(this.nextout == 0) { 8

throw new QueueException("Queue full"); // shift this.shiftleft(); // do it again this.add(newdata); Wir prüfen zunächst, ob noch Platz im Array ist. Das ist der Fall, wenn nextin kleiner ist als die Länge. In dem Fall fügen wir das neue Datum hinzu und erhöhen den Index. Für den Fall, dass wir den ersten Wert einfügen, muss nextout auf 0 gesetzt werden. Das geschieht nur einmal während der Lebensdauer der Queue. Wenn das Ende des Array erreicht ist, gibt es zwei Fälle: Das Array ist voll oder aber wir es wurden Daten vorn entnommen. Diese Fälle lassen sich unterscheiden, indem nextout geprüft wird. Ist der Wert 0, dann liegt der nächste zu entnehmende Werte an der ersten Stelle des Array. Das Array ist damit voll und damit die Queue. Wir werfen eine Exception. Im anderen Fall wurden die Werte am Anfang des Array bereits entnommen. Wir können die Werte nach vorn schieben. Das macht die Methode shiftleft, die wir uns gleich anschauen. Wurde diese abgearbeitet, ist wieder Platz am Ende des Arrays und wir rufen add wiederholt auf. Das ist ein gutes Beispiel für eine Fehlerbehebung und den einfachen rekursiven Aufruf einer Methode. Nun ein Blick in shiftleft: private void shiftleft() { int numberdata = this.nextout - this.nextin; for(int i = 0; i < numberdata; i++) { this.dataarray[i] = this.dataarray[i+this.nextout]; this.nextout = 0; this.nextin = numberdata; Die Methode ist bewusst private deklariert. Sie dient lediglich der Arbeit über der internen Datenstruktur und muss von außen nicht sichtbar sein. Was geschieht? Wie rechnen zunächst aus, wie viele Daten im Array tatsächlich Daten der Queue sind (numberdata). Wir verschieben diese Anzahl von Daten nun nach links. Das erste Datum wird von Index nextout auf die Position 0 geschoben. 9

Nach den Durchlauf steht das nächste zu entnehmende Datum an der Position 0. Das nächste Element das eingetragen werden kann, wird den Index numberdata erhalten. Die Verschiebung der Daten lässt sich umgehen. Man könnte den Fall des Überlaufes auch durch geschickteres Rechnen mit den Indizes bearbeiten. Der nextin Index könnte in dem Fall durchaus kleiner werden als der nextout. Dieses Vorgehen wählt man, wenn man einen Ringpuffer implementiert. Dieses Vorgehen ist eindeutig zu bevorzugen. Das Verschieben von Daten sollte man möglichst vermeiden, wenn es irgendeine Alternative gibt. Aber zu Anschauungszwecken kann man das schon einmal machen. 10