Portal:Fußball/Neue Artikel und Hashtabelle: Unterschied zwischen den Seiten
TSchm (Diskussion | Beiträge) plus Richard Money |
K Bot: Ergänze: fa:جدول هش |
||
Zeile 1: | Zeile 1: | ||
In der [[Informatik]] bezeichnet man als '''Hashtabelle''' bzw. '''Streuwerttabelle''' eine spezielle [[Indexstruktur]]. Hashtabellen eignen sich vor allem dazu, Datenelemente in einer großen Datenmenge aufzufinden. Hashtabellen stehen dabei in Konkurrenz zu [[Baum (Graphentheorie)|Baumstrukturen]] (wie etwa ein [[B*-Baum]]) und der [[Liste (Datenstruktur)#Skip Liste|Skip-List]], die ebenfalls als Indexstruktur dienen können. Beim Einsatz einer Hashtabelle zur Suche in Datenmengen spricht man auch von einem ''Hashverfahren'' oder ''Streuspeicherverfahren''. |
|||
<!-- Die Artikel bitte nach gut zwei Wochen wieder aus der Liste löschen. --> |
|||
<small> |
|||
== Hashverfahren == |
|||
'''04.07.''' [[Richard Money]], [[Thomas Klasen]], [[Nils Andersson (Fußballtrainer)]], [[Sascha Hildmann]], [[Saxonia Tangermünde]], [[Ricky van den Bergh]], [[Karim Bridji]], [[Alexandre Villaplane]], [[Ralf Peter (Trainer)]], [[Niklas Andersen]], [[FC Belize]], [[Harrison Tasher]], [[DDR-Fußball-Liga 1979/80]], [[Daniel Opare]] |
|||
Das Hashverfahren ist ein Algorithmus zum Suchen von Datenobjekten in großen Datenmengen. Es basiert auf der Idee, dass eine mathematische Funktion die Position eines Objektes in einer Tabelle berechnet. Dadurch erübrigt sich die Durchsuchung vieler Datenobjekte, bis das Zielobjekt gefunden wurde. |
|||
'''03.07.''' [[Dennis Serrano]], [[Ludwig Goldbrunner|Ludwig Goldbrunner (Überarbeitung/Ausbau)]], [[Annan Athletic]], [[Jürgen Stoffregen (Fußballspieler)]], [[Stadion Viktoria]], [[FIFA Beach-Soccer Weltmeisterschaft 2007]], [[Tonny van der Linden]], [[Stad Utrecht]], [[Liste der Rekordspieler der Fußball-Bundesliga]], [[Dennis Serrano]] |
|||
'''02.07.''' [[Fotbollsallsvenskan 1974]], [[Fotbollsallsvenskan 1975]], [[SV Johannstadt 90]], [[Stadion der Freundschaft (Frankfurt (Oder))]], [[Dynamo Gera]], [[SpVgg Erlangen]], [[René Ferrier]], [[FIFA Beach-Soccer Weltmeisterschaft 2006]], [[FIFA Beach-Soccer Weltmeisterschaft 2005]], [[Horst Walter (Fußballspieler)]], [[Fußball-Westasienmeisterschaft 2008]], [[Ari Nyman]], [[Gordon Hodgson]] |
|||
== Der Algorithmus == |
|||
'''01.07.''' [[Mikael Källström]], [[Zenon Lissek]], [[Said Huseinovic]], [[Ronnie Pander]], [[Rüdiger Wenzel]], [[Wolfgang Pfeifer]], [[Sepahan Novin]], [[Nicola Pozzi]], [[Abe Lenstra]] |
|||
Beim Hashverfahren werden die Zieldaten in einer Hashtabelle gespeichert. Eine Hashfunktion berechnet zu jedem Datenobjekt einen Hashwert, der als [[Indexstruktur|Index]] in der Tabelle verwendet wird. Zum Berechnen dieses Hashwertes wird ein [[Schlüssel (Datenbank)|Schlüssel]] benötigt, der dieses Objekt eindeutig identifiziert. Dieser Schlüssel wird von der Hashfunktion zum Berechnen des Hashwertes verwendet. Das Datenobjekt wird an dieser Stelle (''Bucket'' genannt) in der Tabelle gespeichert. Im Idealfall bekommt jedes Objekt einen eigenen Bucket. Hashfunktionen müssen jedoch nicht eindeutig sein, so dass sich in der Praxis mehrere Objekte einen Bucket teilen müssen. Diesen Fall nennt man [[Hashtabelle#Kollisionen|Kollision]], er benötigt eine spezielle Behandlung durch das Verfahren. |
|||
'''30.06.''' [[UNCAF Nations Cup 2009]], [[Kim Källström|Kim Källström (ergänzt)]], [[Vorwärts Mühlhausen]], [[Torsten Mattuschka]], [[Tuna Üzümcü]], [[Howard Vaughton]], [[ASEAN-Fußballmeisterschaft 2002]], [[Mirosław Trzeciak]], [[Mike Frantz]], [[Adrie van Kraay]], [[Charles Dempsey]], [[Daniel Brosinski]], [[U-17-Fußball-Europameisterschaft 2009]], [[Daniele Cacia]], [[Simione Tamanisau]] |
|||
'''29.06.''' [[Christophe Landrin]], [[Serdar Bayrak]], [[OFC Champions League 2007/08]], [[Willy Fondja]], [[Shic Rezaei Khonakdar]], [[Dynamo Ost Frankfurt]] |
|||
Bei einer Suche in der Hashtabelle wird nun ähnlich vorgegangen. Zunächst wird aus einem Suchschlüssel wieder ein Hashwert berechnet, der den Bucket des gesuchten Datenobjektes bestimmt. Befinden sich im Bucket mehrere Objekte, muss jetzt nur noch durch direkten Vergleich des Suchschlüssels mit den Objekten das gesuchte Ziel bestimmt werden. |
|||
'''28.06.''' [[József Fogl]], [[Christian Streich]], [[Sorin Cârţu]], [[Hervé Bochud]], [[Klaus Senger]], [[Stephan Flauder]], [[Samassi Abou]], [[Matías Aguirregaray]] |
|||
'''27.06.''' [[SV Prag Stuttgart]], [[U19-Bundesliga 2003/04]], [[1. FC Wilmersdorf]], [[Vilmos Kertész]], [[Bernd Nickel|Bernd Nickel (Überarbeitung/Ausbau)]], [[Rija Rakotomandimby]], [[Albert Ferrer]] [[Fotbollsallsvenskan 1963]], [[Fotbollsallsvenskan 1962]], [[UEFA Women’s Cup 2008/09]], [[Fußball-Europameisterschaft 1976/Jugoslawien]], [[Pierre Wajoka]], [[Martin Britt]], [[ASG Vorwärts Rostock-Gehlsdorf]] |
|||
In der Praxis wird die Tabelle als ein [[Array]] implementiert. Wegen der möglichen Kollisionen werden die Daten aber nicht direkt im Array gespeichert, sondern lediglich die Verweise auf den zugehörigen Bucket. Buckets können nun selbst wiederum so organisiert sein, um eine schnelle Suche der im Bucket enthaltenen Objekte zu ermöglichen. |
|||
'''26.06.''' [[Salvatore Amirante]], [[Bernd Wunderlich (Fußballspieler)]], [[Georges Akiremy]], [[Lazaros Christodoulopoulos]] |
|||
'''25.06.''' [[Marek Leśniak]], [[Rheinlandpokal]], [[Adolf Scheidt (Fußballspieler)]], [[Goldene Generation (Portugal)]], [[Hedvig Lindahl]], [[TSV Ampfing]], [[Milan Kopic]], [[Jean Dockx]], [[ASG Vorwärts Schwerin]] |
|||
Ist die Hashtabelle ausgewogen, befinden sich nur wenige Datensätze in einem Bucket und der letzte Schritt hat einen geringen Aufwand. Im [[Worst-Case]] können Kollisionen zu einer [[Entartung (Informatik)|Entartung]] der Hashtabelle führen, wenn wenige Buckets sehr viele Objekte enthalten, während andere Buckets leer bleiben. |
|||
'''24.06.''' [[United Soccer Leagues 2007]], [[Andrea Raggi]], [[Rangliste der Nationalmannschaften bei Elfmeterschießen]], [[André Simonyi]], [[Jonathan Wilson]] |
|||
'''23.06.''' [[Gáspár Borbás]], [[Richard Kolitsch]], [[Hernán Barcos]], [[Akis Zikos]], [[Open Canada Cup]], [[Ştefan Stoica]], [[Liste der Länderspiele der niederländischen Fußballnationalmannschaft]], [[Andrzej Strejlau]] |
|||
Hashtabellen ermöglichen so eine sehr schnelle Suche in großen Datenmengen, da mit der Berechnung des Hashwertes in einem einzigen Schritt die Anzahl der möglichen Zielobjekte eingeschränkt wird. Damit gehören Hashtabellen zu den effizientesten Indexstrukturen. Ein großer Nachteil ist jedoch die Gefahr der Entartung durch Kollisionen, die bei einem stetigen Wachstum der Datenmenge unausweichlich sind. Siehe dazu [[Hashtabelle#Kollisionen|Kollision]]. Daher werden in [[Datenbanksystem]]en häufig Hashtabellen zu Gunsten von [[B*-Baum|B*-Bäumen]] vermieden. B*-Bäume sind robust gegen Entarten und müssen nicht (oder nur lokal) restrukturiert werden. |
|||
'''22.06.''' [[Luís Oliveira Gonçalves]], [[Jerome James (Fußballspieler)]], [[Albert Thurton]], [[Archie Hunter]], [[ASG Vorwärts Fünfeichen]], [[Harrison Roches]], [[Elroy Smith]], [[Serie B 2002/03]], [[Deividas Šemberas]], [[Dion McCauley]], [[Yaser Yıldız]], [[Olympische Sommerspiele 2008/Fußball/Deutschland]], [[Serie A 2002/03]], [[Carlos Slusher]], [[Tim Carter]], [[Ben Bamfuchile]] |
|||
'''21.06.''' [[Herbert Erhardt|Herbert Erhardt (Überarbeitung/Ausbau)]], [[Selvin De León]], [[Jarbi Álvarez]], [[Shane Moody-Orio]], [[Dynasty Cup (Fußball)]], [[Fußball-Europameisterschaft 2000/Frankreich]], [[EFC Ruhla 08]], [[Witali Bulyga]], [[Alfred Wahl]], [[Ludwig Lurz]], [[Jacek Gmoch]] |
|||
=== Kollisionen === |
|||
Da [[Hash-Funktion]]en im Allgemeinen nicht [[Injektivität|injektiv]] sind, können zwei unterschiedliche Schlüssel zum selben Hash-Wert, also zum selben Feld in der Tabelle führen. Dieses Ereignis wird als ''Kollision'' bezeichnet. In diesem Fall muss die Hashtabelle mehrere Werte an derselben Stelle aufnehmen. Um dieses Problem zu handhaben, gibt es diverse ''Kollisionsauflösungsstrategien''. |
|||
Eine Möglichkeit wird ''offenes Hashing'' genannt. Wenn dabei ein Eintrag an eine schon belegte Stelle in der Tabelle abgelegt werden soll, wird stattdessen eine andere freie Stelle genommen. Es wird häufig zwischen drei Varianten unterschieden: |
|||
# ''lineares Sondieren'' – es wird um ein konstantes Intervall verschoben nach einer freien Stelle gesucht. Meistens wird die Intervallgröße auf 1 festgelegt. |
|||
# ''quadratisches Sondieren'' – Nach jedem erfolglosem Suchschritt wird das Intervall quadriert. |
|||
# ''doppeltes Hashen'' – eine weitere Hash-Funktion liefert das Intervall. |
|||
Eine weitere Möglichkeit ist ''Kollisionsauflösung durch Verkettung''. Anstelle der gesuchten Daten enthält die Hashtabelle hier Behälter ([[Bucket]]s), die alle Daten mit gleichem Hash-Wert aufnehmen. Bei einer Suche wird also zunächst der richtige Zielbehälter gesucht. Damit wird die Menge der möglichen Ziele erheblich eingeschränkt. Dennoch müssen abschließend die verbliebenen Elemente im Behälter durchsucht werden. Im schlimmsten Fall (Worst Case) kann es passieren, dass alle Elemente gleiche Hash-Werte haben und damit im gleichen Bucket abgelegt werden. In der Praxis kann das aber durch die Wahl einer geeigneten Größe für die Hashtabelle sowie einer geeigneten Hash-Funktion vermieden werden. Oft wird die Verkettung durch eine [[Liste (Datenstruktur)|lineare Liste]] pro Behälter realisiert. |
|||
=== Vorteile === |
|||
Trotz der Schwierigkeiten, die sich beispielsweise aus der Kollisionsbehandlung ergeben, bieten Hashtabellen wegen der Vorzüge beim sofortigen Zugriff durch den Hash-Wert auf die Inhalte in einer Tabelle im Vergleich zu der Suche nach dem Schlüssel und wegen der im Idealfall fehlenden Beziehung zweier Hash-Werte, die aus ähnlichen Schlüsseln berechnet wurden, oft Vorteile in der [[Zugriffszeit]] als auch im benötigten [[Speicherplatz]] gegenüber gewöhnlichen Arrays. |
|||
=== Nachteile === |
|||
Hat die Tabelle einen gewissen [[Füllgrad]] überschritten, wird sie zwangsläufig [[entarten]]. Dann kann nur eine Vergrößerung der Tabelle mit nachfolgender Restrukturierung wieder zu akzeptablem Laufzeitverhalten führen. |
|||
Um den Nachteil der Nicht-Injektivität zu konterkarieren, bietet es sich an, die Funktion für den Schlüsselgenerator ein weiteres Mal aufzurufen. Schlüsselgeneratorfunktionen sind höchst performant. |
|||
=== Komplexität === |
|||
Wurden Hash-Funktion und Größe der Hashtabelle geeignet gewählt, ist der Aufwand für die Suche in der Tabelle (Look-Up) [[Landau-Symbole|O(1)]]. Wegen der möglichen Kollisionen hat eine Hashtabelle allerdings im so genannten [[Worst-Case]] ein sehr viel schlechteres Verhalten. Dieser wird mit O(''n'') abgeschätzt, wobei das ''n'' der Anzahl der in der Hashtabelle gespeicherten Einträge entspricht. Es werden dabei also alle Einträge in der Tabelle durchsucht. |
|||
== Varianten des Hashverfahrens == |
|||
Es gibt mehrere Varianten des Hashverfahrens, die sich für bestimmte Daten besser eignen. Ein wichtiger Faktor hierbei ist die Dynamik, mit der sich die Anzahl der Elemente ändert. Das offene Hashing löst dieses Problem, nimmt aber Einbußen bei den Zugriffszeiten in Kauf. Das geschlossene Hashing ist hingegen auf explizite Strategien zur Kollisionsbehandlung angewiesen. Vorsicht: Die Bezeichnungen „offenes“ bzw. „geschlossenes Hashing“ werden auch in genau umgekehrter Bedeutung verwendet. |
|||
=== Hashing mit Verkettung === |
|||
Beim Hashing mit Verkettung (engl. Separate Chaining) ist die Hash-Tabelle so strukturiert, dass jeder Behälter eine dynamische Datenstruktur aufnehmen kann – beispielsweise eine [[Liste (Datenstruktur)|Liste]] oder einen [[Baum (Graphentheorie)|Baum]]. Jeder Schlüssel wird dann in dieser Datenstruktur eingetragen oder gesucht. So ist es problemlos möglich, mehrere Schlüssel in einem Behälter abzulegen, was allerdings zu mehr oder weniger verlängerten Zugriffszeiten führt. Die Effizienz des Zugriffs wird dabei davon bestimmt, wie schnell Datensätze in die gewählte Datenstruktur eingefügt und darin wiedergefunden werden können. Hashing mit Verkettung ist bei Datenbanken eine sehr gängige Indizierungsvariante, wobei sehr große Datenmengen mittels Hashtabellen indiziert werden. Die Größe der Buckets ist in Datenbanksystemen ein Vielfaches der [[Datenblock|Sektor]]engröße des Speichermediums. Der Grund dafür ist, dass die Datenmenge nicht mehr im Hauptspeicher gehalten werden kann. Bei einer Suchanfrage muss das Datenbanksystem die Buckets sektorenweise einlesen. |
|||
=== Hashing mit offener Adressierung === |
|||
Dieses Verfahren wird abgekürzt auch ''offenes Hashing'', bezogen auf die offene Adressierung, aber auch ''geschlossenes Hashing'', bezogen auf die begrenzte Anzahl möglicher Schlüssel im Behälter, genannt. |
|||
Beim Hashing mit offener Adressierung kann jedem Behälter nur eine feste Anzahl von Schlüsseln zugewiesen werden. Häufig wählt man einfach einen einzigen möglichen Schlüssel pro Behälter. Im Kollisionsfall muss dann nach einem alternativen Behälter gesucht werden. Dabei geht man so vor, dass man für <math>m</math> Behälter eine ganze Folge von <math>m</math> Hash-Funktionen definiert. Führt die Anwendung der ersten Hash-Funktion, nennen wir sie <math>h_0</math>, zu einer Kollision, so wendet man <math>h_1</math> an. Führt diese ebenfalls zu einer Kollision (d. h. der entsprechende Behälter ist bereits belegt), so wendet man <math>h_2</math> an, und so weiter bis <math>h_{m-1}</math>. Die Bezeichnung „offene Adressierung“ ergibt sich aus der Eigenschaft, dass durch Kollisionen gleiche Schlüssel unterschiedliche Adressen zugewiesen bekommen können. |
|||
==== Lineares Sondieren ==== |
|||
Die einfachste Möglichkeit zur Definition einer solchen Folge besteht darin, so lange den jeweils nächsten Behälter zu prüfen, bis man auf einen freien Behälter trifft. Die Definition der Folge von Hash-Funktionen sieht dann so aus: |
|||
<math>h_i(x) = (h(x) + i) \bmod m</math> |
|||
Die Anwendung des Modulo hat mit der begrenzten Zahl von Behältern zu tun: Wurde der letzte Behälter geprüft, so beginnt man wieder beim ersten Behälter. Das Problem dieser Methode ist, dass sich so schnell Ketten oder Cluster bilden und die Zugriffszeiten im Bereich solcher Ketten schnell ansteigen. Das lineare Sondieren ist daher wenig effizient. |
|||
==== Implementierung einer Hashtabelle in [[Java (Programmiersprache)|Java 5]] ==== |
|||
<source lang="java"> |
|||
/** Implementierung einer Hashtabelle mit linearer Sondierung |
|||
Autor: Andreas Solymosi, 2007 |
|||
Quelle: Solymosi/Grude: Grundkurs Algorithmen und Datenstrukturen, |
|||
Vieweg Verlag, ISBN 3-528-05743-2 |
|||
*/ |
|||
public class HashTabelle<Element, Schluessel extends Comparable<Schluessel>> { |
|||
// Tabelleneintrag |
|||
private class Eintrag<E, S extends Comparable < S > > { |
|||
E element; |
|||
S schluessel; |
|||
boolean belegt; |
|||
} |
|||
private Eintrag<Element, Schluessel>[] tabelle; // die eigentliche Hashtabelle |
|||
private int anzahl; // der eingetragenen Elemente, anfänglich 0 |
|||
public HashTabelle(int groesse) { |
|||
tabelle = new Eintrag[groesse]; |
|||
for (int i = 0; i < tabelle.length; i++) { |
|||
tabelle[i] = new Eintrag<Element, Schluessel>(); |
|||
tabelle[i].belegt = false; |
|||
} |
|||
anzahl = 0; |
|||
} |
|||
// von vorhanden beschrieben (Nebeneffekt!); von eintragen, suchen und vorhanden gelesen |
|||
private int index; |
|||
public boolean vorhanden(Schluessel schluessel) { |
|||
index = schluessel.hashCode() % tabelle.length; // Nebeneffekt! |
|||
while (tabelle[index].belegt && schluessel != tabelle[index].schluessel) |
|||
index = (index + 1) % tabelle.length; // lineare Sondierungsfunktion |
|||
// Platz gefunden: |
|||
// index zeigt auf den Platz, wo der letzte Schluessel steht |
|||
if (schluessel == tabelle[index].schluessel) |
|||
return true; |
|||
else // ! inhalt[index].belegt, index zeigt auf einen freien Platz |
|||
return false; |
|||
} |
|||
// ein Element mit vorhandenem Schlüssel wird überschrieben (kein Doppeleintrag) |
|||
public void eintragen(Schluessel schluessel, Element element) throws VollException { |
|||
if (anzahl == tabelle.length) |
|||
throw new VollException(); |
|||
// Nebeneffekt: vorhanden() hat index auf einen freien Platz positioniert |
|||
else if (! vorhanden(schluessel)) { |
|||
// neu eintragen, da ! vorhanden(): |
|||
tabelle[index].schluessel = schluessel; |
|||
tabelle[index].belegt = true; |
|||
anzahl++; |
|||
} |
|||
tabelle[index].element = element; // wenn vorhanden(), dann wird überschrieben |
|||
} |
|||
public Element suchen(Schluessel schluessel) throws NichtVorhandenException { |
|||
if (! vorhanden(schluessel)) // Nebeneffekt: vorhanden() hat index positioniert |
|||
throw new NichtVorhandenException(); |
|||
else // vorhanden hat index auf den Platz von schluessel positioniert |
|||
return tabelle[index].element; |
|||
} |
|||
} |
|||
public class VollException extends Exception { } |
|||
public class NichtVorhandenException extends Exception { } |
|||
</source> |
|||
==== Quadratisches Sondieren ==== |
|||
Wie beim linearen Sondieren wird nach einem neuen freien Speicher gesucht, allerdings nicht sequenziell, sondern mit stetig quadratisch wachsender Schrittweite und in beide Richtungen. Verursacht <math>h(k)</math> eine Kollision, so werden nacheinander <math>h(k) + 1 , h(k) - 1 , h(k) + 4 , h(k) - 4 , h(k) + 9</math> usw. probiert. In Formeln ausgedrückt: |
|||
<math>h_i(x) = \left(h(x) + (-1)^{i+1} \cdot \left\lceil\frac{i}{2}\right\rceil^2\right) \bmod m</math> |
|||
Den ständigen Wechsel des Vorzeichens bei dieser Kollisionsstrategie nennt man auch „alternierendes quadratisches Sondieren“ oder „quadratisches Sondieren mit Verfeinerung“. Wählt man die Anzahl der Behälter geschickt (nämlich <math>m = 4 \cdot j + 3</math>, <math>m</math> ist [[Primzahl]]), so erzeugt jede Sondierungsfolge <math>h_0(x)</math> bis <math>h_{m-1}(x)</math> eine [[Permutation]] der Zahlen 0 bis <math>m-1</math>; so wird also sichergestellt, dass jeder Behälter getroffen wird. |
|||
Quadratisches Sondieren ergibt keine Verbesserung bei Primärkollisionen <math>h_0(x) = h_0(y)</math>, kann aber die Wahrscheinlichkeit der Bildung von längeren Ketten bei Sekundärkollisionen <math>h_0(x) = h_k(y)</math> herabsetzen, d. h. Clusterbildung wird vermieden. |
|||
==== Doppel-Hashing ==== |
|||
Beim Doppel-Hashing werden zwei unabhängige Hash-Funktionen <math>h</math> und <math>h'</math> angewandt. Diese heißen unabhängig, wenn die Wahrscheinlichkeit für eine sogenannte Doppelkollision, d. h. <math>h(x)=h(y) \and h'(x)=h'(y)</math> gleich <math>1/m^2</math> und damit minimal ist. Die Folge von Hash-Funktionen, die nun mittels <math>h</math> und <math>h'</math> gebildet werden, sieht so aus: |
|||
<math>h_i(x) = (h(x)+h'(x)\cdot i) ~ \bmod ~ m</math> |
|||
Die Kosten für diese Methode sind nahe den Kosten für ein ideales Hashing. |
|||
=== Dynamisches Hashing === |
|||
Bei steigendem Füllgrad der Tabelle steigt die Wahrscheinlichkeit von Kollisionen deutlich an. Spätestens wenn die Anzahl der indizierten Datensätze größer ist, als die Kapazität der Tabelle, werden Kollisionen unvermeidbar. Das bedeutet, dass das Verfahren einen zunehmenden Aufwand zur Kollisionslösung aufwenden muss. Um dies zu vermeiden, wird beim Dynamischen Hashing die Hashtabelle bei Bedarf vergrößert. Dies hat jedoch zwangsläufig Auswirkungen auf den Wertebereich der Hash-Funktion, der nun ebenfalls erweitert werden muss. Eine Änderung der Hash-Funktion wiederum hat jedoch den nachteiligen Effekt, dass sich ebenfalls die Hash-Werte für bereits gespeicherte Daten ändern. Für das dynamische Hashing wurde dafür eigens eine Klasse von Hash-Funktionen entwickelt, deren Wertebereich vergrößert werden kann, ohne die bereits gespeicherten Hash-Werte zu verändern. |
|||
==== Vorteile ==== |
|||
* Es gibt keine obere Grenze für das Datenvolumen |
|||
* Einträge können ohne Probleme gelöscht werden |
|||
* Adresskollisionen führen nicht zur [[Clusterbildung]]. |
|||
==== Nachteile ==== |
|||
* effektives Durchlaufen der Einträge nach einer Ordnung |
|||
* effektive Suche nach dem Eintrag mit dem kleinsten oder größten Schlüssel |
|||
== Anwendung == |
|||
Ein Anwendungsfall ist das [[Assoziatives Array|Assoziative Array]] (auch ''Map'', ''Lookup Table'', ''Dictionary'' oder ''Wörterbuch''). Das Nachschlagen der mit einem Schlüssel assoziierten Daten kann mittels einer Hashtabelle schnell und elegant implementiert werden. |
|||
Wichtig sind Hashtabellen auch für [[Datenbank]]en. Hier werden sie als Index für Tabellen verwendet. Ein sogenannter [[Hashindex]] kann unter günstigen Bedingungen zu idealen Zugriffszeiten führen. |
|||
Des Weiteren finden Hashtabellen Einsatz in praktisch jeder modernen Applikation. Hier werden sie zur Implementierung von [[Menge (Datenstruktur)|Mengen (Sets)]] oder eines [[Cache]]s verwendet. [[Symboltabelle]]n, die bei [[Compiler]]n oder [[Interpreter]]n Verwendung finden, werden meistens auch als Hashtabelle realisiert. |
|||
== Beispiel == |
|||
Bildlich kann eine Hashtabelle als ein Telefonbuch betrachtet werden. Suchschlüssel sind die Namen, nach denen gesucht werden soll. Als Hash-Funktion wird die Abbildung des Namens auf seinen Anfangsbuchstaben verwendet. Daraus leitet sich im Idealfall genau die Seite des Namens im Telefonbuch ab. Der Idealfall ist aber nur dann gegeben, wenn es zu jedem Anfangsbuchstaben genau einen Namen geben würde. Dies ist offensichtlich falsch (siehe [[Schubfachprinzip]]) und die Folge sind Kollisionen. Als Ausweg kann man versuchen eine geeignetere Hash-Funktion zu finden. |
|||
== Siehe auch == |
|||
* [[Bloomfilter]] |
|||
* [[B*-Baum]] – Schnelles und sehr robustes Indexverfahren bei Datenbanken. |
|||
* [[R*-Baum]] – Effektives Indexverfahren für mehrdimensionale Daten. |
|||
* [[Verteilte Hashtabelle]] |
|||
== Weblinks == |
|||
* [http://libhashish.sourceforge.net/ Umfangreiche Hashbibliothek mit weitreichender Analysefunktionalität] |
|||
* [http://guxx.de/2007/11/11/assoziative-arrays-als-hashmaps-selbst-verwalten/ Einfache Implementation einer Hashtabelle (mit Kollision)] |
|||
== Literatur == |
|||
* [[Thomas H. Cormen]], [[Charles E. Leiserson]], [[Ronald L. Rivest]] – ''Introduction to Algorithms''. 1184 Seiten; The MIT Press 2001, ISBN 0-262-53196-8 |
|||
* Christian Ullenboom – ''Java ist auch eine Insel'', 1444 Seiten; Galileo Press 2007, ISBN 3-89842-838-9; online als [http://www.galileocomputing.de/openbook/javainsel6/ Openbook] verfügbar. |
|||
[[Kategorie:Datenstruktur]] |
|||
[[Kategorie:Datenbankindex]] |
|||
[[Kategorie:Hash]] |
|||
[[Kategorie:Suchalgorithmus]] |
|||
[[cs:Hashovací tabulka]] |
|||
[[da:Hashtabel]] |
|||
[[en:Hash table]] |
|||
[[es:Tabla hash]] |
|||
[[fa:جدول هش]] |
|||
[[fi:Hajautustaulu]] |
|||
[[fr:Table de hachage]] |
|||
[[he:טבלת גיבוב]] |
|||
[[it:Hash table]] |
|||
[[ja:ハッシュテーブル]] |
|||
[[lt:Dėstymo lentelė]] |
|||
[[nl:Hashtabel]] |
|||
[[no:Hashtabell]] |
|||
[[pl:Tablica mieszająca]] |
|||
[[pt:Tabela hash]] |
|||
[[ru:Хеш-таблица]] |
|||
[[sk:Hašovacia tabuľka]] |
|||
[[sv:Hashtabell]] |
|||
[[th:ตารางแฮช]] |
|||
[[zh:哈希表]] |
Version vom 5. Juli 2008, 04:37 Uhr
In der Informatik bezeichnet man als Hashtabelle bzw. Streuwerttabelle eine spezielle Indexstruktur. Hashtabellen eignen sich vor allem dazu, Datenelemente in einer großen Datenmenge aufzufinden. Hashtabellen stehen dabei in Konkurrenz zu Baumstrukturen (wie etwa ein B*-Baum) und der Skip-List, die ebenfalls als Indexstruktur dienen können. Beim Einsatz einer Hashtabelle zur Suche in Datenmengen spricht man auch von einem Hashverfahren oder Streuspeicherverfahren.
Hashverfahren
Das Hashverfahren ist ein Algorithmus zum Suchen von Datenobjekten in großen Datenmengen. Es basiert auf der Idee, dass eine mathematische Funktion die Position eines Objektes in einer Tabelle berechnet. Dadurch erübrigt sich die Durchsuchung vieler Datenobjekte, bis das Zielobjekt gefunden wurde.
Der Algorithmus
Beim Hashverfahren werden die Zieldaten in einer Hashtabelle gespeichert. Eine Hashfunktion berechnet zu jedem Datenobjekt einen Hashwert, der als Index in der Tabelle verwendet wird. Zum Berechnen dieses Hashwertes wird ein Schlüssel benötigt, der dieses Objekt eindeutig identifiziert. Dieser Schlüssel wird von der Hashfunktion zum Berechnen des Hashwertes verwendet. Das Datenobjekt wird an dieser Stelle (Bucket genannt) in der Tabelle gespeichert. Im Idealfall bekommt jedes Objekt einen eigenen Bucket. Hashfunktionen müssen jedoch nicht eindeutig sein, so dass sich in der Praxis mehrere Objekte einen Bucket teilen müssen. Diesen Fall nennt man Kollision, er benötigt eine spezielle Behandlung durch das Verfahren.
Bei einer Suche in der Hashtabelle wird nun ähnlich vorgegangen. Zunächst wird aus einem Suchschlüssel wieder ein Hashwert berechnet, der den Bucket des gesuchten Datenobjektes bestimmt. Befinden sich im Bucket mehrere Objekte, muss jetzt nur noch durch direkten Vergleich des Suchschlüssels mit den Objekten das gesuchte Ziel bestimmt werden.
In der Praxis wird die Tabelle als ein Array implementiert. Wegen der möglichen Kollisionen werden die Daten aber nicht direkt im Array gespeichert, sondern lediglich die Verweise auf den zugehörigen Bucket. Buckets können nun selbst wiederum so organisiert sein, um eine schnelle Suche der im Bucket enthaltenen Objekte zu ermöglichen.
Ist die Hashtabelle ausgewogen, befinden sich nur wenige Datensätze in einem Bucket und der letzte Schritt hat einen geringen Aufwand. Im Worst-Case können Kollisionen zu einer Entartung der Hashtabelle führen, wenn wenige Buckets sehr viele Objekte enthalten, während andere Buckets leer bleiben.
Hashtabellen ermöglichen so eine sehr schnelle Suche in großen Datenmengen, da mit der Berechnung des Hashwertes in einem einzigen Schritt die Anzahl der möglichen Zielobjekte eingeschränkt wird. Damit gehören Hashtabellen zu den effizientesten Indexstrukturen. Ein großer Nachteil ist jedoch die Gefahr der Entartung durch Kollisionen, die bei einem stetigen Wachstum der Datenmenge unausweichlich sind. Siehe dazu Kollision. Daher werden in Datenbanksystemen häufig Hashtabellen zu Gunsten von B*-Bäumen vermieden. B*-Bäume sind robust gegen Entarten und müssen nicht (oder nur lokal) restrukturiert werden.
Kollisionen
Da Hash-Funktionen im Allgemeinen nicht injektiv sind, können zwei unterschiedliche Schlüssel zum selben Hash-Wert, also zum selben Feld in der Tabelle führen. Dieses Ereignis wird als Kollision bezeichnet. In diesem Fall muss die Hashtabelle mehrere Werte an derselben Stelle aufnehmen. Um dieses Problem zu handhaben, gibt es diverse Kollisionsauflösungsstrategien.
Eine Möglichkeit wird offenes Hashing genannt. Wenn dabei ein Eintrag an eine schon belegte Stelle in der Tabelle abgelegt werden soll, wird stattdessen eine andere freie Stelle genommen. Es wird häufig zwischen drei Varianten unterschieden:
- lineares Sondieren – es wird um ein konstantes Intervall verschoben nach einer freien Stelle gesucht. Meistens wird die Intervallgröße auf 1 festgelegt.
- quadratisches Sondieren – Nach jedem erfolglosem Suchschritt wird das Intervall quadriert.
- doppeltes Hashen – eine weitere Hash-Funktion liefert das Intervall.
Eine weitere Möglichkeit ist Kollisionsauflösung durch Verkettung. Anstelle der gesuchten Daten enthält die Hashtabelle hier Behälter (Buckets), die alle Daten mit gleichem Hash-Wert aufnehmen. Bei einer Suche wird also zunächst der richtige Zielbehälter gesucht. Damit wird die Menge der möglichen Ziele erheblich eingeschränkt. Dennoch müssen abschließend die verbliebenen Elemente im Behälter durchsucht werden. Im schlimmsten Fall (Worst Case) kann es passieren, dass alle Elemente gleiche Hash-Werte haben und damit im gleichen Bucket abgelegt werden. In der Praxis kann das aber durch die Wahl einer geeigneten Größe für die Hashtabelle sowie einer geeigneten Hash-Funktion vermieden werden. Oft wird die Verkettung durch eine lineare Liste pro Behälter realisiert.
Vorteile
Trotz der Schwierigkeiten, die sich beispielsweise aus der Kollisionsbehandlung ergeben, bieten Hashtabellen wegen der Vorzüge beim sofortigen Zugriff durch den Hash-Wert auf die Inhalte in einer Tabelle im Vergleich zu der Suche nach dem Schlüssel und wegen der im Idealfall fehlenden Beziehung zweier Hash-Werte, die aus ähnlichen Schlüsseln berechnet wurden, oft Vorteile in der Zugriffszeit als auch im benötigten Speicherplatz gegenüber gewöhnlichen Arrays.
Nachteile
Hat die Tabelle einen gewissen Füllgrad überschritten, wird sie zwangsläufig entarten. Dann kann nur eine Vergrößerung der Tabelle mit nachfolgender Restrukturierung wieder zu akzeptablem Laufzeitverhalten führen.
Um den Nachteil der Nicht-Injektivität zu konterkarieren, bietet es sich an, die Funktion für den Schlüsselgenerator ein weiteres Mal aufzurufen. Schlüsselgeneratorfunktionen sind höchst performant.
Komplexität
Wurden Hash-Funktion und Größe der Hashtabelle geeignet gewählt, ist der Aufwand für die Suche in der Tabelle (Look-Up) O(1). Wegen der möglichen Kollisionen hat eine Hashtabelle allerdings im so genannten Worst-Case ein sehr viel schlechteres Verhalten. Dieser wird mit O(n) abgeschätzt, wobei das n der Anzahl der in der Hashtabelle gespeicherten Einträge entspricht. Es werden dabei also alle Einträge in der Tabelle durchsucht.
Varianten des Hashverfahrens
Es gibt mehrere Varianten des Hashverfahrens, die sich für bestimmte Daten besser eignen. Ein wichtiger Faktor hierbei ist die Dynamik, mit der sich die Anzahl der Elemente ändert. Das offene Hashing löst dieses Problem, nimmt aber Einbußen bei den Zugriffszeiten in Kauf. Das geschlossene Hashing ist hingegen auf explizite Strategien zur Kollisionsbehandlung angewiesen. Vorsicht: Die Bezeichnungen „offenes“ bzw. „geschlossenes Hashing“ werden auch in genau umgekehrter Bedeutung verwendet.
Hashing mit Verkettung
Beim Hashing mit Verkettung (engl. Separate Chaining) ist die Hash-Tabelle so strukturiert, dass jeder Behälter eine dynamische Datenstruktur aufnehmen kann – beispielsweise eine Liste oder einen Baum. Jeder Schlüssel wird dann in dieser Datenstruktur eingetragen oder gesucht. So ist es problemlos möglich, mehrere Schlüssel in einem Behälter abzulegen, was allerdings zu mehr oder weniger verlängerten Zugriffszeiten führt. Die Effizienz des Zugriffs wird dabei davon bestimmt, wie schnell Datensätze in die gewählte Datenstruktur eingefügt und darin wiedergefunden werden können. Hashing mit Verkettung ist bei Datenbanken eine sehr gängige Indizierungsvariante, wobei sehr große Datenmengen mittels Hashtabellen indiziert werden. Die Größe der Buckets ist in Datenbanksystemen ein Vielfaches der Sektorengröße des Speichermediums. Der Grund dafür ist, dass die Datenmenge nicht mehr im Hauptspeicher gehalten werden kann. Bei einer Suchanfrage muss das Datenbanksystem die Buckets sektorenweise einlesen.
Hashing mit offener Adressierung
Dieses Verfahren wird abgekürzt auch offenes Hashing, bezogen auf die offene Adressierung, aber auch geschlossenes Hashing, bezogen auf die begrenzte Anzahl möglicher Schlüssel im Behälter, genannt.
Beim Hashing mit offener Adressierung kann jedem Behälter nur eine feste Anzahl von Schlüsseln zugewiesen werden. Häufig wählt man einfach einen einzigen möglichen Schlüssel pro Behälter. Im Kollisionsfall muss dann nach einem alternativen Behälter gesucht werden. Dabei geht man so vor, dass man für Behälter eine ganze Folge von Hash-Funktionen definiert. Führt die Anwendung der ersten Hash-Funktion, nennen wir sie , zu einer Kollision, so wendet man an. Führt diese ebenfalls zu einer Kollision (d. h. der entsprechende Behälter ist bereits belegt), so wendet man an, und so weiter bis . Die Bezeichnung „offene Adressierung“ ergibt sich aus der Eigenschaft, dass durch Kollisionen gleiche Schlüssel unterschiedliche Adressen zugewiesen bekommen können.
Lineares Sondieren
Die einfachste Möglichkeit zur Definition einer solchen Folge besteht darin, so lange den jeweils nächsten Behälter zu prüfen, bis man auf einen freien Behälter trifft. Die Definition der Folge von Hash-Funktionen sieht dann so aus:
Die Anwendung des Modulo hat mit der begrenzten Zahl von Behältern zu tun: Wurde der letzte Behälter geprüft, so beginnt man wieder beim ersten Behälter. Das Problem dieser Methode ist, dass sich so schnell Ketten oder Cluster bilden und die Zugriffszeiten im Bereich solcher Ketten schnell ansteigen. Das lineare Sondieren ist daher wenig effizient.
Implementierung einer Hashtabelle in Java 5
/** Implementierung einer Hashtabelle mit linearer Sondierung
Autor: Andreas Solymosi, 2007
Quelle: Solymosi/Grude: Grundkurs Algorithmen und Datenstrukturen,
Vieweg Verlag, ISBN 3-528-05743-2
*/
public class HashTabelle<Element, Schluessel extends Comparable<Schluessel>> {
// Tabelleneintrag
private class Eintrag<E, S extends Comparable < S > > {
E element;
S schluessel;
boolean belegt;
}
private Eintrag<Element, Schluessel>[] tabelle; // die eigentliche Hashtabelle
private int anzahl; // der eingetragenen Elemente, anfänglich 0
public HashTabelle(int groesse) {
tabelle = new Eintrag[groesse];
for (int i = 0; i < tabelle.length; i++) {
tabelle[i] = new Eintrag<Element, Schluessel>();
tabelle[i].belegt = false;
}
anzahl = 0;
}
// von vorhanden beschrieben (Nebeneffekt!); von eintragen, suchen und vorhanden gelesen
private int index;
public boolean vorhanden(Schluessel schluessel) {
index = schluessel.hashCode() % tabelle.length; // Nebeneffekt!
while (tabelle[index].belegt && schluessel != tabelle[index].schluessel)
index = (index + 1) % tabelle.length; // lineare Sondierungsfunktion
// Platz gefunden:
// index zeigt auf den Platz, wo der letzte Schluessel steht
if (schluessel == tabelle[index].schluessel)
return true;
else // ! inhalt[index].belegt, index zeigt auf einen freien Platz
return false;
}
// ein Element mit vorhandenem Schlüssel wird überschrieben (kein Doppeleintrag)
public void eintragen(Schluessel schluessel, Element element) throws VollException {
if (anzahl == tabelle.length)
throw new VollException();
// Nebeneffekt: vorhanden() hat index auf einen freien Platz positioniert
else if (! vorhanden(schluessel)) {
// neu eintragen, da ! vorhanden():
tabelle[index].schluessel = schluessel;
tabelle[index].belegt = true;
anzahl++;
}
tabelle[index].element = element; // wenn vorhanden(), dann wird überschrieben
}
public Element suchen(Schluessel schluessel) throws NichtVorhandenException {
if (! vorhanden(schluessel)) // Nebeneffekt: vorhanden() hat index positioniert
throw new NichtVorhandenException();
else // vorhanden hat index auf den Platz von schluessel positioniert
return tabelle[index].element;
}
}
public class VollException extends Exception { }
public class NichtVorhandenException extends Exception { }
Quadratisches Sondieren
Wie beim linearen Sondieren wird nach einem neuen freien Speicher gesucht, allerdings nicht sequenziell, sondern mit stetig quadratisch wachsender Schrittweite und in beide Richtungen. Verursacht eine Kollision, so werden nacheinander usw. probiert. In Formeln ausgedrückt:
Den ständigen Wechsel des Vorzeichens bei dieser Kollisionsstrategie nennt man auch „alternierendes quadratisches Sondieren“ oder „quadratisches Sondieren mit Verfeinerung“. Wählt man die Anzahl der Behälter geschickt (nämlich , ist Primzahl), so erzeugt jede Sondierungsfolge bis eine Permutation der Zahlen 0 bis ; so wird also sichergestellt, dass jeder Behälter getroffen wird.
Quadratisches Sondieren ergibt keine Verbesserung bei Primärkollisionen , kann aber die Wahrscheinlichkeit der Bildung von längeren Ketten bei Sekundärkollisionen herabsetzen, d. h. Clusterbildung wird vermieden.
Doppel-Hashing
Beim Doppel-Hashing werden zwei unabhängige Hash-Funktionen und angewandt. Diese heißen unabhängig, wenn die Wahrscheinlichkeit für eine sogenannte Doppelkollision, d. h. gleich und damit minimal ist. Die Folge von Hash-Funktionen, die nun mittels und gebildet werden, sieht so aus:
Die Kosten für diese Methode sind nahe den Kosten für ein ideales Hashing.
Dynamisches Hashing
Bei steigendem Füllgrad der Tabelle steigt die Wahrscheinlichkeit von Kollisionen deutlich an. Spätestens wenn die Anzahl der indizierten Datensätze größer ist, als die Kapazität der Tabelle, werden Kollisionen unvermeidbar. Das bedeutet, dass das Verfahren einen zunehmenden Aufwand zur Kollisionslösung aufwenden muss. Um dies zu vermeiden, wird beim Dynamischen Hashing die Hashtabelle bei Bedarf vergrößert. Dies hat jedoch zwangsläufig Auswirkungen auf den Wertebereich der Hash-Funktion, der nun ebenfalls erweitert werden muss. Eine Änderung der Hash-Funktion wiederum hat jedoch den nachteiligen Effekt, dass sich ebenfalls die Hash-Werte für bereits gespeicherte Daten ändern. Für das dynamische Hashing wurde dafür eigens eine Klasse von Hash-Funktionen entwickelt, deren Wertebereich vergrößert werden kann, ohne die bereits gespeicherten Hash-Werte zu verändern.
Vorteile
- Es gibt keine obere Grenze für das Datenvolumen
- Einträge können ohne Probleme gelöscht werden
- Adresskollisionen führen nicht zur Clusterbildung.
Nachteile
- effektives Durchlaufen der Einträge nach einer Ordnung
- effektive Suche nach dem Eintrag mit dem kleinsten oder größten Schlüssel
Anwendung
Ein Anwendungsfall ist das Assoziative Array (auch Map, Lookup Table, Dictionary oder Wörterbuch). Das Nachschlagen der mit einem Schlüssel assoziierten Daten kann mittels einer Hashtabelle schnell und elegant implementiert werden.
Wichtig sind Hashtabellen auch für Datenbanken. Hier werden sie als Index für Tabellen verwendet. Ein sogenannter Hashindex kann unter günstigen Bedingungen zu idealen Zugriffszeiten führen.
Des Weiteren finden Hashtabellen Einsatz in praktisch jeder modernen Applikation. Hier werden sie zur Implementierung von Mengen (Sets) oder eines Caches verwendet. Symboltabellen, die bei Compilern oder Interpretern Verwendung finden, werden meistens auch als Hashtabelle realisiert.
Beispiel
Bildlich kann eine Hashtabelle als ein Telefonbuch betrachtet werden. Suchschlüssel sind die Namen, nach denen gesucht werden soll. Als Hash-Funktion wird die Abbildung des Namens auf seinen Anfangsbuchstaben verwendet. Daraus leitet sich im Idealfall genau die Seite des Namens im Telefonbuch ab. Der Idealfall ist aber nur dann gegeben, wenn es zu jedem Anfangsbuchstaben genau einen Namen geben würde. Dies ist offensichtlich falsch (siehe Schubfachprinzip) und die Folge sind Kollisionen. Als Ausweg kann man versuchen eine geeignetere Hash-Funktion zu finden.
Siehe auch
- Bloomfilter
- B*-Baum – Schnelles und sehr robustes Indexverfahren bei Datenbanken.
- R*-Baum – Effektives Indexverfahren für mehrdimensionale Daten.
- Verteilte Hashtabelle
Weblinks
- Umfangreiche Hashbibliothek mit weitreichender Analysefunktionalität
- Einfache Implementation einer Hashtabelle (mit Kollision)
Literatur
- Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest – Introduction to Algorithms. 1184 Seiten; The MIT Press 2001, ISBN 0-262-53196-8
- Christian Ullenboom – Java ist auch eine Insel, 1444 Seiten; Galileo Press 2007, ISBN 3-89842-838-9; online als Openbook verfügbar.