Datenstrukturen

Werbung
Datenstrukturen
Klassifikation von Strukturen
Typen
Laufzeitkomplexitäten
Programmieren 2 - H.Neuendorf
(81)
Datenstruktur-Typen
Implementierungs-Unabhängig
Datenstrukturen = Grundkonzepte unterschiedlicher Semantik
Technische Realisation auf
Basis von Feldlisten (Arrays)
oder Verketteten Referenzen
Sammlung (Collection)
Allg. Ansammlung von Objekten ohne Anforderungen an Elemente oder Zugriffsart
Liste (List)
Indizierte Reihung von Elementen mit Definition von Vorgängern und Nachfolgern
Stapel (Stack)
Chronologische Liste mit Zugriffsbeschränkung auf ein Ende - LIFO-Struktur
Schlange (Queue)
Chronologische Liste mit Zugriffsbeschränkung auf Anfang und Ende – FIFO-Struktur
Menge (Set)
Enthält keine Duplikate - Hinzufügen vorhandener Elemente verändert Menge nicht
Keine Indizierung - speziell : Ordnung der Elemente (Sorted Set)
Abbildung (Map / Key-Value-Store / Assoziation)
Unidirektionale Assoziation zwischen Element-Paaren zweier Mengen Schlüssel → Wert
Schlüssel bilden Set - nur Schlüssel sind eindeutig
speziell : Geordnete Schlüssel (Sorted Map)
Eine Map mit leeren
Values ist ein Set
Programmieren 2 - H.Neuendorf
(82)
Datenstruktur-Typen
Laufzeitkomplexität
Datenstruktur
Einfügen
Löschen
Suchen
Voraussetzung
Feldliste (Array)
O( n )
O( n )
O( n log(n) )
Sortierung
Keine Sortierung
Verkettete Liste
O( 1 )
O( n )
O( n )
Sortierte verk. Liste
O( n )
O( n )
O( n )
Sortierung
Stack + Queue
O( 1 )
O(1 )
O( n )
keine
Set + Map
O( n )
O( n )
O( n )
keine
Sortierter Baum
Hashtable
O( log(n) ) O( log(n) )
O( 1 )
O( 1 )
O( log(n) )
O( 1 )
Einfügestelle beliebig
Ausgeglichener Baum
Sortierung
Füllgrad < 60 %
Gute Hashfunktion
Zur Verwaltung von Objekten als Elemente in sortierten Strukturen müssen diese paarweise vergleichbar sein (equals + hashCode + Comparable)
In diesem Fall zeigen Suchbäume die beste Performanz.
Bei mangelnder Vergleichbarkeit (keine Ordnung definierbar) und ausreichend Speicher ist Hashtable die performanteste Struktur
Programmieren 2 - H.Neuendorf
(83)
Datenstrukturen
Dynamische Datenstrukturen
Realisation durch Pointer = Objektreferenzen
Programmieren 2 - H.Neuendorf
(84)
Dynamische Datenstrukturen
Verketten von Objekten mittels Zeigern / Referenzen
Grundlage dynamischer Datenstrukturen : Verkettete Objekte = Knoten
Dynamisch :
Knoten zur Laufzeit erzeugt + verkettet
Datenstruktur kann dynamisch wachsen + schrumpfen
Elemente können jederzeit instanziiert oder freigegeben werden
Anzahl der Knoten nicht prinzipiell festgelegt !
(≠ Arrays!)
Keine Speicherplatz-Verschwendung / Überläufe wie bei Arrays !
Baum : Verzweigte Struktur
Liste : Lineare unverzweigte Struktur
Jeder Knoten hat nur einen Nachfolger
Jeder Knoten kann mehrere
Nachfolger (childs) haben
Jeder Knoten wird nur von
einem Vorgänger referenziert
(ein parent-Knoten)
Objektreferenz = "Zeiger" auf Objekt
Java :
Objektvariable
Programmieren 2 - H.Neuendorf
(85)
Wiederholung : Objekt-Referenzen
class Mitarbeiter {
public String name ;
public double gehalt ;
}
class Verwaltung {
public static void main( String[ ] args ) {
(2048)
m1
(####)
Schmidt
5000.0
m2
(2048)
m1
(2048)
Mitarbeiter m1 = new Mitarbeiter( ) ;
(2048)
(2048)
Schmidt
5000.0
m2
m1.name = "Schmidt";
m1.gehalt = 5000.0 ;
Mitarbeiter m2 ;
m2 = m1 ;
( null )
m1
( null )
Schmidt
5000.0
m2
m1 = null ;
m2 = null ;
}
Nicht mehr referenzierte Objekte werden durch
Garbage Collection automatisch gelöscht
}
Programmieren 2 - H.Neuendorf
(86)
Knoten (Nodes) als Grundelemente der Verkettung
Datenstruktur / -Typ Node (Knoten) mit (minimal) zwei Komponenten :
val
Datenkomponente / Wert, den der Knoten transportiert
Kann von beliebigem Typ sein (int, Konto, Object, <T> …)
next Verweis / Referenz auf Nachfolger
Stets vom Typ der Knotenklasse selbst (Node)
Selbstbezügliche Datenstruktur :
Attribut next der Knotenklasse Node ist vom gleichen Typ wie tragende Klasse Node selbst :
Jedes Element kann Referenz auf ein Nachfolger-Objekt halten !
Grundmuster der Listenbildung
(5078)
Realisierung mittels Referenzen
(Objektvariablen)
a
(5078)
val :
3
next : null
Programmieren 2 - H.Neuendorf
(87)
Knoten (Nodes) als Grundelemente der Verkettung
class Node {
// Knotenklasse
public int val ;
// Datenelement
public Node next ;
// Objektreferenz !
Typ von val könnte auch Referenztyp sein :
Mitarbeiter, Konto, Object, <T> …
Erzeugen Knoten-Objekt
Node a = null ; // leer
public Node( int v ) {
Node a = new Node( 3 ) ;
val = v ;
next = null ;
(5078)
}
a
}
Extremfall leere Liste :
Einstiegsreferenz ist null
a
(null)
val :
(5078)
3
next : null
a
3
val
next
Programmieren 2 - H.Neuendorf
(88)
Verketten von Knoten via Objektreferenzen
Prinzip der Listenbildung :
An einen Knoten kann der nächste Knoten als Nachfolger gehängt werden
Zugriff auf 1. Knoten
Zugriff auf alle Nachfolge-Knoten
Verketten zweier Node-Objekte :
a
Node a = new Node( 3 ) ;
Zugriff auf alle Elemente
b
3
val
next
5
Node b = new Node( 5 ) ;
a.next = b ;
(5078)
val
3
next (1024)
(1024)
val
next
a
b
5
null
(5078)
b
a
3
5
(1024)
Realisierung Verknüpfung mittels Referenzen
Es entsteht eine (lineare, einfach-verkettete) Liste
Speziell: Ende der Liste gekennzeichnet durch Knoten mit next-Wert null !
Programmieren 2 - H.Neuendorf
(89)
Verketten von Knoten via Objektreferenzen
class Node {
// Knotenklasse
public int val ;
public Node next ;
Selbstbezügliche Datenstruktur durch next :
Zeiger zum Verketten von Node-Objekten =
Referenz auf Nachfolger !
public Node( int v ) {
val = v ;
Inhalt = Referenz auf anderes Node-Objekt
(Nachfolger) oder null (kein Nachfolger / Listenende)
next = null ;
}
}
a
b
3
Liste aus zwei Knoten
val
next
5
Kann von vorne nach hinten durchlaufen werden :
→ Startknoten (head) ist Objekt a
→ Von a kommt man zu b mittels Attribut a.next
Via Referenz auf Startknoten kann man sich durch
gesamte Liste "hangeln"
b
a
3
5
Wenn erster Knoten verfügbar ist, so sind alle Knoten
und deren Elemente verfügbar
Programmieren 2 - H.Neuendorf
(90)
Array ↔ Verkettete Liste :
Komplementäre Leistungen
tail
head
5
3
Kombination beider
Welten in Hashtables
12
Array (Feldliste) :
Verkettete Liste :
Rascher direkter wahlfreier Random AccessZugriff durch Index O(1)
Langsamerer Random Access-Zugriff aufgrund
Durchwanderns der Liste O(n)
Einfügen am Array-Ende schnell wegen direkten
Zugriffs O(1)
Einfügen am Ende bei tail-Pointer schnell O(1)
Einfügen am Anfang bzw. bei Index i langsam, da
n bzw. (n-i) Elemente verschoben werden
müssen: O(n)
Es wird nur ein Träger-Objekt = Array benötigt
Größe fest vorgegeben - statisch
Arrays sind effizienter bei häufigem nur lesenden
Random Access Zugriff
Einfügen relativ schnell, da keine Elemente
verschoben und kopiert werden müssen.
Modifikation bei jeder Größe relativ schnell.
Durch doppelte Verkettung verbesserbar.
Es sind stets zahlreiche Objekte involviert
Nur soviel Speicher wie erforderlich belegt
Verkettete Strukturen sind effizienter bei
häufigem veränderndem Zugriff
Programmieren 2 - H.Neuendorf
(91)
Datenstrukturen
Dynamische Datenstrukturen
Verkettete unsortierte Liste
Operationen in iterativer Formulierung
(Rekursive Formulierungen jedoch auch gut darstellbar … )
tail
head
5
3
12
Knoten-Reihenfolge
entspricht nicht
Sortierreihenfolge ihrer
Datenelemente
Programmieren 2 - H.Neuendorf
(92)
Unsortierte Listen - Typische Operationen
Methoden :
boolean add( Object obj )
// fügt obj am Ende der Liste an und gibt true zurück
boolean addFirst( Object obj )
// fügt obj am Anfang der Liste an und gibt true zurück
int size( )
// Anzahl der Elemente in Liste
Object get( int pos )
// liefert an Position pos gespeichertes Objekt zurück ohne Entfernen
boolean contains( Object obj )
// liefert true, falls Liste spezifiziertes Objekt enthält
boolean remove( Object obj )
// löscht erstes Vorkommen des spezifizierten Objekts
gibt true zurück, falls Objekt gefunden und gelöscht wurde
void clear( )
// leert Liste
boolean isEmpty( )
// testet, ob Liste leer
Anmerkung :
Hinzufügen von Elementen in Liste mittels add( ) funktioniert immer.
In bestimmten Datenstrukturen (zB Set) ist Hinzufügen aber nicht immer
möglich, so dass Erfolg durch Rückgabewert (true / false) ausgedrückt wird.
Um gleiche Semantik für möglichst viele Datenstrukturen zu erhalten, macht
es Sinn, die Methoden wie angegeben zu spezifizieren.
Das List-Interface / die
Listen-Semantik kann auch
auf Basis von Arrays
implementiert werden !
Begrifflich trennen zwischen
dem ADT und seiner
technischen Konkretisierung !
Programmieren 2 - H.Neuendorf
(93)
Listen - Erwägungen
Diskussion :
Ist Methode get( int pos ) sinnvoll parametrisiert ?
1. Auffassung :
(deskriptive Denkweise)
Beim Verwalten von Objekten ist eher Methode get( Key k ) sinnvoll, die Referenz auf
Objekt gemäß key = ID zurückliefert.
Vor- oder Rückgabe von Indexpositionen ist eigentlich weniger sinnvoll, da internes
technisches Detail der Datenstruktur - das sich jederzeit ändern kann
Positionen sind nicht statisch, ändern sich - sind deshalb keine nützliche Information.
2. Gegenposition :
(imperative Denkweise)
Eine Liste ist definitionsgemäß eine indizierte Reihung.
Somit ist Indexzugriff statthaft !
Programmieren 2 - H.Neuendorf
(94)
Listen - Erwägungen
tail
head
3
5
12
1. Doppelte Einträge
Sind Einträge äquivalenter Elemente erlaubt ?
Stack, Queue, Pool, UnsortedList → ja
(tendenziell)
SortedList, Set, Baum, Hashtable → nein
2. Abgriff von Einträgen
Werden Objekte beim Zugriff typischerweise entfernt ?
Stack, Queue, Pool → ja
- ansonsten eher nicht
3. Rückgabewerte
Welche Art von Rückgabewerten machen Sinn ?
Die an der Position pos bzw. unter key gespeicherten Werte bzw. Referenzen
Keine Rückgabe von Referenzen auf Node-Objekte
→ sind internes technisches Detail der Datenstruktur, das Verwender nicht kümmert
Eher keine Rückgabe von Indexpositionen – ebenfalls technisches Detail
Programmieren 2 - H.Neuendorf
(95)
Unsortierte Listen
- Operationen
Vereinfachte Darstellung
tail
head
5
3
mit Typ int und public Zugriffen
12
// Knotenklasse
class Node {
public int val ;
Liste = Knoten + typische Operationen :
public Node next ;
Umsetzung mittels Verkettung :
Klasse LinkedList , die Knoten-Objekte verwaltet
public Node( int v ) {
val = v ;
next = null ;
LinkedList
- Node head, tail
Node
}
}
int val
+ boolean addFirst( int x )
Node next
+ boolean add( int x )
+ boolean remove( int x )
// Listenklasse
class LinkedList {
+ boolean contains( int x )
Es könnten beliebige Datentypen in
Listen gespeichert werden.
// …
Attribute von Node nur deshalb public,
um Implementierung zu vereinfachen.
private Node head = null ;
private Node tail = null ;
// …
}
Programmieren 2 - H.Neuendorf
(96)
Einfügen List-Elemente
→ Am Listenende
tail
head
3
5
class LinkedList {
// kein Zugriff auf Pointer!
12
private Node head = null ;
private Node tail = null;
Liste am Anfang leer
head + tail mit null initialisiert
public boolean add( int val ) {
1. Neuen Knoten p erzeugen und Wert darin speichern
Node p = new Node( val ) ;
2. Mittels tail.next auf den neuen Knoten verweisen:
if ( head == null )
tail.next = p ;
{ head = p ; }
Dadurch ist dieser nun ans Ende der Liste gehängt
else
3. Variable tail auf den neuen Knoten setzen,
{ tail.next = p ; }
da dieser nun das neue Ende der Liste darstellt :
tail = p ;
return true ;
tail = p ;
}
Fälle :
a) Liste noch leer
}
(head == null) :
neuer Knoten p ist einziger Knoten
head = tail = p
b) Liste nicht mehr leer (head != null) :
neuer Knoten p ans Ende der Liste
tail.next = p
Initialzustand
head tail
add12);
head
(p)
tail
12
Programmieren 2 - H.Neuendorf
(97)
Einfügen → Am Listenende
Initialzustand
head tail
(p)
12
tail
private Node tail = null ;
public boolean add( int val ) {
Dann :
Node p = new Node(val) ;
Neuen tail erzeugen, indem tail
nun auf neuen Knoten p zeigt :
tail = p ;
(p)
3
12
if ( head == null )
{ head = p ; }
else
add(3);
head
private Node head = null ;
Alten tail verwenden, um auf
neuen Knoten zu verweisen :
tail.next = p ;
add(12);
head
class LinkedList {
Zuerst :
{ tail.next = p ; }
tail = p
tail = p ;
return true ;
tail.next = p
}
}
add(45);
head
(p)
12
3
45
tail.next = p
Wachstumsrichtung der Liste
tail = p
Objektvariable p ist lokale
Variable der Methode add :
Lebt nur während Ausführung
der Methode - im Gegensatz zu
globalen Variablen head + tail !
Programmieren 2 - H.Neuendorf
(98)
Einfügen
→ Am Listenanfang
tail
head
5
3
class LinkedList {
private Node head = null ;
12
private Node tail = null ;
public boolean addFirst( int val ) {
1. Neuen Knoten p erzeugen und Wert darin speichern
Node p = new Node( val ) ;
2. Der neue Knoten hat als Nachfolger den bisherigen
if( tail == null ) tail = p;
p.next = head ;
Listenanfang (head)
head = p ;
p.next = head ;
return true ;
3. Der neue Kopf der Liste ist nun der neue Knoten :
Knoten p wird zum neuen Listenanfang
head = p ;
Keine Unterscheidungen hinsichtlich Listenlänge (leer /
nicht leer) erforderlich
Bei unsortierten Listen ist es egal, wo man ein neues
Element einfügt …
}
}
Vorteil gegenüber Arrays :
Einfügen von Elementen am Anfang erfordert
kein Verschieben von Elementen !
Initialzustand
head
addFirst(12);
head
12
Programmieren 2 - H.Neuendorf
(99)
Einfügen → Am Listenanfang
class LinkedList {
Initialzustand
private Node head = null ;
head tail
private Node tail = null ;
addFirst(12);
head = p
(p)
12
public boolean addFirst( int val ) {
tail = p
Node p = new Node( val ) ;
if( tail == null ) tail = p;
p.next = head ;
head = p ;
p.next = head = null
head = p
return true ;
}
addFirst(3);
}
(p)
3
12
Zuerst :
Next-Zeiger des neuen Knotens p
auf alten head zeigen lassen :
p.next = head ;
p.next = head
head = p
addFirst(45);
Wachstumsrichtung der Liste
(p)
45
Dann :
3
p.next = head
12
Neuen head erzeugen, indem head
nun auf neuen Knoten p zeigt :
head = p ;
Programmieren 2 - H.Neuendorf (100)
Suchen von Elementen
tail
head
3
5
12
class LinkedList {
private Node head = null ;
Suchen
private Node tail = null ;
1. An den Anfang der Liste gehen :
public boolean addFirst( int val ) { … }
Hilfszeiger p = head ;
2. Knoten der Liste durchlaufen + prüfen, ob
gesuchter Wert enthalten :
public boolean contains( int val ) {
p.val == val
Node p = head ;
Wenn nicht, zum nächsten Knoten übergehen :
while( p != null && p.val != val ) {
p = p.next ;
p = p.next ;
3. Wenn p == null ist Listenende erreicht
}
return p != null ;
Abbruchkriterium : Knoten mit next == null
}
Wert nicht gefunden
}
Umsetzung while-Schleife →
Läuft solange gesuchter Wert noch nicht gefunden evtl. bis ans Ende der Liste !
Erstes vorkommendes Element wird gefunden –
Mehrfacheinträge können nicht erfasst werden …
Bei Objekten mit equals( ) arbeiten
Programmieren 2 - H.Neuendorf (101)
Löschen von Elementen
Problem :
head
Klasse Node kennt nur Nachfolger
3
5
12
Beim Durchlauf zweiten Hilfszeiger prev
mitführen, der auf Knoten vor p zeigt !
p
head
3
5
Wie bekommt man Vorgängerknoten ?
12
"Umbiegen" / Übernrücken :
prev.next = p.next ;
prev
p
Sonderfall :
Löschen
1. Zu löschenden Knoten suchen
Hilfszeiger p darauf setzen
2. Next-Zeiger des Vorgängerknotens auf Nachfolger
p ist erstes Element der Liste →
keine Vorgänger !
(prev == null)
Neuer Listenanfang ist Nachfolger von p
Liste beginnt nun mit Nachfolger von p
des zu löschenden Knotens setzen ("überbrücken")
"Umbiegen" :
head = p.next ;
Zu löschender Knoten nur noch von p referenziert
Zeiger p ist lokale Variable der Methode
GC entsorgt nicht mehr referenzierten Knoten nach Methodenende !
Programmieren 2 - H.Neuendorf (102)
Löschen von Elementen
class LinkedList {
private Node head ;
private Node tail ;
public boolean remove( int val ) {
Node p = head ;
Node prev = null ; // Hilfszeiger
while( p != null && p.val != val ) {
prev = p ;
p = p.next ;
}
if ( p != null ) {
if ( p == head ) { head = p.next ; }
else
{ prev.next = p.next ; }
if( p == tail )
{ tail = prev ; }
return true ;
}
else return false ;
}
}
Abfrage darf nicht lauten :
if ( p.val == val )
da p auch null sein könnte, wenn
Element nicht in Liste enthalten
Weiterführen von prev und p
p == head : Lösch-Element am List-Anfang
p == tail : Tail-Pointer umsetzen !
Wenn p == null ist zu löschendes
Element nicht in Liste enthalten
false zurückgeben !
Programmieren 2 - H.Neuendorf (103)
Ausgabe Listenelemente →
class LinkedList {
private Node head ;
private Node tail ;
public String toString( ) {
StringBuffer sb = new StringBuffer( ) ;
toString( )
Ausgabe Liste :
Werte werden in Stringbuffer geschrieben und
am Ende in String umgewandelt.
String wird zurückgegeben.
Liste von Anfang bis Ende durchlaufen.
sb.append( "( " ) ;
Node p = head ;
if( p == null ) return "Leer" ;
Alternative Schleifen-Formulierung :
for( Node p = head; p!=null; p=p.next ) { … }
while( p != null ) {
sb.append( p.val ) ;
sb.append( " " ) ;
p = p.next ;
Weiterführen von p
}
sb.append( " )" ) ;
return sb.toString( ) ;
}
}
Übung :
Wie lauten Methoden void clear( )
und boolean isEmpty( ) ?
Programmieren 2 - H.Neuendorf (104)
Doppelt verkettete Liste
Erlaubt Traversieren in zwei Richtungen
Node
class DLinkedList {
prev
private Node head ;
val
private Node tail ;
next
public boolean add( int val ) {
Node p = new Node( val ) ;
class Node {
if( head == null ){
tail = p ;
// angespasst an DLL
public int val ;
head = p ;
}
else{
public Node next ;
// Nachfolger-Referenz
public Node prev ;
// Vorgänger-Referenz
public Node( int v ) {
tail.next = p ;
val = v ;
p.prev = tail ;
next = null ;
}
tail = p ;
}
}
return true ;
prev = null ;
tail
head
}
// weitere Methoden : Übung …
}
tail-Pointer macht dadurch erst Sinn !
warum ?
Programmieren 2 - H.Neuendorf (105)
Verkettete Liste - Rekursive Formulierungen
class LinkedList {
private Node head = null ;
public boolean addFirst( int val ) { /*… */ }
public int sizeRekursiv( ) {
return sizeRek( head ) ;
}
private int sizeRek( Node p ) {
if( p == null ) return 0 ;
return 1 + sizeRek( p.next ) ;
Typischer Aufbau :
Public Zugriffsmethode ruft intern mit
privaten Argumenten die private
eigentliche rekursive Implementierung
Bsp : head ist kein public Attribut !
Rekursionsschritt :
Statt while-Iteration erfolgt rekursiver
Aufruf mit p.next
}
public boolean containsRekursiv( int val ) {
return containsRek( head, val ) ;
}
private boolean containsRek( Node p, int val ) {
if( p == null ) return false ;
Alle iterativen List-Methoden lassen sich
problemlos rekursiv darstellen - denn eine
Liste ist eine rekursive Struktur !
Alternative Definition der tail-Position erleichtert
rekursive Formulierungen : tail = direkt hinter head
if( p.val == val ) return true ;
return containsRek( p.next, val ) ;
}
}
Übungen :
Rekursive Ausgabe aller Listelemente
Rekursive Erstellung einer invertierten Liste
Programmieren 2 - H.Neuendorf (106)
Datenstrukturen
Dynamische Datenstrukturen
Verkettete sortierte Liste
Programmieren 2 - H.Neuendorf (107)
Sortierte Liste
tail
head
5
7
Knoten-Reihenfolge entspricht
Sortierreihenfolge ihrer Elemente
12
Suchen
Vorteil :
class SortedList {
Sortierung erlaubt sortierte Ausgabe der Werte
ohne extra Sortieroperation
private Node head ;
private Node tail ;
Schnelleres Aufsuchen von Elementen - Liste
muss nicht bis Ende durchsucht werden :
//
public boolean contains( int val ) {
Abbrechen der Suche, wenn man auf Element stößt,
das "größer" ist als gesuchtes Element
Node p = head ;
while( p! = null && p.val < val )
p = p.next ;
Aber :
if ( p != null && p.val == val ) return true ;
Für effizientes Suchen in großen Datenbeständen
sind Binäre Suchbäume geeigneter
Lineare Liste zum Durchsuchen nicht gut geeignet,
da kein wahlfreier Zugriff
Binary Search nicht effizient darstellbar !
else return false ;
}
}
Programmieren 2 - H.Neuendorf (108)
Lineares Suchen in verketteter Liste
class LinkedList {
private Node head ;
class SortedList {
private Node head ;
private Node tail ;
private Node tail ;
public boolean contains( int val ) {
Node p = head ;
public boolean contains( int val ) {
Node p = head ;
while( p! = null && p.val < val )
{ p = p.next ; }
while( p != null && p.val != val )
{
p = p.next ;
}
if( p != null ) return true ;
if ( p != null && p.val == val ) return true ;
else return false ;
else return false ;
}
}
Zeitbedarf :
neg. Fall → n Vergleiche
pos. Fall → ∅ n /2 Vergleiche
Komplexität O(n)
Wächst linear mit Zahl der Elemente
}
}
Zeitbedarf :
verändert sich kaum
neg. Fall → ∅ n / 2 Vergleiche
kann früher abgebrochen werden
pos. Fall → ∅ n /2 Vergleiche
Komplexität bleibt O(n)
Wächst linear mit Zahl der Elemente
Programmieren 2 - H.Neuendorf (109)
Sortierte Liste - Einfügen
class SortedList {
private Node head ;
tail
head
7
5
prev
q
public boolean insert( int val ) {
12
Node p = head;
Node prev = null;
Node q = new Node( val ) ;
p
if( p==null ) { // leere Liste
head = q ;
Neues Element an richtiger Position einfügen - nicht einfach
am Anfang / Ende wie bei unsortierter Liste
prev = p ;
// Keine doppelten Einträge :
if( p!=null && p.val==val ) return false ;
q.next = p ;
if( p==head ) head = q ;
Neuen Knoten q einfügen zwischen p und prev :
else
q.next = p ;
q wird zum neuen Kopf
// am Anfang
prev.next = q ;
if( prev==tail ) tail = q ;
Sonderfälle :
p == head
p = p.next ;
}
Zeiger p auf Knoten setzen, dessen Wert größer ist als
einzufügendes Element + Zeiger prev auf Vorgänger von p.
q wird am Ende eingehängt
return true ;
while( p!=null && p.val<val ) {
Zum Einfügen die Einfügeposition suchen :
p == null
tail = q ;
}
Erfordert nur Zeigeroperationen - kein Verschieben von
Elementen nötig. Doppelt-Eintragungen unterbunden !
prev.next = q ; und
private Node tail ;
// am Ende
return true ;
}
}
Programmieren 2 - H.Neuendorf (110)
Sortierte Liste - Einfügen
insert( 6 )
head
7
5
prev
Lokale Variablen :
tail
p
6
q, p, prev
Globale Variablen : head tail
12
tail
head
7
5
q
1. Node q = new Node(6) ;
prev
q
12
p
6
q.next = p ; 2.
3.
head
tail
prev.next = q ;
7
5
prev
6
12
p
q
Programmieren 2 - H.Neuendorf (111)
Sortierte Liste - Einfügen Sonderfälle
Sonderfälle :
public boolean insert( int val ) {
Node p = head;
Node prev = null;
Node q = new Node(val);
1. p ist der erste Knoten der Liste : p == head
if( p==null ) { // leere Liste
Es gibt keinen Vorgänger prev
head = q ; tail = q ; return true ;
q wird neuer Listenkopf: head = q ;
}
while( p!=null && p.val<val ) {
2. Leere Liste : head = null p == null
prev = p ;
q wird neuer Listenkopf : head = q ;
p = p.next ;
}
q hat noch keinen Nachfolger : q.next = null
if( p!=null && p.val==val ) return false;
q.next = p ;
3. q muß ans Ende der Liste :
if( p==head ) head = q ;
Schleife läuft bis ans Ende der Liste
p hat dort den Wert null
else
prev zeigt auf den letzten Knoten
if( prev==tail ) tail = q;
q kommt ans Ende :
return true;
prev.next = q ;
prev.next = q ;
}
q.next = p ; // = null
}
Programmieren 2 - H.Neuendorf (112)
Sonderfall Einfügen am Anfang p == head
tail
insert( 4 )
head
7
5
prev p
12
Lokale Variablen : q, p, prev
Globale Variablen : head tail
4
tail
head
q
7
5
12
1. Node q = new Node(4) ;
prev p
4
2.
q.next = p ;
q
tail
7
5
prev p
12
4
3.
head
q
if ( p == head ) head = q ;
Programmieren 2 - H.Neuendorf (113)
Sonderfall Einfügen am Ende p == null
tail
insert( 14 )
head
7
5
Lokale Variablen: q, p, prev
12
Globale Variablen: head tail
p
1. Node q = new Node(14) ;
prev
14
q
tail
head
7
5
12
p
prev
head
5
7
3. prev.next = q ;
2. q.next = p ;
14
q
12
p
prev
tail
14
// tail = q ;
q
Programmieren 2 - H.Neuendorf (114)
Datenstrukturen
Dynamische Datenstrukturen
Design-Prinzipien & Varianten
Generische Formulierung
Programmieren 2 - H.Neuendorf (115)
Strategy-Interfaces
Strategy- Methoden : Aktionsobjekte als Parameter
interface Action {
// Strategy-IF
public abstract Object act( Node h ) ;
}
class LinkedList { // Liste ganzer Zahlen
private Node head = null ;
private Node tail = null ;
public boolean add( int val ) {
// …
}
public Object work( Action obj ) {
return obj.act( head ) ;
}
}
Ohne LinkedList zu verändern kann man
zusätzliche Funktionalität injizieren …
Dadurch besitzt Klasse Schnittstelle, an die beliebige
Funktionalität PlugIn-artig übergeben werden kann
class Sum implements Action {
public Object act( Node h ) {
Node p = h ;
int summe = 0 ;
while( p != null ) {
summe = summe + p.val ;
p = p.next ;
}
return new Integer( summe ) ;
}
}
class Output implements Action {
public Object act( Node h ) {
Node p = h ; String str = null ;
while( p != null ) {
str = str + p.val + " " ;
p = p.next ;
}
return str ;
}
}
Programmieren 2 - H.Neuendorf (116)
Strategy-Interfaces
class UList {
public static void main( String[ ] args ) {
LinkedList MyList = new LinkedList( ) ;
MyList.add( 17 ) ;
MyList.add( 3 ) ;
MyList.add( 6 ) ;
Sum su = new Sum( ) ;
Integer summe = (Intreger) MyList.work( su ) ;
Output out = new Output( ) ;
String output = (String) MyList.work( out ) ;
}
}
class Sum implements Action {
public Object act( Node h ) {
Node p = h ;
int summe = 0 ;
while( p != null ) {
summe = summe + p.val ;
p = p.next ;
}
return new Integer( summe ) ;
}
}
class Output implements Action {
public Object act( Node h ) {
Node p = h ; String str = null ;
while( p != null ) {
str = str + p.val + " " ;
p = p.next ;
}
return str ;
}
}
Verschiedene Aktionen ("Strategien") erfordern verschiedene
Aktionsobjekte mit Methode korrekter Signatur, die über "PlugIn"Methode der Ziel-Klasse (work() von LinkedList) aufgerufen wird.
Implementierende Klassen sind typischerweise zustandslos (ohne Attribute),
besitzen nur entsprechende Aktionsmethode.
Referenzen auf Aktionsobjekte
als Ersatz für in Java nicht
vorhandene Funktions- Pointer
Programmieren 2 - H.Neuendorf (117)
Listen - Strukturvarianten
Spezifikation des Abstrakten Datentyps List :
public interface List { // interface List<T>
public boolean add( Object obj ) ;
public boolean addFirst( Object obj ) ;
public int size( ) ;
public Object get( int pos ) ;
public boolean contains( Object obj ) ;
public boolean remove( Object obj ) ;
public void clear( ) ;
public boolean isEmpty( ) ;
}
Eleganter : Definition von Node als
private Klasse innerhalb von LinkedList
Konzept der Inneren Klasse
Somit Node von außen nicht sichtbar !
Implementierung von List :
class LinkedList implements List {
private Node head ;
public boolean add( Object obj ) { /* …*/ }
// …
}
class Node {
public Node( Object obj ) { val = obj ; next = null ; }
public Object val ;
public Node next ;
}
class LinkedList implements List {
private Node head ;
public boolean add( Object obj ) { /* …*/ }
// …
private class Node {
Node( Object obj ) { val = obj ; next = null ; }
Object val ; Node next ;
}
}
Programmieren 2 - H.Neuendorf (118)
LinkedList via Object
class Node {
Elemente vom Typ Object :
Kann beliebige Objekte aufnehmen
public Object val ;
Speichern einfacher Datentypen durch Hüllklassen
public Node next ;
public Node( Object obj ) {
val = obj ;
// … Fortsetzung :
next = null ;
public boolean contains( Object obj ) {
}
Node p = head ;
}
for( Node p = head; p!=null; p=p.next) {
if( p.equals( obj ) ) return true ;
class LinkedList {
private Node head = null ;
}
private Node tail = null ;
return false ;
public boolean addFirst( Object obj ) {
if( obj==null ) return false ;
}
}
Node p = new Node( obj ) ;
if ( head == null ) { head = p ; }
else { tail.next = p ; }
tail = p ;
return true ;
}
// …
Keine null-Werte
als Datenelemente
akzeptiert !
List MyList = new LinkedList( ) ;
MyList.addFirst( "Test" ) ;
MyList.addFirst( new Long(27) ) ;
Programmieren 2 - H.Neuendorf (119)
Speichern spezieller Objekte
// Nutzdatenklasse
class Mitarbeiter {
private String name ;
public String getName( ) { return name ; }
public Mitarbeiter( String n ) { name = n ; }
}
// Knotenklasse = Behälter für Mitarbeiter-Objekte
class Node {
private Mitarbeiter val ;
public Node next ;
public Node( Mitarbeiter obj ) {
val = obj ;
next = null ;
}
}
class LinkedList {
private Node head, tail ;
public boolean addFirst( Mitarbeiter obj ) {
if( obj==null ) return false ;
Node p = new Node( obj );
if( tail == null ) tail = p;
p.next = head ;
head = p ;
Keine null-Werte
return true ;
als Datenelemente!
}
}
In Node Elemente vom Typ
Mitarbeiter speicherbar
// Alternativ : Verkettungsfähige Mitarbeiterklasse
//
Verfügt über passendes next-Attribut
class Mitarbeiter {
private String name ;
public Mitarbeiter next ;
public String getName( ) { return name ; }
public Mitarbeiter( String n ){ name = n ; }
}
class Linkedist {
private Mitarbeiter head, tail ;
public boolean addFirst( Mitarbeiter obj ) {
if( obj == null ) return false ;
Mitarbeiter p = obj ;
if( tail == null ) tail = p ;
p.next = head ;
head = p ;
return true ;
}
}
Wie sehen die entstehenden Objekt-Strukturen aus ?
Programmieren 2 - H.Neuendorf (120)
Speichern von Objekten in Listen - Referenzbild
a) Mit separater Knotenklasse Node :
Müller
Haller
Gross
Mitarbeiter
head
List
val
next
Node
Jede Klasse kann durch entsprechendes next Attribut verkettungsfähig gemacht werden
Vorteil :
b) Mit verkettungsfähiger Mitarbeiter-Klasse :
Keine separate Knoten-Klasse nötig
Nachteil : Klasse muss angepasst werden
head
Müller
next
Mitarbeiter
Haller
Gross
List
Beim Verwalten von
Objekten ist Methode get( )
sinnvoll, die Referenz auf das
Objekt gemäß Schlüsselwert
(ID) = key zurückliefert
Programmieren 2 - H.Neuendorf (121)
Generische Liste mittels Generics
class Node<T> {
Typ-Variable <T>
public T val ;
public Node<T> next ;
val = obj ;
Man vermeidet den total generischen, völlig unspezifizierten
Typ Object und gibt Verwender trotzdem Möglichkeit zur
flexiblen Typ-Vorgabe mit Typsicherheit :
next = null ;
Compiler prüft bei konkreter Verwendung auf korrekten Typ !
public Node( T obj ) {
}
}
class LinkedList<T> {
class ListUser {
public static void main( String[] args) {
private Node<T> head = null ;
private Node<T> tail = null ;
LinkedList<String> MyList = new LinkedList<String>( ) ;
public boolean addFirst( T obj ) {
MyList.add( "dies geht" ) ;
if( obj == null ) return false ;
Node<T> p = new Node<T>( obj ) ;
MyList.add( "und auch" ) ;
if ( head == null ) { head = p ; }
MyList.add( new Long(27) ) ; // Compilerfehler !!
else { tail.next = p ; }
tail = p ;
return true ;
}
}
}
}
Programmieren 2 - H.Neuendorf (122)
Datenstrukturen
Dynamische Datenstrukturen
Stack und Queue auf Basis verketteter Listen
Verschiedene Varianten …
Listen sind allgemeinere Datenstrukturen als Stack und Queue
Somit können die spezielleren Datenstrukturen Stack und Queue
durch Listen dargestellt werden
Programmieren 2 - H.Neuendorf (123)
Stack als verkettete Liste
Nachteil Array-Implementierung :
Fest vorgegebene Größe
kann überlaufen !
Vorteil List-Implementierung :
Beliebig viele Elemente hinzufügbar - bis Heap erschöpft
Darstellung als verkettete Liste :
Werte könnten auch
Referenzen sein ...
Zeiger top zeigt auf obersten Knoten
next-Zeiger liefern darunterliegende Elemente
Operation push( ) : Einfügen eines neuen Knotens am Anfang der Liste
Operation pop( ) :
Entfernen eines Knotens vom Anfang der Liste
top
- Node top
+ int pop( )
val
next
Stack
+ void push( int x )
Node
Node
int
val
Node next
Schnittstelle der Stack-Klasse ist die selbe wie bei Realisation mit Array
Außensicht + Verwendbarkeit unverändert. (Node als private innere Klasse!)
Interne Umsetzung ist völlig anders - ohne dass Verwender dies wahrnehmen
Konstanz der Methodenschnittstellen + ihrer wahrnehmbaren Funktionalität ↔
Verbergen der internen Abläufe + Datenstrukturen Information Hiding !
Abstrakte
Datenstrukturen
Programmieren 2 - H.Neuendorf (124)
Stack als verkettete Liste
class Stack {
private Node top = null ;
Einfügen push( ) :
1. Neuen Knoten p mit Wert erzeugen
public void push( int x ) {
2. Nachfolger des neuen Knotens ist bisheriger top :
Zugriff nur über
öffentliche
Schnittstelle!
Kein direkter Zugriff
auf top-Pointer !
Node p = new Node( x ) ;
p.next = top ;
p.next = top ;
3. Neuer top ist nun der neue Knoten p :
top = p ;
}
top = p ;
public int pop( ) {
if ( top == null ) { /* Exception */ }
Entfernen pop( ) :
else {
1. Wenn Stack leer ist ( top == null ) Exception werfen
Node p = top ;
2. Wenn Stack nicht leer top-Element entfernen :
top = top.next ;
(p)
top
return p.val ;
2.1. Lokalen Hilfszeiger p auf top setzen: Node p = top ;
}
2.1. Nachfolger des bisherigen top-Elements wird top :
}
top = top.next ;
public boolean isEmpty( ) {
2.2. Wert des bisherigen top-Elements zurückgeben
return top == null ;
return p.val ;
3. Altes top-Objekt nun nicht mehr referenziert
top
}
}
von GC nach Methodenende entsorgt
Programmieren 2 - H.Neuendorf (125)
Stack als verkettete Liste :
top
1.
5
push( )
push( 5 )
class Stack {
private Node top = null ;
Node p = new Node( 5 ) ;
public void push( int x ) {
p.next = top; // = null !
p
Node p = new Node( x ) ;
p.next = top ;
top
top = p ;
2.
5
}
top = p ;
p
// …
}
8
p
top
push( 8 )
1. Node p = new Node( 8 ) ;
p.next = top;
top
8
2. top = p ;
p
5
5
Programmieren 2 - H.Neuendorf (126)
Stack als verkettete Liste :
top
pop( )
pop( )
3
p
8
class Stack {
1. Node p = top ;
private Node top = null ;
…
public int pop( ) {
if ( top == null ) { /* Exception */ }
else {
Node p = top ;
top = top.next ;
return p.val ;
}
}
// …
return p.val ;
5
Methodenende :
Garbage Collection
3
}
top
(p)
8
5
Referenz p ist lokale Variable der Methode pop( )
2. top = top.next ;
Nach Methodenende verschwindet p und somit die
letzte Referenz auf Element
Garbage Collection entfernt Element !
Programmieren 2 - H.Neuendorf (127)
Queue als verkettete Liste
Nachteil Array-Implementierung :
Fest vorgegebene Größe
Vorteil List-Implementierung :
Beliebig viele Elemente hinzufügbar - bis Heap erschöpft
Darstellung als verkettete Liste :
kann überlaufen
head
tail
Zeiger head zeigt auf Anfang der Schlange
Zeiger tail zeigt auf Ende der Schlange
Verkettung läuft von Anfang Richtung Ende
Operation put( ) :
Anhängen neues Element ans Ende
Operation get( ) :
Entfernen des ältesten Elements vom Anfang
Sonderfall :
Schlange ist leer
es kann nichts entnommen werden
Queue
- Node head, tail
+ void put ( int x )
+ int get ( )
Node
int val
Node next
tail-Pointer ermöglicht bei put( )Operation direkten Zugriff auf
das letzte Element …
Programmieren 2 - H.Neuendorf (128)
Queue als verkettete Liste
class Queue {
private Node head = null ;
private Node tail = null ;
Einfügen put( ) :
1. Neuen einzufügenden Knoten p erzeugen
2. Leere Schlange
(head == p) :
public void put( int x ) {
Node p = new Node( x ) ;
if ( head == null ) head = p ;
else
tail.next = p ;
tail = p ;
}
public int get( ) {
if ( head == null ) { /* Exception */ }
else {
Node p = head ;
head = head.next ;
if ( head == null ) tail = null ;
return p.val ;
}
}
neuer Knoten p wird neuer head : head = p ;
Nichtleere Schlange - neuen Knoten p ans Ende :
tail.next = p ;
3. Nach Anhängen ist neuer Knoten p das neue Ende :
tail = p ;
Entfernen get( ) :
1. Wenn Schlange leer ( head == null ) Exception werfen
2. Hilfszeiger p auf head setzen - soll entfernt werden :
Node p = head ;
Rückgabe von p.val
3. Neuer Kopf ist der Nachfolger des bisherigen Kopfes:
head = head.next ;
4. Wenn letztes Element der Schlange entfernt wird, dann
auch tail = null ;
head
head + tail nun beide null
public boolean isEmpty( ) {
tail
return head == null ;
}
}
Programmieren 2 - H.Neuendorf (129)
head
put( 9 )
Queue als verkettete Liste
tail
class Queue {
private Node head = null ;
private Node tail = null ;
1.
Node p = new Node( 9 ) ;
9
public void put( int x ) {
p
head
Node p = new Node( x ) ;
tail
if ( head == null) head = p ;
else
tail.next = p ;
tail = p ;
}
2.
tail.next = p ;
// ...
9
}
p
Sonderfall : Leere Queue
head
- head ist noch null
- head zeigt nach Erzeugen von
tail
3.
tail = p ;
9
(p)
neuem Knoten p auf diesen
head = p ;
Methodenende: Lokale Variable p gelöscht
Programmieren 2 - H.Neuendorf (130)
get( )
head
Queue als verkettete Liste
tail
class Queue {
private Node head = null ;
private Node tail = null ;
1. Node p = head ;
p
head
2.
p
GC !
(p)
public int get( ) {
if ( head == null ) { /* Exception */ }
else {
Node p = head ;
head = head.next ;
if ( head == null ) tail = null;
return p.val ;
}
}
// ...
tail
head = head.next ;
…
return p.val ;
head
tail
}
Methodenende : Lokale Variable p gelöscht
Altes head-Objekt nicht mehr referenziert
Garbage Collection !
Programmieren 2 - H.Neuendorf (131)
head
tail
Queue als verkettete Liste
get( )
Sonderfall :
class Queue {
Queue enthält nur noch ein Element
p
head
private Node head = null ;
private Node tail = null ;
1. Node p = head ;
tail
public int get( ) {
if ( head == null ) { /* Exception */ }
else {
Node p = head ;
head = head.next ;
if ( head == null ) tail = null ;
return p.val ;
}
}
// …
2. head = head.next ; // = null !
…
return p.val ;
p
head
GC !
tail
3. if( head == null ) tail = null ;
Methodenende : Lokale Variable p gelöscht
Objekt nicht mehr referenziert
(p)
}
Garbage Collection !
Es bleibt leere Queue zurück - head + tail sind wieder null !
Programmieren 2 - H.Neuendorf (132)
Generischer Stack + Knotenklasse
class Node<E> {
private Node<E> next ;
private E element ;
Typ der generischen Knotenklasse Node ist nicht
mehr einfach "Node", sondern nun :
public Node( E element ) {
this.element = element ;
Node<E>
// sprich : "Node of E"
}
public void setNext( Node<E> next ) {
this.next = next ;
}
public Node<E> getNext( ) {
return next ;
Diese Typbezeichnung muss somit durchgängig
verwendet werden – wie zB beim Attribut next und
bei der Methode setNext( ) !
Ansonsten raw-type Warnung vom Compiler
}
public E getElement( ) {
return element ;
}
public void setElement( E element) {
this.element = element ;
}
}
Namenskonvention - Typvariablen tragen
kurze Namen, meist nur ein Buchstabe :
E für Datenelement
K für ein Schlüsselobjekt
V für ein Wertobjekt
T für einen allgemeinen Typ …
Programmieren 2 - H.Neuendorf (133)
Generischer Stack + Knotenklasse
class Stack<E> {
Auch Klasse Stack ist generisch.
private Node<E> top ;
Sie gibt den bei der Type-Invocation
für E zugewiesenen Typ an die
Objekte der generischen Klasse
Node weiter !
public void push( E item ) {
Node<E> p = new Node<E>( item ) ;
p.setNext( top ) ;
top = p ;
}
public E pop( ) {
class TestStack {
public static void main( String[ ] args) {
if( top == null ) throw new RuntimeException( "Empty!" ) ;
Node<E> p = top ;
Stack<String> s = new Stack<String>( ) ;
top = top.getNext();
s.push( "Hallo" ) ;
return p.getElement() ;
s.push( "Ihr" ) ;
}
String test = s.pop( ) ;
public boolean isEmpty( ){
// ...
Stack<Double> d = new Stack<Double>( ) ;
return top == null ;
// ...
}
}
}
}
Programmieren 2 - H.Neuendorf (134)
Generischer Stack + Innere Knotenklasse
class Stack<E> {
private Node top ;
// ... Fortsetzung
private class Node { // inner class
private Node next ;
public void push( E item ) {
private E element ;
Node p = new Node( item ) ;
public Node( E element ) {
p.setNext( top ) ;
this.element = element ;
top = p ;
}
}
public void setNext( Node next ) {
public E pop( ) {
this.next = next ;
if( top == null ) throw new RuntimeException( "Empty!" ) ;
}
Node p = top ;
public Node getNext( ) {
return next ;
top = top.getNext( ) ;
}
- NodeKlasse
übernimmt
Typ
!
return p.getElement( ) ; Die
// okinnere
Node
istEnun
nicht mehr generisch, sondern
}
direkt
deneigenen
Typ ETypder
Funktioniertübernimmt
nicht, wenn innere
Klasse
// ....
Parameter E
hätte, der äußeren
verdeckt:
äußeren
KlasseTyp-Parameter
Stack
Die innere Klasse Node übernimmt den Typ E. Sie selbst ist
nun nicht mehr generisch, sondern hängt von dem Typ ab, der
ihr von der äußeren Klasse Stack vorgegeben wird.
Typ-Variablen einer äußeren Klasse sind in einer inneren Klasse
sichtbar + verwendbar.
public E getElement( ) {
return element ;
}
public void setElement( E element) {
this.element = element ;
}
}
} // Ende der äußeren Klasse Stack
Programmieren 2 - H.Neuendorf (135)
Stack mittels interner LinkedList
class Stack<E> {
private List<E> list ;
public Stack( ) { list = new LinkedList<E>( ) ; }
public void push( E item ) {
list.addFirst( E ) ;
}
public E pop( ) {
if( list.isEmpty( ) ) throw new RuntimeException( "Empty!" ) ;
E element = list.get( 0 ) ; list.remove( element ) ;
return element ;
Die innere Klasse Node ist nun
nicht mehr generisch, sondern
public E peek( ) {
übernimmt direkt den Typ E der
äußeren
Klasse Stack
if( list.isEmpty( ) ) throw
new RuntimeException(
"Empty!" ) ;
Klasse Stack kümmert sich
nicht selbst um den Aufbau
einer verketteten Liste …
… sondern verwendet ein
Attribut vom Typ List …
… mit desen Methoden die
Stack-Semantik (ADT)
umgesetzt wird.
Verwender merkt keinen
Unterschied !
}
return list.get( 0 ) :
}
public boolean empty( ) { return list.isEmpty( ) ; }
Stack<String> s = new Stack<String>( ) ;
s.push( "Hallo" ) ; s.push( "Ihr" ) ;
String test = s.pop( ) ;
// ...
}
Programmieren 2 - H.Neuendorf (136)
Datenabstraktion
ADT
Grundprinzip = Information Hiding
↔
Ziel = Reduktion von Komplexität :
Implementierungsdetails verbergen →
Zugriff auf komplexe Strukturen nur über öffentliche Methoden = einfache Schnittstelle
ohne technische interne Implementierungsdetails offenzulegen !
Vorteil: Interne Implementierungsdetails können jederzeit geändert werden …
… solange Schnittstelle konstant bleibt
Datenabstraktion :
"Wie" der Implementierung wird versteckt
Nur das "Was" der Datenhaltung exponiert
in öffentlicher Schnittstelle
Verwender / Clients werden nicht invalidiert
Nicht Implementierung ist vorgegeben, sondern die
semantischen Operationen auf Datenelementen.
Somit kann Implementierung jederzeit geändert und
neuen Erfordernissen angepasst werden – ohne dass
verwendende Programme verändert werden müssten.
Stack-Klasse mit konventioneller Schnittstelle
pop( )
push( … )
Implementierung via Array oder Verketteter Liste
Implementierungsdetail - von Außen nicht sichtbar
Austauschbar !
Programmieren 2 - H.Neuendorf (137)
Datenstrukturen
Java-Standard-Interfaces im Datenstruktur-Umfeld
Ordnungs-Pattern : Comparable & Comparator
Iterator-Pattern :
Iterable & Iterator
Einige Inhalte + Abbildungen wurdem dem Skript
meines Kollegen Prof. Dr. Deck entnommen
Programmieren 2 - H.Neuendorf (138)
Ordnungspattern : IF java.lang.Comparable
java.util.Comparator
Vergleichsmethoden :
compareTo( ) und compare( ) implementieren totale Ordnungsrelation mit gleicher Semantik
a.compareTo( b )
compare( a, b )
<0
=0
>0
bedeutet:
a<b
a gleichgroß b
a>b
Absolutwerte <0 oder >0 egal
Keine weitere Semantik einbauen !
Totale Ordnung :
Alle Elemente einer
Menge können
aufgrund einer
Größer-GleichRelation linear
angeordnet werden
Allerdings unterschiedliche Signatur der Vergleichsmethoden :
public interface Comparable {
public interface Comparator {
public int compare( Object o1, Object o2 ) ;
public int compareTo( Object o ) ;
}
Implementierung von Comparable
bei "natürlicher" Ordnung als Teil
der Klassenlogik
public boolean equals( Object o ) ;
}
Implementierung von Comparator für weitere
Ordnungskriterien
z.B: durch separate Comparator-Klassen :
Definieren weitere Kriterien …
Davon kann es beliebig viele geben !
Programmieren 2 - H.Neuendorf (139)
Ornungspattern :
IF
java.lang.Comparable
java.util.Comparator
class Mitarbeiter implements Comparable {
public int persNr ; // eindeutige Ordnung
public String name ; public int alter ;
public Mitarbeiter( int nr, String n ) { /* … */ }
Implementierung von Comparable
bei "natürlicher" Ordnung als Teil
der Klassenlogik - hier :
persNr als Haupt-Kriterium
public int compareTo( Object o ) {
Mitarbeiter m = (Mitarbeiter) o ;
int cmp = this.persNr – m.persNr ;
Vorsicht :
if( cmp == 0 ) return 0 ;
Differenz zweier negativer Werte
kann Überlauf bewirken
if( cmp > 0 ) return +1;
else
return -1 ;
// alternativ:
// return this.persNr – m.persNr ;
}
// konsistente equals() + hashCode() -Implementierungen
Zwei Objekte, für die equals true
liefert sollten auch bei Vergleich
mit compareTo 0 liefern !
// Bezug auf selbe Attribute !
}
Programmieren 2 - H.Neuendorf (140)
Ordnungspattern :
IF
java.lang.Comparable
java.util.Comparator
import java.util.* ;
import java.util.*;
class Mitarbeiter implements Comparable {
class Sorted {
public static void main( String[] args ) {
// …
Mitarbeiter[ ] m = new Mitarbeiter[4] ;
}
m[0] = new Mitarbeiter( 11, "Anna" ) ;
m[1] = new Mitarbeiter( 5, "Ursula" ) ;
// Weiteres Sortierkriterium: Name
m[2] = new Mitarbeiter( 7, "Wolf" ) ;
class NameComparator implements Comparator {
m[3] = new Mitarbeiter( 13, "Bert" ) ;
public int compare( Object p, Object q ) {
// Sortieren nach compareTo() :
String s1 = ( (Mitarbeiter)p ).name ;
Arrays.sort( m ) ;
String s2 = ( (Mitarbeiter)q ).name ;
System.out.println( Arrays.toString( m ) ) ;
return s1.compareTo( s2 ) ;
}
// Sortieren nach NameComparator :
}
Arrays.sort( m, new NameComparator( ) ) ;
System.out.println( Arrays.toString( m ) ) ;
}
// Weitere Komparator-Klassen für weitere Attribute …
// Auch als Innere Klasse darstellbar
}
Programmieren 2 - H.Neuendorf (141)
Ordnungspattern : Generische Darstellung
import java.util.* ;
class Mitarbeiter implements Comparable<Mitarbeiter> {
public int persNr ; // eindeutige Ordnung
public String name ;
public Mitarbeiter( int nr, String n ) { /* … */ }
public int compareTo( Mitarbeiter m ) {
Ab Java5 generisch definiert
für Referenztyp T :
interface Comparable<T>
interface Comparator<T>
return this.persNr – m.persNr ;
}
}
Wegfall aller Downcasts
// Weiteres Sortierkriterium: Name
class NameComparator implements Comparator<Mitarbeiter> {
public int compare( Mitarbeiter p, Mitarbeiter q ) {
String s1 = p.name ;
String s2 = q.name ;
return s1.compareTo( s2 ) ;
}
}
Programmieren 2 - H.Neuendorf (142)
Iterator-Pattern
Ziel :
Sequentieller Zugriff auf alle Elemente einer Datensammlung (Kollektion)
Einheitliche Behandlung ohne Kenntnis + Preisgabe der Datenstruktur !
Verwender bekommt keinen direkten Zugriff auf zugrundeliegende Datenstruktur !
Kapselung der technischen strukturabhängigen Umsetzung hinter Interface = java.util.Iterator
Verwaltung eines internen Zeigers auf aktuelle Position in zugrunde liegender Datenstruktur
Mehrere Iteratoren können gleichzeitig auf der selben Kollektion arbeiten
Iteratoren lassen sich für alle Datenstrukturen bauen – nicht an Listenkonzept gebunden !
Klassen mit Iterator sind iterierbar – können IF java.lang.Iterable implementieren
Abbruch-Kriterium – gibt es noch weitere Elemente?
Object next( )
Referenz auf nächstes Element
Anwendungs-Idiom :
// ref auf Datenstruktur
boolean hasNext( )
while( ref.hasNext( ) ) {
Object o = ref.next( ) ;
Wird von Implementierungen des JavaCollection-Frameworks unterstützt …
}
Programmieren 2 - H.Neuendorf (143)
Iterator – Abgrenzung zu konkreten Iterationen
Bisherige Iterationsmethoden setzten Kenntnis der inneren Datenstruktur voraus
+
greifen direkt darauf zu - so dass private Details exponiert werden
Bedienen sich spezieller, stets anders-namiger und -artiger Methoden :
Iteration über Array :
for( int i=0; i<arr.length; i++ ) {
Object o = arr[ i ] ; // Zugriff
}
Iteration über String :
for( int i=0; i<str.length( ); i++ ) {
char c = str.charAt( i ) ; // Zugriff
}
Iteration über Verkettete Liste :
for( Node p=head; p!=null; p=p.next ) {
Object o = p.o ; // Zugriff
}
Programmieren 2 - H.Neuendorf (144)
Iterator – Verallgemeinerung
Ohne Kenntnis der internen Datenstruktur kann durch vordefinierte Semantik + Methoden
universell gearbeitet werden - sofern Datenstruktur Iterator-Konzept unterstützt
Implementieren von Interfaces :
interface Iterator {
interface Iterable { // java.lang
Iterator iterator( ) ;
// java.util
}
boolean hasNext( ) ; // Noch weitere Elemente in Kollektion ?
Object next( ) ;
// Liefert aktuelles Element + setzt internen Zeiger auf nächstes Element
void remove( ) ;
// Löscht das durch letzten next()-Aufruf (!) erhaltene Element
// muss nicht unterstützt werden - optional !
}
Ausnahmen
(unchecked)
// Generics :
java.util.NoSuchElementException
Wenn bei next()-Aufruf keine Elemente mehr vorhanden
java.util.ConcurrentModificationException
next()-Aufruf muss vorausgegangen
sein - sonst Exception !
interface Iterator<T> {
boolean hasNext( ) ;
// Fast-Fail-Semantik
Wenn Datenstruktur durch Fremdeinwirkung verändert wurde (s.u.)
T next( ) ;
java.lang.UnsupportedOperationException
Falls remove( ) ohne Funktionalität implementiert wird
java.lang.IllegalStateException
Aufruf von remove() ohne jeweils direkt vorheriges next()
void remove( ) ;
}
Programmieren 2 - H.Neuendorf (145)
Iterator – Besonderheit von next( )
interface Iterator {
boolean hasNext( ) ;
Object next( ) ;
// Liefert aktuelles Element + setzt internen Zeiger auf nächstes Element
void remove( ) ;
}
Jeder Aufruf von next( ) liefert ein weiteres Element der Datensammlung
Es wird jedoch keine bestimmte Ordnung der Elemente vorausgesetzt.
Somit liefert next( ) die Elemente nicht in einer irgendwie garantierten Reihenfolge !
Einzige Garantie : Jedes Element wird durch next() genau einmal geliefert
Alle Elemente des Datenbehälters sind durch Iterator abgreifbar
// Beispiel : Entfernen aller Elemente
Iterator it = collect.iterator ;
while( it.hasNext( ) ){
it.next( ) ; it.remove( ) ;
}
Es sind auch weitere Iterations-Methoden denkbar und umsetzbar, wie
previous(), hasPrevious() …
Sind aber in Interface Iterator nicht vorgesehen !
Dafür im Interface java.uiti.ListIterator<T> extends Iterator<T>
Programmieren 2 - H.Neuendorf (146)
Iterator – mit separater Iterator-Klasse
class LinkedList implements Iterable { // ist iterabel !
private Node head ;
private Node tail ;
class Node {
public boolean add( Object obj ) { /* … */ }
public Object val ;
// …
public Node next ;
public java.util.Iterator iterator( ) { // IF Iterable
public Node( Object obj ) {
return new ListIterator( head ) ;
val = obj ; next = null ;
}
}
}
class ListIterator implements java.util.Iterator {
private Node cursor ;
}
public ListIterator( Node n ){ cursor = n ; }
public boolean hasNext( ) {
return cursor != null ;
}
public void remove( ) { throw new UnsupportedOperationException( ) ; }
Nach einem kompletten
Durchgang ist ein gewöhnlicher
Iterator "verbraucht"
public Object next( ) {
if( !hasNext( ) ) throw new java.util.NoSuchElementException( ) ;
Object o = cursor.val ;
cursor = cursor.next ;
return o ;
// oder : Clonen!
Nachteil :
Verwender hat es mit mehreren
separaten Klassen zu tun …
}
}
Programmieren 2 - H.Neuendorf (147)
Iterator – Verwendung
import java.util.Iterator ;
class IterTest {
public static void main( String[] args ) {
LinkedList MyList = new LinkedList( ) ;
Auf einer Datenstruktur könnten mehrere
Iteratoren gleichzeitig störungsfrei arbeiten.
Allerdings nur, wenn Methode remove( ) nicht
unterstützt wird → Umgang damit s.u..
MyList.add( "das" ) ;
Testen : Iterator muss auch mit noch leerer
Datenstruktur funktionieren, ohne Abbrüche
(NullpointerException) zu produzieren !
Iterator it = MyList.iterator( ) ;
Methode next( ) liefert Typ Object .
while( it.hasNext( ) ) {
Zwingt Verwender zum Casten !
MyList.add( "dies" ) ;
String s = (String) it.next( ) ;
}
Ist bei Verwendung der generischen Variante
nicht mehr erforderlich (s.u.) !
for( Object s : MyList ) {
IO.writeln( (String)s ) ;
}
}
Alle iterablen Strukturen können mit der seit
Java5 verfügbaren erweiterten for-Schleife
(forEach-Loop) komplett iteriert werden !
}
Programmieren 2 - H.Neuendorf (148)
Iterator – Klasse selbst als Iterator
class LinkedList implements Iterable, java.util.Iterator {
private Node head ;
private Node tail ;
private Node cursor ;
public boolean add( Object obj ) { /* … */ }
// …
public java.util.Iterator iterator( ) { // IF Iterable durch Klasse selbst impl.
cursor = head ;
return this ;
Vorteil :
Der technische Iterator der
verketteten Liste ist nach Außen
hin nicht mehr als separate
Klasse sichtbar.
Seine Implementierung ist nun ein
technisches Detail der Internas der
Klasse LinkedList.
}
public boolean hasNext( ) {
return cursor != null ;
}
public void remove( ) { throw new UnsupportedOperationException( ) ; }
public Object next( ) {
if( !hasNext( ) ) throw new java.util.NoSuchElementException( ) ;
Object o = cursor.val ; // oder : Clonen!
cursor = cursor.next ;
return o ;
}
Nachteil :
Nun sind nicht mehr mehrere
Iteratoren gleichzeitig für dieselbe
Kollektion darstellbar !
Kommen sich über gemeinsame
Referenz-Variable cursor ins
Gehege …
Clonen der gesamten Struktur in
Methode iterator( ) wäre ziemlich
aufwendig …
}
Programmieren 2 - H.Neuendorf (149)
Iterator – Erlaubt einfache Definition weiterer Methoden
class LinkedList implements Iterable, java.util.Iterator {
private Node head ;
private Node tail ;
private Node cursor ;
public boolean add( Object obj ) { /* … */ }
// …
public java.util.Iterator iterator( ) { cursor = head ; return this ; }
public boolean hasNext( ) { /* … */ }
public void remove( ) { /* … */ }
public Object next( ) { /* … */ }
public int size( ) {
int n = 0 ;
for( Object o : this ) n++ ;
return n ;
}
public boolean contains( Object val ) {
for( Object o : this ) if( o.equals( val ) ) return true ;
return false ;
}
public String toString( ) {
StringBuffer sb = new StringBuffer( ) ;
for( Object o : this ) sb.append( o ) ;
return sb.toString() ;
}
}
Programmieren 2 - H.Neuendorf (150)
Iterator – mit innerer Klasse
class LinkedList implements Iterable {
private Node head ;
private Node tail ;
public boolean add( Object obj ) { /* … */ }
// …
private class ListIterator implements java.util.Iterator {
private Node cursor = head ;
public boolean hasNext( ) {
return cursor != null ;
}
Vorteil :
Der technische Iterator der
verketteten Liste ist nach
Außen hin nicht mehr als
separate Klasse sichtbar.
Seine Implementierung ist nun
ein technisches Detail der
Internas der Klasse LinkedList.
public void remove( ) { throw new UnsupportedOperationException( ) ; }
public Object next( ) {
if( !hasNext() ) throw new java.util.NoSuchElementException( ) ;
Object o = cursor.val ; // oder : Clonen!
cursor = cursor.next ;
Als separate innere Klasse
gekapselt, so dass Klasse
intern "aufgeräumter" (besser
strukturiert) ist
return o ;
}
}
public java.util.Iterator iterator( ) { // IF Iterable
return new ListIterator( ) ;
Es sind wieder mehrere
Iteratoren möglich, die
störungsfrei gleichzeitig auf
derselben Liste arbeiten !
}
private class Node { /* … */ }
}
Programmieren 2 - H.Neuendorf (151)
Iterator – mit innerer Klasse - Generics
class Node<T> {
public T val ;
public Node<T> next ;
public Node( T obj ) {
val = obj ;
next = null ;
}
}
class LinkedList<T> implements Iterable<T> {
private Node<T> head ;
private Node<T> tail ;
public boolean add( T obj ) { /* … */ }
// …
private class ListIterator implements java.util.Iterator<T> {
private Node<T> cursor = head ;
public boolean hasNext( ) {
return cursor != null ;
}
public void remove( ) { throw … ; }
public T next( ) {
import java.util.Iterator ;
class IterTest { // Verwender
public static void main( String[ ] args ) {
// Rückgabetyp T !!
LinkedList<String> MyList =
new LinkedList<String>( ) ;
MyList.add( "dies geht" ) ;
if( !hasNext( ) ) throw … ;
T ref = cursor.val ;
cursor = cursor.next ;
return ref ;
Iterator<String> it = MyList.iterator( ) ;
while( it.hasNext( ) ) {
// Typ passt nun ohne Cast :
String s = it.next( ) ; // !!
}
}
}
public java.util.Iterator<T> iterator( ) {
return new ListIterator( ) ;
}
}
}
}
Programmieren 2 - H.Neuendorf (152)
Iterator – mit korrekter Fast-Fail-Technik
Prinzip :
Verwalten eines separaten Modifikations-Counters innerhalb der
Datenstruktur und innerhalb jedes ihrer Iteratoren
Beim Erzeugen des Iterators wird dessen Modifikations-Counter auf den
Wert des Modifikations-Counters der zugehörigen Datenstruktur gesetzt
Wenn Datenstruktur ihren Bestand durch eigene Methoden verändert (z.B.
add(), remove() ) dann zählt sie nur ihren eigenen Mod-Counter hoch
Wenn Iterator durch remove()-Method den Datenbestand verändert, dann zählt
er eigenen Mod-Counter und Mod-Counter der Datenstruktur hoch !
Bevor Iterator eine seiner Methoden durchführt, vergleicht er seinen ModCounter-Stand mit dem Mod-Counter-Stand der Datenstruktur
Wenn die Stände identisch sind führt der Iterator seine Aktion aus
Wenn die Stände unterschiedlich sind wirft der Iterator eine
ConcurrentModificationException
Dieses Verhalten wird nicht durch
das IF Iterator vorgeschrieben, aber
von den Klassen des Collections
Frameworks praktiziert
Bei CopyOnWrite-Datenstrukturen
ist dies nicht nötig. Jede Änderung
erzeugt Kopie, Iterator arbeitet auf
unveränderlichem Bestand.
Programmieren 2 - H.Neuendorf (153)
Iterator – mit korrekter Fail-Fast-Technik
class LinkedList implements Iterable {
private int modCount = 0 ;
private Node head ; private Node tail ;
public boolean add( Object obj ) { /* … */ modCount++ ; } // in allen verändernden Methoden !!
private class ListIterator implements java.util.Iterator {
private int myCount = 0 ;
private Node cursor = head ;
public ListIterator( ){ myCount = modCount ; }
public boolean hasNext( ) {
if( myCount != modCount ) throw new java.util.ConcurrentModificationException( ) ;
return cursor != null ;
}
public void remove( ) {
if( myCount != modCount ) throw new java.util.ConcurrentModificationException( ) ;
// Implementierung …
myCount++ ; modCount++ ; // synchrones Verändern
}
public Object next( ) {
if( myCount != modCount ) throw new java.util.ConcurrentModificationException( ) ;
// Implementierung …
}
}
public java.util.Iterator iterator( ) { return new ListIterator( ) ; }
private class Node { /* … */ }
}
Programmieren 2 - H.Neuendorf (154)
Herunterladen