Java2_DS_BaumHashtable

Werbung
Datenstrukturen
Bäume als dynamische Datenstrukturen
Grundlegende Baumstrukturen und - Konzepte
Binärbäume = Sortierter Suchbaum
Einige Inhalte + Abbildungen wurdem dem Skript
meines Kollegen Prof. Dr. Deck entnommen
Programmieren 2 - H.Neuendorf (155)
Baumstrukturen
Baum
=
Hierarchische Datenstruktur
Verzweigte Struktur aus Knoten + Folge von Nachfolgern
Binärbaum :
Jeder Knoten hat maximal zwei Nachfolger
Jeder Knoten hat genau einen Vorgänger – mit Ausnahme Wurzelknoten
Ordnungsprinzip :
( ≠ Graph )
Zu jedem Knoten führt ausgehend von Wurzel (root) ein eindeutiger Weg
Motivation :
Effiziente binäre Suche darstellbar im Gegensatz zur verketteten Liste
Hierarchische Daten-Repräsentation
Es gibt auch den Begriff des n-ären Baums (n-ary tree) bei dem
maximal n Nachfolger erlaubt sind.
Wir konzentrieren uns auf den wichtigsten Fall n = 2
Programmieren 2 - H.Neuendorf (156)
Baumstrukturen - voller Baum
Wurzelknoten ( root )
Ebene 0
Ebene 1
innere Knoten
Tiefe T
= Zahl der Ebenen = 4
Gewicht = Zahl der Knoten = 9
Ebene 2
Ebene 3
Blätter (Blattknoten) = Elemente ohne Nachfolger
Baum ist voll, wenn außer letzter Ebene alle seine Ebenen komplett besetzt sind :
Ebene k → max. 2
k
- Elemente
Ausgeglichener (balancierter) voller Baum hat Elementzahl :
n = 2 T- 1
Programmieren 2 - H.Neuendorf (157)
Darstellung von Binärbäumen
analog Listen
Knoten
val: 8
Binärbaum (Tree) besteht aus Knoten
links
Knoten besteht aus :
rechts
1. Datenkomponente :
Wert, Objekt = val
class Knoten {
2. Zwei Nachfolgern
// evtl. als private innere Klasse *)
public int val ;
vom Typ Knoten :
// bzw. Object / Comparable
public Knoten links, rechts ;
links rechts
public Knoten( int n ) {
Selbstbezügliche Datenstruktur
val = n ;
Jeder Knoten hat zwei Nachfolger
links = null ; rechts = null ;
}
public class Tree {
public Knoten( int n, Knoten li, Knoten re ) {
// Einstieg = Wurzelknoten :
val = n ; links = l ; rechts = r ;
private Knoten root ;
}
// private class Knoten { … } *)
}
}
Programmieren 2 - H.Neuendorf (158)
Realisierung Binärbaum (TreeSet) durch Knoten-Objekte
Objekt Baum
Wenn Datenelemente val
eindeutig (ohne Dubletten)
sind, dann liegt TreeSet vor
Objekt
val: 8
Knoten
Wurzel
links
rechts
Attribut Wurzel
(root) zeigt auf
root-Knoten
Objekt
val: 4
Knoten
Objekt
val:11
Knoten
links
links
rechts
rechts
null
Objekt
val: 2
Knoten
Objekt
val: 6
Knoten
Objekt
val: 9
Knoten
links
rechts
links
rechts
links
rechts
null
null
null
null
null
null
Programmieren 2 - H.Neuendorf (159)
Sortierter Binärbaum - Schnittstellenvarianten
Schnittstellen bzw. zu speichernde Objekte können unterschiedlich konzipiert sein – zB :
boolean add( int data )
Einfache Demo – es werden primitive Werte (da direkt vergleichbar) im Baum gehalten.
boolean add( Comparable data )
// Set - Struktur
Die zu speichernden Objekte selbst erfüllen eine Ordnungsrelation - dienen selbst als keys.
Keine separaten key-Objekte nötig.
boolean add( Comparable key, Object data )
// Map - Struktur
Es werden beliebige Objekte gespeichert.
Diesen ist Suchschlüssel-Objekt zugeordnet.
Die keys müssen eine Ordnungsrelation erfüllen.
Durch Vorgabe des keys kann man zugeordnetes Objekt finden
JDK:
java.util.TreeSet
boolean add( Comparable data )
Programmieren 2 - H.Neuendorf (160)
Darstellungsvarianten Binärbäume : TreeSet - TreeMap
class Knoten {
// Grundlage TreeSet
class Knoten {
// Grundlage TreeMap
private Comparable val ;
private Comparable key ;
private Knoten links, rechts ;
private Object val ;
public Knoten( Comparable n ) {
private Knoten links, rechts ;
val = n ;
public Knoten( Comparable k, Object n ) {
links = null ; rechts = null ;
key = k ; val = n ;
}
links = null ; rechts = null ;
// …
}
}
// …
}
Knoten
val: …
links
rechts
Datenelemente sind
eindeutig und dienen
zugleich als Such- und
Vergleichsschlüssel :
Baum bildet ein TreeSet
Knoten
key:…
val:…
links
rechts
Separate key-Objekte sind
eindeutig und dienen als
Suchschlüssel - zugeordnete
Datenelemente können
mehrfach vorkommen :
Baum bildet eine TreeMap
Programmieren 2 - H.Neuendorf (161)
Darstellungsvarianten Binärbäume : TreeSet - TreeMap
Generische Knoten-Varianten
class Knoten< K extends Comparable<K> > {
class Knoten < K extends Comparable<K>, V > {
private K val ;
private K key ;
private Knoten<K> links, rechts ;
private V val ;
public Knoten( K n ) {
private Knoten<K,V> links, rechts ;
val = n ;
public Knoten( K k, V n ) {
links = null ; rechts = null ;
key = k ;
}
val = n ;
// …
links = null ; rechts = null ;
}
}
// …
}
Programmieren 2 - H.Neuendorf (162)
Baumstrukturen : Sortierter Binärbaum
root
Bei der sortierten verketteten Liste ist
binäre Suche (im Gegensatz zu Arrays)
nicht effizient implementierbar – deshalb
Übergang zum sortierten Binärbaum …
9
5
(Binärer Suchbaum)
15
Ebene 1
Bsp. bei Verwaltung von Comparables :
3
12
Ebene 2
19
Mitarbeiter gemäß PersNr.
Für Mitarbeiter m jedes Knotens gilt :
PersNr des linken NF
1
4
32
Ebene 3
<
PersNr von m
PersNr des rechten NF > PersNr von m
Binärbaum ist sortiert, wenn für jeden Knoten und dessen beide Unterbäume gilt :
1. Alle Knoten im linken Unterbaum tragen kleinere Schlüssel
und …
2. Alle Knoten im rechten Unterbaum tragen größere Schlüssel … als ihr Vorgängerknoten.
Es existiert eine Ordnungsrelation. Keine Dubletten sind zugelassen.
Elementzahl Voller Baum :
n = 2 T- 1
Anzahl Ebenen T somit :
T = ld( n +1 ) = max. Anzahl nötiger Suchschritte
Programmieren 2 - H.Neuendorf (166)
Sortierter Binärbaum
Binäre Suche
Wurzel
x
Hier sind alle Elemente < x
Hier sind alle Elemente > x
linker Unterbaum
rechter Unterbaum
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 mehr existiert.
Programmieren 2 - H.Neuendorf (167)
Binäre 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 !
root
Gute Bäume :
9
5
15
Ebene 1
Regelmäßige Verteilung der n Elemente auf
die einzelnen Ebenen ⇒
Ebenen möglichst voll besetzt
3
1
12
Ebene 2
19
4
⇒ Tiefe minimal :
⇒
32
Ebene 3
T = ld( n+1 )
Suchvorgang sehr effektiv
Schlechte Bäume :
Maximale Gesamtknotenzahl n = 2T - 1
Unausgeglichene Verteilung der n Elemente
auf einzelne Ebenen ⇒
⇒ Tiefe eines ausgeglichenen vollen Baums :
Ebenen schwach besetzt
T = ld( n + 1)
Vergleich Liste : "Tiefe" = n
Mittlere Suchkomplexität :
Liste = n / 2
⇒ Tiefe groß :
Extremfall = n
Entartung Baum zur linearen Liste
⇒ Suche nicht effektiver als bei Listen
Baum = ld( n + 1 )
Programmieren 2 - H.Neuendorf (168)
Aufbau Sortierter Binärbaum - Einfügen
Einfügen eines Elements m
Suche der Einfügeposition
1. beginne mit Wurzelknoten k
2. falls m < Element in k
falls k keinen linken NF hat :
sonst : k = k.links
Einfügeposition gefunden
(weiter mit 2.)
3. falls m > Element in k
falls k keinen rechten NF hat : Einfügeposition gefunden
sonst : k = k.rechts (weiter mit 2.)
4. sonst :
m bereits im Baum enthalten (fertig, kein Erfolg)
Einfügen eines neuen Knotens mit Datenkomponente m (fertig, Erfolg)
Sonderfall : Baum leer, dann Erstellen des Wurzelknotens mit m
Programmieren 2 - H.Neuendorf (169)
Aufbau Sortierter Binärbaum - Einfügen
Übung
Skizzieren Sie graphisch die Suchbäume die durch die jeweils
dargestellten Einfügereihenfolgen entstehen :
105 – 107 – 109 – 110 – 111 – 115 – 120
120 – 115 – 111 – 110 – 109 – 107 - 105
110 – 109 – 115 – 105 – 107 – 120 – 115
In welchem Fall wird ein Element in der Regel am schnellsten gefunden?
⇓
Struktur (und "Qualität") eines Baumes hängt von der Einfügereihenfolge ab!
Programmieren 2 - H.Neuendorf (170)
Iterativer Aufbau Sortierter Binärbaum
Prinzip : Vergleich einzufügender Neuer Wert mit Wert des aktuellen Knotens k
Neuer Wert < Knotenwert
⇒
links davon fortsetzen
Andernfalls
⇒
rechts davon fortsetzen
class Tree {
private Knoten root ;
// …
public boolean add( int data ) {
if ( root == null ) { root = new Knoten( data ) ; return true ; }
Knoten k = root ; // "Baumpointer" k
Knoten prev ; boolean links ;
do {
prev = k ;
if( data = = k.val ) { return false ; } // Wert bereits vorhanden !
if( data < k.val ) { k = k.links ;
links = true ; }
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
}
}
Programmieren 2 - H.Neuendorf (171)
Rekursiver Aufbau Sortierter Binärbaum
Prinzip : Vergleich einzufügender Neuer Wert mit Wert des aktuellen Knotens k
Neuer Wert < Knotenwert
⇒
links davon fortsetzen
Andernfalls
⇒
rechts davon fortsetzen
class Tree {
private Knoten root ;
Eleganter - aber eben
doch langsamer als
iterative Formulierung !
// ...
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
return k ; }
{ k.rechts = add( data, k.rechts ) ; return k ; }
}
}
}
Rekursion beendet, wenn man auf Blatt-Knoten stößt
An diesen Knoten wird neues Blatt angehängt - links oder rechts - je nach Wert
Programmieren 2 - H.Neuendorf (172)
Iterative Suche 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 boolean contains( int data ) {
Knoten k = root ;
// Beginn Suche an der Wurzel
while( k != null ) {
if( k.val == data ) return true ;
// Element gefunden!
if( k.val > data )
k = k.links ;
// im linken Teilbaum weitersuchen!
else
k = k.rechts ;
// im rechten Teilbaum weitersuchen!
}
return false ;
}
}
Iteration endet, wenn Element gefunden, oder man
Baumenden erreicht - dort nimmt k Wert null an.
Programmieren 2 - H.Neuendorf (173)
Rekursive Suche in Sortiertem Binärbaum
Durchlaufen Baumknoten ab Wurzel :
Für leeren Baum wird null als Ergebnis geliefert
Wenn aktueller Knoten gesuchten Wert enthält, wird Referenz auf Knoten zurückgeliefert
Wenn nicht - dann im richtigen Unterbaum rekursiv nach Inhalt weitersuchen
⇒
Rekursiver Aufruf der Such-Methode !
class Tree {
private Knoten root ;
public Tree( ) { root = null ; }
public boolean contains( int data ) { return contains( data, root ) != null ; }
private Knoten contains( int data, Knoten k ) {
if ( k == null ) return null ; // Basisfall
if ( k.val == data ) return k ;
else{
if( k.val > data ) return contains( data, k.links ) ;
else
return contains( data, k.rechts ) ;
}
return s ;
}
// linker Teilbaum
// rechter Teilbaum
Rekursion beendet, wenn man auf
Knoten ohne Nachfolger stößt = Blatt
}
Programmieren 2 - H.Neuendorf (174)
Generischer Sortierter Binärbaum
Vergleichsoperation – Baum mit Datenelementen vom Typ Comparable
class Mitarbeiter implements Comparable<Mitarbeiter> {
// aus java.lang :
private int persNr ;
public interface Comparable<T> {
private String name ;
// Vergleichskriterium :
// ...
public Mitarbeiter( int pn, String nn ) {
public int compareTo( T t ) ;
persNr = pn ;
}
name = nn ;
}
public int compareTo( Mitarbeiter m ) { // nach persNr
if( this.persNr < m.persNr ) return -1 ;
if( this.persNr > m.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
Anpassen der Klassen Knoten und Baum …
Programmieren 2 - H.Neuendorf (175)
Sortierter Binärbaum – Iterative Suche für Comparables
class Tree< K extends Comparable<K> > {
Dieser Klasse Tree ist konkreter Typ der
gespeicherten Objekte egal.
private Knoten root ;
private class Knoten {
K val ;
Knoten links, rechts ;
}
public Tree( ) { root = null ; }
Voraussetzung ist nur deren Vergleichbarkeit / Sortierbakeit gemäß IF
Comparable
public boolean contains( K data ) {
Knoten k = root ; // Beginn Suche an Wurzel
while( k != null ) {
int cmp = k.val.compareTo( data ) ;
Statt Typ Object wird deshalb
generischer Typ verwendet, der zu
Comparable kompatibel ist.
if( cmp==0 ) return true ;
if( cmp > 0 )
k = k.links ;
else
k = k.rechts ;
}
return false ;
}
}
Programmieren 2 - H.Neuendorf (176)
Sortierter Binärbaum – Rekursive Suche für Comparables
class Tree < K extends Comparable<K> > {
private Knoten root ;
public Tree( ) { root = null ; }
private class Knoten {
K val ;
Knoten links, rechts ;
}
public boolean contains( K data ) { return contains( data, root ) != null ; }
private Knoten contains( K data, Knoten k ) {
if ( k == null ) return null ;
int cmp = k.val.compareTo( data ) ;
if ( cmp == 0 ) return k ;
if( cmp > 0 )
return contains( data, k.links ) ;
// linker Teilbaum
else
return contains( data, k.rechts ) ;
// rechter Teilbaum
}
}
Programmieren 2 - H.Neuendorf (177)
Binärbaum - Rekursives Durchlaufen - Traversierung
Durchlaufen aller Baumknoten :
(z.B. zur Wertausgabe oder -Verrechnung)
Erst wird linker Unterbaum eines Knotens k durchlaufen
Dann wird Knoten k selbst durchlaufen / bearbeitet / …
Dann wird rechter Unterbaum von k durchlaufen
class Tree {
private Knoten root ;
public Tree( ) { root = null ; }
public void durchlaufe( ) { durchlaufe( root ) ; }
private void durchlaufe( Knoten k ) {
if ( k != null ) {
// Knoten verarbeiten
durchlaufe( k.rechts ) ;
}
Reihenfolge hier :
links / Knoten / rechts
durchlaufe( k.links ) ;
IO.writeln( k.val ) ;
Rekursion beendet,
wenn man auf Knoten
ohne Nachfolger stößt
Natürlich auch andere Reihenfolgen der Berabeitung möglich :
preorder inorder postorder
Traversieren
}
}
Programmieren 2 - H.Neuendorf (178)
Binärbaum - Traversierung / 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 besucht
inorder
→ Knoten k selbst wird zwischen seinen Unterbäumen besucht
postorder
→ Knoten k selbst wird nach seinen Unterbäumen besucht
public void preorder( Knoten k ) {
if( k =! null ) { IO.writeln( k.val ) ;
// 1. Knoten k bearbeiten
preorder( k.links ) ;
// 2. Linker Unterbaum
preorder( k.rechts ) ; }
// 3. Rechter Unterbaum
}
public void inorder( Knoten k ) {
if( k =! null ) { inorder( k.links ) ;
// 1. Linker Unterbaum
IO.writeln( k.val ) ;
// 2. Knoten k bearbeiten
inorder( k.rechts ) ; } // 3. Rechter Unterbaum
}
public void postorder( Knoten k ) {
if( k =! null ) { postorder( k.links ) ;
// 1. Linker Unterbaum
postorder( k.rechts ) ;
// 2. Rechter Unterbaum
IO.writeln( k.val ) ; } // 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
Programmieren 2 - H.Neuendorf (179)
Durchlaufreihenfolge Binärbaum
Start
1.
2.
3.
4.
Ende
9.
6.
7.
4.
8.
3.
5.
9.
Ende
7.
2.
Start
Bewirkt "von oben nach unten"
Durchlauf ("außen herum")
5.
1.
Preorder-Durchlauf
8.
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 !
Dagegen Breitendurchlauf durch
Levelorder-Durchlauf realisiert
→ Saake & Sattler, Kap.14
Programmieren 2 - H.Neuendorf (180)
Sortierter Binärbaum - Löschen von Elementen
Aufwendigste Operation - Sortierung muss aufrecht erhalten werden
Vorgehen : Suche des zu löschenden Knotens k mit vorgegebenem key
Fall 0 :
Nur qualitativ
ohne Code
Grundprinzip einfach,
Verwaltung der
Referenzen im Code
aufwendig
Falls kein solcher Knoten existiert : fertig (Misserfolg)
Fall 1: Knoten k ist Blatt
Löschen des Blatts im Vorgänger-Knoten von k
Programmieren 2 - H.Neuendorf (181)
Sortierter Binärbaum - Löschen von Elementen
Vorgehen : Suche des zu löschenden Knotens k mit vorgegebenem key
Fall 2 : Knoten k hat genau einen Nachfolger
Überbrücken der Verbindung zwischen Vorgänger und Nachfolger von k
Programmieren 2 - H.Neuendorf (182)
Sortierter Binärbaum - Löschen von Elementen
Vorgehen : Suche des zu löschenden Knotens k mit vorgegebenem key
Fall 3 : Knoten k hat zwei Nachfolger
Welcher Knoten muss an Stelle des zu löschenden k rücken ?
Maximum des linken Teilbaums
(alternativ: Minimum des rechten) !
Umhängen des Teilbaums des Maximums (Minimums) an dessen Vorgänger
Sonderfall Wurzelknoten kann nach selbem Prinzip behandelt werden - muss aber bei
Implementierung besonders behandelt werden, damit Attribut root korrekt gesetzt bleibt
Programmieren 2 - H.Neuendorf (183)
Ausgeglichene Bäume
Ziel beim Einfügen von n Elementen : Baum mit gleichmäßig verteilten Elementen
Minimale Tiefe ≈ ld(n)
⇒
⇒ optimale Suchkomplexität O( ld( n ) )
Problem :
Eintragen von Daten in ungeordneter Folge :
liefert ausgeglichenen Baum
Eintrag einer sortierten Folge von Daten :
liefert zu linearer Liste entarteten Baum !
alle Referenzen links (rechts) sind null
alle Referenzen rechts (links) bilden lineare Liste
⇒ Tiefe Baum ist n
Suchkomplexität :
O( n ) >> O( ld( n ) )
Lösung : Ausgeglichene Bäume
AVL - Baum :
Höhen je zweier Teilbäume unterscheiden sich um maximal 1
Verweis auf Literatur zu A&D
Wichtige Konzepte in der Informatik :
effizientes Suchen / aufwendige Änderungsoperationen
B - Baum : (balanced)
Daten-/Indexstrukturen in Datenbanken oder
Dateisystemen.
Wege von Wurzel zu Blättern sind gleich lang
Programmieren 2 - H.Neuendorf (184)
Ausgeglichene Bäume
"P"
Fall 2:
Ausgeglichener
Baum
"A"
null
"Y"
"C"
"C"
null
null
null
null
"P"
Entartung zur
linearen Liste
null
null
null
"W"
null
null
Problem beim Einfügen von Elementen :
null
Fall 1:
"N"
"A"
"N"
Baumstruktur hängt von Einfügereihenfolge ab
Fall 1: Eingabereihenfolge in sortierter Folge
"W"
"A" "C" "N" "P" "W" "Y"
Fall 2: Eingabereihenfolge in Zufallsfolge
null
"P" "Y" "C" "W" "A" "N"
"Y"
null
null
Programmieren 2 - H.Neuendorf (185)
Datenstrukturen
Kombination von Arrays und dyn. Liststrukturen
Hashtables
Programmieren 2 - H.Neuendorf (186)
Hashtables
Effizienz von Suchbäumen beruht auf Reduktion der Suchmenge an jedem Knoten
Binärbaum: 2 Nachfolger
⇒
Halbierung Suchmenge an jedem Knoten
Tiefe ausgeglichener Baum mit n Elementen :
T = ld( n +1 )
Anzahl nötiger Suchschritte im Mittel ≈ ld( n )
Höher verzweigte Bäume : z.B. 100 Nachfolger ⇒ Suchmenge / 100 an jedem Knoten !
Tiefe stark reduziert :
T ∝ log 100 n → Anzahl Suchschritte reduziert
Probleme bei Verzweigung mit mehr als zwei Nachfolgern
Nach welchen Kriterien / Wertgrenzen wird entschieden, wie zu verzweigen ist ?
⇓
Verbesserung :
Grobe Lokation wird einfach über Zahl ausgewählt
O(1)
Dient als Array-Index, das Verweise auf die Knoten enthält
Geschwindigkeitsvorteil durch wahlfreien Array-Index-Zugriff
Array = Hash-Tabelle
Kombination der
Vorteile von Arrays
und Verketteten
Datenstrukturen
Index = Hash-Index
Programmieren 2 - H.Neuendorf (187)
Hashtables
Hashknoten
Hashtabelle
0
1
2
3
4
5
6
7
Lokalisation / direkter Zugriff
durch Array-Indizierung :
tab[ h( key ) ]
"Zerhacken" (hashing) von
key-Daten zu Index-Zahl
Nachfolgeknoten des Hashknotens
Indizierung Hashtabelle durch Hashindex
⇓
Zahlen-Berechnung für key aus Hash-Funktion h( key )
Abbildung eindeutige key-Wert auf Hashindex h(key) :
key → h(key) = Hashindex = Arrayindex
Wertebereich Hashfunktion soll Platz-Anzahl der Hashtabelle = Array entsprechen …
… bzw. wird darauf abgebildet
Programmieren 2 - H.Neuendorf (188)
Hashtables - Hashfunktion h( key )
keys sind eindeutig – Hashwerte h( key ) nicht
Sollte mit elementaren Operationen effizient berechenbar sein
Typisch sind gewichtete Quersummen und Modulo-Operationen mit Primzahlen
Abschließende Modulo-Bildung mit Array-Größe n : h[key] % n
um Wertebereich von h an Array-Indizes anzupassen.
Funktion, deren Werte gut streuen solle :
h( key ) bildet ähnliche key auf unähnliche Hashindex-Werte ab.
Somit Kopplung an Wahrscheinlichkeits-Verteilung der key aufgehoben
Häufung bei bestimmten Indices vermeiden :
Gleichmäßige Ausnutzung des Arrays durch Gleichverteilung der Hashwerte !
Füllfaktor (load factor) der HashTable :
f=m/n
Theorie: Durchschnittliche Zahl von Suchschritten = 1 + f / 2
O(1)
ideal !
Performante Hashtables erfordern f < 0.6 – 0.7
Programmieren 2 - H.Neuendorf (189)
Hashtables - Offenes Hashing
Array = ausreichend große Hashtabelle
Array-Plätze = Hash Buckets
Hashtabelle
Array-Index = Bucket-Index
Unterbäume
Alternatives Verfahren ist Sondieren :
Bei Kollosion wird noch leerer Platz im Array
gesucht, indem man mit zu berechnender
Schrittweite zyklisch weitergeht …
Durch ihre keys werden Einträge identifiziert.
Pro HashBucket mehrere Einträge möglich.
Mehrere keys durch Hashfunktion auf gleichen Bucket
abgebildet = Kollision
Bsp: h(k) = k%10 ⇒
gleicher Bucket für k = 49 und 119
Ausreichend große Hashtabelle
Menge möglicher Elemente m ist immer größer als Anzahl n verfügbarer Positionen – aber :
Viele kleine Unterbäume aus nur wenigen Knoten
Praxis :
⇒
rascher Zugriff !
Statt Unterbäumen einfache LinkedLists – ausreichend performant da relativ kurz …
Komplexität des Umgangs mit Baumstrukturen vermieden !
Umsetzung …
Programmieren 2 - H.Neuendorf (190)
Hashtables
Aufbau
class MyHashtable {
private Node[ ] table ; // Array = Hashbuckets
Knoten Node halten den key und das eigentliche
zugeordnete val-Objekt
private int size ;
Attribut next zum Aufbau verkettete Liste
// Menge der Objekte
Hashtable selbst ist Array vom Typ Node
private class Node {
public Node( Object key, Object val ){
Typ Node jedoch außen unbekannt
this.key = key; this.val = val; next = null;
}
public Object val ;
Node
public Object key ;
public Node next ;
key
val
}
public MyHashtable( int tablesize ){
next
table = new Node[ tablesize ] ; // Hashtable
}
private int hash( Object key ){
// Wiederverwenden Java-Infrastruktur
Node
return Math.abs( key.hashCode() % table.length ) ;
…
}
// …
}
Abweisen von null-Werten als
Methoden-Parameter noch ergänzen …
Logische Zuordnung :
Schlüssel Wert
HashMap
Programmieren 2 - H.Neuendorf (191)
Hashtable
hash()-Funktion liefert sofort richtige Array-Position =
HashBucket, ab dem Suche in Liste beginnt.
class MyHashtable {
// …
Jeder Node kann nach seinem key-Wert befragt werden.
private Node find( Object key ) {
Wir nutzen Javas equals-hashCode-Infrastruktur !
Nach Implementierung von find( ) sind Methoden get( )
und contains( ) trivial. Node-Referenzen werden nur
intern verwendet – nicht nach außen gegeben.
int bucket = hash( key ) ;
Node p = table[ bucket ] ;
while( p!=null ){
if( p.key.equals( key ) ) return p ;
p = p.next ;
private Node find( Object key )
}
liefert intern Referenz auf Knoten
return null ;
public boolean put( Object key, Object val )
}
legt val ab, wenn nicht schon vorhanden
public Object get( Object key ){
public Object get( Object key )
Node p = find( key ) ;
if( p!=null ) return p.val ; // besser : DefensiveCopy
else return null ;
}
}
return find( key ) != null ;
}
gibt zu key gehöriges Wertobjekt zurück, evtl, null
public boolean remove( Object key )
public boolean contains( Object key ){
}
Hashtable-Schnittstelle
entfernt zu key gehöriges Wertobjekt, wenn vorhanden
public boolean contains( Object key )
ermittelt ob zu key bereits Eintrag vorhanden
// …
Programmieren 2 - H.Neuendorf (192)
Hashtable
table
table[ bucket ]
class MyHashtable {
// …
(2)
public boolean put( Object key, Object value ) {
if( key==null || value == null ) return false ;
// Einträge sollen eindeutig sein : *)
if( find( key ) != null ) return false ;
(1)
p
Node p = new Node( key, value ) ;
int bucket = hash( key ) ;
// Einfügen am Bucket-Anfang der Liste :
p.next = table[ bucket ] ; // (1)
table[ bucket ] = p ;
// (2)
size++ ;
return true ;
}
Keine null-Werte
als Datenelemente
akzeptiert !
}
Vorgehen entspricht grundsätzlich addFirst( ) Methode bei verketteten Listen:
Das neue Element kommt immer an den Anfang
der Liste und die Referenz der Array-Position muss
angepasst werden.
Erspart das Durchwandern der gesamten Liste.
Anmerkung : Häufiger Aufruf von find( ) ist nicht
besonders performant – macht aber das Coding
leichter lesbar …
*) Vereinfachtes Vorgehen - Alternative :
Ersetzen altes val-Objekts durch neues val-Objekt
Weitere Methoden als Übung …
Programmieren 2 - H.Neuendorf (193)
class MyHashtable {
// …
public boolean remove( Object key ) {
int bucket = hash( key ) ;
Node p = table[ bucket ] ; // (1)
if( p==null ) return false ; // Nicht vorhanden
else{
// Sonderfall erstes Element in Liste :
if( p.key.equals( key ) ) {
table[ bucket ] = p.next ; // (2)
size-- ;
return true ;
}
// Mitten in Liste :
(3)
else {
Node prev = null ;
while( p != null && !p.key.equals( key ) ) {
prev = p ;
p = p.next ;
}
if( p==null ) return false ;
else{
prev.next = p.next ;
size-- ;
return true ;
}
}}}}
Hashtable
table
table[ bucket ]
(1)
(2)
p
(3) : Löschen mitten aus Liste entspricht
Vorgehen bei Behandlung verketteter Listen. In
diesem Fall muss die Referenz vom Array auf
die Liste nicht verändert werden
Korrektes Arbeiten der Hashtable-Implementierung
setzt voraus, dass verwendete key-Objekte den
equals()-hashCode()-Kontrakt korrekt umsetzen.
Sollten ferner gerne clone-bar sein …
Programmieren 2 - H.Neuendorf (194)
Hashtable auf Basis von LinkedList
class LinkedHashtable {
private LinkedList[ ] table ;
private int hash( Object key ){ return Math.abs( key.hashCode( ) % table.length ) ; }
public LinkedHashtable( int size ){
table = new LinkedList[ size ] ;
Objekte werden
hier selbst als
keys verwendet
for( int i=0; i<table.length; i++ ){ table[ i ] = new LinkedList( ) ; }
}
public boolean add( Object val ){
int bucket = hash( val ) ;
if( table[ bucket ].contains( val ) ) return false ;
table[ bucket ].add( val ) ;
return true ;
}
public boolean contains( Object val ){
int bucket = hash( val ) ;
return table[ bucket ].contains( val ) ;
}
Vereinfachte Implementierung – Prinzip :
Semantisch + Technisch höherwertige
Datenstruktur wird auf Basis primitiverer
Datenstruktur erstellt
// …
}
Programmieren 2 - H.Neuendorf (195)
JDK-Hashtables
Klasse HashMap
bzw.
java.util.HashMap + Hashtable
HashMap<K,V>
generische Variante
Schlüssel-Wert-Tabelle :
Zuordnung
Schlüssel-Objekte → Wert-Objekte
HashMap-Methoden (u.a.) :
Rawtype-Version
Object put( Object key, Object element )
Generics-Version
V put( K key, V value )
…
Legt element = Wertobjekt unter Schlüsselobjekt key in Tabelle ab.
Ersetzt und gibt dort bereits abgelegtes Element zurück - oder null, falls Schlüssel noch nicht belegt
Object get( Object key )
Gibt Element zurück, das unter Schlüssel key gespeichert ist
boolean containsKey( Object key )
Ermittelt, ob Schlüsseleintrag schon vorhanden
Object remove( Object key )
Löscht das unter Schlüssel key gespeicherte Element und gibt es (oder null, falls nicht vorhanden) zurück
int size( )
Liefert Anzahl der Schlüssel in Tabelle
boolean isEmpty( )
void clear( )
…
Klasse Hashtable hat gleiche Struktur wie HashMap
Unterschied:
Hashtable ist synchronized (threadsicher), HashMap
nicht - und somit leichtgewichtiger
Programmieren 2 - H.Neuendorf (196)
JDK-Hashtables
import java.util.* ;
class HashMapTest {
public static void main( String[ ] args ) {
HashMap<Date, String> table =
new HashMap<Date, String>( ) ;
Date d ;
// Datumsobjekte als Schlüssel
Mittels Schlüsselobjekt (key) wird
Wertobjekt in HashMap abgelegt
⇓
Via Schlüsselobjekt kann Wertobjekt
wiedergefunden werden.
z.B. : Klasse Date als Schlüsselklasse
nicht perfekt - aber zeigt die einzige …
Vorraussetzung :
String s1 = "Anton Wagner" ;
d = new Date( 1960, 12, 2 ) ; // = Key
table.put( d, s1 ) ;
String s2 = "Doris Bach" ;
d = new Date( 1970, 4, 3 ) ; // = Key
table.put( d, s2 ) ;
Verwendete Schlüsselklasse muss
konsistente Methoden
boolean equals( Object obj )
int hashCode( )
besitzen, damit Schlüsselobjekte bei Suche
verglichen werden können
⇓
// Mitarbeiter suchen :
Date sd = new Date( 1970, 4, 3 ) ; // Such-Key
String s3 = table.get( sd ) ;
}
}
Man kann natürlich auch eigene
Schlüsselklassen formulieren !
Dank Generics entfällt Downcasting !
Frage: An welcher Stelle ?
Programmieren 2 - H.Neuendorf (197)
Herunterladen