Iteration versus Rekursion
Hashtabelle mit linearer Verkettung
Pessimistischer Ansatz: Kollisionen treten häufig auf, deshalb wird unter jedem Hashindex gleich eine Liste angelegt, in der Einträge aufgenommen werden können.
Implementation in Python
HashNode ist eine Hilfsdatenstruktur, die Schlüssel und Wert speichert und mit hilfe von next eine verkettete Liste darstellt
class HashNode: def __init__(self,key,data,next): self.key = key self.data = data self.next = next #Verkettung!
Die eigentliche Hashtabelle wir in der Klasse HashTable implementier:
class HashTable: def __init__(self): self.capacity = ... #Geigneter Wert self.size = 0 self.array = [None]*self.capacity
In Python wird der Zugriffsoperator [ ] für eine Datenstruktur wie folgt (innerhalb einer Klasse) implementiert:
def __setitem__(self, key, value)
So dass im Programmtext dann folgender Syntax möglich ist: a[key] = value
Genauso wir die Zuweisung value=a[key] wie folgt umgesetzt
def __getitem__(self, key, value)
Implementierung der __setitem__ Funktionen in der HashTable Klasse:
def __setitme__(self, key, value): index = hash(key) % self.capacity node = self.array[index] while node is not None: if node.key == key: #Element key ist schon in der Tabelle #Überschreibe die Daten mit dem neuen Wert node.data = value return #Kollision des Hashwerts, probiere nächsten Key aus node = node.next #Kein Element hatte den richtigen Schlüssel. #==>Es gibt diesen Schlüssel noch nicht #Füge also ein neues Element in die Hashtabelle ein self.array[index] = HashNode(key, value, self.array[index]) #Der alte Anfang der List wurde der Nachfolger vom neue eingefügten #ersten Element size+=1
Und die Implementierung der __getitem__ Funktionen:
def __getItem__(self, key): index = hash(key) % self.capacity node = self.array[index] while node is not None: if node.key == key #Gefunden! return Node.data node = node.next raise KeyError(key)
Hashtabelle mit offener Adressierung (offenes Hashing)
Optimistischer Ansatz: Kollisionen werden nicht so häufig auftreten.
Idee
Wenn array[index] durch Kollision bereits vergeben ist, probiere einen anderen Index aus.
- Das Array enthält pro Element höchstens ein (key,value)-Paar
- Das Array muss stets mindestens einen freien Platz haben (sonst gäbe es eine Endlosschleife). Es gilt immer self.size < self.capacity. Dies galt für die vorige Hash Implementation nicht.
Vorgehen bei Kollisionen
Sequentielles Suchen
Probiere den nächsten Index: index = index+1 % capacity
- Vorteil: einfach
- Nachteil: Clusterbildung
Clusterbildung heißt, dass sich größere zusammenhängende Bereiche bilden die belegt sind, unterbrochen von Bereichen die komplett frei sind. Beim Versuche des Einfügens eines Elements an einen Platz, der schon belegt ist, muss jetzt das ganze Cluster sequentiell durchlaufen werden, bis ein freier Platz gefunden wird. Damit entspricht die Komplexität der Suche der mittleren Länge der belegten Bereiche, was sich entsprechend in einer langsamen Suche widerspiegelt.
Doppeltes Hashing
Bestimme einen neuen Index (bei Kollisionen) durch eine 2. Hashfunktion.
Das doppelte Hashing wird typischerweise in der Praxis angewendet und liegt auch der Python Implementierung des Datentyps Dictionary (Syntax {'a':1, 'b':2, 'c':3} zugrunde.
Eine effiziente Implementierung dieses Datentyps ist für die Performance der Skriptsprache Python extrem wichtig, da z.B. beim Aufruf einer Funktion der auszuführunde Code in einem Dictionary unter dem Schlüssel Funktionsname nachgeschlagen wird oder die Werte lokaler Variablen innerhalb einer Funktion ebenfalls in einem Dictionary zu finden sind.
Für die Implementierung in Python werden wieder die obigen Klassen HashNode und HashTable benötigt, es folgen die angepassten Implementationen von __setitem und __getitem__:
def __setitem__(self, key, value): h = hash(key) index = h % self.capacity while True: if self.array[index] is None or self.array[index].key is None #das Feld ist frei (1. Abfrage) #oder das feld ist als frei markiert (2. Abfrage) self.array[index] = HashNode(key, value) self.size +=1 return if self.array[index].key == key: #Es gibt diesen Schlüssel schon, #überschreibe die Daten self.array[index].data = value #Letzter Fall: Kollision index = (5*index+1+h) % self.capacity h = h >> 5
Die vorgestellte Implementierung orientiert sich an Pythons interner Dictionary Implementierung, der zugehörige Quelltext mit ausführlichem Kommentar) findet sich unter dictobject.c Python Implementation (SVN)
Komplexität des offenen Hashings
- Annahme: uniformes Hashing, das heißt alle Indizes haben gleiche Wahrscheinlichkeit
- Füllstand <math>\alpha = \frac{\text{size}}{\text{capacity}}</math>
- Erfolglose Suche (d.h. es wird entweder ein neues Element eingefügt oder ein keyError geworfen): Untere Schranke für die Komplexität ist <math>\Omega\left(\frac{1}{1-\alpha}\right)</math> Schritte bzw. Anzahl von neuen index Berechnungen.
- Erfolgreiche Suche <math>\Omega\left(\frac{1}{\alpha}\ln\left(\frac{1}{1-\alpha}\right)\right)</math>
<math>\alpha</math> | 0.5 | 0.9 |
---|---|---|
erfolglos | 2.0 | 10 |
erfolgreich | 1.4 | 2.6 |