Unterschiede
Hier werden die Unterschiede zwischen zwei Versionen der Seite angezeigt.
Beide Seiten, vorherige Überarbeitung Vorherige Überarbeitung Nächste Überarbeitung | Vorherige Überarbeitung | ||
gf_informatik:daten:processing:dictionaries_tutorial [2023-05-29 09:25] – [Variante 3 - Hash-Map] hof | gf_informatik:daten:processing:dictionaries_tutorial [2024-06-10 10:53] (aktuell) – hof | ||
---|---|---|---|
Zeile 8: | Zeile 8: | ||
* `insert(key, | * `insert(key, | ||
* `remove(key)`: | * `remove(key)`: | ||
- | |||
### Variante 1: Tupel-Liste | ### Variante 1: Tupel-Liste | ||
Zeile 14: | Zeile 13: | ||
Eine einfache Möglichkeit, | Eine einfache Möglichkeit, | ||
- | Wenn wir möchten, dass unsere | + | Wenn wir möchten, dass unsere |
* Lookup: '' | * Lookup: '' | ||
* Insert: `__setitem__(key, | * Insert: `__setitem__(key, | ||
* Remove: `__delitem__(key)` | * Remove: `__delitem__(key)` | ||
+ | * Optional: `__iter__` um den `for`-Loop über die Keys zu unterstützen. | ||
Deren Spezifikation besagt, dass Lookup und Remove einen `KeyError` auslösen sollen, wenn der Schlüssel nicht existiert. | Deren Spezifikation besagt, dass Lookup und Remove einen `KeyError` auslösen sollen, wenn der Schlüssel nicht existiert. | ||
<code python> | <code python> | ||
- | class TupelList: | + | class TupleList: |
""" | """ | ||
def __init__(self): | def __init__(self): | ||
Zeile 48: | Zeile 48: | ||
# add new mapping | # add new mapping | ||
self._tuples.append((key, | self._tuples.append((key, | ||
+ | |||
+ | def __len__(self): | ||
+ | return len(self._tuples) | ||
+ | |||
+ | def __iter__(self): | ||
+ | for k, _ in self._tuples: | ||
+ | yield k | ||
| | ||
- | map = TupelList() # calls __init__ | + | map = TupleList() # calls __init__ |
map[' | map[' | ||
map[' | map[' | ||
Zeile 57: | Zeile 64: | ||
print(map[' | print(map[' | ||
</ | </ | ||
+ | |||
+ | |||
#### Laufzeit-Messung | #### Laufzeit-Messung | ||
Zeile 79: | Zeile 88: | ||
print(f" | print(f" | ||
- | perf_test(TupelList) | + | perf_test(TupleList) |
</ | </ | ||
Output: | Output: | ||
< | < | ||
- | TupelList: N= | + | TupleList: N= |
- | TupelList: N= 1000 Time per Op: 2.9e-05 | + | TupleList: N= 1000 Time per Op: 2.9e-05 |
- | TupelList: N= 10000 Time per Op: 2.5e-04 | + | TupleList: N= 10000 Time per Op: 2.5e-04 |
</ | </ | ||
- | Die Zeit, um eine einzelne Zuordnung abzurufen wächst also linear mit der Anzahl Zuordnungen - wir sagen auch, dass die Lookup-Operation eine Laufzeit-Komplexität von $\mathcal{O}(n)$ hat. | + | Die Zeit, um eine einzelne Zuordnung abzurufen wächst also linear mit der Anzahl Zuordnungen - wir sagen auch, dass die Lookup-Operation eine Laufzeit-Komplexität von $O(n)$ hat. |
#### Aufgabe 1 - Zeitmessung | #### Aufgabe 1 - Zeitmessung | ||
Zeile 99: | Zeile 108: | ||
Falls die verwendeten Schlüssel miteinander vergleichbar sind, könnten wir die Tupel-Liste ja sortieren, der Zugriff sollte dementsprechend schneller sein. Mathematiker sprechen auch davon, dass eine [[wpde> | Falls die verwendeten Schlüssel miteinander vergleichbar sind, könnten wir die Tupel-Liste ja sortieren, der Zugriff sollte dementsprechend schneller sein. Mathematiker sprechen auch davon, dass eine [[wpde> | ||
+ | |||
#### Aufgabe 2 - Sortierte Tupel-Liste | #### Aufgabe 2 - Sortierte Tupel-Liste | ||
- | Implementiere eine `class | + | Implementiere eine `class |
- | * Wir können von `TupelList` erben: `class | + | * Wir können von `TupleList` erben: `class |
* Die private Methode `_finditem(key)` muss angepasst werden um die Binärsuche zu implementieren. | * Die private Methode `_finditem(key)` muss angepasst werden um die Binärsuche zu implementieren. | ||
* In `__getitem__` und `__delitem__` bleiben sich gleich und müssen nicht angepasst werden. | * In `__getitem__` und `__delitem__` bleiben sich gleich und müssen nicht angepasst werden. | ||
Zeile 111: | Zeile 121: | ||
++++Lösung: | ++++Lösung: | ||
<code python> | <code python> | ||
- | class SortedTupelList(TupelList): | + | class SortedTupleList(TupleList): |
""" | """ | ||
# Override _finditem to use binary search | # Override _finditem to use binary search | ||
Zeile 148: | Zeile 158: | ||
Können wir noch schneller sein als Binärsuche? | Können wir noch schneller sein als Binärsuche? | ||
- | Binärsuche ist sehr viel schneller als lineare Suche: jedes Mal wenn wir die Anzahl Einträge | + | Binärsuche ist sehr viel schneller als lineare Suche: jedes Mal, wenn wir die Anzahl Einträge |
- | Allerdings wissen wir auch, dass der eigentliche Zugriff auf die zugrundeliegende Liste nicht von deren Grösse abhängt. Wenn wir den richtigen Index kennen würden, könnten wir den Zugriff in konstanter Zeit (oder $\mathcal{O}(1)$) schaffen. Dies wird mit einer Hashmap erreicht, indem der Index aus dem Key berechnet wird. Dazu wird für jedes Key-Objekt ein _Hashwert_ (eine Ganzzahl) berechnet. Aus dem Hashwert wird der Index in der Tupel-Liste mittels `hash % len(self.tuples)` berechnet. | + | #### Hashing |
+ | |||
+ | Allerdings wissen wir auch, dass der eigentliche Zugriff auf die zugrundeliegende Liste nicht von deren Grösse abhängt. Wenn wir den richtigen Index kennen würden, könnten wir den Zugriff in konstanter Zeit (oder $O(1)$) schaffen. Dies wird mit einer Hashmap erreicht, indem der Index aus dem Key berechnet wird. Dazu wird für jedes Key-Objekt ein _Hashwert_ (eine Ganzzahl) berechnet. Aus dem Hashwert wird der Index in der Tupel-Liste mittels `hash % len(self.tuples)` berechnet. | ||
In Python können wir die eingebaute `hash(object)` Funktion verwenden, um für jedes Objekt einen Hashwert zu erhalten. Für die meisten Objekte ist dies ein Wert, der aus der internen Speicheradresse abgeleitet wird; für Typen, die `__eq__` implementieren, | In Python können wir die eingebaute `hash(object)` Funktion verwenden, um für jedes Objekt einen Hashwert zu erhalten. Für die meisten Objekte ist dies ein Wert, der aus der internen Speicheradresse abgeleitet wird; für Typen, die `__eq__` implementieren, | ||
- | Natürlich kann es vorkommen, dass zwei unterschiedliche Keys denselben | + | #### Kollisionen |
+ | |||
+ | Natürlich kann es vorkommen, dass zwei unterschiedliche Keys denselben | ||
+ | |||
+ | * Probing: In der Liste weitersuchen nach dem nächsten freien Slot. | ||
+ | * Dies führt zu _Clustering_: | ||
+ | * Bei `Remove` müssen wir sicherstellen, | ||
+ | * Chaining: Jedes Listenelement ist eine weitere Liste, die wir nach dem richtigen Key durchsuchen. | ||
+ | * Solange die Kollisionsrate tief ist, kostet die lineare Suche kaum Zeit. | ||
+ | * Es bietet sich an, gleich eine `TupleList` von oben zu verwenden - oder `SortedTupleList`, | ||
+ | |||
+ | Um die Anzahl Kollisionen tief zu halten ist es wichtig, dass die Liste nie ganz gefüllt ist - die Tupel-Liste wird also bewusst grösser gewählt als nötig und bei Bedarf ein _rehash_ ausgeführt: | ||
- | * Probing: In der Liste weiterzusuchen nach dem nächsten freien Slot. | ||
- | * ... aber dann müssen wir bei `Remove` sicherstellen, | ||
- | * Chaining: Jedes Listenelement ist eine simple Tupel-Liste, | ||
- | Um die Anzahl Kollisionen tief zu halten ist es wichtig, dass die Liste nie ganz gefüllt ist - die Tupel-Liste wird also bewusst grösser gewählt als nötig und bei Bedarf ein _rehash_ ausgeführt: | ||
- | . | ||
#### Aufgabe 3 - HashMap | #### Aufgabe 3 - HashMap | ||
Implementiere eine `class HashMap`. | Implementiere eine `class HashMap`. | ||
- | < | + | Hinweise: |
- | ++++Lösung:| | + | * wir können `_tuples` und `__getitem__` wieder von `TupleList` erben. |
+ | |||
+ | < | ||
+ | ++++Mit linear Probing:| | ||
<code python> | <code python> | ||
- | class HashMap(TupelList): | + | class HashMap(TupleList): |
def __init__(self): | def __init__(self): | ||
- | self._tuples = [None] * 10 # The tuples | + | self._tuples = [None] * 10 # The entries |
self._len = 0 # The number of elements we contain. | self._len = 0 # The number of elements we contain. | ||
def __len__(self): | def __len__(self): | ||
- | return | + | return |
+ | |||
+ | def __iter__(self): | ||
+ | for entry in self._tuples: | ||
+ | if entry: | ||
+ | yield entry[0] | ||
def _hash(self, key): | def _hash(self, key): | ||
Zeile 183: | Zeile 209: | ||
def _finditem(self, | def _finditem(self, | ||
- | """ | + | """ |
- | the index represents | + | the index where key were to be inserted, or raises an error.""" |
h = self._index(key) | h = self._index(key) | ||
- | # Find the first matching or empty slot using linear probing | + | # Find the first matching or empty slot using linear probing. |
entry = self._tuples[h] | entry = self._tuples[h] | ||
while entry and entry[0] != key: | while entry and entry[0] != key: | ||
Zeile 195: | Zeile 221: | ||
raise KeyError(key) | raise KeyError(key) | ||
return h | return h | ||
- | + | ||
# Any mappings that were shifted due to collision need to be moved to the gap. | # Any mappings that were shifted due to collision need to be moved to the gap. | ||
def __delitem__(self, | def __delitem__(self, | ||
Zeile 202: | Zeile 228: | ||
self._tuples[index] = None | self._tuples[index] = None | ||
self._len -= 1 | self._len -= 1 | ||
- | self.clean_up_after_delete(index) | + | self._clean_up_after_delete(index) |
return v | return v | ||
- | def clean_up_after_delete(self, index): | + | def _clean_up_after_delete(self, index): |
""" | """ | ||
- | moved due to collisions are """ | + | moved due to collisions are moved forward until we find another gap.""" |
probe = index + 1 | probe = index + 1 | ||
while True: | while True: | ||
Zeile 230: | Zeile 256: | ||
def _attempt_rehash(self): | def _attempt_rehash(self): | ||
+ | """ | ||
# Rehash if fullness is more than 50% | # Rehash if fullness is more than 50% | ||
if self._len > len(self._tuples) * 0.5: | if self._len > len(self._tuples) * 0.5: | ||
Zeile 240: | Zeile 267: | ||
index = self._finditem(entry[0], | index = self._finditem(entry[0], | ||
self._tuples[index] = entry | self._tuples[index] = entry | ||
+ | </ | ||
+ | ++++ | ||
+ | |||
+ | ++++Mit Chaining:| | ||
+ | <code python> | ||
+ | class ChainingHashMap: | ||
+ | """ | ||
+ | def __init__(self): | ||
+ | self._tuples = [None] * 10 # The entries, either None or a linear TupleList. | ||
+ | self._len = 0 # The number of elements we contain. | ||
+ | |||
+ | def __len__(self): | ||
+ | return self._len | ||
+ | |||
+ | def __iter__(self): | ||
+ | for entry in self._tuples: | ||
+ | if entry: | ||
+ | yield from entry | ||
+ | |||
+ | def _hash(self, key): | ||
+ | return hash(key) | ||
+ | | ||
+ | def _finditem(self, | ||
+ | """ | ||
+ | If raise_on_notfound is False, an empty TupleList is inserted and returned.""" | ||
+ | h = self._hash(key) % len(self._tuples) | ||
+ | |||
+ | # Find the first matching or empty slot | ||
+ | entry = self._tuples[h] | ||
+ | if entry is None: | ||
+ | if raise_on_notfound: | ||
+ | raise KeyError(key) | ||
+ | # create a new sublist | ||
+ | entry = TupleList() | ||
+ | self._tuples[h] = entry | ||
+ | return entry | ||
+ | |||
+ | def __getitem__(self, | ||
+ | return self._finditem(key)[key] | ||
+ | |||
+ | def __delitem__(self, | ||
+ | return self._finditem(key).__delitem__(key) | ||
+ | |||
+ | def __setitem__(self, | ||
+ | entry = self._finditem(key, | ||
+ | is_addition = key not in entry | ||
+ | entry[key] = value | ||
+ | |||
+ | if is_addition: | ||
+ | self._len += 1 | ||
+ | self._attempt_rehash() | ||
+ | |||
+ | def _attempt_rehash(self): | ||
+ | """ | ||
+ | # Rehash if fullness is more than 50% | ||
+ | if self._len > len(self._tuples) * 0.5: | ||
+ | old_tuples = self._tuples | ||
+ | | ||
+ | # Re-insert all elements into twice as large list | ||
+ | self._tuples = [None] * (len(old_tuples) * 2) # Double the size! | ||
+ | for entry in old_tuples: | ||
+ | if entry: | ||
+ | for key in entry: | ||
+ | self._finditem(key, | ||
</ | </ | ||
++++ | ++++ | ||
</ | </ |