So, guten Morgen. Können Sie mich hören?
Ja, okay, prima. Wir sind im Kapitel Datenstrukturen
. Wir haben uns in der letzten Vorlesung mit Bäumen beschäftigt
und mit der Fragestellung, wie wir Bäume balancieren können.
Das war zugegebenermaßen ein recht anstrengendes Thema. Also zu erreichen,
dass diese Bäume tatsächlich nur luggerithmische Tiefe haben,
egal in welcher Reihenfolge wir Objekte einfügen und löschen.
Das war relativ schwierig, aber wir haben gesehen, dass das möglich ist.
Also wir konnten mit einer Schranke von zweimal Log-in,
wenn Endknoten enthalten sind, mit Rot-Schwarzbäumen erreichen,
dass wir die Objekte eben einfügen und dann entsprechend wiederfinden können.
Okay, es war ein Beispiel für balancierte Suchbäume und zu Bäumen
könnte man eine ganze Vorlesung halten.
Es sollte Ihnen hier nur einen Einstieg geben, wie sowas gemacht wird.
Das ist auch, wie gesagt, nur eine ganz elementare Datenstruktur darin.
Ja, also es gibt noch viele weitere Möglichkeiten,
das zu machen und sich damit zu beschäftigen.
Wir werden das hier nicht tun, weil diese Vorlesung ist ja
quasi die Grundlagenvorlesung und wir werden lieber noch andere Themen anreißen.
Und wir werden von den Suchbäumen jetzt voranschreiten zu einer anderen Technik,
zu der zweiten wichtigen Technik, um große Datenmengen wie Wörterbücher zu speichern.
Und das ist das Hashing. Ja, mit dem werden wir uns jetzt heute auseinandersetzen. So,
wenn man mal, wenn Sie sich zurückerinnern an das Kapitel,
wo wir uns mit Such, mit Sortieren auseinandergesetzt haben,
da haben wir dann erst die ganzen vergleichenden Sortieralgorithmen gesehen und haben dann schon gedacht,
wir wären fertig, weil wir die untere Schranke hatten und gesagt,
okay, besser geht es nicht und haben dann rausgefunden,
es geht vielleicht doch besser,
wenn wir keine Vergleichsoperationen verwenden
Ja, und dann konnten wir mit,
konnten wir entsprechend durch eine gezieltes Zuordnen von Daten,
zu Positionen eigentlich erreichen, dass die Datenmenge auch sortiert wird,
ohne dass wir überhaupt Elemente vergleichen.
Und dann haben wir auch eine bessere Laufzeit nochmal erreicht.
Die Bäume, die wir gesehen haben, basieren ebenfalls wieder auf Vergleiche. Ja,
also die elementarste Operation beim Arbeiten mit Suchbäumen ist,
dass wir an einem Knoten ein Objekt mit dem Objekt vergleichen,
was in den Knoten gespeichert ist und dann sagen,
das ist das gleiche Objekt oder wir gehen nach links oder wir gehen nach rechts.
Also es ist wieder diese Geschichte,
die mit Vergleichen zu tun hat und wir kommen ja auch wieder auf diese Logerien,
rhythmische Laufzeitkomponente, weil sie N-Objekte in dem Baum suchen,
hätten sie auch wieder N-Log-in an Laufzeit.
Log-in-Zeit, um ein Objekt wiederzufinden. So,
also auch hier die Frage, können wir vielleicht etwas erreichen,
wenn wir nicht unbedingt sie ganzes Verfahren auch vergleichebasieren? Das macht man,
wenn wir Hashing-Techniken verwenden und zwar die Annahme ist jetzt,
dass wir die Schlüssel, die wir ablegen wollen in unserem Wörterbuch,
dass wir die abbilden können auf Zahlen.
Und das wäre natürlich super, wenn das geht,
weil dann können wir einfach ein Array nehmen, können die Objekte,
für ein Objekt eine Zahl kriegen, können wir einfach sagen,
okay, das speichern wir jetzt an dieser Position in dem Array.
Und wenn wir so ein Objekt wiederfinden wollen, ist es auch ganz einfach.
Wir nehmen das Objekt, wir rechnen wieder diese Zahl aus,
gehen an diese Position in dem Array und haben das Objekt wiedergefunden.
Und das sollte eigentlich relativ effizient sein.
Diese Strategie als grundlegende Strategie bezeichneten wir als Hashing. Okay,
genau wie das auch beim Sortieren der Fall war,
weil die linie an Sortierverfahren machen wir Annahmen,
wann das gut geht. Und wir werden sehen,
dass wir hier auch Annahmen machen müssen und dass wir auch in Situationen reinkommen,
wo es vielleicht dann doch nicht ganz so elegant zu lösen ist,
wie sich das jetzt auf den ersten Blick,
die das auf den ersten Blick aussieht
Also unsere Annahme ist jetzt, dass wir einen Weg haben,
die Schlüssel auf natürliche Zahlen abzubilden.
Und natürliche Zahlen sind für uns gut, weil natürliche Zahlen
beschreiben einen Index in einem Array oder in einer Speicherstruktur.
Und weil wir eine RAM voraussetzen, also weil wir voraussetzen,
dass wir von unserem Computer auf jede Speicherzelle mit gleicher Zeit,
mit gleicher Konstante zur Zeit zugreifen können,
wäre das also ein Modell, um Daten abzulegen und wiederzufinden.
Wir berechnen also aus dem Schlüssel unseres Objekts einen Index,
den bezeichnen wir mal als H von Key.
H heißt unsere Hash-Funktion im Folgenden. Und dann speichern wir
die Daten in einem Array an diese Position H von Key
Und dann könnten wir konstant, dass halt diese Daten wieder abgreifen.
Das wäre alles einfach und die Lösung wäre jetzt eigentlich
auch schon perfekt, aber das Problem ist natürlich,
dass wir wesentlich weniger Speicher haben werden als mögliche Schlüssel.
Das heißt, diese Hash-Funktion wird die Aufgabe haben aus einem großen Universum von Schlüssel,
eine Adresse auszurechnen, wo wir dann das Objekt speichern können.
Wenn wir viel mehr Schlüssel haben als das,
was wir in unserem Speicher ablegen können und das ist der Regelfall,
dann kommt es augenblicklich zu Kollisionen und das können wir nicht vermeiden.
Kollision bedeutet, wir haben zwei Objekte,
die eigentlich unterschiedlich sind
Wenden diese Funktion Haar an und diese Funktion Haar sagt uns
die gleiche Stelle im Speicher, wo wir dieses Objekt speichern sollen.
Dann haben wir eine Kollision, wir wollen beides da speichern,
aber in der Speicherzelle können wir nur eines der beiden Objekte speichern.
Das heißt, wir brauchen eine Strategie, um mit diesen Kollisionen umzugehen.
Wir werden sie bis zum Ende, wir werden uns sehr damit beschäftigen,
sie zu vermeiden, so gut es geht.
Aber wir werden es nicht vollständig schaffen. Es ist einfach
in der Natur der Dinge, dass Kollision auftreten müssen,
weil wir viel mehr mögliche Schlüssel haben als Positionen,
wo wir Objekte speichern können.
Beachten Sie bitte Dass die Anzahl der Objekte, die wir speichern,
die ist natürlich schon kleiner als der Index-Bereich. Das,
was wir speichern können, aber die Anzahl der möglichen Schlüssel,
die wir generieren können, die ist viel größer.
Okay, so. Also, wir beginnen, wir sagen mal,
unser Schlüssel, der ist aus einer Menge U,
das U wird verwendet, weil das unsere Universalenmenge ist.
Und das ist aus dem Bereich von 0 bis M minus 1.
Und wir gehen davon aus, dass alle Schlüssel voneinander verschieden sind.
Wir machen erstmal wieder ein paar ganz grundsätzlich einfache Annahmen
Und wir gehen davon aus,
dass wenn K die Menge der verwendeten Schlüssel ist,
dass M mal so in O von K ist.
So, was wir also machen, ist,
wir verwenden als erstes mal eine Adresstabelle mit direktem Zugriff.
Also wir haben hier die Universalmenge,
das sind alle möglichen Schlüsselwerte, die vorkommen können.
Und wir haben einen Array hier, wo wir die Objekte speichern können.
Wir haben quasi für jedes Objekt der Universalmenge eine Möglichkeit,
das hier zu speichern.
Dann haben wir eine Teilmenge der Universalmenge.
Das sind die Sachen,
die tatsächlich vorkommen
Ja, ich könnte es dir ja zum Beispiel so vorstellen,
wie, ja, nehmen Sie, Sie haben die Menge aller Bücher.
Alle Bücher auf der Welt, die es gibt,
ist die Universalmenge, eine gewisse Anzahl oder eine Menge davon,
ist die, die Sie privat besitzen.
Ja, das ist die tatsächlich vorkommenden Schlüssel, ja.
Und das ist Ihr Regal. Ja, und da schieben Sie die rein.
Dann sehen Sie, die Universalmenge ist eben viel,
viel größer als der Platz in ihrem Regal, offensichtlich, ja.
Die Anzahl der tatsächlichen Schlüssel, also der tatsächlichen Bücher,
ist eher etwas kleiner als der Platz in ihrem Regal.
Wollen wir es hoffen, ja, dass das so ist.
Und wir brauchen jetzt eine Abbildung, die uns sagt,
wenn jetzt so ein Buch einen speziellen Titel hat,
das ist hier nur eine Nummer,
wo in dem Regal werden wir das ablegen können
Okay, das, die eine Möglichkeit ist,
also wir haben jetzt eine Funktion, die bildet das ab,
im Regal, vielleicht machen sie das alphabetisch sortiert und dann
haben sie ein Regal für A und eins für B und eins für C und so weiter.
Und dann merken sie schon, dass sie hier potenziell ein Problem haben,
weil sie vielleicht ganz viele Bücher haben, die mit A anfangen. So,
bei uns sieht das jetzt erst mal so aus,
wir bilden diese Objekte ab,
also zwei kommt an die Position 2 und hier wird dann
das Objekt gespeichert und so ein Objekt besteht eben aus,
naja, irgendwo diesem Schlüssel,
auf den müssen wir zugreifen und den ganzen Rest dieses Objekts.
Sehr interessiert uns im Folgenden überhaupt nicht mehr. Wir gehen davon aus,
es gibt noch irgendwelche Daten, die daran assoziiert sind,
das nennen wir Satellitendaten und die brauchen wir den weiteren nicht betrachten.
Wir interessieren uns nur für den entsprechenden Schlüssel, der das Kriterium ist,
mit dem wir das Objekt in den Speicher hängen wollen. Okay,
so, jetzt haben wir also unsere Objekte hier,
wir können die also zuordnen und wenn wir das Objekt wiederfinden wollen,
sagen wir dann,
wo ist denn das Buch mit dem Titel 2,
dann können wir hier in unserem Array an die Position 2 springen und kriegen unser Objekt.
Also, in diesem Szenario ist Insert, Delete und Search
in O von 1 Zeit, also in konstanter Zeit realisierbar.
Wir haben aber ein ganz großes Problem,
damit das Ganze funktioniert, setzen wir voraus,
dass wir für jedes Objekt der Universalmenge einen Platz haben.
Und das ist natürlich genau das, was wir nicht realisieren können.
So groß kann unser Regal nicht sein. Und selbst wenn
es das wäre, dann wäre es wahrscheinlich zu 99,99 Prozent leer.
Was wir natürlich auch nicht wollen, das wäre sehr ineffizient.
Okay, also in der Praxis ist die Universalmenge wesentlich größer
als die Anzahl der tatsächlich zu speichernden Schlüssel.
Wir haben also nur einen ganz kleinen Bruchteil der Objekte,
die man theoretisch konstruieren kann, die wir auch haben und speichern müssen.
Das heißt, direkte Adressierung ist speicherineffizient und kommt eigentlich
nicht wirklich in Frage
Was wir erreichen wollen, ist,
dass wir so ein Verfahren, wie diese direkte Adressierung haben,
wo wir Objekte in den Speicher legen können,
was idealerweise auch wieder so mit konstanter Zeit geht,
also Hash-Funktion, die sagt uns, wo steht das,
dann nehmen wir das raus, aber wir wollen,
dass der Speicherbedarf, den wir dafür benötigen,
möglichst nicht viel größer ist als K. Also als die Anzahl der Objekte,
die wir wirklich speichern wollen.
Also das heißt, wir wollen eine Speicherung mit Platzbedarf,
Tetter von K, also es ist ja mindestens K,
wir haben ja K-Objekte, die wir speichern wollen,
unter Erhalt dieser Laufzeit,
die wir schon gesehen haben,
also möglichst konstanter Laufzeit
Okay, wie machen wir das? Wir verwenden also eine Hash-Funktion,
die Hash-Funktion macht Folgendes. Die Hash-Funktion bildet die Universalmenge,
also die Menge aller möglichen Schlüssel, die es geben kann,
auf die Menge von 0 bis M minus 1 ab.
Und M ist quasi die Größe unseres Regals oder die Größe
der Hashtag-Welle, wo wir die Daten einspeichern können.
Gut, H ist diese Hash-Funktion und H von K bezeichnen wir
als den Hash-Wert des Schlüssels K. Also K ist das,
wonach wir eigentlich suchen.
H von K ist der entsprechende Hash-Wert und der sagt uns
an welcher Stelle im Speicher wir diese Information finden werden und die Daten liegen in einer Hash-Tabelle,
die werden wir mal mit Groß-Tee bezeichnen und die hat eben genau M-Speicherpositionen von L0 bis M minus 1.
So, eben haben wir schon gesehen, es kann passieren,
dass zwei Objekte über diese Hash-Funktionen, die gleiche, den gleichen Hash-Wert bekommen.
Also die Hash-Funktion entscheidet, dass beide Objekte an der gleichen Stelle gespeichert werden sollen. Dies nennen wir Kollision,
ja, also gilt für zwei Schlüssel K und K-Strich,
die ungleich sind, das sind also andere Objekte,
dass der Hash-Wert gleich ist,
also Ha von K ist gleich Ha von K spricht,
dann sprechen wir von einer Kollision beim Einfügen der Objekte in die in den Speichern.
So, und dann ist auch schon klar,
weil die Universalmenge wesentlich größer sein wird als M in der Praxis,
sind die Kollisionen nicht vermeidbar.
Wir werden auf alle Fälle Kollisionen haben müssen.
Und das Ziel der Entwicklung von Hash-Funktionen von Hash-Datenstrukturen sind,
oder die Ziele sind erstens, wir müssen uns überlegen,
wie wir mit den Kollisionen umgehen, wenn wir sie haben.
Ja, also was wollen wir dann tun?
Wir können nicht einfach sagen, wir schmeißen das Objekt weg.
Ja, wir müssen es ja trotzdem speichern.
Also wie wollen wir damit umgehen? Und das andere ist,
wie gehen wir möglichst geschickt vor,
sodass wir die Anzahl der auftretenden Kollisionen minimieren
Und das hat sehr viel mit der Wahl der geeigneten Hash-Funktionen zu tun.
So, gerade diese Wahl von Hash-Funktionen ist nicht so ganz ohne.
Also das hat sehr viel mit diskreter Mathematik zu tun. Ja,
sie müssen sich dann überlegen, sie rechnen hier modulo,
bilden das ab und dann überlegen sie, wann tritt eine Kollision auf, etc.
Das werden wir hier nicht in aller Tiefe machen können. Also ich werde ihn hier,
ich werde ihnen hier die Hashing-Fahren zeigen,
ich werde ihnen auch gute Hash-Funktionen zeigen, aber immer dann,
wenn es darum geht, warum diese Funktion eine gute Hash-Funktion ist,
werden wir den Level der, sagen wir mal,
der Beweise verlassen, mit denen man das eigentlich nachweisen müsste,
weil wir da sehr in die diskrete Mathematik abdriften.
Also ich werde ihnen noch ein paar gute Gründe liefern,
warum das so ist, aber wir werden das dann entsprechend nicht mehr beweisen.
So, okay, jetzt kümmern wir uns erstmal um
die Frage, wie wir mit Kollision umgehen.
Unabhängig davon, wie die Hash-Funktion aussieht.
Die Kollision treten ja auf und wir müssen eine Strategie haben.
Und die erste Strategie, die erstmal kommt, ist,
naja, wir haben ja Datenstrukturen,
mit denen wir in der Lage sind,
eine größere Menge von Objekten
zu speichern
Und die einfachste Datenstruktur wäre eine Liste. Bei einer Liste können wir
eine Menge von Objekten speichern. Also können wir doch einfach so vorgehen,
dass wir sagen, in dieser Tabelle hier, in der Hash-Tabelle,
da steht hier vorne nicht einfach nur ein Verweis auf ein Objekt,
was wir dort speichern können,
sondern an der Stelle hier steht eine Liste.
Und in der Liste können wir dann Objekte einfach einfügen.
Und wenn wir dann eine Kollision haben, dann sagen wir,
okay, da steht zwar schon ein Objekt,
das macht aber nichts, wir haben ja eine Liste,
das heißt, wir können noch ein Element anfügen an die Liste.
Das ist die, oder das ist eine der beiden Kollisionsstrategien.
Es gibt zwei, das ist die erste. Diese nennt man
Kollisionsauflösung durch Verkettung oder kurz in Englisch, ist es eben hashing with chaining.
Also Hashen mit Verkettung von Elementen. Okay,
also die Idee ist ganz einfach,
wir speichern alle Elemente mit gleichem Hashwert, also alle,
die eine Kollision verursachen, in einer linearen Liste,
die dann an der Tabelle abgelegt wird.
Und die Operationen sehen dann so aus, eine Inside-Operation,
wie also ein Objekt X in die Hash-Tabelle T einfügt,
fügt einfach X an den Kopf der Liste,
die in der Tabelle an der Position H von X. Key steht.
Also X ist unser Objekt, X. Key ist der Schlüsselwert,
wir entscheiden, wo das Objekt gespeichert werden soll.
H ist die Hash-Funktion, daraus kriegen wir den Index.
Daraus kriegen wir die Liste, die steht hier an der,
in der Tabelle, an der Position Tee.
Und dann fühlen wir das Objekt vorne in die Liste ein.
Geht in konstanter Zeit. Ist schon mal super. Die Löschoperation,
also wenn wir jetzt davon ausgehen,
wir kennen das Objekt X und damit,
da setzen wir jetzt mal voraus, wir wissen auch,
wo das genau steht,
dann können wir X einfach aus der Liste hier entfernen,
wo das enthalten ist.
Und das können wir auch in konstanter Zeit machen. Wenn Sie jetzt sagen,
Ja, aber das muss ich ja erstmal da drin finden.
Dann haben sie natürlich theoretisch recht, aber wir gehen jetzt davon aus,
dass wir hier schon einen Zugriff auf das Objekt haben.
Also wir haben, hier steht kein K, also kein Schlüsselwert.
Wir sagen nicht, gib uns mal das Objekt mit
dem Schlüssel K und das möchten wir löschen,
sondern wir sagen, das ist das Objekt, lösche das.
Wenn es so ist, geht das in konstanter Zeit.
Die Verkettung muss dann eine doppelte Verkettung sein.
Also das können sie nochmal bei den Listen nachgucken,
wenn sie so ein Objekt haben, um das zu löschen,
müssen die natürlich den Vorgänger wissen, brauchen sie also eine doppelte Verkettung.
Gut, so und ja, was bedeutet es jetzt
fürs Suchen
Besuchen, müssen wir jetzt irgendwie die bittere Pille schlucken,
weil wir haben jetzt an so einer Position nicht mehr ein Objekt, sondern eine Liste.
Und wenn wir ein Objekt suchen, müssen wir diese Liste durchlaufen.
Und das dauert natürlich so lange, wie Elemente in der
Liste sind. Das ist proportional dazu. Das heißt,
die Suchzeit hier für eine Liste,
die dann an der Position H von K ist,
ist irgendwie O von Betrag von Tee, von H von Xbox Key.
Also das ist die Liste hier und die Anzahl der Objekte,
die da drin ist, entscheidet darüber,
wie lang die Laufzeit ist.
Das ist noch ein bisschen unpräzise. Ja,
das ist ja der Worst Case hier,
also im ungünstigsten Fall ist ja
unser Objekt
ganz hinten
An der Liste müssen wir die auch komplett durchlaufen,
um das Objekt zu finden. So, und wie immer
, wir gucken uns hier den Worst Case an.
Dann überlegen wir uns mal, was denn das Schlimmste
wirklich sein könnte. Wir haben K-Objekte, alle K-Objekte,
die wir speichern wollen, haben zufälligerweise die Eigenschaft,
dass die Hash-Funktion sie auf die gleiche,
auf die gleiche Position verweist.
Alle Objekte kollidieren, paarweise miteinander. Könnte ja so sein
. Also sie haben hier eine Hash-Tabelle mit zehn Einträgen.
Sie haben sich überlegt, oh klar, die beste Hash-Funktion,
die ich hier mal hätte,
ist,
ich nehme einfach Modulu
In ihrer tatsächlichen Schlüsselmenge kommen, nur vielfache von zehn vor.
So, dann werden die alle in der gleichen Liste.
Sie hatten alle K-Objekte, die sie speichern in einer Liste,
wenn sie das Objekt suchen, brauchen sie K-Zeit und sie haben,
ja, nicht nur den Worst Case fürs Hashing,
sie haben auch den Worst Case,
den man sich überhaupt nur vorstellen kann.
Ja, sie gucken sich alle Objekte nacheinander an und schauen,
ob das Objekt entsprechend das Gesuchte ist.
Dieses ungünstige Verhalten können wir nicht wirklich vermeiden, ja,
egal welche Hash-Funktionen wir uns ausdenken,
wird es immer irgendeine Reihenfolge oder Schlüsselmenge geben,
die gerade diesen Worst Case produziert
Ist einfach so. Und wir können nur statistisch,
also stochastisch, muss ich sagen, stochastisch argumentieren,
dass wir genau eine Zahlenfolge, also eine Hash-Funktion haben,
die quasi eine mögliche Gleichverteilung hier verursacht.
Frage. Ja, das könnten Sie machen, das stimmt.
Das wäre eine Strategie, die Sie wählen könnten.
Dann hätten Sie noch eine Login-Site im Worst Case.
Das stimmt, das könnte man machen
Ja, wir werden aber natürlich sehen,
also wenn wir davon ausgehen, dass diese Tabelle hier,
die sollte ja mindestens so groß sein wie die Anzahl der Schlüssel, die wir speichern.
Und wenn wir dann im Wesentlichen davon ausgehen,
dass dieser Fall einfach nicht eintritt, dann werden diese Listen natürlich sehr kurz sein.
Also das ist also, der Worst Case,
den kann man damit verbessern, aber der mittlere Fall,
da ist einfach der Aufwand dann sehr hoch,
wenn man da noch Bäume einsetzen würde.
So, wir müssen also über das durchschnittliche Verhalten dieser Hashing-Methoden diskutieren.
Das heißt, wir kommen wieder so ein bisschen in
die Welt der Erwartungswerte
Sich hier nicht vermeiden. Und die Frage ist,
was ist so unser Kriterium dafür? Wie wollen wir die
Laufzeit bestimmen? Wir haben ja offensichtlich offensichtlich zwei Parameter.
Wir haben ja die Größe der Hash-Tabelle und wir haben die Anzahl
der Objekte, die wir in der Hash-Tabelle speichern wollen.
Wir wissen schon, eben im Worst Case haben wir gesehen,
naja, wenn wir N-Objekte speichern, könnte es sein,
dass es O von N wird, wenn die alle in einer Liste sind.
Wenn wir ein bisschen über die Verteilung und den durchschnittlichen Fall reden,
dann ist natürlich das, was uns am meisten interessiert,
wie lang sind denn diese Listen so im Durchschnitt
Und das können wir ganz leicht ausrechnen,
wenn wir mal von der Gleichverteilung ausgehen, dann haben wir N-Objekte,
die wir speichern wollen. Wir haben M-Plätze,
wo wir speichern können, dann ist natürlich im Schnitt,
sind dann N durch M viele Objekte in einer Liste.
Dieser Quotient N durch M, also Anzahl der gespeicherten Objekte
durch die Größe der Häshstelle, Tabelle nennen wir Belegungsfaktor.
Das ist also der Prozentsatz der Positionen,
die wir zur Speicherung zur Verfügung stellen,
an denen
ein Objekt
tatsächlich gespeichert ist
Der ist auf alle Fälle kleiner als eins,
weil wenn der größer wird als eins, dann hätten wir
gar keinen Platz, um das nächste Objekt zu speichern.
Also naja, es stimmt nicht.
Also hier haben wir ja wieder Listen. Das heißt,
in den Listen hängt man das ja dann auch entsprechend an.
Also das ist unser Belegungsfaktor hier. Und der wird jetzt
entscheidend sein, wenn wir die Laufzeit analysieren.
Wir werden also unsere Laufzeit angeben in Abhängigkeit von der durchschnittlichen Anzahl
Objekte, die wir in den Listen erwarten. So,
also unsere Annahme, die wir im Folgenden machen wollen,
ist, wir wollen diesen extrem ungünstigen Fall einfach mal ausschließen.
Ja, wir sagen, naja, wir gehen von einem gleichmäßigen Hashing aus.
Also da kommen Objekte, die wollen wir speichern und die
sind einfach zufällig oder gleichmäßig verteilt auf diese möglichen Hash-Werte.
Wenn wir diese Annahme machen, dann sprechen wir von einem einfachen, gleichmäßigen Hashing.
Was das bedeutet, ist, wir sagen mal,
XIJ sei ein Ereignis, und zwar ist es das Ereignis,
dass das Objekt i, den Hashwert J bekommt.
Also die Hash-Funktion sagt zu Objekt i, du musst an
Position J in dem Speicher. Und wenn das so ist,
dann ist XIJ gleich eins und sonst wäre das null.
Also beim Ereignis registrieren wir immer, ob das Ereignis eintritt und
dann ist der Wert eins und sonst ist das null.
So, die Wahrscheinlichkeit, dass dieses Ereignis eintritt,
ist eins durch
M war die Anzahl der Speicherplätze, die wir zur Verfügung haben.
Und wenn wir sagen, das ist gleichmäßig,
dann ist jede Position in diesem Array gleich wahrscheinlich, dass die eintritt.
Ja, also ist die Wahrscheinlichkeit, dass ein Objekt I
an die Position J gespeichert wird, ist eins durch M. Okay.
So, und des Weiteren gehen wir auch davon aus,
dass XIJ und XIK, also wir Das ist komisch.
Also des Weiteren, ich sage nicht mal,
was da erst mal stehen muss, ich bin irritiert über dieses I,
das passt nicht. Also des Weiteren gehen wir davon aus,
dass alle diese Ereignisse, dass wir Objekte auf Hash-Werte abbilden,
also diese XIJs, dass die stochrastisch unabhängig voneinander sind.
Wir haben also keine Abhängigkeit in dem Sinne,
wenn das IT-Objekt an die eine Position kommt,
kommt das JT-Objekt immer an die andere Position,
sondern die haben eigentlich nichts miteinander zu tun.
Das Irritieren ist hier, dass sie zweimal I steht,
also hier müsste wahrscheinlich ein L stehen, also XIJ und XLK, also zwei völlig
Und zwei Ereignisse, die stattfinden, dass ein Objekt an
einer Position in die Hersteller kommt, sind einfach unabhängig voneinander.
Die haben nichts miteinander zu tun. Okay, so,
und dann können wir zeigen, dass die durchschnittliche Listenlänge tatsächlich Alpha ist.
Also wenn NJ jetzt mal die Liste der Länge J ist,
also an der Position J in dem Array,
haben wir ja unsere Verkettung und die Listen,
das Listenobjekt hat hier dann das Attributpunkt längst,
sagt die Anzahl der Objekte,
die da drin ist.
Und dann wissen wir,
dass die erwartete Listenlänge gleich Alpha ist,
also der Belegungsfaktor N durch
Das ist, glaube ich, naheliegend. Also wir verteilen
einfach gleichmäßig enden Objekte auf M-Kisten. Und dann sind da
halt N durch M viele Objekte in einer Kiste.
So, was bedeutet das für die Laufzeit?
Das sagt dir das erste Theorie. In einer Hash-Tabelle mit Verkettung
unter Annahme des einfachen gleichmäßigen Hashings. Also wir setzen das
jetzt voraus. Das ist alles schön gleichverteilt ist, ja.
Benötigt die erfolglose Suche O von 1 plus Alpha erwartete Laufzeit.
Also wir machen eine Unterscheidung danach, ob wir bei
der Suche erfolgreich oder erfolglos sind bei der Analyse.
Das ist nicht ganz unwichtig, weil Wenn wir erfolglos sind,
bedeutet das beim Hashing immer, dass wir die Liste komplett betrachten müssen.
Das Objekt ist ja nicht drin in der Liste. Und um das herauszufinden,
wir suchen jetzt einen Schlüssel, keine Ahnung, K10,
der K10 hätte, der ist hier, der hat die Hash-Funktion,
liefert uns den Wert 5.
Wir müssen in dieser Liste gucken. Um festzustellen,
dass KC nicht vorkommt, müssen wir die komplette Liste betrachten.
Das heißt, erfolglose Suche ist teuer. Wir müssen die
ganze Liste bis zum Ende durchlaufen. Eine erfolgreiche Suche,
Das heißt, wir finden, das Objekt ist günstiger,
weil bei einer erfolgreichen Suche können wir im Mittel erwarten,
dieses Objekt, naja, so stochastisch in der Mitte der Liste gefunden zu haben.
Manche finden wir ganz am Anfang, manche ganz am Ende,
aber im Mittel haben wir das so,
nachdem wir so die Hälfte der Liste durchlaufen haben, gefunden.
Deswegen unterscheiden wir diese beiden Fälle. Wir können also genau sagen,
wie die Laufzeit ist, je nachdem,
ob das Objekt gefunden wird oder ob es nicht gefunden wird.
Okay, also die Laufzeit für die erfolglose Suche
ist O von 1 plus Alpha
Also das bedeutet, wir haben irgendwie eine konstante Zeit,
die wir benötigen. Klar, es ist eigentlich zu logisch.
Und dann haben wir eine Zeit, die proportional ist zu Alpha,
also zum Belegungsfaktor. So, und wenn wir jetzt sagen,
okay, wir haben wesentlich weniger Objekte hier in der Tabelle,
als wir Platz haben, diese zu speichern,
dann wäre ja Alpha auch kleiner als eins und dann
hätten wir wieder eine konstante Zeit.
Aber beachten Sie, das ist jetzt keine Worst-Case-Laufzeit,
das ist eine erwartete Laufzeit.
Also das, was wir im Mittel erwarten können.
So, warum ist das so? Ein nicht enthaltener Schlüssel,
K wird mit Wahrscheinlichkeit eins durch M
in der Liste Ha von
Abgelegt. Wenn wir den einfügen würden, dann würde er,
naja, mit einer Hashfunktion H an die Position H von K gelegt und wir gehen davon aus,
dass es alles gleichmäßig ist, das heißt,
alle möglichen Positionen sind gleich wahrscheinlich und das ist,
sie haben die Wahrscheinlichkeit von eins durch M.
Und dann wissen wir, dass die erwartete Listendenke für
diese Position gleich Alpha ist, also gleich den Belegungsfaktor.
So, also meine erfolglose Suche würde jetzt diese Liste Half
von K in O von Alphazeit vollständig durchlaufen.
Wir müssen also die Liste bis zum Ende durchlaufen,
die ist Alpha lang und deswegen brauchen wir O von Alpha Zeit,
um diese Liste zu durchlaufen.
Und da müssen natürlich noch die Hash-Funktionen berechnen.
Wir gehen davon aus, dass es eine Funktion,
die wir konstanter Zeit berechnen können, macht also eins plus Alpha.
So, was passiert bei einer Erfolgreichensuche? Also jetzt haben wir das Szenario,
wir suchen einen Schlüssel und der kommt in der Hash-Tabelle tatsächlich vor.
Das ist natürlich der häufigere Fall. Was passiert nun?
In einer Hash-Tabelle mit Verkettung und der Annahme des einfachen gleichmäßigen Hashings,
gleiche Annahme wie eben, benötigt die erfolgreiche Suche,
ebenfalls O von 1 plus Alpha,
erwartete Laufzeit
Es ist eigentlich schon naheliegend, dass das da rauskommen muss,
weil ich habe gerade schon gesagt, das,
was wir uns ersparen,
ist so ein konstanter Faktor von einem Halt,
weil wir die Liste nicht mehr bis zum Ende laufen müssen,
sondern im Schnitt nur ungefähr bis zur Mitte.
Und das kann man auch ausrechnen, dass das so ist.
Und wir werden gleich auch diesen Faktor ein Halb tatsächlich sehen.
So, wie kann man das jetzt begründen, dass das da rauskommt?
Also wir haben also eine N-Objekte, die haben wir gespeichert
in der Hash-Tabelle. Und diese N-Objekte haben Schlüssel K1 bis K1.
Und die sind alle in die Hash-Tabelle eingefügt worden.
Und wir wissen, wir bezeichnen jetzt mit XIJ Das ist
was anderes als das X gerade. Das XEJ ist jetzt
unser Kollisionsereignis. Kollisionsereignis bedeutet, der Wert XEJ wird eins,
wenn der Ite und der Jte Schlüssel kollidieren.
Also die gleiche Hash-Adresse kriegen. Dieses Kollisionsereignis ist das elementare Ereignis,
was dazu führt, dass die Listen länger werden.
Mit jeder Kollision wird eine Liste um eine Position halt verlängert.
So, jetzt führen wir also eine Suche durch und
diese Suche durchläuft die Objekte, irgendwelche Objekte XJ.
Und zwar, die werden ja immer vorne eingefügt
Also, das heißt, wir finden alle Objekte,
die wir nach XI, also das XI,
suchen wir und alle, die wir danach noch eingefügt haben,
die stehen davor in der Liste,
weil wir immer vorne in der Liste was einfügen.
Also, Search, T von XI, also wir suchen das XI,
durchläuft alle Objekte XJ, die nach XI eingefügt wurden.
Also, die sind nachher eingefügt worden, deswegen stehen die
in der Liste davor, weil wenn wir am Anfang eingefügt haben.
Und die müssen natürlich auch den gleichen Hash-Wert haben,
sonst würden die ja gar nicht in der Liste stehen.
Das heißt, das XIJ gleich 1 ist, also alle Objekte XJ,
die nach XI eingefügt wurden und eine Kollision mit I haben.
Die stehen davor und die müssen wir betrachten
Das heißt, die erwartete Laufzeit, die wir jetzt haben,
um ein Objekt zu finden, ist, naja,
wir müssen jetzt den Mittelwert bilden über alle Objekte,
die in der Hash-Tabelle gespeichert sind.
Es sind en viele Werte gespeichert,
also wir haben irgendwie eine Konstante, so irgendeine Konstante,
wegen dem OK-Spiel später, also kannst du ja normal,
eins durch N und hier ist die Summe von Igarch 1 bis N,
also hier betrachten wir einfach den Mittelwert über alle Objekte,
die wir suchen können.
Wir wissen ja, es ist eine erfolgreiche Suche,
das heißt, alle möglichen Anfragen,
die vorkommen können,
sind Schlüssel,
die in der Tabelle vorkommen
Also das hier ist nur der Mittelwert,
also zehnmal der Mittelwert über alle Objekte, die vorkommen.
So, wie viel Zeit brauchen wir jetzt um ein spezielles Objekt I, XI zu finden?
Na, das ist erstmal eine konstante Zeit. Eins,
plus, naja, die Anzahl der Kollisionen,
die wir haben, aber nicht alle,
sondern nur die, die wir nach I eingefügt haben,
weil die Kollisionen, die wir mit Objekten haben,
die wir vorher eingefügt haben,
stehen ja in der Liste dahinter und wir durchlaufen die Liste von vorne nach hinten.
Okay, so, das heißt, das ist die Summe für
das J von I plus 1 bis N läuft, Und dann XEJ.
Also ein XEJ ist 1, wenn wir eine Kollision hatten. Okay,
das beschreibt die Laufzeit und das ist eigentlich eine relativ einfache Gleichung,
die müssen wir jetzt nur noch auflösen. Und da gilt erstmal,
dass dieser Erwartungswert, der hat eine Linearitätskriterium, das heißt,
wir können alle Faktoren und Summationen hier sozusagen aus der Klammer
herausziehen und haben dann also C durch N mal die Summe,
gleich 1 bis N, das sind also alle Objekte,
1 plus und dann die Summe J. I plus 1 bis N.
Und hier ist die erwartete Kollision zwischen Objekt I und
So, was ist der Erwartungswert einer solchen Kollision? Naja,
das ist eins durch M, weil die ja gleichmäßig verteilt sind,
ist die Wahrscheinlichkeit, dass zwei Objekte in das Gleiche,
in den gleichen Topf kommen, gerade eins durch M,
wo wir M-Objekte haben.
Das können wir also hier einsetzen. Und jetzt haben wir eine Formel,
nur also eine doppelte Summenformel, die müssen wir nur noch auflösen.
Ja, und dann haben wir hier den ersten Teil,
da hätten wir C durch N mal,
die Summe von Ilch 1 bis N von 1. Das ist hier ja offensichtlich N,
diese Summenteile.
Und C mal C durch Ein Malende ist gerade zäh.
Also der Teil hier wird zu diesem
einen konstanten
So, und die Summe hier hinten ist dann C durch N mal M.
Das M hier hängt nicht von J ab,
das können wir auch vor die Summe ziehen.
Also C durch N mal M. Und dann haben wir diese beiden Summen,
I gleich 1 bis N und hier J gleich i plus
1 bis N von der 1. Ja,
J gleich i plus 1 bis N von 1 ist das Gleiche wie N-I.
Das sind ja einfach nur, wir addieren immer eins,
für i plus 1, i plus 2, i plus 3 bis N.
Das sind gerade N-I, viele Einsen, die wir addieren.
Also diese innere Summe hier löst sich auf zu N-I. Ja,
und dann können wir diese Summe wieder zerlegen
Und haben dann die Summe von Igarch 1 bis N von N.
Das ist offensichtlich ein Quadrat, also End plus End Quadrat.
Und dann haben wir die Summe von Igarch 1 bis N über I.
Das ist die gauische Summenformel, das ist N mal N plus 1 halbe.
So, und wenn wir das zusammenfassen hier,
dann steht da zehn mal 1 plus N minus 1 sich 2 M.
Oder wenn wir das jetzt, wenn wir jetzt einen Belegungsfaktor einsetzen,
also wir wissen N durch M ist der Belegungsfaktor,
dann ist das zehn mal 1 plus Alpha halbe,
minus Alpha durch zwei durch N.
Und das ist offensichtlich O von 1 plus Alpha. Das heißt,
die erfolgreiche Suche geht auch in O von 1 Plus Alpha.
Und hier ist diese zwei, die ich gerade erwähnt habe,
das heißt, wenn sie hier etwas genauer hinschauen,
dann sehen sie, dass sie im Mittel das Objekt in der,
die Liste ist Alpha-Elemente lang, ungefähr bei Alpha-Halbe schon finden.
Und das ist einfach hier noch so ein Thermensehen,
das sogar theoretisch ist, sogar ein ganz klein bisschen kleiner,
aber das ist einfach nur so ein Korrekturtherm,
der ja relativ klein ist, weil dieses Alpha nochmal durchendividieren.
Okay, also wir könnten auch sagen, oh,
von 1 plus Alpha-Halbe, im Okalkül,
das ist jetzt nicht mehr so wichtig,
aber für die realen Zahlen ist das natürlich nicht ganz unerheblich
Okay, so, das ist erst mal das Hashing mit Verketten.
Ja, also wir sehen unter der Annahme,
dass alles schön gleich verteilt wird auf unserem Speicher,
ist das alles sehr effizient.
Wir haben eins plus Alphazeit, Alpha ist eigentlich kleiner als eins,
wir haben konstante Zeit, um die Objekte wiederzufinden.
Jetzt brauchen wir natürlich noch gute Hash-Funktionen, um das zu machen.
Ja, und da stellt sich jetzt erst mal die Frage,
was nimmt man da denn so für Funktionen?
Also, natürlich sollten die möglichst leicht und schnell berechenbar sein.
Sie wollen keine super komplizierte Funktion, weil das kostet sie
dann vielleicht konstante Laufzeit, aber viel konstante Laufzeit.
Das wäre schon unangenehm. Sie wollen irgendwas, was Sie schnell ausrechnen können.
Dann müssen Sie irgendwas erzeugen, was eine gleichmäßige Verteilung der Schlüsselmenge verursacht.
Also es wäre ungünstig, eine Hash-Funktion zu haben,
die 99 Prozent der Daten auf einen Schlüsselwert projizieren würde und nur ein Prozent auf die anderen. Also die Hash-Funktion sollte natürlich auch möglichst gleichwert, gleichverteilt die möglichen, die möglichen Hash-Positionen ausgeben können. Gut,
allerdings kann man schon direkt sagen,
Das kann man natürlich eigentlich im Vorhinein nicht so sagen,
ja, weil ihre Daten sind ja typischerweise nicht so gleich verteilt,
ja, wenn sie spezielle Häufungen in den Daten haben,
dann kann es ja durchaus sinnvoll sein,
dass die Hash-Funktion einen speziellen Bereich auf nur einen Schlüssel abbildet,
selbst wenn das 90 Prozent der Daten sind,
weil die einfach selten vorkommen, ja,
wenn sie zum Beispiel Wörter speichern und sie speichern die nach dem Anfangsbuchstaben des Alphabets, ja,
dann wäre es ja durchaus sinnvoll, eine Hash-Funktion zu haben,
die nicht alle 26 Buchstaben gleichmäßig auf 26 Felder projiziert,
sondern dann wäre es ganz sinnvoll, eine Funktion zu haben,
die vielleicht X und Y wesentlich,
also stärker akkumuliert als vielleicht S oder Tee, ja,
weil diese Buchstaben einfach viel häufiger vorkommen.
Das heißt, die Verteilung,
die sie in der Eingabe in den Schlüsseln haben,
müssen sie eigentlich berücksichtigen, um es richtig gut zu machen.
Okay, das sind Anwendungsverursachte Häufungen, die auftreten können und die müsste man,
müsste man vermeiden und dafür kann man kein Rezept angeben.
Dafür muss man wissen, wie die Daten verteilt sind.
So, wie kann man jetzt die Qualität von Hash-Funktionen anschauen
Also, es gibt sehr viele mögliche Hash-Funktionen und eigentlich können
wir das nicht. Wir können es nur, wenn wir
etwas über die Schlüssel wissen. Und wenn wir sagen,
die Schlüssel sind gleich verteilt, okay,
dann können wir auch was über die Qualität einer Hash-Funktion sagen,
aber in der Regel ist es dann eben nicht so.
Was man einfach macht, ist, man sucht irgendetwas,
um zumindest so klassische Muster zu vermeiden.
Was hilft in der Regel beim Hashen? Also so Muster
sind zum Beispiel, wenn sie mal angenommen ihre Schlüssel sind,
ganz zahlig und sie würden das wieder auf ganze Zahlen ab.
Also sowas, wenn sie zum Beispiel immer alle geraden Zahlen,
auch auf gerade Hash-Werte abbilden,
das wäre so ein Muster,
was sie hätten
Und dann könnte es schon sein,
dass eine Anwendung viel mehr gerade zahlen hat als ungerade.
Und dann hätten sie so eine ungewollte Häufung. Also irgendwas,
was, naja, es hat so ein bisschen was von Zufallszahlengeneratoren,
die irgendwas, was möglichst keinerlei Muster,
die sie haben können, wieder abbildet auf diese Hash-Adressen.
Okay, also im Folgenden machen wir das wie folgt.
Wir haben also kaum mögliche Schlüsselwerte und unsere Schlüssel sind schon überführt in ganze Zahlen.
Also die sind alle aus N-0. Und das ist auch in Ordnung,
also selbst wenn wir Buchstabenkombinationen oder irgendwas haben,
wir können, wir wissen ja sowieso,
unsere Daten werden binärkodiert Nullen und Einzelnen im Computer.
Das heißt, jeder Schlüssel können konvertiert werden in eine 0
-1-Folge und damit können wir noch eine natürliche Zahl überfüllen.
Diese Konvertierung ist aber auch nicht ohne,
wenn wir die Konvertierung ungeschickt machen, produzieren wir bei der Konvertierung Häufung.
Kann auch passieren. Deswegen sollte man diesen Schritt bei
der Entwicklung von Hashing-Verfahren nicht außer Acht lassen.
Überlegen Sie sich genau, wie Sie einen Schlüssel auf eine Zahl abbilden.
So, okay, was sind typische Hash-Funktionen,
das sind die zwei geläufigsten, die es gibt.
Die klassische ist die Divisionsmethode, die Visionsmethode bedeutet einfach,
Sie nehmen den Schlüsselwert, den Sie haben und rechnen den
Modul OM
M ist ihre Tabellengröße, die zur Verfügung haben. Und dann wissen sie,
da kommt als Ergebnissenwert von 0 bis M minus 1 raus.
Und das ist einfach die Adresse, wo sie das Objekt speichern.
Da treten dann aber genau diese Probleme auf, die ich gerade schon erwähnt habe.
Also jetzt müssen wir aufpassen, was verwenden wir als M?
Wie groß sollte diese Tabelle sein? Wenn M gerade ist,
also mal angenommen, das wäre jetzt irgendwie, keine Ahnung,
16, dann ist, haben wir schon diese Häufung,
die ich gerade beschrieben habe, also ein Schlüssel,
der gerade ist, führt auch zu einer Hash-Adresse,
die gerade ist.
Also wenn sie mal Modulo 16 rechnen,
dann werden gerade Zahlen beim Gradezahlen ungerade,
werden Ungrade zahlen.
Das ist ungünstig
Und das Nächste, was man machen könnte, wäre zu sagen,
wir nehmen eine Zweierpotenz, zwei hochp. Da gibt es Gründe,
das zu tun, weil wenn sie zwei hochp nehmen,
dann rechnen sie hier K-Modulo einer Zweierpotenz und für die,
die etwas einen Blick ein bisschen noch für Hardware haben,
das ist bei Informatikern ja sehr unterschiedlich.
Der eine oder andere hat das vielleicht noch, der weiß,
dass eine Monoloop-Operation durch eine Zweierpotenz wesentlich schneller zu berechnen ist,
als die Modulo-Operation durch eine beliebige andere Zahl,
weil die Monolooperation durch eine Zweierpotenz einfach nur ein Bitshift
ist.
Sie können das Bit-Muster einfach verschieben
So, das könnte man machen, aber dann hat man natürlich genau diesen Effekt,
dann ist das gerade, ist irgendwie nicht so günstig.
Was man dann als Nächstes sofort denkt, ist,
man sagt, ja, wir brauchen eine Primzahl, klar,
dann haben wir keine Tyler in M und das ist gut,
das führt dazu, dass es alles schön verteilt wird.
Das ist korrekt. Deswegen beiden häufig bei Hash-Tabellen Primzahlen verwendet. Ich kann ihn aus der Praxis berichten,
wir haben sowas lange gemacht, wir haben das in der Praxis eingesetzt,
also solche Hashing-Verfahren haben natürlich immer artig Primzahlen verwendet,
weil man das so sagt,
dass man das macht und haben dann spaßeshalber,
also bei wirklich großen Anwendungen, wo wir tausend von Objekten den Hash-Tabellen speichern,
haben wir spaßeshalber mal das gemacht,
was man vielleicht eigentlich eher nicht tun sollte,
nämlich eine Zweier-Potenz genommen, ja.
Und wenn die Daten dann keine Häufung produzieren,
das wissen sie nicht, aber wenn sie keine produzieren,
können sie diese Hash-Funktion so viel schneller ausrechnen,
wie die Division durch eine Primzahl,
dass sie damit potenzielle Randeffekte der Häufung sofort in der Laufzeit wettmachen, ja.
Das Verfahren war mit einer Zweier-Potenz viel schneller als mit einer Primzahl. Das heißt,
man muss genau gucken, wie die Anwendung aussieht,
wie sehen die Schlüssel aus, die gespeichert werden,
um so eine Entscheidung dann zu treffen, ja.
Okay, Also steht hier auch noch eine Oma drunter,
wählen die Schlüsselmenge K in der Universalmenge gleich verteilt ist,
dann ergibt auch so eine Tabellengröße wie zwei Hoch-P. Keine Häufung.
Nur wenn sie quasi schon in der Schlüsselmenge Häufung haben,
die übertragen sich eben schneller.
Und in der Anwendung haben wir typischerweise Muster in Daten.
Das ist das, was typischerweise eben vorkommt. Das heißt,
wenn das so ist,
dann ist die Modulu 2 Hoch-P-Operation wesentlich effizienter als die Modulu-M-Operation,
wenn er meine Prinzahl ist und dann ist durchaus
das eine sehr gute Tabellengröße
Okay, gut. So, eine andere Möglichkeit,
Hash-Funktionen zu bilden, die auch noch parametrisiert werden kann.
Die ist ein bisschen schöner,
ist die folgende und dafür brauchen wir erst nochmal eine Definition.
Wir werden hier definieren, was es bedeutet,
Modulu 1 zu rechnen. Modulu 1 heißt eigentlich nur,
also dass wenn X eine reelle Zahl ist,
dann meinen wir damit einfach den ganz zahligen Rest.
Also mit dem X-minus den ganzzahligen Anteil von X,
das ist einfach der Rest von X. Also 7,3,
7,3 Modulu 1 wäre 0,3. Ganz teiliger Rest,
das ist alles
So, wenn wir das definiert haben,
dann können wir die Folgen der Hash-Funktion beschreiben.
Das macht die Multiplikationsmethode. Die Hash-Funktion bildet also die Universalmenge
wieder auf die Schlüsselwenge von 0 bis M minus 1 ab.
Und die Hash-Funktion geht dann so, also hier kommt der Schlüssel rein.
Wir multiplizieren diesen Schlüssel mit einer Zahl A. Die Zahl A
habe ich ihn nicht verraten. Dann rechnen wir das Modulo 1.
Dann kriegen wir also diesen ganz zahligen Rest dieser Zahl heraus.
Und das multiplizieren wir mit der Größe M. Also diese Zahl hier
in den Klammern, die ist ja zwischen 0 und 1.
Das ist ja eine Zahl, sozusagen der ganz zahlige Rest,
der muss ja zwischen 0 und 1 liegen.
Und das multiplizieren wir mit M. Und dann runden wir das
nach unten ab. Und dann kriegen wir eine Hash-Adresse heraus.
Okay, das Gute daran ist, wir haben jetzt so
einen freien Parameter A, den können wir wählen.
Und das ist unabhängig von der Tabellengröße.
Die Tabellengröße hat ja noch einen anderen Effekt.
Also die Tabellengröße entscheidet darüber, wie viel Speicher sie reservieren.
Und das geht wieder an diejenigen, die etwas Hardware näher denken.
Speichermanagement ist eben auch nicht ohne. Und es ist eben
ein Unterschied, ob ich eine Primzahlanzahl von Bytes brauche oder
ob ich eine Zweierpotenz an Anzahl von Bytes-Wacher.
Das passt besser zum Paging, ja. Also das Betriebssystem geht
wesentlich effizienter damit um, wenn sie natürlich entsprechende Speicherblöcke verwenden,
die kompatibel ist zum Speichermanagement des Betriebssystems.
Also es wäre sehr schön, wenn man die Hash-Funktion
eben unabhängig von der Tabellengröße wählen kann.
Und das leistet eben diese Hash-Funktion hier.
Und wir können also jetzt ohne Großnachteile zu befürchten,
können wir also eine Zweierpotenz verwenden für die Tabellengröße.
Und können dann noch den Parameter A wählen. Und wie wählt man den? So,
und jetzt kommt wieder so ein Teil, wo ich sage,
das mathematisch gut zu begründen, dass das eine gute Wahl ist,
ist sehr, sehr aufwendig.
Das ist jetzt was, was wir mir lang glauben müssen.
Also, was man macht, ist, man verwendet,
man sagt, ah, sollte irgendwas sein,
irgendeine Zahl S durch zwei hoch weh und w sollte die Wortlänge des Computers sein.
Und das hat wieder damit zu tun,
dass sie schnell berechnen können, möchten. Ja,
das ist halt mit der Berechnungsgeschwindigkeit dieser Funktion zu tun.
Und wenn sie dem Informatik-Guru glauben,
dann müssen sie A wählen in der Nähe des goldenen Schnitzs,
also irgendwo bei Wurzel 5 minus 1 halbe.
Was müssen sie jetzt, wie gesagt, glauben,
dass das eine gute Wahl ist,
das zu machen
Okay, so, wenn sie das A so beschreiben,
also als eine Zahl S durch 2 HoW,
dann können sie relativ effizient multiplizieren, weil wenn das die Wortlänge ist,
dann haben sie hier einen Faktor 2 Hochweh und dann können
sie einfach die Hash-Adresse aus dem Bitmuster extrahieren.
Aber das ist sozusagen die technische Kür. Hier geht es darum,
wirklich die Berechnung einer so einfachen, für sie einfach mathematischen Funktion,
so effizient wie möglich zu gestalten.
Okay, also das sind die zwei klassischen Verfahren.
Und wenn Sie in der Praxis in die Verlegenheit kommen,
eine Hash-Funktion zu entwickeln, nehmen Sie eine von den beiden
So, nun, werden wir nochmal überlegen,
ob wir nicht vielleicht mit den Hash-Funktionen noch
ein bisschen effizienter umgehen können und überlegen können,
ob es nicht irgendwie so eine Menge von Hash-Funktionen gibt,
aus der wir uns das Ideales rauspicken können.
Das bezeichnen wir als universelles Hashing. Also so eine Hash-Funktion
für so eine Hash-Funktion gibt es immer natürlich eine Kollision.
Es gibt immer eine Menge von Objekten,
die auf das Gleiche, auf den gleichen Schlüssel abgebildet werden.
Das wissen wir schon. Und die Frage ist jetzt,
wie können wir im Mittel
eine gute Performance haben
Und was wir machen, ist, wir sagen einfach,
wir nehmen zufällig eine Hash-Funktion, unabhängig von der Schlüsselmenge.
Wir suchen uns einfach irgendeine zufällig raus und dafür brauchen wir
eine Menge von Hash-Funktionen, wo wir zufällig auswählen können.
Ich gebe Ihnen mal wieder eine Analogie,
wenn Ihnen das jetzt so abstrakt erscheint,
denken Sie mal an den Quicksort, den haben wir ja gesehen.
Und beim Quicksort war es ja so, dass wir auch gesagt haben,
Worst Case war das alles ungünstig, ein Quadrat.
Und dann haben wir gesagt,
wenn wir aber zufällig ein Opiveau-Element wählen,
dann können wir zeigen,
dass unabhängig wie die Daten verteilt sind,
wir immer N-Log-N-Laufzeit im Mittel erreichen können
Ist es ja auch. Wir wollen keine Aussage mehr darüber machen,
wie die Daten verteilt sind. Wir wollen es mit dem Benutzerproblem
nicht mehr auseinandersetzen an dieser Stelle. Und wir sagen einfach,
naja, wenn wir zufällig eine Hashing-Funktion auswählen,
dann haben wir genau den Effekt, wie bei dem Quicksal,
wo wir zufällig das Büroelement ausgewählt haben.
Das heißt, im Mittel werden wir eine gute Performance erreichen.
Jetzt brauchen wir aber so eine Menge, aus der wir auswählen können.
Das war ja beim Fivolement ganz einfach. Da hatten wir
einfach Zahlen. Bei dem Quicksal konnten wir uns eine aussuchen.
Wir zufällig alle eine von diesen Endzahlen genommen. Hier schon
schwieriger. Jetzt brauchen wir eine große Menge von Funktionen,
aus der wir dann zufällig eine Funktion auswählen können
So, also so eine Menge Haar von Funktionen,
das sind also Hash-Funktionen, ha,
die bilden alle die Universalmenge auf diesem Bereich von 0 bis M minus 1 Funktionen ab,
so eine Menge Haar von solchen Funktionen, die nennen wir universell,
genau dann, wenn sie die Eigenschaft hat,
das für alle Schlüsselpaare, die wir bilden können,
aus der Universalmenge, also es waren beliebige Objekte,
die wir speichern können, ist die Anzahl der Hash-Funktionen,
die für diese zwei Objekte eine Kollision bilden,
gerade kleinergleich die Anzahl der Hash-Funktionen,
die wir haben durch M, ja, das heißt,
wir gucken jetzt in die Menge aller Hash-Funktionen und wir sehen,
die sind bezüglich der Wahrscheinlichkeit,
dass sie eine Kollision produzieren,
zwischen zwei Objekten
immer gleich verteilt
Das ist eine super Eigenschaft, wenn wir die haben,
dann wissen wir tatsächlich, dass wir hier aus dieser Menge
was auswählen können und wir mit hoher Wahrscheinlichkeit, ob eine Hash-Funktion haben,
die keine Kollision oder eine geringe Anzahl von Kollisionen,
also zumindest keine Häufung haben wird.
Okay, also bei zufälliger Wahl einer Hash-Funktion und zwar ja
Zahlen K&L aus der Universalenmenge ist dann eben die Wahrscheinlichkeit für
eine Kollision kleiner gleich eins durch M.
Das ist genau das, was wir gerne hätten.
Wie findet man so
eine Menge von universellen Funktionen
Alles andere als trivial, aber wir wollen zunächst erst mal schauen,
was jetzt für die Laufzeit gilt,
wenn wir tatsächlich so eine Menge von Hash-Funktionen haben.
Also wir nehmen jetzt also eine Funktion H aus der Universalmenge
H raus, also eine Hash-Funktion aus dieser Menge heraus.
Wir wissen, die haben diese Gleichverteilungseigenschaft und H,
ist jetzt unsere Hash-Funktion, bildet also die Universalmenge auf die Hash-Adressen.
So, jetzt speichern wir die Objekte in der Tabelle und
nach Speicherung von Endschlüsseln in der Tabelle haben wir also N-Objekte gespeichert.
Wir verwenden Hashing mit Verkettung nach wie vor
Gilt für die erwartete, für einen Schlüssel K,
die erwartete Listenlänge. Alpha, wenn das K nicht in
der Menge drin ist und eins plus Alpha sonst.
Das wollen wir zeigen. Also wenn wir jetzt also neuen
Schlüssel einfügen, wie lang ist dann entsprechend schon diese Liste.
Okay, wie sehen wir das? Wir haben also ein paar von Objekten,
die wir speichern wollen, aus der Universalmenge und wir betrachten wieder das Kollisionsereignis.
Das heißt, dass für diese zufällig ausgewählte Hash-Funktion der Hash-Wert gleich ist, haben wir eine Kollision. Wir wissen,
H ist universell, das heißt, die Wahrscheinlichkeit, dass Kollision auftritt,
ist kleiner gleich eins durch M und dann wissen wir,
was ist die erwartete Kollisionszahl, die ist einfach nur,
naja, alle möglichen Ereignisse, mal die Wahrscheinlichkeit,
dass ein Ereignis eintritt, also von Z gleich 0 bis 1,
also 0 oder eins, Kollision oder keine Kollision,
Wahrscheinlichkeit für die Kollision,
ist eins durch M und die Wahrscheinlichkeit für die Tatsache,
dass keine Kollision auftritt, wäre eins, minus eins durch M,
das interessiert uns aber nicht, weil wir zählen ja nur die Kollisionen,
das heißt, das ist eins durch M.
Also die Wahrscheinlichkeit, dass eine Kollision auftritt ist eins durch M. Ja,
die erwartete Listendänge ist dann nichts weiter als die Anzahl der Kollisionen,
die wir haben, wir gehen also durch die Tabelle und gucken,
wie viel haben wir,
das ist eins durch M und das summieren wir auf und dann haben wir den Belegungsfaktor Alpha.
Das ist ja das, was wir erwarten würden. Wenn das K in der Tabelle vorkommt,
dann ist die Situation ein ganz klein bisschen anders,
weil wir wissen, dass wir an einer Stelle hier diese Variable,
dass diese Variable eins ist, nämlich dann,
wenn wir das Objekt K tatsächlich sehen, das müssen wir aus der Summe herausziehen.
Also das ist alles noch gleich und wir sagen dann,
wir haben eins plus und dann haben wir die Summe über alle Objekte aus der Hash-Tabelle,
ohne das K und dann haben wir wieder den Fall,
dass die alle nicht in der Tabelle vorkommen
Können wir dann wieder eins durch M einsetzen. Und dann
kommt es ja eins plus Alpha raus. Das heißt,
unabhängig von der Verteilung oder von der Häufung,
die wir in den Eingabedaten haben, haben wir die Performance,
die wir gesehen haben für das Hashing mit Verkettung.
Und vorher haben wir noch vorausgesetzt,
dass unsere Eingabeschlüssel bezüglich der Hashfunktion diese Gleichverteilung haben.
Das brauchen wir jetzt nicht mehr.
Wir müssen aber aus dieser universellen Funktionsmenge
eine zufällig auswählen
So, okay, dann können wir direkt folgern,
also universelles Hashing mit Verkettung und M-Speicherplätzen.
Da haben wir die erwartete Laufzeit für P-Einfügen suchen und Löschoperationen,
wobei höchstens M-Einfüge-Operationen da sind, das brauchen wir,
sonst ist der Belegungsfaktor zu hoch, ist ein O von P,
also konstante Zeit pro Einfüge, Such- und Löschoperation.
Und das ist auch klar, dass das so ist,
also nach Ofer und M-Inside-Operation, haben wir den Bewegungsfaktor Alpha,
haben wir also M-Objekte gespeichert,
maximal M-Objekte und das ist konstant.
Wir dürfen also nicht mehr Objekte einfügen,
wie die Hash-Tabelle groß ist
Und dann haben wir, wissen wir,
dass die Inset und die DVD-Operation O von 1 ist,
das haben wir vorher schon gesehen, die Suchoperation hängt von der Listenlänge ab.
Das hatten wir auch schon gesehen.
Die Listenlänge hängt von Alpha ab, haben wir gerade gesehen.
Das heißt, die Suche läuft in 1 plus Alpha.
Und wenn das Alpha kleiner gleich einer Konstanten ist,
dann ist das auch in konstanter Zeit möglich.
Auch hier, also perfekt, ein Wörterbuch, einfügen,
löschen, suchen alle Operationen in konstanter Zeit,
unabhängig davon, wie die Verteilung der Eingabedatenschlüssel ist,
wie das ist,
ob da Häufung spielt alles
keine Rolle
Aber ganz wichtig, das ist die erwartete Laufzeit,
also wieder nur das, was sie im Mittel erreichen können.
Es ist nicht der Worst Case.
Der Worst Case ist nach wie vor Ovon N.
Alles in einer Liste und dann liegen die da dran.
So, jetzt brauchen wir so eine universelle Hash-Funktion.
Jetzt kommt wieder der mathematische Teil. Jetzt müssen wir sagen,
wie sieht denn so eine universelle Menge von Hash-Funktionen aus?
Und da sage ich ihnen wieder nur, wie sie aussieht,
nicht warum das eine gute universelle Menge von Hash-Funktion ist.
Also so bildet man eine universelle Menge von Hash-Funktionen.
Wir wählen eine Primzahl P,
die größer ist als
Ja, als die Größe der Hash-Tabelle, die wir haben.
Und P sollte auch größer sein als der größtmögliche Schlüssel der Universalmenge.
Also eine richtig große Primzahl. So,
dann bezeichnen wir mal mit ZDX, das ist nur eine Bezeichnung,
die Menge der ganzen Zahlen von 0 bis 0 bis x
minus 1. Also eine beschränkte Menge von ganzen Zahlen.
Und was wir dann machen ist, wir sagen also HPM,
das ist jetzt also eine Menge von Hash-Funktionen und die Hash-Funktionen da drin, die sehen alle so aus.
HAB, die haben zwei Parameter, die bilden dann U nach ZM-Up, in diese Schlüsselmenge. Und die Hash-Funktion ist einfach so,
dass wir sagen, okay,
wir haben zwei Parameter AB und hier haben wir das K,
den Schlüssel und statt einfach nur K-Modul-OM zu rechnen,
das hatten wir vorher, machen wir jetzt folgendes,
wir rechnen A mal K plus B,
wir multiplizieren das mit A und addieren da noch ein B drauf und das waren die Parameter der Hash-Funktion hier,
also die man noch einstellen kann,
rechnen dann Module dieser großen Primzahl und danach Module M,
ja.
So, und wie wählen wir jetzt A und B,
das sind jetzt also dadurch, dass wir A und B unterschiedlich wählen,
kriegen wir eine Menge von Hash-Funktionen raus. So,
und A und B können wir beliebig wählen mit der Eigenschaft,
dass A aus der Menge der ganzen Zahlen bis zu dieser Primzahl P ist,
ohne die Zahlen null, weil wenn das null wäre,
das wäre blöd, dann wäre das ja nicht mehr abhängig vom Schlüssel.
Und B kann eine beliebige Zahl aus der Menge von null
bis maximal zu dieser Brimenzahl sein.
Diese Konstruktion, also diese Menge von Hash-Funktionen,
hat die Eigenschaft, dass sie universell ist.
Das kann man beweisen. Da müsste ich jetzt hier,
weiß ich nicht, 15 Folien, mathematische Beweise ihnen zeigen.
Und dann würden sie sehen, aha,
das erfüllt alle Eigenschaften der universellen Hash-Funktion.
Das machen wir nicht. Das ist wieder der Teil,
den sie mir glauben müssen. Okay, so,
jetzt wollen wir uns einer anderen Strategie zuwenden,
wie wir mit den Kollisionen umgehen
Bis gerade haben wir Listen gebildet. Wir haben einfach gesagt,
Kollision, kein Problem. An jeder Stelle ist eine Liste,
da hängen wir das an. Aber in der Praxis ist
das Verwenden von Listen im Kontext mit von Hashing eigentlich sehr ungünstig.
Weil sie müssen überlegen, also Hashing wird häufig sehr hardwarenah eingesetzt.
Hashing benutzt das Betriebssystem, damit sie damit das Memory-Management funktioniert und solche Dinge.
Da sind wir sehr hardwarenah. Da wollen wir nicht irgendwie
mit Listen operieren. Und was noch schlimmer ist, ist,
eigentlich wollen sie ja mit M sagen, wie viel Platz sie reservieren wollen.
Sie wollen sagen, als Programm Mira oder Entwickler. Okay,
also für diese Hash-Tabelle, da opfere ich jetzt,
keine Ahnung, 10 Gigabyte oder 1 Gigabyte oder so.
Und das ist der Platz dafür. Beim Hashen mit Verketten funktioniert das aber nicht,
weil sie sagen dann einmal, ja, okay,
ich gehe mir jetzt 1 Gigabyte für die Tabelle,
aber dann kommen ja die Listen.
Und es kann sein, dass ihre ganze Tabelle leer ist,
aber sie haben ein paar Positionen, lange Listen haben.
Das heißt, sie können als Programmierer nicht von vornherein festlegen,
wie viel Platz sie maximal verwenden wollen zur Speicherung.
Das ist ungünstig. Also was irgendwie schöner wäre, wäre,
wir würden wirklich einmal sagen, das ist der Platz,
sollen die Daten gespeichert werden. Und wenn es eine Kollision gibt,
dann sucht ihr halt eine andere Stelle,
aber bitte in diesem Speicherbereich und nicht dadurch,
dass du noch ein Objekt in der Liste hast und das irgendwo anhängst.
Das ist eigentlich die bessere Strategie, um mit Kollisionen
umzugehen und wird im Allgemeinen als offene Adressierung bezeichnet.
Okay, also Nachteile von Hashing mit Verkettung sind.
Wir haben immer einen zusätzlichen Speicherbedarf, also schon mal für die Zeiger,
den habe ich noch gar nicht geredet,
aber wir haben auch eine komplexe Speicherverwaltung.
Wir müssen immer gucken, wo die Listenelemente sind,
die frei, sind die belegt,
etc. und wir haben eine ineffiziente Speichernutzung bei Häufung.
Das sind die ungünstigen
Aspekte. Offene Adressierung funktioniert also so, wir legen nur einmal Speicher an für die Hash-Tabelle.
Das muss reichen. Alle Objekte müssen in diese Tabelle hinein.
Ja, das ist die Bedingung. Das erreichen wir dadurch,
dass wir für Überläufer, also für Kollision,
wir haben so ein Objekt, das können wir da nicht speichern,
suchen wir einfach mit einer geschickten Strategie einen anderen Platz,
aber in der Tabelle und nicht,
indem wir ein zusätzliches Objekt irgendwo anhängen.
Den Platz müssen wir berechnen und wenn wir den berechnen,
müssen wir uns keine Zeiger merken. Wir müssen da nicht sagen,
wo steht denn das nächste Objekt,
wenn wir eine Regel haben, was uns sagt,
das nächste Objekt steht an der und der Position
und wir haben eine Funktion,
können wir
das ausrechnen
Also kein Overhead, dadurch, dass wir noch Zeiger brauchen,
um die Listenstruktur zu realisieren.
Die Strategie, die man jetzt braucht, ist das sogenannte Sondieren.
Ja, Sondieren heißt in diesem Kontext, wir suchen nach
einer Position in dieser Tabelle, wo wir das Objekt speichern können.
Ja, wir sondieren die Tabelle. Wir gucken,
wo ist die, wo ist der freie Platz für unser Objekt?
Das machen wir in einer Reihenfolge. Also wir sagen erstmal,
wir wollen das da speichern und wenn du sagen,
da ist aber eine Kollision, dann sagen, okay,
dann suchen wir eben eine andere Position.
Dann nehmen wir eben diese Speicherzelle. Wenn wir dann auch
eine Kollision haben, müssen wir noch eine und dann finden
wir irgendwann eine Position und da legen wir das rein
Das ist die Sondierungssequenz. Also die Reihenfolge, in der
wir Speicherzellen überprüfen, ob wir das Objekt dort ablegen können.
Okay, so, wir haben jetzt also wieder eine Hash-Funktion.
Die sieht jetzt ein klein bisschen komplizierter aus.
Also wir haben wieder die Universal-Menge und die bildet natürlich auch
auf diese Speicheradressen von 0 bis M minus 1 ab.
Aber die hat noch einen zweiten Parameter.
Der zweite Parameter läuft hier von 0 bis M minus 1.
Und der zweite Parameter, der sagt uns,
der wievielte Versuch es ist, dieses Objekt in der Tabelle zu speichern.
Also wir nehmen jetzt also unsere Hash-Funktion,
wir haben also den Schlüssel K und sagen jetzt,
okay, jetzt machen wir wir den nullten Versuch,
fangen wir mal bei Null an zu zählen.
Den nullten Versuch und gucken mal, ob wir das in die Hashtag-Welle reinlegen können.
Und dann rufen wir die Hash-Funktion auf mit K und dem Parameter Null.
Nullter Sondährungsversuch. Wenn das okay ist, dann speichern wir
das Objekt da. Wenn da schon was steht,
dann haben wir eine Kollision und dann sagen wir,
okay, kein Problem,
wir rufen die Hash-Funktion nochmal auf mit unserem Schlüssel und jetzt den Parameter 1 für den dann zweiten Sondierungsversuch.
Dann kriegen wir eine andere Speicheradresse genannt und dann gucken wir da,
ob wir das Objekt abspeichern können. Und dann machen wir das so lange weiter,
bis wir durch sind und quasi alle Positionen geprüft haben,
wenn das so ist,
also wenn wir hier das M-1 mal gemacht haben und wir eine gute Hash-Funktion haben,
dann haben wir keinen Platz mehr.
Dann haben wir überall geguckt in der Hash-Tabelle, es war
überhaupt keinen Platz mehr da. Und das ist natürlich der Punkt,
wo auch die offene Adressierung nicht mehr auskommt mit der Speichermenge.
Also wenn sie natürlich mehr Objekte speichern wollen,
als sie Platz haben, das geht natürlich da auch nicht mehr.
Okay. So, wie funktioniert das jetzt? Jetzt müssen wir Objekte erstmal einfügen und es geht jetzt also so,
wir haben also eine Hash-Funktion, die kriegt zwei Parameter,
K ist der Schlüssel und J ist der Sondierungsversuch Ja,
und wir müssen darauf achten,
dass diese Funktion für alle Werte K eine bijektive Funktion ist.
Wissen Sie alle noch, was bijektive Funktionen sind?
Ja, ganz eindeutig. Okay, also, egal welchen Schlüssel
wir hier einsetzen, haben wir hier M-1-Sondierungsversuche. Und diese M-1-Sondierungsfunktionen versuche,
müssen uns am Ende alle M viele, nee,
wir haben, Entschuldigung,
wir haben M viele Sondierungsversuche von 0 bis M minus 1
und jeder Sondierungsversuch liefert uns eine Speicheradresse und wir müssen am Ende,
wenn wir die M-Versuche haben,
Auch alle M-Speicherpositionen durchprobiert haben.
Das heißt, die Funktion muss die Eigenschaft haben,
dass sie jedes Mal eine andere Speicherzelle vorschlägt.
Die darf beim fünften Mal nicht die gleiche Speicherzelle vorschlagen,
wie beim dritten Mal.
Das muss immer eine andere sein. Und das bedeutet nichts weiter,
als dass die Funktion für einen speziellen Schlüssel bjektiv ist.
Das heißt, die Menge von 0 bis M minus 1
auf 0 bis M minus 1 wiederum abbildet.
Und zwar so, dass jeder Wert hier eindeutig einen Wert auf dieser Seite bekommt.
Das ist bjektiv. So, und das sichert,
dass im Laufe der Sondierung alle Speicherplätze getestet werden.
Das ist eher einfach wichtig, weil sonst haben wir
die Tabelle vielleicht halb voll und wir sagen, es ist kein Platz mehr.
Das wäre ja ungünstig. So, die Sondierungssequenz hängt aber
jetzt natürlich vom Schlüssel K ab. Das ist entscheidend.
Also je nachdem, was für einen Schlüssel wir reintun,
kriegen wir eine andere Sequenz.
Okay, so wie gehen wir jetzt vor?
Also wir werden unsere Tabelle jetzt wie folgt umbauen.
Wir haben jetzt also eine Tabelle. Hier,
die hat wieder diese M-Speicherplätze und an der Stelle TI.
Da können wir natürlich einen Schlüssel speichern. Das ist die
Universalmenge. Wir haben aber noch zwei zusätzliche Sonderwerte,
zwei außergewöhnliche Werte und die heißen Neil und Deleted
Warum wir Deleted brauchen, werden wir gleich sehen. Das Neil besagt einfach,
dass wir an dieser Stelle momentan noch nichts gespeichert haben.
Und ja, zum Deleted kommen wir später. Also am Anfang
ist es so, also wir ignorieren ja alle Satellitendaten,
die können sie natürlich auch alle speichern,
aber wir gucken uns nur die Schlüssel an.
Am Anfang ist es so, diese Tabelle ist leer.
Das heißt, da stehen nur Neilwerte drin.
Also im ganzen Array, überall, alles Neil.
Das heißt, keine Sachen sind dort gespeichert. So,
wenn wir jetzt ein, was speichern wollen, dann verwunden,
verwenden wir diese Funktion, die ist noch nicht fertig,
deswegen heißt die hier pre,
wir werden gleich die korrekte Funktion sehen.
Ja, also, aber im Moment, machen wir folgendes.
Wir sagen, okay, also I ist null.
Und I zählt jetzt quasi unsere Sondierungsversuche.
Wir haben hier diese Repeach-Leife, die läuft so lange,
bis das I gleich Emm ist. Dann haben wir alle,
haben wir M mal versucht zu sondieren und das Objekt zu speichern.
Und wenn das M mal fehlgeschlagen ist,
dann wissen wir, es ist einfach kein Platz da.
Wir haben alle Speicherzellen überprüft. Das heißt, es gibt
da eine Fehlermeldung hier, dass die Hashtagel voll ist.
Anderenfalls wenden wir jetzt unsere Hash-Funktion an mit dem Schlüssel K und dem Iden Sondierungsversuch.
Wir kriegen dann die Hash-Adresse J. Wir gucken,
ob an der Stelle T von J. Nil steht,
dann müssen wir,
da ist nichts gespannt
Und sagen, super, keine Kollision, gespeichert an der Stelle dann unseren Schlüssel K.
Und dann können wir auch noch J zurückgeben und der
aufrufenden Funktion mitteilen, wo das in der Tabelle steht.
Müssen Sie nicht, können Sie machen. Und wenn wir
eine Kollision haben, also das ist hier ungleich,
Nil, dann steht da schon was,
dann erhöhen wir den Zähler für die Sondierungsversuche und wiederholen das Ganze nochmal.
Wenn hier wieder die Hash-Funktion an und so weiter, ja.
Ja, da haben Sie vollkommen rechts und das
werden wir gleich sehen, wenn ich hier das Pre lösche
Okay, so, mit dem Löschen haben wir uns noch nicht beschäftigt.
Also wir gucken uns gleich das Löschen an. Jetzt machen
wir momentan nur einfügen und suchen und tu mal so,
als würden wir nie löschen.
Okay, so, wie suche ich jetzt in der Tabelle?
Natürlich genauso. Der Suchalgorithmus ist genauso. Also Suche ist,
ich habe wieder meine Sondierungssequenz, ich packe an der Position null an.
Ich habe meine Repeat an Tischleife. Ich hole mir hier die Ite,
die Ite-Position mit der Hash-Funktion für Schlüssel K. Ich gucke,
ob an der Stelle K gespeichert ist.
Da kann ja jetzt auch was anderes stehen. Könnte ja
sein, dass ich eine Kollision hatte und da stand,
steht jetzt
was anderes
Wenn das da steht, habe ich es gefunden, dann gebe ich J zurück.
Anderenfalls nehme ich die nächste Position in der Sondierungsfolge.
Und jetzt muss ich aufpassen, jetzt können zwei Fälle eintreten.
Entweder ich habe die Sondierungsfolge komplett abgearbeitet, dann ist I gleich M.
Das wäre der Fall, wenn die Tabelle tatsächlich voll ist.
Und anderenfalls, dann höre ich natürlich auf.
Anderenfalls könnte es aber auch die Situation eintreten,
dass an dieser Stelle Nil steht. Und dann,
wenn hier Nil steht, weiß ich,
ich habe am Ende einer Liste erreicht, weil wenn K vorkäme,
wäre das spätestens die Position gewesen,
wo ich
es gespeichert hätte
Da habe ich es aber nicht gespeichert. Also du kannst
auch nirgendwo anders in der Hash-Tabelle stehen. Also das markiert
quasi das Ende dieser Folge und da kann ich dann aufhören zu suchen.
Und dann würde ich dann nie auch zurückgeben.
Okay, das ist eigentlich relativ trivial, so vorzugehen.
Und das Einzige, was wir uns anschauen müssen, ist,
ja, wie geht denn das jetzt mit dem Löschen?
Weil das Löschen ist jetzt ein bisschen unangenehm,
weil wenn wir jetzt etwas löschen und wir stellen uns mal vor,
wir schreiben dann wieder ein Neil da rein,
dann unterbrechen wir ja die Folge und wir würden bei der Suche aufhören,
obwohl vielleicht irgendwo weiter hinten noch das K kommt.
Das heißt, wir können nicht einfach beim Löschen an
einer Speicherzelle wieder nie reinschreiben.
Das macht die Struktur kaputt
Wir müssen irgendwie anders vorgehen. Und dazu braucht man jetzt dieses zusätzliche Flag,
diese zusätzliche Markierung, das ein Feld gelöscht wurde.
Was wir tun, ist, wir suchen einen Platz Tee von,
wir suchen den Platz Tee von Ivo,
das K ist, also wenn wir jetzt löschen wollen,
wenn wir das Objektal löschen wollen.
Und was wir machen, ist, wir markieren diesen Platz
als deleted. Wenn Sie sich vorstellen,
dass wir das K wirklich rausnehmen wollen,
dann müssten Sie streng genommen die Sondierungsfolge weiter betrachten und
die Objekte nach vorne schieben
Selbst wenn sie den Aufwand treiben wollen, wenn sie darüber nachdenken,
stellen sie fest, dass selbst dann es nicht geht.
Weil sie haben ja nicht eine Liste,
sondern sie verweisen immer auf Positionen in einem Array.
Und es kann sein, dass sie ein Objekt haben,
was zwar kollidiert, was aber aus einer anderen Sondierungsfolge kommt.
Sie können die nicht mehr verschieben. Sie können es
nicht reparieren. Geht nicht, ja. Okay, deswegen
markieren wir das. Also wir markieren das mit deleted.
Und jetzt müssen wir unserer Such und Einfügen, Operationen,
die müssen wir leicht modifizieren,
weil wir uns überlegen müssen,
was passiert,
wenn wir auf ein Deleted-Feld stoßen
Wenn wir einfügen, das haben Sie gerade schon korrekt gesagt,
dann machen wir Folgendes. Wir durchlaufen unsere Sondierungssequenz. Und
wenn jetzt ein Feld Neil ist, dann wissen wir,
können wir einfügen, wenn ein Feld deleted ist,
dann wissen wir, an der Stelle steht eigentlich nichts mehr.
Da stand nur vorher was und wir haben die Markierung,
dass da nicht Neil stehen darf,
sonst würden wir ja die Sondierungsfolge unterbrechen.
Das ist aber super. Da können wir ja unser Objekt speichern.
Warum nicht? Dann steht da ja wieder was anderes.
Das heißt also, wir speichern jetzt an der Stelle TVJSK
und geben das zurück.
Das heißt, beim Einfügen sind diese Deleted-Felder eigentlich
gar nicht schädlich
Wir stören uns nicht. Wir haben dann so ein paar,
die sind delited, aber egal, wenn wir einfügen,
schreiben wir dann eben was wieder da drüber.
Und dann ist es alles ja gut. Okay,
das ist die einzige Änderung hier und dann haben wir unsere Inside-Funktion.
Ja, wie geht das löschen? Das Löschen geht so
wie hier beschrieben. Wir folgen wieder der Sondierungssequenz. Wir
suchen quasi das Objekt. Das ist also wie suchen,
wir suchen das Objekt, bis wir es gefunden haben.
Und dann schreiben wir an dieser Stelle die Leaded hin.
Ja, und das ist alles, das ist das Löschen.
Okay, ja, und wenn wir suchen,
wenn wir suchen, dann müssen wir eben aufpassen,
dass wir das deleted so behandeln,
als würde da noch was stehen.
Sonst würde unsere Sondierungsfolge abreißen. Dann finden wir das nicht wieder.
Okay, alles wunderbar. Es sieht zuerst mal so aus,
als hätten wir dieses schwierige Problem mit dem Löschen gelöst.
Aber was man sich vergegenwärtigen muss, ist,
wir müssen dafür einen Preis bezahlen und zwar einen sehr unangenehmen Preis.
Denn die Laufzeit unserer Suche hängt jetzt nicht mehr vom Belegungsfaktor ab.
Sie hängt nämlich davon ab, wie viele Felder entweder belegt sind.
Okay, das wäre der Belegungsfaktor
Aber es hängt auch davon ab, wie viele Felder diese Deleted-Markierung haben.
Weil wenn so ein Feld eine Deleted-Markierung hat,
dann ist das noch mit in der Liste drin,
in der Verkettungsfolge, in der Sondierungsfolge.
Und dann laufe ich da auch noch drüber. Das heißt,
die Suche hängt davon ab, wie viele Felder nicht Neil sind.
Und das heißt nicht, dass da ein Objekt steht,
sondern dass da entweder ein Objekt steht oder die Markierung, dass das gelöscht ist.
Wenn sie also eine Situation in der Hashtagwelle haben,
wo sie viele Objekte eingefügt haben,
viele Objekte danach gelöscht haben und dann suchen, ist die Suche teuer,
dann ist die noch genauso teuer wie zu dem Zeitpunkt,
wo alle Objekte drin waren.
Wenn Sie das vergleichen mit Hashing, mit Verkettung, ist die Situation anders.
Bei der Verkettung fügen Sie die Objekte ein,
dann dauert das lange, wenn Sie was suchen.
Wenn Sie die dann alle wieder löschen, dann sind die
aber aus den Listen raus und dann ist die Suche auch wieder schnell.
Und das ist ein ganz gravierender Unterschied und der
lässt sich bei der offenen Adressierung nicht wirklich lösen.
Also das heißt, wenn Sie viele die Lead-Operationen haben,
also viele die Lead-Operationen und danach wieder viele Inside-Operationen,
ist auch okay, dann füllen Sie ja die Lücken wieder.
Aber wenn Sie irgendwie zu einer Situation haben, wo Sie viele
die Lead-Operationen haben und danach suchen, dann ist es ungünstig.
Dann wird die Laufzeit hochgehen. So,
weil das so ein bisschen verwirrend ist, hier ein Beispiel,
wie das aussehen kann, das ist meine Hash-Tabelle hier.
Die ist relativ klein, haben also sieben Felder.
Sie sehen, hier werde ich einfach die Werte reinschreiben,
die wir da speichern. Und hier sehen Sie eine Markierung,
da steht jetzt das N, das ist für Neil,
das heißt, dieses Feld ist unbenutzt und dann gibt es
noch ein D für Deleted und dann gibt es noch ein U für Houst,
also da steht da eine Zahl drin.
Also das ist nur eine Markierung für Sie,
dann wird das ein bisschen leichter nachvollziehbar ist,
diesen zusätzlichen Speicher würde man eigentlich vermeiden und man würde einfach N und D auf Werte in naja,
in der Datenstruktur dieser Universalmenge abbilden, die einfach nicht benutzt werden.
Also dann hätte man nur ein, eins an Feld.
Also wenn zum Beispiel die Universalmenge positive ganze Zahlen werden,
dann würden sie, keine Ahnung, minus eins und minus zwei verwenden,
um das zu machen. Okay, so,
also erste Operation, Insert 15, also unsere Hash-Funktion,
ist ganz einfach, Carplus-I-Modulo 7. Ja, das bedeutet,
15 Modulo 7 ist eins und an der Position eins steht ja nicht,
schreiben wir die 15 rein.
Das ist jetzt also used. Dann fügen wir die 36
ein, 36 Modulo 7 wäre wieder eins.
Wir sehen an der Position eins steht schon was.
Wir gehen in der Sondierungsfolge einen Schritt weiter
Dann haben wir den Wert K plus 1 Module 7, also das nächste Feld hier und das ist unbenutzt, steht da die 36 drin. Wenn wir jetzt die 71 suchen würden,
würden wir sagen, okay, Sondierungsfolge,
71 Module 7 bis 1, an der Position 1,
es steht zwar ein Element,
es ist aber nicht die 71, sondern die 15,
dann gehen wir in der Sondierungsfolge 1 weiter,
dann sehen wir die 76, das ist auch nicht die 71,
gehen wir eins weiter und dann sehen wir einen Neil und dann wissen wir die 71 kommt nicht vor,
würden also zurückgeben, ja,
Neil oder minus 1 oder kommt nicht vor oder irgendwas.
So, wenn wir 17 einfügen, dann passiert jetzt Folgendes,
nein, dann passiert noch nichts Besonderes,
also 17 Module 7 wäre 3 und wir schreiben die 17 hier hin.
Und was Sie jetzt sehen, wenn wir jetzt nochmal überlegen,
wir suchen jetzt ein Objekt, wir suchen die 29,
29. Ich hoffe, Sie sind alle fit im kleinen Alma
1. 29 hat durch 7 auch den Rest 1.
Da beginnen wir wieder an dieser Position 1 mit der Sondierung.
Probieren wir diese Position aus und dann sagen wir,
steht aber nicht die 29, also die nächste Position.
Hier ist wird die 36,
ist auch nicht die 29. Nächste Position, hier steht die 17.
Und auf diese 17 an dieser Position ist sie nochmal explizit hinweisen,
diese 17 hat eigentlich einen anderen Hash-Wert,
Die 17 war Modulo 7, ja 3. Das heißt,
die 17 ist hier reingerutscht in die Liste.
Wir haben jetzt nicht mehr Sondierungsfolgen, wo alle Objekte immer
die gleiche Hash-Adresse gehabt haben und sie deswegen weiter nach hinten gerutscht sind.
Die vermischen sich. Und deswegen können wir nicht löschen.
Weil wir müssen dann, wir müssen dann rausfinden,
müssen das alles wieder aufribbeln und gucken, wo das hingehört.
Okay, können wir also nicht machen. Also wir gucken
dann die 17, das ist auch nicht die 29,
wir nehmen die nächste und da können wir jetzt die 29 hier einfügen.
So, jetzt können wir die 36 suchen,
die würden wir dann finden an der Position 2,
ist glaube ich klar.
Die 36 können wir auch löschen
Folgen wir der Sondierungsfolge, bis wir der Sondierungssequenz, bis wir die 36 finden.
Wir markieren dieses Feld als deleted. Das ist wichtig,
weil wenn wir jetzt nämlich die 29 wiederfinden wollen,
wenn jetzt hier Nil drin stehen würden, würden wir das nämlich nicht schaffen,
also 29 Modulu 7 wäre wieder die 1, wir gucken hier,
steht die 15, nächste Position der Sondierungsfolge ist diese 36, ist diese Position 2.
Hier steht ein D, das heißt, das,
was hier drin ist, ist nicht mehr valide,
es steht da eigentlich nicht mehr wirklich,
es ist dann nicht mehr wirklich gespeichert,
aber wir wissen, wir müssen in der Sondierung weitermachen,
weil da stand mal was und wir folgen also jetzt der Sondierungsfolge weiter,
wir würden dann gehen wir zu 17,
Zur Position 3 steht die 17, Position 4,
29. Und da würden wir die finden.
Antwort wäre also 4. Und wenn wir jetzt die 71 einfügen,
dann wären wir die 71, Module 7,
die 1, da steht die 15 an der Position.
Wir gehen ein Feld weiter. Da steht ein Deleted,
diese Zelle können wir füllen dann. Da schreiben wir in
die 71 rein und dann ist sie wieder just.
Ja? Okay. So funktioniert, dass die offene Adressierung. Okay,
so und wieder mal, genau wie vorher auch,
ist es eigentlich eine Frage einer geschickten Hash-Funktion,
um das richtig in Szene zu setzen.
Ja, wir brauchen eine richtig gute Hash-Funktion, die das kann.
Und auch hier zeige ich ihnen wieder,
wie man es machen sollte
Also, das erste, was wir tun können,
wir können sagen, wir sagen mal,
wir haben mal eine Hash-Funktion, so wie die,
wie wir vorher so hatten,
also Universalmenge wird abgebildet auf null bis minus eins.
Also eine klassische Hash-Funktion, Schlüssel rein, kommt eine Position,
ein Index raus und die eignet sich natürlich noch nicht für die offene Adressierung.
Wir brauchen ja auch den zweiten Parameter wegen der Sondierungsfolge.
Okay, das erste, was man macht, ist lineares Sondieren.
Das ist das, was wir gerade gesehen haben.
Wir gehen einfach in den Array,
in dem Array von einer Position immer an die nächste Position
im Array so lange,
bis wir
einen freien Platz finden
Oder als Hash-Funktion ausgedrückt, wir sagen H von K,
Kommai, Schlüssel K, Sondierungsschritt I,
ist im H-Strich von K, also irgendeine Hash-Funktion hier, plus I.
Und dann müssen wir im ersten Sondierungsschritt addieren wir null dazu,
dann addieren wir eins dazu, zwei, drei, vier.
Und dann gehen wir quasi in dem Array von einer Position zur nächsten.
Das ist exakt das, was wir gerade gemacht haben.
Also von H-Strich von K, die erste Position,
die wird immer dann der nächstgrößere Platz getestet.
Und wenn wir dann eine freie Position finden,
dann fügen wir das da ein.
Die Funktion sollte man nicht verwenden, die ist tatsächlich sehr ungünstig,
und zwar aus dem folgenden Grund.
Es entstehen lange Ketten
Sie haben dann irgendwie, das haben wir hier schon gesehen.
Also ich meine, hier ist natürlich auch,
der ist halt eine lange Kette entstanden,
weil ich immer hier an der gleichen Stelle was eingefügt habe.
Aber sie sehen, sobald sie hier auf so eine Kette
stoßen, wird die Kette automatisch auch länger.
Wenn sie jetzt irgendwas einfügen wollen, sie kommen hier hin,
dann stehen sie in dieser Position.
Zwei, ja. Und dann wissen sie, sie haben
eine Kette getroffen und dann hängen sie das hinten an die Kette an.
Je länger die Kette ist, umso größer ist die Wahrscheinlichkeit,
dass wir die Kette auch treffen.
Ja, also in dem Fall ist sie schon mehr als
50 Prozent, dass wir die Kette treffen.
Das heißt, mit der Wahrscheinlichkeit von mehr als 50 Prozent
wird mit der nächsten Einfüge pro Operation die Kette auch wieder verlängert.
Das heißt, lange Ketten, das ist Eine ganz ungünstige Situation,
die muss man vermeiden. Und dieses lineare Sondieren verursacht lange Ketten.
Deswegen verwendet man das nicht. Also die Wahrscheinlichkeit,
dass eine Kette der Länge I auf eine Kette der Länge I
plus 1 verlängert wird, ist eben I plus 1 durch M.
Das heißt, je länger die Kette, umso größer die Wahrscheinlichkeit.
Okay, dieses Problem nennen wir primäres Clustern. Das heißt,
die Objekte landen in der Hash-Tabelle immer an benachbarten Positionen und
sie haben dann so einen Bereich, wo die alle stehen.
Und die Wahrscheinlichkeit, dass der Bereich wächst, ist groß.
Das ist primäres Lastern, das wollen wir nicht
Könnte man besser sowas machen, wie quadratisches Sondieren?
Das ist das quadratic Probing. Da nehmen wir einfach
zwei Parameter und wir sagen dann, okay,
wir haben einen Haarstrich von KSCI, also C1,
ein Parameter mal i plus C2 mal i-Quadrat.
Das hätte die Folge, dass wir immer größere Sprünge machen.
Also bei der ersten Sondierung, denen wir vielleicht einfällt weiter,
bei der nächsten gehen wir dann aber vielleicht vier Felder
weiter oder neun Felder weiter, 16 Felder weiter.
Dadurch entstehen nicht so lange zusammenhängende Ketten.
Also wir vermeiden damit, dass primäre Clustern.
Aber es gibt jetzt noch ein anderes Problem.
Also erstmal müssen wir natürlich
die Funktion korrekt wählen
C1 und C2 müssen so sein, dass wir natürlich eine
korrekte Hashfunktion haben. Wie muss bijektiv sein? Okay,
das ist noch ein anderes Problem, ist wieder was mit Mathematik.
Aber es gibt noch ein anderes Problem. Wenn wir mal davon ausgehen,
ein, zwei Objekte, K und K, Strich,
die haben eine Kollision. In der Funktion H,
Strich, ja, dann hätte das ja zur Folge,
dass die ganze Sondierungsfolge durch immer eine Kollision haben.
Egal, wo die auch sind, die haben immer,
die haben genau die gleiche Sondierungsfolge, ja, weil der hintere Zeit hier,
das hängt ja gar nicht mehr von K ab.
Ja, das heißt, für zwei Objekte,
die kollidieren, haben wir genau die gleiche Folge von Sondierungsschritten
Problem, das wiederum ungünstig ist, nennen wir sekundäres Clustern.
Also primäres Clustern, alles hängt in der Hash-Tabelle aneinander,
sekundäres Clustern ist, zwei Schlüssel haben die gleiche Sondierungsfolge.
Und wir haben dann irgendwann lange Folgen, selbst wenn die
nicht hintereinander stehen, die wir immer wieder langlaufen.
Ist ebenfalls ungünstig. Jetzt kommt wieder, wie man es machen muss.
Wir nehmen einfach zwei Hash-Funktionen. Mit zwei Hash-Funktionen lösen wir
das Problem. Diese Strategie nennen wir doppeltes Hashing.
Zwei Hash-Funktionen, die erste bildet ab von null nach M-1
und die zweite von eins nach M-Strich.
Und das M-Strich ist irgendwas,
was kleiner ist als
So, die erste Hash-Funktion, die benutzen wir so wie eben.
Damit sagen wir, wo beginnen wir mit der Sondierungsfolge?
Wo ist die erste Position, wo wir gucken,
wo passt das in diese Tabelle rein?
Die zweite Hash-Funktion, die verwenden wir, um den Offset zu bestimmen.
Also, um wie viel gucken wir beim nächsten Sondieren weiter?
Es könnte eins sein, könnte aber auch fünf sein.
Dann gucken wir jede fünfte Speicherzelle. Oder vielleicht ist das
17. Dann gucken wir uns jede 17. Speicherzelle an.
Okay, so benutzen wir das. Also das heißt,
das doppelte Hashing ist also die Hash-Funktion für den Schlüssel K,
ITA Sondierungsschritt ist H1 von K, also erste Hash-Funktion,
plus e-mal, 2 von K.
Also H2 bestimmt quasi so eine Sprungweite. Und so entsprechend
laufen wir dann durch das Arry. Jetzt ist wieder die Frage,
wie muss man das machen, damit diese Funktionen auch die Eigenschaften haben,
dass sie wieder bijektiv ist und so.
Das ist wieder nicht so ganz so trivial.
Die Regel ohne Beweis ist, dass sie dann bijektiv ist,
wenn H2 von K ungleich null ist, das ist klar.
Also null darf das natürlich nicht sein, dann haben wir keine Sondierungsfolge.
Und H2 von Car muss Teil der Fremd zu M sein.
Dann kann man das zeigen, dass das so ist.
Es ist naheliegend, aber da müsste man natürlich streng genommen beweisen
So, wenn man jetzt es schafft, zwei Hash-Funktionen haben,
die stodrastisch unabhängig sind bezüglich ihrer Kollisionswahrscheinlichkeiten. Das heißt,
die Wahrscheinlichkeit, dass das mit H1 eine Kollision passiert und
gleichzeitig mit H2 eine Kollision passiert, die sollte möglichst gering,
also die sollte möglichst gering sein und gering heißt hier unabhängig.
Das heißt, die Wahrscheinlichkeit, dass in der ersten Hash-Funktion
eine Kollision auftritt, hat nichts damit zu tun,
dass in der zweiten Funktion eine Kollision auftritt.
Wenn sie das haben, dann haben sie tatsächlich M-Quadrat
viele verschiedene Sondierungssequenzen, die sie ausprobieren.
Das ist optimal. Das ist eine super Strategie
Die man verwenden kann. Also so sollte man hashen.
Sicherstellen können sie diese Eigenschaften, die sie benötigen,
wenn sie als M eine Primzahl wählen.
Jetzt müssen sie leider doch wieder durch eine Primzahl Module rechnen.
Das ist halt so. Und die zweite Hash-Funktion,
die sie wählen, ist 1 plus K,
Module M minus 2. Das sind die beiden Hash-Funktionen und mit den beiden
können sie doppeltes Hashing machen und dann ist es, funktioniert das.
Es gibt auch ein Schema für doppeltes Hashing, wo sie
Zweierpotenzen verwenden können. Da haben wir eben gesehen,
dass es eigentlich für die Größe der Hash-Tabelle günstiger ist.
Also M ist eine Zweierprozent, zwei Hochel, die Tabellengröße
Und die erste Hash-Funktion ist wieder so, wie die,
die wir hatten, also K-Modulu-M,
jetzt haben wir auch die schnelle Modulu-Operation.
Und die zweite Hash-Funktion, die sie dann bewählen müssen,
ist eins plus zweimal K, mal Modulu-M-Halbe.
Das ist super, weil N-Halbe ist nämlich auch wieder
eine Zweier-Potenz und auch diese Modulu-Operation geht sehr schnell.
Dieses Paar von Funktionen können sie sehr effizient berechnen auf dem Computer.
Und erfüllt ebenfalls die Eigenschaften, die sie brauchen,
um doppeltes Hashing effizient durchzuführen.
Also so, das ist eine super Strategie,
so sollte man
das machen
Okay, so, dass das aber tatsächlich gut ist,
das müssen wir uns noch anschauen, ja,
das habe ich Ihnen jetzt irgendwie, ich habe Ihnen gesagt,
dass diese Hash-Funktion, diese Eigenschaften haben,
aber wie lange wir jetzt tatsächlich brauchen für diese Sache,
hängt natürlich davon ab, wie lang diese Sondierungssequenzen werden.
Und um das zu analysieren, haben wir ein gewisses Problem,
weil wir sehen, wir können nicht mehr einfach sagen,
okay, wie viele gibt es denn,
die hier den gleichen Hash-Wert haben,
ja,
weil wir in den Sondierungssequenzen Elemente haben,
die unterschiedliche ursprüngliche Hash-Werte hatten
Okay, das heißt, die Analyse davon, um rauszufinden,
welche Laufzeit jetzt die Inside und die Lili-Operation haben,
die müssen wir noch machen.
Allerdings nicht mehr heute,
sondern am Freitag