• Keine Ergebnisse gefunden

Abstrakte Datentypen und Verkettete Listen

N/A
N/A
Protected

Academic year: 2022

Aktie "Abstrakte Datentypen und Verkettete Listen"

Copied!
19
0
0

Wird geladen.... (Jetzt Volltext ansehen)

Volltext

(1)

Algorithmen und Datenstrukturen

– Wintersemester 2019 –

Kapitel 05:

Abstrakte Datentypen und Verkettete Listen

Fachbereich Informatik TU Kaiserslautern

Dozent: Dr. Patrick Michel

Folien urspr¨unglich von Prof. Dr. Adrian Ulges (Hochschule RheinMain)

8. Dezember 2019

1

Abstraktion in der Informatik

“Computer Science: The Mechanization of Abstraction”

(Aho, Ullmann: Foundations of Computer Science)

Abstraktion ist in der Informatik wichtig!

Typisches Vorgehen (Bsp. TSP)

1. Bilde ein (abstraktes) formales Modell 2. Transformiere es in ein

”Maschinenmodell“.

Beispiel: Algorithmus → Programm

1. Algorithmus = abstrakte Vorgehensbeschreibung (unabh¨angig von Programmiersprache:

”=“ vs.

”:=“, etc.) 2. Programm = konkrete Implementierung.

Beispiel: Anforderungsanalyse → Software-System

1. Pflichtenheft = Abstrakte Gliederung des Systems 2. Abbildung in Software.

2 Abstraktion

(2)

Abstrakte Datentypen

Bei Algorithmen ging es um die Abstraktion von Verfahren.

Im Folgenden wollen wir Daten abstrahieren:

Abstrakte Datentypen

Abstrakte Datentypen (ADTs) = Spezifikation von Daten

Mathematische Beschreibung von Aufbau und Verhalten

Keine Realisierung, unabh¨angig von Programmiersprachen Konkrete Datentypen

... implementieren ADTs auf einem Rechner.

Informatiker...

... sollten in ADTs “denken”.

Beispiele: boolesche Logik, Arrays, Stacks, Queues, ...

Egal welche Sprache modern wird, ATDs wird es weiter geben! → Wissen bleibt anwendbar.

3

Outline

1. Der Abstrakte Datentyp “Stack”

2. Stack: Implementierung

3. Dynamische Datenstrukturen 4. Verkettete Listen

5. Doppelt Verkettete Listen

(3)

Der Datentyp “Stack”

Der Stack (dt. Stapel/Keller) ist ein einfacher Container-Datentyp.

Analogie

Ein Stapel von Objekten. Wir k¨onnen ...

ein Objekt oben auf den Stapel legen.

das oberste Objekt vom Stapel nehmen.

Anwendungen

Unterprogrammaufrufe (“Stack Overflow”)

Undo in Text-Editoren

Syntax-Parsing in Compilern.

def fac(n):

if n == 1:

return 1 else:

return fac(n-1)*n print fac(4)

fac(4)

Der Call-Stack, wenn wir fac(4) aufrufen...

fac(4) fac(3)

fac(4) fac(3) fac(2)

fac(4) fac(3)

fac(4) fac(3)

fac(4)

fac(3) fac(2) fac(1)

return 2

return 1 fac(2) return 6 return 12

5

Der ADT Stack < T > : Spezifikation

1. Verwendete Typen

T (Typ der zu speichernden Objekte, vgl. Java Generics)

Boolean

2. Operatoren

empty // Konstruktor, leerer Stack

push // Element oben auf den Stack legen

top // oberstes Element zurückgeben

pop // oberstes Element entfernen

is_empty // Test ob Stack leer.

Anmerkungen

In Java werden Funktionen zu Instanzvariablen und Methoden.

Beispiel: Aus s = push(s,e) wird in Java s.push(e).

6

(4)

Beispiel (Stack < int > )

Anweisungsfolge

11

1175

→ true

→ 5

117

→ false

Zustandsfolge S = empty

is_empty(S) S = push(S,11) S = push(S,7) S = push(S,5) top(S)

S = pop(S) top(S)

is_empty(S)

→ 7

Stacks operieren nach dem sogenannten LIFO-Prinzip (engl.

Last-In-First-Out): Was zuletzt auf den Stack gelegt wurde, wird als erstes wieder entfernt.

7

Queues (Warteschlangen)

Anweisungsfolge

11

→ true

→ 11

→ false

Zustandsfolge q = empty

is_empty(q) q = push(q,11) q = push(q,7) q = push(q,5) top(q)

q = pop(q) top(q)

is_empty(q)

→ 7

5 7 11

5 7

Das “Gegenteil” eines Stacks ist eine Queue / Warteschlange. Hier gilt das FIFO-Prinzip (engl. First-In-First-Out): Was zuerst in die Queue geschoben wurde, wird als erstes wieder entfernt.

(5)

ADT “Stack”: Spezifikation II

Wie spezifizieren wir das Verhalten eines Stacks formal?

3. Axiome

Die folgenden Grundannahmen / Axiome gelten f¨ur alle Stacks:

1. is empty(empty) = true

empty ist leerer Stack.

2. is empty(push(s,e)) = false

Wenn als letztes ein Element auf Stack gepusht wurde, ist dieser nicht leer.

3. top(push(s,e)) = e

Wenn ein Element e auf Stack gepusht wird, dann ist es das oberste.

4. pop(push(s,e)) = s

Wenn Elt. auf Stack gepusht wird und dann wieder entnommen wird, erhalten wir wieder den vorherigen Stack.

9 type Stack(T)

import Bool operators

empty: -> Stack

push: Stack × T -> Stack pop: Stack -> Stack top: Stack -> T

is_empty: Stack -> Bool axioms

is_empty(empty) = true

is_empty(push(s,x)) = false pop(push(s,x)) = s

top(push(s,x)) = x

ADT “Stack”: Beispiel-Verhalten

Die Axiome stellen die LIFO-Eigenschaften des Stacks sicher:

S = empty;

S = push(S,1); S = push(S,2);

S = pop(S);

S = push(S,3); S = push(S,4);

S = pop(S); S = pop(S);

top(S) → ?

12

13 4

1

10

(6)

Outline

1. Der Abstrakte Datentyp “Stack”

2. Stack: Implementierung

3. Dynamische Datenstrukturen 4. Verkettete Listen

5. Doppelt Verkettete Listen

11

Stacks in Java

Erste Umsetzung von Stacks in Java

Wir definieren eine Schnittstelle (engl. Interface) auf Basis des ADTs.

Diese erg¨anzen wir durch ein internes Verhalten (Klasse).

Die Klasse verwendet ein Array zur Speicherung der Objekte.

Eine Indexvariable i speichert die Position des obersten Elements.

Operationen … im Array empty: i = -1 is_empty: i == -1 push: a[++i] = e

pop: i--

top: a[i]

1175 2

i a

Beispiel // Stack-OPs s.empty();

s.push(11);

s.push(7);

s.push(5);

s.pop();

Beispiel // Array-OPs i = -1;

a[++i] = 11;

a[++i] = 7;

a[++i] = 5;

i--;

class ArrayStack<T>

implements Stack { ...

}

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

public void pop();

public T top();

public boolean isEmpty();

}

(7)

Java-Stack: Implementierung

Die Klasse ArrayStack

implementiert die Schnittstelle Stack (Schl¨usselwort

’implements’). Alle Methoden

(Prototypen) des Interfaces m¨ussen implementiert werden!

Array vom Typ Object[]: Kann Objekte eines beliebigen Typs T speichern (Object = allgemeinste Oberklasse).

13

public class ArrayStack implements Stack { private Object[] a;

private int i;

public ArrayStack() { // empty a = new Object[1000];

i = -1;

}

public void push(T e) { a[++i] = e;

}

public void pop() { a[i] = null;

i--;

}

public T top() { return a[i];

}

public boolean isEmpty() { return i==-1;

} }

Java-Stack: Implementierung

Wir haben ein Interface Stack<T>

definiert. Was ist der Vorteil?

Austauschbarkeit

// Schlecht wäre:

// ArrayStack<String> s = … Stack<String> s =

new ArrayStack<String();

boolean b = s.isEmpty();

s.push(“Winter”);

s.push(“is”);

...

Wir k¨onnen sp¨ater ArrayStack durch andere Klassen ersetzen.

Der aufrufende Code muss nicht angepasst werden!

Grundregel: Immer gegen die allgemeinste Schnittstelle

implementieren. 14

public class ArrayStack implements Stack { private Object[] a;

private int i;

public ArrayStack() { // empty a = new Object[1000];

i = -1;

}

public void push(T e) { a[++i] = e;

}

public void pop() { a[i] = null;

i--;

}

public T top() { return a[i];

}

public boolean isEmpty() { return i==-1;

} }

(8)

Java-Stack: Implementierung

Geheimnisprinzip

boolean isEmpty() { return i == -1;

}

// Schlecht

if (s.i == -1) ...

// Gut

if (s.isEmpty()) ...

Zur Austauschbarkeit darf der verwendende Code nicht auf Interna der Klasse zugreifen.

Attribute a und i sind private → kein Zugriff von außen m¨oglich.

15

public class ArrayStack implements Stack { private Object[] a;

private int i;

public ArrayStack() { // empty a = new Object[1000];

i = -1;

}

public void push(T e) { a[++i] = e;

}

public void pop() { a[i] = null;

i--;

}

public T top() { return a[i];

}

public boolean isEmpty() { return i==-1;

} }

Java-Stack: Implementierung

Die aktuelle Implementierung weist noch Fehlerf¨alle auf.

1. top() ist bei leerem Stack nicht definiert.

2. push() funktioniert nicht bei vollem Array (1000 Elemente).

L¨osung: Exceptions

Wir definieren eine Klasse StackException.

Im Fehlerfall ( ¨Uberlauf / Unterlauf des Stacks) signalisieren wir Fehler, indem wir Exceptions werfen.

public void push(T e) throws StackException {

if (i >= 999) throw new

StackException(“Full!”);

a[++i] = e;

}

public void pop()

throws StackException { if (isEmpty())

throw new

StackException(“Empty!”);

a[i] = null;

i--;

}

public class StackException extends RuntimeException {

public StackException (String msg) { super(msg);

} }

(9)

Outline

1. Der Abstrakte Datentyp “Stack”

2. Stack: Implementierung

3. Dynamische Datenstrukturen

4. Verkettete Listen

5. Doppelt Verkettete Listen

17

Datenstrukturen: Motivation

Arrays als Datenstruktur

Wir haben bisher den Stack durch ein internes Array realisiert.

Was ist der Nachteil?

Der Stack fasst nicht mehr als 1000 Elemente /

Speicher wird nicht effizient genutzt /(bei vielen kleinen

Stacks hoher Speicherverbrauch!).

(Teil-)L¨osung: Dynamische Allokation Bei einem ¨Uberlauf des Arrays ...

... wird ein um den Faktor α vergr¨oßertes Array erstellt.

... und alle Elemente werden in das neue Array kopiert.

In Java: Typ ArrayList (α = 1.5).

18

public ArrayIntStack() { a = new int[1000];

i = -1;

}

5 2 8

5 2 8 4 3 1 9 a

a

5 2 8 4 3 1 9 a

5 2 8 4 3 1 9 5 2 8 4 3 1

a 9

Ein Überlauf tritt auf

Neues (größeres) Array anlegen.

Werte kopieren, neues Array verwenden.

(10)

Datenstrukturen: Effizienz

Effizienz von Datenstrukturen

Wir beurteilen die Effizienz anhand folgender Schl¨usselfragen:

Wieviel kostet es, ein neues Element einzuf¨ugen?

Wieviel kostet es, ein Element zu suchen?

Wieviel kostet es, ein Element zu l¨oschen?

Wieviel Speicher wird verbraucht?

Sind dynamisch allokierte Arrays eine “gute” Datenstruktur?

Wir berechnen jeweils die Worst-Case-Komplexit¨at:

(Siehe ArrayList aus den ¨Ubungen.)

19

Dynamische Datenstrukturen

Array = Statische Datenstrukturen

Fixe Gr¨oße, fixe Anordnung der Objekte im Speicher.

Ab jetzt: Dynamische Datenstrukturen

Struktur entsteht durch Referenzierung (effizienter!).

Variable Gr¨oße.

Reminder: Referenzen

Referenzen = Zeiger auf Objekte (sicher+getypt).

Wir verwenden in Java Referenzen, nicht die Objekte selbst!

Jedes Objekt existiert (mindestens) solange eine Referenz auf es verweist.

public class Student { String name;

Studiengang studiengang;

Bafoeg bafoeg;

int semester;

...

}

Studiengang bwl = new Studiengang(...);

Student alex = new Student ("Alex", bwl, null, 3);

alex

3

„Alex“

bwl null

(11)

Dynamische Datenstrukturen: Collections

Schl¨usselidee: Datenstruktur durch Referenzierung

Objekte liegen nicht mehr in einem koh¨arenten

Speicherbereich (siehe Array), sondern sind beliebig im Speicher verteilt.

Objekte referenzieren andere Objekte ¨uber spezielle Attribute.

Meist Referenzen zwischen gleichartigen Objekten.

Durch diese Verkettung entsteht ein Container-Datentyp (engl. “Collection”)!

class Entry { Entry n1;

Entry n2;

Entry n3;

Nutzdaten obj;

...

}

n1 n2 n3 obj

n1 null

n2 n3 obj

null null

n1 n2 n3 obj

null

n1 null

n2 n3 obj

null

...

...

...

...

...

...

...

...

21

Dynamische Datenstrukturen: Collections

Collections bestimmter Typen?

H¨aufig sollen Collection Objekte eines bestimmten Typs speichern (Kunden, Produkte, ...). Es gibt zwei Optionen:

1. Polymorphie: Collection enth¨alt Typ Object.

Verlust der Typsicherheit /

2. Generics: Ein zus¨atzlicher Parameter T

(sog. “Sortenparameter”) gibt den Typ der Objekte an.

// Polymorphie class Entry { Entry n1;

Entry n2;

Entry n3;

Object obj;

...

}

// Generics

class Entry<T> { Entry<T> n1;

Entry<T> n2;

Entry<T> n3;

T obj;

...

}

// Polymorphie

// -> nicht typsicher :-(

Entry e1 = new Entry();

e1.obj = “Ein String”;

e1.obj = 17; // möglich!

// Generics

// -> typsicher :-)

Entry<String> e1 = new Entry<String>();

e1.obj = “Ein String”;

e1.obj = 17; // nicht möglich!

22

(12)

Outline

1. Der Abstrakte Datentyp “Stack”

2. Stack: Implementierung

3. Dynamische Datenstrukturen 4. Verkettete Listen

5. Doppelt Verkettete Listen

23

Verkettete Listen

Einfachste dynamische Datenstruktur?

Jedes Element referenziert auf genau ein anderes Element.

Es ergibt sich eine Sequenz, genannt verkettete Liste.

obj next obj next head

"Winter" "is"

obj next

null

"coming"

...

Attribute

Die Liste besteht aus Knoten:

head: zum Einstieg, Referenz auf ersten Knoten.

obj: Referenz auf Nutzdaten (hier: Strings).

next: Referenz auf n¨achsten Knoten. (am Ende: null).

(13)

Verkettete Listen

Spezifikation

Die verkettete Listen soll folgende Operationen bieten:

addFirst()/...Last(): Element vorne/hinten einf¨ugen.

getFirst()/...Last(): erstes/letztes Element erhalten.

removeFirst()/...Last(): erstes/letztes Element l¨oschen.

Anmerkungen

Wir k¨onnen z.B. Stacks mit verketteten Listen implementieren:

push() → addFirst()

top() → getFirst()

pop() → removeFirst()

25

Verkettete Listen: Implementierung

Klassenger¨ust

Innere Klasse Node f¨ur die einzelnen Knoten der Liste.

Referenz head auf ersten Knoten.

Information Hiding wird unterst¨utzt:

Zugriff auf Details der

Implementierung (head, Node) nicht m¨oglich.

Existenz von Knoten

unbekannt: Methoden decken nur Objekte vom Typ T ab.

26

class LinkedList<T> {

// information hiding:

// Interne Struktur nach // außen nicht bekannt.

private Node head;

private class Node { T obj;

Node next;

public Node(T obj) { this.obj = obj;

} }

public LinkedList() { head = null;

}

public void addFirst(T obj);

public void addLast(T obj);

public T getFirst();

public T getLast();

public void removeFirst();

public void removeLast();

}

(14)

Implementierung: getFirst()

obj next obj next obj next

null head

"Winter" "is" "coming"

Ergebnis (Referenz auf erstes Objekt)

Implementierung

1. Ausnahme falls Liste leer ist.

2. Ansonsten wird Referenz auf das Datenobjekt des heads zur¨uckgegeben.

27

public T getFirst()

throws ListException { if (head == null) { // empty String msg = "List is empty.";

throw new ListException(msg);

}

return head.obj;

}

...

String s = list.getFirst();

Implementierung: addFirst()

obj next obj next

null

"is" "coming"

head vorher

obj next obj next obj next

null head

"Winter" "is" "coming"

nachher

Implementierung

1. Neuer Knoten n (mit

Referenz auf einzuf¨ugendes Objekt) wird erzeugt

2. n wird am Anfang der Liste eingef¨ugt (→ neuer Head) 3. N¨achster Listenknoten ist

der vorherige Head!

public void addFirst(T t) {

} ...

list.addFirst("Winter");

(15)

Implementierung: addLast()

obj next

null

"Winter"

head vorher

obj next

"is"

obj next

"coming"

obj next

null

"Winter"

head nachher

obj next

"is"

obj next

"coming"

obj next

"!"

last

Implementierung

1. Sonderfall: Liste leer.

2. Referenz last auf letzten Knoten berechnen (Liste durchlaufen).

3. Neuen Noten erstellen, auf Knoten verweisen.

4. Nachfolger des ehemals letzten Knotens ist neuer Knoten.

29

public void addLast(T t) {

} ...

list.addLast("!");

Verbesserung der Implementierung: head und tail

obj next null

"Winter"

head

obj next

"is"

obj next

"coming"

obj next

"!"

obj next obj next tail

null null

Wir verwenden gesonderte Knoten f¨ur Kopf und Ende:

Kopfelement: head(), Endeelement: tail().

Beide Elemente besitzen keine Nutzdaten, sondern dienen ausschließlich zur Navigation.

Vorteile

Vereinfacht Implementierung (Bsp. removeLast()).

Einelementige Liste erfordert keine Spezialbehandlung mehr.

30

(16)

Outline

1. Der Abstrakte Datentyp “Stack”

2. Stack: Implementierung

3. Dynamische Datenstrukturen 4. Verkettete Listen

5. Doppelt Verkettete Listen

31

Verkettete Listen: Effizienz

Gegeben eine Liste mit n Elementen, was ist die Komplexit¨at von...

... addFirst()? → O(1) ,(wir ¨andern nur zwei Referenzen)

... addLast()? → O(n) /(Durchlaufen der kompletten Liste) L¨osung 1: Alle Referenzen “umdrehen”

Nun hat addFirst() die Komplexit¨at O(n) /

L¨osung 2: Letztes Element (vor tail) speichern

Umst¨andlich... was wenn das letzte Element entfernt wird? /

L¨osung 3: Doppelt verkettete Liste

Jedes Element verweist nicht nur auf seinen Nachfolger (next),

sondern auch seinen Vorg¨anger (prev).

obj next prev

(17)

Doppelt Verkettete Listen

obj next

tail

null obj next prev obj next prev obj

"coming"

"is"

"Winter"

prev

null prev

obj next prev

next null null

head

obj: Referenz auf Nutzdaten

next: Referenz auf n¨achstes Element (oder null, f¨ur tail)

prev: Referenz auf vorheriges Element (oder null, f¨ur head)

Das letzte Element ist nun einfach tail.prev

→ Zugriff in O(1).

33

Doppelt Verkettete Listen: Implementierung

Innere private Klasse Node f¨ur die

Knotenstruktur.

Private Referenzen head und tail f¨ur den Einstieg.

Konstruktur “Leere Liste”: Zwei Knoten head und tail, beide verweisen

aufeinander.

34

class DLinkedList<T> { private Node head;

private Node tail;

private class Node { T obj;

Node next;

Node prev;

}

public DLinkedList() { head = new Node();

tail = new Node();

head.prev = null; head.next = tail;

tail.prev = head; tail.next = null;

}

public void addFirst(T obj);

public void addLast(T obj);

public T getFirst();

public T getLast();

public void removeFirst();

public void removeLast();

}

(18)

Doppelt Verkettete Listen: addLast()

obj next

tail

null obj next prev obj

"is"

"Winter"

prev

null prev

obj next prev

null null

head

next

obj next

tail

null obj next prev obj

"is"

"Winter"

prev

null prev

obj next prev

null null

head

next prev obj

"coming"

next

nachher vorher

35

Doppelt Verkettete Listen: addLast()

public void addLast(T t) {

Sauberer Code!

Keine Schleife mehr

Kein Spezialfall

”Leere Liste”

(19)

Doppelt Verkettete Listen: length() vs. size()

Wie implementieren wir length() (#Elemente in der Liste) ? Option 1: Elemente z¨ahlen

Durchlaufe die Liste, inkrementiere Z¨ahler f¨ur jedes Element.

Nachteil? → Komplexit¨at O(n). Option 2: Separates Attribut ’length’

Speichere Anzahl in Wert length, lese length aus.

Vorteil? → Komplexit¨at O(1).

Nachteil? → length-Attribut muss gepflegt werden.

int length() { // Komplexität? :-(

while (l.length() != 0) { l.pop();

}

// Komplexität? :-) while (!l.isEmpty()) {

l.pop() }

37

References I

38

Referenzen

ÄHNLICHE DOKUMENTE

Eine doppelt verkettete Liste speichert die Listenelemente als Kette, wobei jedes Listenelement seinen Nachfolger und Vorgänger kennt.. z.B.. Verkettete

• Im Gegensatz zu einfach verketteten Listen haben doppelt verkettete Listen in den Knoten eine zusätzliche Instanzvariable für die Referenz auf den Vorgängerknoten. class Node

Ausdruck, der wahr zurückliefert (Boolesche Ausdrücke können wahr oder falsch zurückliefern, Axiome sind. spezielle Ausdrücke, die immer wahr

Zachmann Grundlagen der Programmierung in C - WS 05/06 Pointer &amp; Co, 32.. Beispiel: Verkettete Listen

I Einkaufswagen implementiert Funktionen artikel und menge, die auch aus Posten importiert werden:. import Posten hiding (artikel, menge) import qualified Posten as P(artikel,

Ein abstrakter Datentyp (ADT) besteht aus einem (oder mehreren) Typen und Operationen darauf, mit folgenden Eigenschaften:. I Werte des Typen können nur über die

import OrdTree hiding (member) Tree, empty, insert import OrdTree(empty). import OrdTree

• hiding: Liste der Bezeichner wird nicht importiert import Prelude hiding (foldr). foldr f e ls