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)