homedukeAlgorithmen & Datenstrukturen mit Java: Hash-Tabellen Prof. Dr. Uwe Schmidt FH Wedel

Hash-Tabellen

weiter

weiter

Implementierung von Hashtabellen

Hashtabellen
werden zu effizienten Implementierung von Mengen und Tabellen verwendet.
Voraussetzung
ist eine Hash-Funktion auf dem Element- oder Schlüssel-Datentyp.
 
Eine Hash-Funktion ist eine Abbildung des Elementbereiches auf ein Intervall der natürlichen Zahlen [0 .. max-1].
Idee
Die Suche nach einem Element wird durch einen indizierten Zugriff in ein Feld realisiert.
 
Wenn dieses alleine zum Ziel führen würde, hätte man eine Suche in O(1), also nicht mehr abhängig von der Größe der zu durchsuchenden Menge.
schlecht
Dieses Ziel kann nicht vollständig erreicht werden.
Dazu wäre eine injektive Hash-Funktion Voraussetzung.
 
Solche Funktionen existieren im Allgemeinen nicht, da der Elementbereich üblicherweise unbeschränkt viele Elemente enthält.
gut
Aber es gibt sehr effizente Implementierungen,
die für einen großen Bereich von Anwendungen diese Laufzeit fast erreichen.
Effizienz
Die Qualität der Implementierung hängt stark von der Wahl der Hash-Funktion ab.
Anforderungen
Die Hash-Funktion sollte alle Werte des Elementbereichs gleichmäßig auf das Intervall abbilden.
 
Ähnliche Elemente sollten möglichst nicht auf den gleichen Wert abgebildet werden und auch nicht einen ähnlichen Hash-Wert besitzen.
weiter
Implementierung
Die hier vorgestellte Implementierung ist eine sehr rudimentäre Implementierung.
 
Sie verwendet ein Feld fester Länge. Elemente,
deren Hash-Wert gleich dem eines schon eingetragenen Wertes sind, werden in die erste freie Stelle hinter dem Hash-Index eingetragen.
 
Dieses funktioniert nur dann, wenn die Anzahl der zu speichernden Elemente kleiner als die Länge des Index-Intervalls ist.
 
Sie funktioniert effizient nur dann, wenn die Anzahl signifikant kleiner ist, z.B. max 70% der Größe der Tabelle beträgt.
 
In der hier vorgestellte Lösung wird, wenn die Anzahl der Elemente steigt, eine Vergrößerung und Reorganisation der gesamten Hash-Tabelle vorgenommen.
Dadurch bleibt der effiziente Zugriff auf Elemente erhalten.
merke
Solche Reorganisationen der gesamten Tabelle dürfen nicht zu häufig gemacht werden,
da sonst die Gesamt-Rechenzeit zu sehr steigt.
 
Das Löschen von Elementen in dieser einfachen Implementierung ist weder effizient noch einfach zu realisieren.
Daher sind diese Methoden nicht implementiert.
schlecht
Persistente Implementierungen sind nicht sinnvoll, da keine Objekte gemeinsam genutzt werden können.
Es müsste immer die gesamte Tabelle kopiert werden.
weiter
Geschicktere Implementierungen
1. Strategie
In der Hash-Tabelle werden nicht die Elemente selbst gespeichert, sondern verkettete Listen von Schlüssel-Wert-Paaren.
gut
Dadurch wird das Löschen einfach.
Es wird auf das Löschen in den verketteten Listen zurückgeführt.
gut
Solange die verketteten Listen nur wenige Elemente enthalten, wird eine gute Laufzeit erreicht und es wird wenig unbenutzter Speicher im Array verschwendet.
schlecht
Globale Reorganisationen sind aber immer noch notwendig, damit die Überlauf-Listen nicht zu lang werden.
weiter
2. Strategie
Kombination einer Hash-Funktion mit einem binären Suchbaum
 
Anstatt mit dem Schlüssel direkt zu arbeiten, wird der Hash-Wert des Schlüssels im binären Suchbaum genutzt.
Zusätzlich wird in den Knoten des Baumes der echte Schlüssel und eine (immer kurz bleibende) Liste von Werten gespeichert.
gut
Wenn mit einer guten Hash-Funktion und 64- oder 32-Bit Hash-Werten gearbeitet wird, gibt es nur sehr, sehr selten Kollisionen und die Wertelisten sind immer sehr kurz, fast immer mit der Länge 1.
gut
Eine Reorganisation entfällt.
gut
Der schlechteste Fall, Einfügen aus einer sortierten Liste, entfällt, da die Hash-Werte nicht mehr sortiert sind.
gut
Es muss also nicht mit einem ausbalancierten Suchbaum gearbeitet werden.
gut
Persistente Implementierungen für diese Hash-Tabellen sind wieder Speicher- und Laufzeit-effizient.
weiter
3.Strategie
Kombination mit einem binären Patricia-Baum
 
Binären Patrizia-Bäume sind eine sehr effiziente Implementierung für Maps, bei denen der Schlüssel ein Int-Wert ist (32- oder 64-Bit Zahlbereich).
 
Die Laufzeit für Suchen und Einfügen ist dort immer in der Kompexitätsklasse O(1).
 
Die Kombination mit einer Hash-Funktion ermöglicht es, auch Maps mit anderen Schlüsseln, z.B. Zeichenreihen zu verwenden.
 
Für Kollisionen braucht wieder nur ein ganz einfaches Verfahren gewählt werden, z.B. wieder eine verkettete Liste.
weiter
4.Strategie
Hash-Funktionen mit 64, 128 oder mehr Bits für den Hash-Wert
 
Bei der Verwendung einer guten 64-Bit Hash-Funktion oder einer auch einfachen kryptografischen Hash-Funktion, MD5, SHA1, ... werden Kollisionen sehr selten.
 
Bei kryptografischen Hash-Funktionen sind Kollisionen so selten, dass die Wahrscheinlichkeit eines Fehlers im Rechner, z.B. das Kippen eines Bits, sehr viel höher ist als eine Kollision.
 
Dadurch kann man so arbeiten, als sein die Hash-Funktion injektiv, Kollisionen werden einfach ignoriert.
 
Dieses Vorgehen wird z.B. in Versionsverwaltungssystemen, z.B. git, genutzt, um alle Versionen aller Quellen eindeutig durch Hash-Werte zu identifizieren und im Dateisystem einfach zugreifbar zu machen.
Einfache Implementierung
wird in der Vorlesung entwickelt

Letzte Änderung: 16.01.2018
© Prof. Dr. Uwe Schmidt
Prof. Dr. Uwe Schmidt FH Wedel