head - DHBW Mosbach

Werbung
Datenstrukturen
Schwerpunkte
Dynamische Datenstrukturen mit Java
Umgang mit Objektreferenzen als "Pointer"
Praktische Ergänzung zur Vorlesung A&D
Vorbereitung zum Collection Framework
Noch keine / kaum Verwendung von Generics
Praktisch-orientierte Literatur :
G.Saake, K-U. Sattler
Algorithmen & Datenstrukturen. Eine Einführung mit Java
dpunkt.Verlag
© H.Neuendorf
(44)
(45)
Datenstrukturen
Zusammenfassung von Daten in semantisch strukturierter Weise
Kennzeichen :
Datenanordnung + erlaubte Operationen
Bereits behandelte "Datenstrukturen" :
1. Klassen → Attribute enthalten Daten + Methoden regeln Zugriff
Sehr allgemein - damit beliebige konkrete Datenstrukturen darstellbar
2. Arrays → Indizierte Anordnung : Datenzugriff via Index
Wahlfreier Zugriff = Elemente in beliebiger Reihenfolge zugreifbar
aber : Einfügen zwischen belegte Plätze muss implementiert werden
Anzahl der Elemente steht fest - nicht veränderbar (statisch) !
unflexibel !
Dynamische Datenstrukturen = Container, die sich dynamisch der wachsenden oder
verringernden Datenmenge anpassen !
© H.Neuendorf
Datenstrukturen
Stack und Queue zur Einführung
Realisation auf Basis von Arrays
© H.Neuendorf
(46)
Stack
(47)
( Keller, Stapel )
Stapel von Daten (Objekten), die "übereinander liegen" :
LIFO = Last In First Out - Zuletzt abgelegtes Element wird als erstes wieder entnommen
Typische Operationen:
Stapel s, Element x :
Ablegen neuer Objekte oben auf Stapel
push
Zurückliefern + Entfernen des obersten Stapelobjekts
pop
Zurückliefern oberstes Stapelobjekt ohne Entfernen
peek
s.push( x ) ;
// Element abgelegt
x = s.pop( ) ;
// Rückgabe des entfernten obersten Elements
KEIN wahlfreier Zugriff - nur oberstes Element jeweils ansprechbar
x
s.push(x);
y
x
s.push(y);
Einfügen und Entnehmen
am selben Ende = "oben"
x
zB Bücherstapel …
z = s.pop(); z = s.pop();
// z == y
// z == x
Informatik → Methodenstack / Rekursions-Zwischenergebnisse auf Stack ( kein Basisfall
© H.Neuendorf
stack overflow! )
Stack-Implementierung via Array
push : Anfügen des Elements "am Ende" des Arrays
pop :
(48)
an jeweils oberster Position
Abholen des Elements "vom Ende" des Arrays
Fehlersituationen :
Kein Platz mehr für neue Elemente
overflow
Keine Elemente mehr im Array
underflow
Stack
- int [ ] s
- int pos
+ void push( int x )
Durch Hilfsmethoden
wie isFull() isEmpty()
kann man Prüfung auf
RuntimeExceptions
vermeiden
Bedeutung pos :
Anzahl der momentan im Stack gespeicherten Datenelemente
Zugleich Index des nächsten freien Platzes, auf den das nächste
einzufügende Datenelement zu setzen ist = aktuelle Einfügeposition
+ int pop( )
Belegung beginnt bei Indexposition 0
// …
Wirkung von
Array s
pos = pos - 1 ;
nächster freier Platz hat Index pos -1;
dortiger Wert wird überschrieben
Statt int könnte man beliebige Objekte im Stack ablegen
durch entsprechenden Typ der Arrayelemente
Bsp: Mitarbeiter[ ] s
Hier :
© H.Neuendorf
Array fester Größe !?
push
...
s[3]
s[2]
s[1]
s[0]
pop
Stack-Impl. via Array
(49)
class Stack {
Stack hat "Gedächtnis" - merkt
sich zuletzt besetzten Platz
private int[ ] s ;
// privater Datenbehälter
private int pos ;
// Anzahl gespeicherter Elemente
public Stack( int size ) {
s = new int[size] ;
Array gewünschter
Größe anlegen
pos = 0 ;
}
Zugriff nur über öffentliche Schnittstelle!
public void push( int x ) {
boolean overflow = ( pos >= s.length ) ;
Kein direkter Zugriff auf Daten im Array !
if ( overflow ) { /* Ausnahmebehandlung */ }
s[ pos ] = x ;
Prüfung, ob Array voll ist
pos++ ;
}
public int pop( ) {
Wenn noch Platz, dann
Datenelement einfügen
boolean underflow = ( pos == 0 ) ;
if ( underflow ) { /* Ausnahmebehandlung */ }
Zähler hochzählen
pos-- ;
return s[ pos ] ;
Prüfung, ob Array leer ist
}
Wenn nicht leer, dann
Datenelement zurückliefern
public boolean isFull( ) { return pos >= s.length ; }
Zähler vermindern
© H.Neuendorf
public boolean isEmpty( ) { return pos == 0 ; }
public void clear( ) { pos = 0 ; }
}
(50)
Queue ( Schlange, Puffer )
Daten-Röhre : An einem Ende rein, am andern raus ...
FIFO = First In First Out - Zuerst abgelegtes Element als erstes wieder entnommen
Typische Operationen : Anfügen neuer Objekte am Queue-Ende
Queue q, Element x :
put
Zurückliefern + Entfernen vorderstes Objekt
get
Zurückliefern vorderstes Objekt ohne Entfernen
peek
q.put( x ) ;
// Element abgelegt
x = q.get( ) ;
// Rückgabe + Entfernen vorderstes Element
KEIN wahlfreier Zugriff - nur vorderstes Element ansprechbar !
x
xy
x
q.put(x);
y
x
q.put(y);
z
q.put(z);
y
z
e = q.get();
z
e = q.get();
// e == x
x
y
z
Head
Tail
Entnahmestelle
Einfügestelle
Einfügen und Entfernen an
verschiedenen Enden !
// e == y
Warteschlangen dienen der Entkopplung.
Zwischengeschalteter Puffer zwischen Erzeuger
(Einfügen am tail) und Verbraucher (Entnahme
am head) von Daten, Nachrichten etc.
Keine direkte Interaktion.
Voraussetzung ist korrekte Synchronisation.
Informatik / Modellierung : Warteschlangen, Tasks, Message Queues, Puffer ...
© H.Neuendorf
Queue-Implementierung via Array
(51)
put : Anfügen Element am Array-Ende
get : Abholen "ältestes" Elements vom am "längsten belegten" Array-Platz
Index head : erstes = "ältestes" Element
Index tail :
Entnahme alter Elemente aus q[ head ]
nächster freier Platz
Fehlersituationen :
Queue
- int[ ] q
Kein Platz mehr für neue Elemente
overflow
Keine Elemente mehr im Array
underflow
0
1
2
4
5
6
7
4
5
6
7
tail
head
0
1
2
3
q
head
+ void put( int x )
+ int get( )
3
q
- int head, tail
- int size
Einfügen neuer Elemente in q[ tail ]
tail
Vereinfachung: Nach Entnahme wird Inhalt nicht nach links verschoben !
Stattdessen Index head erhöht
belegter Teil wandert nach rechts
Belegter Teil erreicht Arrayende - obgleich Array nicht voll ist
Nutzung leerer Plätze am Anfang :
© H.Neuendorf
Zyklisches Array …
Queue-Implementierung via Array
0
1
2
3
4
5
6
7
q
(52)
Zyklisches Array (Ringpuffer) :
Indices head und tail laufen "im Kreis"
tail
head
0
1
2
3
4
5
6
7
Darstellbar durch Berechnung des Index tail
nach Einfügen :
tail = ( tail + 1 ) % 8 ;
q
allgemein : modulo size
head
tail
Berechnung Index head nach Entnahme :
7
head = ( head + 1 ) % size ;
0
Erkennen Ausnahmesituationen
6
1
a) Leere Schlange (underflow) :
head == tail ;
b) Volle Schlange
(overflow) :
tail ist "rundherum gelaufen" - also wiederum head == tail.
Zur Unterscheidung von leerer Schlange wird deshalb als
overflow definiert, dass tail direkt vor head steht, d.h. :
( tail +1 ) % size == head ;
Man verschenkt einen Platz zugunsten Eindeutigkeit …
© H.Neuendorf
Queue via Array
Queue
- int[ ] q
class Queue {
private int[ ] q ;
private int head ;
private int tail ;
private int size ;
- int head, tail
+ void put( int x )
+ int get( )
Zugriff nur über öffentliche
Schnittstelle!
Kein direkter Zugriff auf die
Daten im Array!
Statt int könnte man beliebige Objekte
in Queue ablegen durch entsprechenden
Typ der Arrayelemente
Hier : Array fester Größe !?
© H.Neuendorf
//
//
//
//
privater Datenbehälter
Index erstes Element
Index nächster freier Platz = Einfügeposition
Queue-Größe
public Queue( int n ) {
q = new int[ n ] ;
size = n ;
tail = 0 ; head = 0 ;
}
public void put( int x ) {
boolean overflow = ( (tail + 1) % size == head ) ;
if ( overflow ) throw new RuntimeException( "Full" ) ;
q[ tail ] = x ;
tail = (tail + 1) % size ;
}
public int get( ) {
boolean underflow = ( head == tail ) ;
if ( underflow ) throw new RuntimeException( "Empty" ) ;
int x = q[ head ] ;
head = (head + 1) % size ;
return x ;
}
public boolean isEmpty( ) { return head == tail ; }
public boolean isFull( ) { return ( (tail + 1) % size == head ) ; }
public void clear( ) { head = tail = 0 ; }
- int size
Umständliche Verwaltung der Belegung
als zyklisches Array
(53)
}
Abstrakte Datentypen
(54)
ADT
Abstrakter Datentyp
von Implementierung unabhängige Spezifikation einer Datenstruktur
unterschiedliche Implementierungen sind möglich
Details der Implementierung interessieren Anwender nicht – sind weggekapselt
universelle, von der Implementierung unabhängige Verwendung
"abstrakt" - da nach außen nur öffentliche Methoden bekannt sind, nicht aber Impl.-Details
Realisierung von ADTs in Java
Spezifikation (der Methoden) der Datenstruktur durch ein Interface
verschiedene implementierende Klassen
Unabhängigkeit von Implementierungen durch Verwendung des Interface-Typs beim Aufrufer
Instanziiert werden jedoch natürlich stets konkrete Implementierungen
Beispiel
Abstrakte Datentypen 'Stack' + 'Queue'
© H.Neuendorf
→
intern array-basiert oder list-basiert realisierbar
Abstrakte Datentypen
Bsp :
Datentyp Stack
(55)
Spezifikation (der Methoden) des Datentyps durch Interface
public interface Stack {
public void push( Object obj ) ;
public Object pop( ) ;
public Object peek( ) ;
public void clear( ) ;
public boolean isEmpty( ) ;
}
Wichtige Detail-Entscheidung bei Dokumentation
des Interfaces + seiner Implementierung (zB auch
im Collection Framework) :
Werden null-Werte als Datenelemente akzeptiert
– und führen dann zur Erhöhung der internen size ?
Können null-Werte von Methoden zurückgeliefert
werden ?
Zurückliefern von null sollte eher bedeuten, dass
gesuchtes Elemente nicht vorhanden ist !
Bereitstellen (mindestens) einer Implementierung
public class ArrStack implements Stack {
private Object[ ] s = new Object[ 100 ] ;
private int pos = 0 ;
public void push( Object obj ) { // Implementierung …
}
/* … */
}
Anwendung : Methodenaufruf via Interface-Typ – sorgt für semantische Klarheit
Stack s = new ArrStack( ) ;
s.push( "Hallo" ) ;
© H.Neuendorf
Datenstrukturen
Dynamische Datenstrukturen
Realisation durch Pointer = Objektreferenzen
© H.Neuendorf
(59)
(60)
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
Objektreferenz = "Zeiger" auf Objekt
Java :
© H.Neuendorf
Objektvariable
Jeder Knoten kann mehrere
Nachfolger (childs) haben
Jeder Knoten wird nur von
einem Vorgänger referenziert
(ein parent-Knoten)
Wiederholung : Objekt-Referenzen
class Mitarbeiter {
public String name ;
public double gehalt ;
}
class Verwaltung {
public static void main( String[ ] args ) {
(61)
(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 ;
}
}
© H.Neuendorf
Nicht mehr referenzierte Objekte werden durch
Garbage Collection automatisch gelöscht
Verketten von Knoten via Objektreferenzen
class Node { // Knotenklasse
public int val ;
// Datenelement
// Objektreferenz!
public Node next ;
(62)
Selbstbezügliche Datenstruktur :
Attribut next der Knotenklasse Node ist
vom gleichen Typ wie Klasse Node selbst :
public Node( int v ) {
val = v ;
next = null ;
}
Jedes Element kann Referenz auf ein
Nachfolger-Objekt halten !
Typ von val könnte auch Referenztyp
sein, zB Mitarbeiter, Konto, Object …
}
Verketten zweier Node-Objekte :
Node a = new Node(3) ;
a
b
3
Node b = new Node(5) ;
val
next
5
a.next = b ;
a
(5078)
(1024)
b
© H.Neuendorf
val
3
next (1024)
val
next
5
null
(5078)
(1024)
b
a
Realisierung
mittels
Referenzen
3
5
Verketten von Knoten via Objektreferenzen
class Node {
// Knotenklasse
public int val ;
public Node next ; // Objektreferenz!
public Node( int v ) {
val = v ;
next = null ;
}
}
(63)
Selbstbezügliche Datenstruktur
Bedeutung der Variablen next :
Zeiger zum Verketten von Node-Objekten =
Referenz auf Nachfolger !
Inhalt = Referenz auf anderes Node-Objekt
(Nachfolger) oder null (kein Nachfolger)
null kennzeichnet Listenende
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
b
a
3
Via Referenz auf Startknoten kann man sich durch
gesamte Liste "hangeln"
Wenn erster Knoten verfügbar ist, so sind alle Knoten
und deren Elemente verfügbar
© H.Neuendorf
5
Array ↔ Verkettete Liste :
Komplementäre Leistungen
tail
head
5
3
12
Array :
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 bei primitiven Datentypen nur ein Objekt
= Array benötigt
Größe ist fest vorgegeben
Arrays können effizienter sein für Daten mit
häufigem nur lesenden Random Access Zugriff
© H.Neuendorf
(64)
Einfügen generell 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
Es wird nur soviel Speicher wie erforderlich belegt
Datenstrukturen
Dynamische Datenstrukturen
Verkettete unsortierte Liste
Operationen in iterativer Formulierung
(Rekursive Formulierungen jedoch auch gut darstellbar)
© H.Neuendorf
(65)
Unsortierte Listen
-
(66)
Operationen
tail
head
5
3
12
Knoten-Reihenfolge entspricht nicht
Sortierreihenfolge ihrer Datenelemente
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 ?!
besser : int key
// liefert Objekt mit bestimmtem key (ID) zurück
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 1: Manche Spezifikationen fordern für add()-Methoden keinen Rückgabewert, da Hinzufügen von Elementen in Liste immer funktioniert.
In bestimmten Datenstrukturen (zB Set) ist Hinzufügen nicht immer möglich, so dass Erfolg oder Misserfolg durch den Rückgabewert ausgedrückt wird.
Um gleiche Semantik dieser Methode für möglichst viele Datenstrukturen zu erhalten, macht es Sinn, die Methoden wie angegeben zu spezifizieren.
Anmerkung 2: Beim Verwalten von Objekten (statt einfacher Werte) ist Methode get( ) sinnvoll, die Referenz auf das Objekt gemäß ID = key
zurückliefert. Vor- oder Rückgabe von Indexpositionen ist weniger sinnvoll.
© H.Neuendorf
Listen - Erwägungen
(67)
tail
head
3
5
12
1. Doppelte Einträge
Sind Einträge äquivalenter Objekte erlaubt ?
Stack, Queue, Pool, UnsortedList → ja
(tendenziell)
Baum, Hashtable → nein
2. Abgriff von Einträgen
Werden Objekte beim Zugriff 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
am besten als private innere Klasse darstellen
Keine Rückgabe von Indexpositionen – ebenfalls technisches Detail
© H.Neuendorf
Unsortierte Listen
Spezifikation des abstrakten Datentyps
List als Interface :
public interface List {
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.
Somit Node von außen nicht sichtbar
© H.Neuendorf
(68)
- Operationen
Implementierung von List durch die Klasse
LinkedList und Klasse Node :
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 ;
}
}
Unsortierte Listen
(69)
- Operationen
tail
head
5
3
12
Liste = Knoten + typische Operationen :
Vereinfachte Veranschaulichung
mit Datentyp int
// Knotenklasse
class Node {
Anhängen von neuen Werten / Knoten am Ende
public int val ;
Einfügen von neuen Werten / Knoten am Anfang
public Node next ;
Löschen von Werten / Knoten
public Node( int v ) {
Suchen nach Werten / Knoten
val = v ;
Objektorientiertes Umsetzung :
next = null ;
Klasse LinkedList , die Knoten-Objekte enthält
}
LinkedList
- Node head, tail
Node
int val
+ boolean addFirst( int x )
}
// Listenklasse
class LinkedList {
Node next
private Node head = null ;
+ boolean add( int x )
+ boolean remove( int x )
private Node tail = null ;
+ boolean contains( int x )
Es könnten beliebige Datentypen in
Listen gespeichert werden.
// …
Attribute von Node nur deshalb public,
um Implementierung zu vereinfachen
© H.Neuendorf
// …
}
Einfügen List-Elemente
→ Am Listenende
tail
head
3
5
(70)
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
© H.Neuendorf
tail.next = p
Initialzustand
head tail
add12);
head
(p)
12
tail
(71)
Einfügen List-Elemente → 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
© H.Neuendorf
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 !
Einfügen List-Elemente
→ Am Listenanfang
tail
head
5
(72)
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 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
© H.Neuendorf
(73)
Einfügen List-Elemente → Am Listenanfang
class LinkedList {
Initialzustand
head tail
private Node head = null ;
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 ;
p.next = head = null
head = p
head = p ;
return true ;
addFirst(3);
}
(p)
3
12
}
Zuerst :
p.next = head
head = p
addFirst(45);
Wachstumsrichtung der Liste
Dann :
(p)
45
3
p.next = head
© H.Neuendorf
Next-Zeiger des neuen Knotens p
auf alten head zeigen lassen :
p.next = head ;
12
Neuen head erzeugen, indem head
nun auf neuen Knoten p zeigt :
head = p ;
Unsortierte Listen : Suchen von Elementen
(74)
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 und 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 ;
Wert nicht gefunden
}
Umsetzung durch 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
© H.Neuendorf
Unsortierte Listen : Löschen von Elementen
head
(75)
Problem :
3
5
12
Klasse Node kennt nur Nachfolger
Wie bekommt man Vorgängerknoten ?
p
head
Beim Durchlauf zweiten Hilfszeiger prev
mitführen, der auf Knoten vor p zeigt !
3
5
12
"Umbiegen" :
prev
p
prev.next = p.next ;
Sonderfall :
Löschen
p ist erstes Element der Liste → hat
keinen Vorgänger !
(prev == null)
1. Zu löschenden Knoten suchen
Neuer Listenanfang ist Nachfolger von p
Hilfszeiger p darauf setzen
2. Next-Zeiger des Vorgängerknotens auf Nachfolger
Liste beginnt nun mit Nachfolger von p
"Umbiegen" :
head = p.next ;
des zu löschenden Knotens setzen ("umbiegen, überbrücken")
Zu löschender Knoten nun nur noch von p referenziert
Zeiger p ist lokale Variable der Methode
Erstes vorkommendes Element gelöscht
Garbage Collection entsorgt den nicht mehr referenzierten
Knoten nach Methodenende !
Mehrfacheinträge nicht erfassbar …
© H.Neuendorf
Unsortierte Listen : 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 ;
}
}
© H.Neuendorf
(76)
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 !
Ausgabe Listenelemente → Verwendung toString( )
class LinkedList {
Ausgabe Liste :
private Node head ;
private Node tail ;
z.B. durch runde Klammern begrenzt +
Listenelemente durch Leerzeichen getrennt.
public String toString( ) {
Werte werden in Stringbuffer geschrieben
und am Ende in String umgewandelt.
StringBuffer sb = new StringBuffer( ) ;
String wird zurückgegeben.
sb.append( "( " ) ;
Liste von Anfang bis Ende durchlaufen.
Node p = head ;
if( p == null ) return "Leer" ;
while( p != null ) {
sb.append( p.val ) ;
sb.append( " " ) ;
p = p.next ;
Weiterführen von p
}
sb.append( " )" ) ;
return sb.toString( ) ;
}
}
© H.Neuendorf
Übung :
Wie lauten Methoden void clear( )
und boolean isEmpty( ) ?
(77)
Doppelt verkettete Liste
Erlaubt Traversieren in zwei Richtungen
(78)
Node
class DLinkedList {
private Node head ;
prev
private Node tail ;
val
public boolean add( int val ) {
next
Node p = new Node( val );
if( head == null ){
tail = p ;
class Node {
head = p ;
}
public int val ;
else{
public Node next ;
public Node( int v ) {
tail.next = p ;
val = v ;
p.prev = tail ;
next = null ;
tail = p ;
}
return true ;
public boolean remove( int val ) {
/* Übung */
}
}
© H.Neuendorf
prev = null ;
}
}
}
public Node prev ;
head
tail
Datenstrukturen
Dynamische Datenstrukturen
Verkettete sortierte Liste
© H.Neuendorf
(79)
(80)
Sortierte Liste
tail
head
5
7
Knoten-Reihenfolge entspricht
Sortierreihenfolge ihrer Datenelemente
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 Elemente
stößt, die "größer" sind als gesuchtes Element
Node p = head ;
Aber :
while( p! = null && p.val < val )
Nicht für alle Typen existiert Ordnungskriterium
p = p.next ;
Für effizientes Suchen in großen Datenbeständen
sind sortierte Suchbäume geeigneter als Listen
Lineare Liste zum Durchsuchen nicht gut geeignet,
da kein wahlfreier Zugriff
Binary Search nicht effizient darstellbar !
© H.Neuendorf
if ( p != null && p.val == val ) return true ;
else return false ;
}
}
(81)
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 : hoch, da ungeordnet !
neg. Fall → n Vergleiche
pos. Fall → ∅ n /2 Vergleiche
Komplexität O(n)
Wächst linear mit Zahl der Elemente
© H.Neuendorf
}
}
Zeitbedarf : bleibt hoch !
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
Datenstrukturen
Dynamische Datenstrukturen
Stack und Queue auf Basis verketteter Listen
© H.Neuendorf
(87)
(88)
Stack via verketteter 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 :
Gespeicherte 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 gerne 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 !
© H.Neuendorf
Abstrakte
Datenstrukturen
Stack via verketteter Liste
Einfügen push( ) :
(89)
class Stack {
private Node top = null ;
1. Neuen Knoten p mit Wert erzeugen
2. Nachfolger des neuen Knotens ist bisheriger top :
public void push( int x ) {
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( ) {
Entfernen pop( ) :
if ( top == null ) { /* Exception */ }
1. Wenn Stack leer ist ( top == null ) Exception werfen
else {
Node p = top ;
2. Wenn Stack nicht leer top-Element entfernen :
top = top.next ;
2.1. Lokalen Hilfszeiger p auf top setzen: Node p = top ;
return p.val ;
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
von GC nach Methodenende entsorgt nachdem lokale Referenz p verschwunden
© H.Neuendorf
}
}
top
top
(p)
Stack via verketteter Liste :
top
1.
5
push( )
push( 5 )
(90)
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
p
5
5
© H.Neuendorf
2. top = p ;
Stack via verketteter Liste :
top
pop( )
3
p
8
(91)
pop( )
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
© H.Neuendorf
}
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 !
(92)
Queue via verketteter 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 ( )
© H.Neuendorf
Node
int val
Node next
tail-Pointer ermöglicht bei put( )Operation direkten Zugriff auf
das letzte Element …
(93)
Queue via verketteter 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 Kopf 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 ;
}
}
© H.Neuendorf
head
put( 9 )
Queue via verketteter Liste
tail
(94)
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)
© H.Neuendorf
neuem Knoten p auf diesen
head = p ;
Methodenende: Lokale Variable p gelöscht
get( )
head
Queue via verketteter Liste
tail
(95)
class Queue {
private Node head = null ;
private Node tail = null ;
1. Node p = head ;
p
head
2.
p
GC !
(p)
© H.Neuendorf
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 !
head
tail
Queue via verketteter Liste
get( )
Sonderfall :
(96)
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)
© H.Neuendorf
}
Garbage Collection !
Es bleibt leere Queue zurück - head + tail sind wieder null !
(97)
Datenabstraktion
Grundprinzip = Information Hiding
↔
Ziel = Reduktion von Komplexität :
Implementierungsdetails verbergen →
Zugriff auf komplexe Strukturen nur über wenige öffentliche Methoden = einfache Schnittstelle
ohne technische interne Implementierungsdetails offenzulegen !
Vorteil: Interne Implementierungsdetails können jederzeit geändert werden …
… solange Schnittstelle konstant bleibt
Verwender / Clients werden nicht invalidiert
Datenabstraktion :
Abstrakte Datentypen :
"Wie" der Implementierung wird versteckt
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.
Nur das "Was" der Datenhaltung exponiert
in öffentlicher Schnittstelle
Bsp : Stack-Klasse mit konventioneller Schnittstelle
public int pop( )
public void push( int x )
Implementierung via Array oder Verketteter Liste
Implementierungsdetail - von Außen nicht sichtbar
Ist austauschbar !
© H.Neuendorf
Datenstrukturen
Dynamische Datenstrukturen
Design-Varianten
© H.Neuendorf
(98)
Strategy-Interfaces
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 ) {
Node p = new Node( val ) ;
if ( head == null ) { head = p ; }
else { tail.next = p ; }
tail = p ;
return true ;
}
public Object work( Action obj ) {
return obj.act( head ) ;
}
}
Ohne Klasse List zu verändern kann man
zusätzliche Funktionalität einfügen …
© H.Neuendorf
(99)
Strategy- Methoden : Aktionsobjekte als Parameter
deren Methoden auf anderen Objekten arbeiten
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 ;
}
}
Strategy-Interfaces
class UList {
public static void main( String[ ] args ) {
LinkedList MyList = new LinkedList( ) ;
MyList.add( 17 ) ;
MyList.add( 3 ) ;
MyList.add( 6 ) ;
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 ) ;
}
}
Sum su = new Sum( ) ;
Integer summe = (Intreger) MyList.work( su ) ;
Output out = new Output( ) ;
String output = (String) MyList.work( out ) ;
}
}
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 ("konkrete Strategien") erfordern verschiedene
Aktionsobjekte mit Methode korrekter Signatur, die über "PlugIn"Methode der Ziel-Klasse (hier: work() von LinkedList) aufgerufen wird.
Implementierende Klassen sind typischerweise zustandslos (ohne Attribute),
besitzen nur entsprechende Aktionsmethode.
© H.Neuendorf
Referenzen auf Aktionsobjekte
als Ersatz für in Java nicht
vorhandene Funktions- Pointer
(100)
(101)
Generische Datenstruktur via Object
class Node { // Generische Knotenklasse
public Object val ;
public Node next ;
public Node( Object obj ) {
val = obj ;
next = null ;
}
}
class LinkedList {
private Node head = null ;
private Node tail = null ;
© H.Neuendorf
Ein Datencontainer für verschiedene Datentypen !
Kann beliebige Objekte aufnehmen, da jede JavaKlasse implizit Unterklasse von Object ist
Speichern einfacher Datentypen durch Hüllklassen
Anm: Liste kann sich nicht selbst enthalten !
MyList.add( MyList )
Keine null-Werte
als Datenelemente
akzeptiert !
public boolean add( 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 ;
}
}
Elemente vom generischen Typ Object :
// LZ-StackOverflow
LinkedList MyList = new LinkedList( ) ;
int[ ] ary = { 1 , 6 , 8 , 3 } ;
String s = "Test" ;
Long Lg = new Long( 27 ) ;
MyList.add( ary ) ;
// geht alles – leider …
MyList.add( s ) ;
MyList.add( Lg ) ;
// besser : Generics …
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
als Datenelemente
return true ;
akzeptiert !
}
}
© H.Neuendorf
In Node Datenelemente vom Typ
Mitarbeiter speicherbar
(102)
// 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 ?
(103)
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
© H.Neuendorf
Haller
Gross
List
Beim Verwalten von
Objekten ist Methode get( )
sinnvoll, die Referenz auf das
Objekt gemäß Schlüsselwert
(ID) = key zurückliefert
(104)
Generische Datentypen mittels Generics
// Generische Klassen :
Typ-Variablen / - Platzhalter :
class Node<T> {
public T val ;
<T>
Man vermeidet den total generischen, völlig unspezifizierten
Typ Object und gibt Verwender die Möglichkeit zur flexiblen
Typ-Vorgabe mit Typsicherheit :
public Node<T> next ;
public Node( T obj ) {
val = obj ;
Compiler kann bei konkreter Verwendung auf einen korrekten
Typ prüfen !
next = null ;
}
}
class ListUser {
public static void main( String[] args) {
class LinkedList<T> {
private Node<T> head = null ;
private Node<T> tail = null ;
LinkedList<String> MyList = new LinkedList<String>( ) ;
public boolean add( T obj ) {
MyList.add( "dies geht" ) ;
Node<T> p = new Node<T>( obj ) ;
MyList.add( "dies auch" ) ;
if ( head == null ) { head = p ; }
MyList.add( new Long(27) ) ; // Compilerfehler !!
else { tail.next = p ; }
tail = p ;
return true ;
}
}
}
}
© H.Neuendorf
Unterdrücken von null-Werten als
Datenelemente noch hinzufügen ..
Thema Generics wird separat behandelt …
Datenstrukturen
Bäume als dynamische Datenstrukturen
© H.Neuendorf
(118)
Baumstrukturen
(119)
Hierarchische Datenstruktur
Baum = Verzweigte Struktur aus Knoten + Folge von Nachfolgern
(≠ Graph)
Binärbaum : Jeder Knoten hat maximal zwei Nachfolger
Jeder Knoten hat einen Vorgänger – mit Ausnahme Wurzelknoten
Wurzelknoten (root)
Ebene 0
innere Knoten
Ebene 1
Ebene 2
Ebene 3
Blätter (Blattknoten) = Elemente ohne Nachfolger
Tiefe T
= Zahl der Ebenen = 4
Gewicht = Zahl der Knoten = 9
© H.Neuendorf
Zu jedem Knoten führt
ausgehend von Wurzel
ein eindeutiger Weg
Baum ist voll, wenn außer letzter Ebene alle
seine Ebenen komplett besetzt sind :
Ebene k → max. 2 k - Elemente
Binärbaum ist streng sortiert, wenn für jeden
Knoten und dessen beide Unterbäume gilt :
1. Alle Knoten im linken Unterbaum haben
kleinere Schlüssel
und …
2. Alle Knoten im rechten Unterbaum haben
größere Schlüssel …
… als ihr Vorgängerknoten
Motivation :
Effiziente Suche
Hierarchische Datenrepräsentation
(120)
Binäre Baumstrukturen :
Sortierter Suchbaum
Wurzel
x
Bei einer sortierten verketteten Liste ist
eine binäre Suche (im Gegensatz zu
Arrays) nicht sinnvoll implementierbar –
deshalb Übergang zum Binärbaum …
Hier sind alle Elemente < x
Suchvorgang
Hier sind alle Elemente > x
linker Unterbaum
rechter Unterbaum
Bsp: Mitarbeiterobjekte
gemäß Personalnummern
An jeder Verzweigung (beginnend mit Wurzelknoten) wird geprüft :
1. Gesuchtes Element == Knoten-Wert
Suche erfolgreich beendet, gefunden !
2. Gesuchtes Element > Knoten-Wert
Suche im rechten Unterbaum fortsetzen
falls Knoten keinen rechten Nachfolger besitzt : fertig - Misserfolg
3. Gesuchtes Element < Knoten-Wert
Suche im linken Unterbaum fortsetzen
falls Knoten keinen linken Nachfolger besitzt : fertig - Misserfolg
Fahre fort, bis Element gefunden - oder kein entsprechender Teilbaum existiert.
Ausgeglichener (balancierter) voller Baum hat Elementzahl :
Anzahl Ebenen T somit :
© H.Neuendorf
n = 2 T- 1
T = ld( n +1 ) = Anzahl erforderlicher Suchschritte
(121)
Effizientes Suchen in sortierten Binärbäumen
Beim Durchlauf reduziert sich an jeder Verzeigung die Anzahl noch zu prüfender Elemente
Deutlich weniger zu durchlaufende Einträge als bei linearen Strukturen !
Wurzelknoten (root)
Gute Bäume :
9
Regelmäßige Verteilung der n Elemente auf
die einzelnen Ebenen
5
15
Ebene 1
Ebenen möglichst voll besetzt
Tiefe möglichst klein : T = ld( n+1 )
3
1
12
Ebene 2
19
4
32
Maximale Gesamtknotenzahl n = 2T - 1
Tiefe eines ausgeglichenen vollen Baums :
T = ld( n + 1)
Vergleich lineare Liste : "Tiefe" = n
Zu durchsuchende Elemente im Mittel :
lineare Liste = n / 2
© H.Neuendorf
Baum = ld( n + 1 )
Ebene 3
Suchvorgang sehr effektiv
Schlechte Bäume :
Unausgeglichene Verteilung der n Elemente
auf einzelne Ebenen
Ebenen schwach besetzt
Tiefe sehr groß :
Extremfall = n
Entartung des Baums zur linearen Liste
Suchen nicht effektiver als bei Listen
(122)
Effizientes Suchen in sortierten Binärbäumen
Suchvorgang = Durchlaufen des Baums von Wurzel bis
Aufwandsvergleich im Mittel:
zum gesuchten Knoten oder Blatt
Einträge
Liste
Suchbaum
7
3.5
3
31
15.5
5
Suchwert = Knotenwert
Gesuchter Wert gefunden!
127
63.5
7
Suchwert > Knotenwert
weiter im rechten Teilbaum
1023
511.5
10
Suchwert < Knotenwert
weiter im linken Teilbaum
16383
8191.5
14
Verzweigung: An jedem Knoten wird gesuchter Schlüssel
mit Schlüssel / Wert des Knotens verglichen :
class Knoten {
Darstellung Binärbäume :
public int val ;
ähnlich wie bei Listen
public Knoten links, rechts ;
→ Baum besteht aus Knoten
→ Knoten besteht aus :
a) Inhalt ( Wert, Objekt = val )
public Knoten( int n ) {
+
val = n ;
b) zwei Nachfolgern :
links = null ;
→ wiederum vom Typ Knoten ( links, rechts )
rechts = null ;
Selbstbezügliche Datenstruktur
Jeder Knoten hat zwei Nachfolger
// bzw. Object
}
}
// evtl. als innere Klasse
© H.Neuendorf
(123)
Realisierung Binärbaum durch Knoten-Objekte
Objekt Baum
Maximale
Objekt
val: 8
Knoten
Wurzel
links
Gesamtknotenzahl :
n = 2 T- 1
rechts
Attribut Wurzel
zeigt auf root
key
Objekt
val: 4
Knoten
links
Objekt
val:11
Knoten
Weitere Attribute
und Methoden
Nutzdaten
rechts
links
rechts
val
null
Objekt
val: 2
Knoten
Objekt
val: 6
Knoten
Objekt
val: 9
Knoten
links
links
links
null
© H.Neuendorf
rechts
null
null
rechts
null
null
rechts
null
Allgemeiner Fall :
Es werden von den Knoten
Objekte refereziert, die
über eindeutigen key
(Such-/ Ordnungskriterium)
verfügen.
Sortierter Binärbaum - Schnittstellenvarianten
Schnittstellen bzw. zu speichernde Objekte können unterschiedlich konzipiert sein – zB :
boolean add( int data )
Triviale Demo – es werden primitive Werte (da direkt vergleichbar) in Baumstruktur gehalten.
boolean add( Comparable key, Object data )
Es werden beliebige Objekte gespeichert. Diesen ist Suchschlüssel-Objekt zugeordnet. Die
keys müssen eine totale Ordnungsrelation erfüllen (>,<,==). Die keys müssen nicht mit den
Objekteigenschaften korreliert sein. Durch Vorgabe des keys kann man zugeordnetes Objekt
finden. Einfachste keys könnten jedoch auch primitive Werte sein.
boolean add( Comparable data )
Die zu speicherden Objekte selbst erfüllen eine Ordnungsrelation (>,<,==), dienen praktisch
selbst als keys. Kein separater key nötig. Nachteilig beim Suchen – man muss das zu
suchende Objekt im Grunde schon haben …
boolean add( IFKey data )
Die Objekte besitzen ein typisches key-Attribut (zB Mitarbeiter-ID) und eine vordefinierte
Abfragemethode, zB int getKey(), evtl. gefordert durch ein Interface. Somit können Objekte
verglichen, angeordnet und gefunden werden.
JDK:
java.util.TreeSet
boolean add( Comparable data )
© H.Neuendorf
(124)
Iterativer Aufbau Sortierter Binärbaum
(125)
Durchlaufen Baumknoten ab Wurzel :
→ Vergleich einzufügenden Werts mit Wert des aktuellen Knotens
Neuer Wert < Knotenwert
links davon fortsetzen
Andernfalls
rechts davon fortsetzen …
class Tree { // Naiver Algorithmus !
private Knoten root ;
public Tree( ) { root = null ; } // Baum anfangs leer
public boolean add( int data ) {
if ( root == null ) { root = new Knoten( data ) ; return true ; }
Knoten k = root ; // "Baumcursor" k
Knoten prev ; boolean links;
do {
prev = k ;
if( data = = k.val ) { return false ; } // Wert bereits vorhanden !
links = true ; }
if ( data < k.val ) { k = k.links ;
else
{ k = k.rechts ; links = false ; }
Iteration beendet, wenn man auf
} while( k != null ) ;
Blatt-Knoten stößt : k wird null
if ( links == true ) { prev.links = new Knoten( data ) ; }
An diesen Knoten wird neues Blatt
else
{ prev.rechts = new Knoten( data ) ; }
angehängt - links oder rechts, je
return true;
nach Wert
}
}
© H.Neuendorf
(126)
Rekursiver Aufbau Sortierter Binärbaum
Durchlaufen Baumknoten ab Wurzel :
→ Vergleich des einzufügenden Inhalts (Wert) mit Wert des betrachteten Knotens
Neuer Wert < Knotenwert
Links davon einfügen
Andernfalls
Rechts davon einfügen
Eleganter - aber doch
langsamer als iterative
Formulierung !
class Tree { // Naiver Algorithmus !
private Knoten root ;
public Tree( ) { root = null ; }
// Baum anfangs leer
public boolean add( int data ) { root = add( data, root ) ; return true; }
private Knoten add( int data, Knoten k ) {
if ( k == null ) { return new Knoten( data ) ; }
else { if ( data < k.val ) { k.links = add( data, k.links ) ;
else
{ k.rechts = add( data, k.rechts ) ;
return k ; }
return k ; }
}
}
}
© H.Neuendorf
Rekursion beendet, wenn man auf Blatt-Knoten stößt
An diesen Knoten wird neues Blatt angehängt - links
oder rechts - je nach Wert
Iterative Suche von Elementen in Sortiertem Binärbaum
Durchlaufen der Baumknoten ab Wurzel :
Wenn aktueller Knoten gesuchten Inhalt enthält, wird true zurückgeliefert
Wenn nicht
in richtigem Unterbaum iterativ Suche fortsetzen
class Tree {
private Knoten root ;
public Tree( ) { root = null ; }
public boolean contains( int data ) {
Knoten k = root ; // Beginn der Suche an der Wurzel
while( k!= null ) {
if( k.val == data )
return true ;
// Element gefunden!
else{
if( k.val > data ) k = k.links ;
else
// im linken Teilbaum weitersuchen!
k = k.rechts ; // im rechten Teilbaum weitersuchen!
}
}
return false ;
}
}
© H.Neuendorf
Iteration endet, wenn Element gefunden, oder man
Baumenden erreicht - dort nimmt k Wert null an.
(127)
Rekursives Durchlaufen des Sortierten Binärbaums
Durchlaufen aller Baumknoten :
(zB Ausgabe der Inhalte)
Erst wird linker Unterbaum eines Knotens k durchlaufen
Dann wird Knoten k selbst durchlaufen / bearbeitet / ausgegeben / …
Dann wird rechter Unterbaum von k durchlaufen
class Tree {
Rekursion beendet,
wenn man auf Knoten
ohne Nachfolger stößt
private Knoten root ;
public Tree( ) { root = null ; }
public void durchlaufe( ) { durchlaufe( root ) ; }
private void durchlaufe( Knoten k ) {
if ( k != null ) {
durchlaufe( k.links ) ;
IO.writeln( k.val ) ;
// Wert verarbeiten
durchlaufe( k.rechts ) ;
}
}
}
© H.Neuendorf
hier → willkürliche Reihenfolge :
links / Knoten / rechts
Natürlich auch andere Reihenfolgen der Berabeitung möglich :
preorder inorder postorder
Traversieren
(129)
(130)
Sortierter Binärbaum - Durchlauf-Arten
Reihenfolge, in der Unterbäume des Baums durchlaufen + Knoten aufgesucht werden
Tiefendurchlauf : Ausgehend von Knoten k wird ein Unterbaum von k vollständig durchlaufen,
ehe man zum nächsten Unterbaum von k übergeht
preorder
→ Knoten k selbst wird vor seinen Unterbäumen ausgegeben
inorder
→ Knoten k selbst wird zwischen seinen Unterbäumen ausgegeben
postorder
→ Knoten k selbst wird nach seinen Unterbäumen ausgegeben
public void preorder( Knoten k ) {
if( k =! null ) { IO.writeln( k.val ) ;
preorder( k.links ) ;
preorder( k.rechts ) ; }
}
public void inorder( Knoten k ) {
if( k =! null ) { inorder( k.links ) ;
IO.writeln( k.val ) ;
inorder( k.rechts ) ; }
}
public void postorder( Knoten k ) {
if( k =! null ) { postorder( k.links ) ;
postorder( k.rechts ) ;
IO.writeln( k.val ) ; }
}
© H.Neuendorf
// 1. Knoten k bearbeiten
// 2. Linker Unterbaum
// 3. Rechter Unterbaum
// 1. Linker Unterbaum
// 2. Knoten k bearbeiten
// 3. Rechter Unterbaum
// 1. Linker Unterbaum
// 2. Rechter Unterbaum
// 3. Knoten k bearbeiten
Methoden unterscheiden
sich nur in Reihenfolge der
rekursiven Aufrufe relativ
zur Bearbeitung des Knotens
k selbst !
Statt Wertausgabe könnte
irgend eine Operation an
Knoten k durchgeführt werden
(131)
Durchlaufreihenfolge Sortierter Binärbäume
Start
1.
2.
3.
4.
Ende
9.
6.
7.
8.
4.
8.
3.
5.
9.
Ende
1.
Preorder-Durchlauf
7.
2.
Start
Bewirkt "von oben nach unten"
Durchlauf ("außen herum")
5.
6.
Postorder-Durchlauf
Bewirkt "von unten nach oben" Durchlauf
5.
Bsp: Hierachie Dateisystem
4.
2.
Start
1.
7.
6.
8.
3.
Inorder-Durchlauf
9.
Ende
Bewirkt in sortierten Bäumen eine
sortierte Durchwander-Folge !
© H.Neuendorf
Dagegen Breitendurchlauf durch
Levelorder-Durchlauf realisiert
→ Saake & Sattler, Kap.14
Sortierter Binärbaum – Objekte mit totaler Ordnung
(132)
Vergleichsoperation implementiert – Baum mit beliebigen Objekten vom Typ Comparable
class Mitarbeiter implements Comparable {
// aus java.lang :
private int persNr ;
public interface Comparable {
private String name ;
// Vergleichskriterium :
// ...
public Mitarbeiter( int pn, String nn ) {
public int compareTo( Object o ) ;
persNr = pn ;
}
name = nn ;
}
public int compareTo( Object o ) { // Sort nach persNr
Mitarbeiter that = (Mitarbeiter)o ;
if( this.persNr < that.persNr ) return -1 ;
if( this.persNr > that.persNr) return +1 ;
return 0 ;
}
}
Methode compareTo( ) beschreibt totale Ordnung für Mitarbeiter-Objekte gemäß Kriterium persNr :
m1.compareTo( m2 ) < 0
wenn
m1.persNr < m2.persNr
m1.compareTo( m2 ) > 0
wenn
m1.persNr > m2.persNr
m1.compareTo( m2 ) = 0
wenn
m1.persNr = m2.persNr
© H.Neuendorf
Anpassen der Klassen Knoten und Baum …
Sortierter Binärbaum – Objekte mit totaler Ordnung
(133)
class Tree {
private Knoten root ;
private class Knoten {
Comparable val ;
Knoten links, rechts ;
}
public Tree( ) { root = null ; }
public boolean contains( Comparable data ) {
Dieser Klasse Tree ist konkreter Typ der
gespeicherten Objekte egal.
Voraussetzung ist nur deren Vergleichbarkeit gemäß IF Comparable …
Statt Typ Object wird deshalb der Typ
Comparable verwendet.
Knoten k = root ; // Beginn Suche an Wurzel
while( k != null ) {
int cmp = data.compareTo( k.val ) ;
if( cmp==0 ) return true ;
if( cmp<0 )
k = k.links ;
else
k = k.rechts ;
}
return false ;
}
}
© H.Neuendorf
Noch mehr Typsicherheit durch
Verwendung von Generics …
Ausgeglichene Bäume
(134)
Ziel bei Einfügen von n Elementen :
Minimale
Suchkomplexität
Baum mit gleichmäßig verteilten Zweigen und Blättern !
minimale Tiefe ≈ ld(n)
optimale Struktur für Suchoperationen! O( ld( n ) )
Problem :
Eintragen von Werten in ungeordneter Folge :
liefert ausgeglichenen Baum
Eintrag einer sortierten Folge von Werten :
liefert entarteten Baum !
alle Referenzen links (rechts) sind null (keine Einträge)
alle Referenzen rechts (links) bilden verkettete Liste
→ Baum zu linearer verketteter Liste entartet !
Tiefe des Baumes ist n
Suchen hat Zeitkomplexität
O( n ) >> O( ld( n ) )
Lösung :
Aufwendigere Routinen zum Aufbau + Umsortieren von Bäumen
Löschen von Baumknoten :
Vorlesung A&D (AVL- und B-Bäume)
Literatur :
Saake, Sattler "Algorithmen &
Datenstrukturen" dpunkt
→ erfordert Umsortieren des Baums (Rotationen), wenn innere Knoten entfernt werden
(nur für Blattknoten einfach: Referenz des jeweiligen Elternknotens auf null setzen)
© H.Neuendorf
Ausgeglichene Bäume
"P"
Fall 2:
"A"
(135)
Ausgeglichener
Baum
null
"Y"
"C"
"C"
null
null
null
null
"P"
Entartung zur
linearen Liste
null
null
null
null
→ Baumstruktur hängt von Einfügereihenfolge ab
Fall 1: Eingabereihenfolge in sortierter Folge
"W"
"A" "C" "N" "P" "W" "Y"
null
Fall 2: Eingabereihenfolge in Zufallsfolge
"P" "Y" "C" "W" "A" "N"
"Y"
null
© H.Neuendorf
null
"W"
Problem beim Einfügen von Elementen
null
Fall 1:
"N"
"A"
"N"
null
Herunterladen