Schön, dass ihr so zahlreich erschienen seid. Herzlich willkommen zur
zwölften Vorlesung von SE1. Ich erzähle ja mal ein bisschen was
vorneweg und heute erzähle ich mal ein bisschen was Technisches vorneweg.
Nichts organisiert, oh, Entschuldigung, hier mal wieder den Freeze vergessen.
Genau, danke. Worte vorneweg. Sonst immer was Organisatorisches,
da gibt es heute nicht so wahnsinnig viel.
Die leere Evolution läuft gerade. Es ist gut,
wenn ihr da eure Meinung abgebt,
nutzt die Gelegenheit
Uns Positives, wie negatives Feedback zu geben. Das ist
immer hilfreich. Gut, jetzt aber ein bisschen technischer.
Ihr habt ja in der letzten Woche jetzt,
in der Übungswoche, an Aufgabenblatt 11,
habt ihr euch ja mit zwei Implementationen einer Liste beschäftigt,
mit der Ray-List und der Linklist.
Und wir haben euch Testklassen vorgegeben. Und diese Testklassen haben immer
relativ schonungslos aufgedeckt, wenn in eurer Implementation noch Fehler waren.
Es ist sogar ganz lustig, wir hatten ja auch hier
eine Meldung im Komsi
Also wenn ihr das gesehen habt, Diskussionen,
fehlerhafter Unit-Test, Blatt 11, Bug oder Feature.
Da hat also jemand angenommen, dass die Testfälle fehlerhaft sind,
weil die haben halt Fehler gemeldet in seiner Implementation.
Und er ist davon ausgegangen, dass es seine Implementation eigentlich richtig sein müsste.
Wie sie es dann rausgestellt hat, war es dann doch okay.
Also die Testfälle, die wir da haben, die sind ziemlich stabil.
Wir sind so stabil, dass wir inzwischen seit drei oder vier
Jahren nichts mehr daran geändert haben. Das heißt also,
die decken schon ziemlich viele Fehler auf in euren Implementationen.
Aber Im Allgemeinen ist das eben nicht so, dass wir
davon ausgehen können, dass eine Testklasse alle Fehler aufdeckt.
Eine Testklasse kann immer nur genau die Fehler aufdecken,
an die gedacht worden ist, als der Test geschrieben wurde.
Da kann es immer wieder Lücken geben. Vor ein paar Jahren,
als diese Testklassen noch nicht so vollständig waren,
da sah das noch ein bisschen anders aus,
da hatten wir statt 20 Testfan hatten wir nur 16 und wir haben festgestellt,
also ich hatte eine Übungsgruppe und da haben zwei Studierende eine Implementation gehabt,
die war, okay, die Testklassen haben gesagt,
das ist okay,
aber man hat deutlich gesehen,
sie haben
eine Sache vergessen
Nämlich die Rückwärtsverkettung. Bei der doppelt verketteten Liste,
ihr wisst, da müssen ja so ein neues Element eingefügt wird,
dann muss nach hinten und nach vorne muss ja irgendwie verkettet werden,
zweimal in beide Richtungen.
Und die hatten eine Rückwärtsverkettung vergessen. Tatsächlich einfach vergessen.
Und wir haben uns gewundert, warum unsere Testklassen das nicht aufgedeckt haben.
Ich kann das mal zeigen, ich habe ein BlueJay-Projekt dabei aus der damaligen Zeit.
Das ist jetzt hier ein bisschen historische Source-Code-Betrachtung. Das ist
nur die eine Hälfte dieses Projektes. Also hier Link-Titelliste,
doppelt den Knoten, die Titelliste, also alles,
wie ihr es kennt. Und hier ist der Unit Test.
Und wenn ich den jetzt ausführe, Dann 16 Testhelle,
seht ihr hier, ja, und alles ist grün, wunderbar.
Und was die jetzt vergessen hatten, und das hier ist eine Musterlösung,
ja, die können wir uns jetzt einfach mal kurz angucken.
Und da ist diese eine Stelle beim,
also hier nach der Konstruktor, dann bei Füge ein.
Wenn ein Knoten eingefügt wird, da ist hier der neue Doppel-Link-Knoten,
der wird irgendwie hier eingefügt, dann davor Sätze,
Nachfolger und danach sätze Vorgänge.
Und dieses danach sätze Vorgänger, das haben die vergessen.
Ich kommentiere das mal aus, ja, dass wir jetzt sozusagen simulieren,
als wenn das die Implementation der Studierenden wäre.
Das lässt sich übersetzen,
ja,
Wir wissen ja, der Compather akzeptiert ja auch Code,
der nicht das Richtige tut, solange er nur syntaktisch richtig ist.
So, jetzt habe ich hier unsere Testfälle und teste das
und die Tests laufen alle durch. Offensichtlich ist die Implementierung
nicht korrekt, aber die Tests decken das nicht auf.
Das haben wir also vor ein paar Jahren gemerkt.
So, jetzt ist die Frage, was machen wir?
Einfach mal so in die Runde gefragt,
was würdet ihr machen jetzt an der Stelle?
Was würdet ihr jetzt tun? Also,
wenn ihr euch jetzt einen Testfall ausdenken solltet,
der feststellt, ob die Rückwärtsverkettung funktioniert,
wie würde der
grob aussehen
Hat mal jemand eine Idee dazu? Das Problem ist aber
schon klar geworden, oder? Also doppelte Verkettung und eine Verkettungsverbindung fehlt.
Und ich möchte Tests haben, die das merken, wenn das fehlt.
Ja, da ist ein Vorschlag. Und auch den Nachfolgern,
als uns auf der Vorgänger nach neuen 5 Elementen unter Vorgängen
, sagt man der Nachfolger zu euch. Okay,
also er sagt einfach ein Element einfügen und dann auf die
Nachfolger positionieren und dann sozusagen, also andersrum,
für euch jetzt hier ist der Nachfolger auf den Nachfolgerpositionieren und dann von da aus zurückgehen.
Gut, das ist ganz spannend. Also das heißt,
man weiß in dem Fall, dass es eine verkettete Liste ist,
weil man ja bewusst zurückgeht über eine Verkettung.
Genau, es wäre dann ein Whitebox-Test.
Ich wüsste ja, dass die Listen-Implementation, hier,
das ist das Interface, eine Linktitelliste ist.
Kann man so machen. Das ist auch das,
was ich hier auf der Folie schon so ein bisschen mir überlegt habe.
Also Blackbox versus Whitebox-Test, was wir haben hier,
diese Testfälle,
das sind alles Blackbox-Tests
Das heißt, die wissen nichts darüber,
ob die Implementation eine LinkedIn-Titelliste ist oder eine Array-Titelliste.
Die testen einfach nur Operationen der Schnittstelle. Also Operationen des Interfaces.
Und cool wäre jetzt, wenn man ein Blackbox-Test konstruieren könnte,
also denn nur über die Schnittstelle geht, der diese Rückwärtsverkettung irgendwie braucht,
hat jemand da vielleicht eine Idee, wie man das irgendwie erreichen kann.
Wann man denn diese Rückwärtsverkettung überhaupt vielleicht braucht?
Sehr guter Vorschlag. Er sagt beim Entfernen,
Wenn ich nämlich ein Element entferne, dann muss ich ja,
also wenn ich hier die Elemente habe,
dieses Element möchte ich entfernen, hier sind die nachfolgenden Elemente.
Das Element möchte ich entfernen, dann muss ich ja von diesem Element
den Vorgänger setzen und zwar den Alten, der hier gesetzt war.
Und wenn diese Rückwärtsverkettung gar nicht funktioniert hat, dann wird das irgendwie schiefgehen.
Also irgendwas mit Löschen. Gut, da kann man jetzt
ziemlich lange drüber nachdenken. Wir haben uns da so ein
bisschen das Gehirn verrenkt und haben dann irgendwann festgestellt,
es gibt eigentlich einen relativ kleinen Test, der das testet.
Zufällig habe ich den hier dabei.
Der sieht so aus. Also wir haben eine Titelliste,
ja, das ist tatsächlich hier vom Typ Titelliste,
also ihr seht, es ist ein Blackbox-Test.
Ich benutze nur
die Operation des Hinterfaces
Dann habe ich hier Listefüge ein,
und zwar an Positionen null, zweimal an Position null.
Also es wird zuerst etwas an Position null eingefügt.
Und dann wird davor an Position null überhaupt mal was eingefügt.
Und zwar erst Element null und dann Element 1. Das heißt,
das hier ist Element null, und das ist Element 1.
Von euch aus betrachtet. Okay, dann wird das entfernt,
und das zwar an Position 1,
also von den beiden hier, das wird entfernt.
Was also das Element null sein müsste. So,
und dann müsst ihr natürlich das Listen-Element 1,
also diese beiden unterschiedlichen Testtitel hier.
Eins müsste dann ja
noch drin sein
Also das ist, da sieht man jetzt gar nichts von Verkettung.
Es ist einfach nur, ich benutze die Liste über die Schnittstelle.
Und tatsächlich, wenn ich diesen Test ausführe,
dann fällt es auf, dass die Rückwärtsverkettung fehlt.
Kann ich jetzt auch beweisen. Ich kann einfach
das Zeug mal kopieren hier. Nehm das hier raus,
schmeiß das einfach in diese Testklasse rein als Testfall.
Zack, hier ist der Test, der lässt sich übersetzen.
Jetzt kann ich den Test ausführen und dann schlägt der Test tatsächlich fehl.
Genau dieser Test. Also hier dich gerade eingefügt hat,
teste, einfügen und entfernen.
Genau. Also ich habe das mit dem Zeiger-Quelltext hier funktioniert.
Es geht irgendwie gerade nicht, es ist kaputt.
Gut. Aber das ist jetzt ganz spannend. Was
hätte man jetzt machen können? Also in dem Moment,
wo wir den Fehler entdeckt haben, da ist ja der erste Impuls,
oh, Rückwärtsvergettung vergessen, Rückwärtsverkettung einfügen, fertig.
Fehler beheben. Das ist tatsächlich nicht, wie man vernünftigerweise vorgeht.
Wenn man mit Testklassen arbeitet. Das heißt also,
da ist eine Frage
Okay. In der letzten Vorlesung. Und die Frage war,
sie hat nicht ganz verstanden, was Rückwärtsverkettung ist.
Das heißt, Rückwärtsverkettung heißt, wir haben Knotenelemente, die miteinander verkettet sind.
Bei einer einfach verketteten Liste sind sie vorwärts verkettet.
Und bei einer rückwärts, bei einer Doppelverkettenliste sind sie auch rückwärts verkettet.
Das ist also diese Verkettung der Knotenelemente in beide Richtungen.
Rückwärts verkettet heißt einfach, dass praktisch gegen die Listenrichtung.
Okay, alles klar. Also, was ich sagen wollte,
ist, wenn wir so einen Fehler finden,
Das ist der erste Impuls, ihn einfach zu beheben.
Das ist nicht das richtige Vorgehen, wenn man mit Testklassen arbeitet.
Die Idee ist eine andere. Und das ist tatsächlich was,
was ihr euch schon mal merken könnt,
wenn ihr in Richtung professionelle Softwareentwicklung gehen wollt.
Zuerst, was wir gerade auch gemacht haben,
ersten Testfall finden, der den Fehler aufdecken würde.
Also nicht den Fehler beheben, sondern versuchen,
sich zu überlegen, mit welchem Testfall hätte ich diesen Fehler bemerkt?
Den finden, das kann schwierig sein. Es kann teilweise
wirklich kompliziert sein, sich was zu überlegen.
Dann den Testfall implementieren, haben wir auch gerade gemacht.
Wir haben einfach implementiert. Dann den Test fehlschlagen lassen,
habe ich eben auch gemacht. Also wirklich sehen,
aha, ja, tatsächlich, dieser Test entdeckt diesen Fehler
Dann den Fehler beheben, das kann ich jetzt machen,
ich kann also jetzt wieder hier reingehen.
Und hier dieses Ding wieder wegnehmen, zack,
jetzt ist das hier wieder korrekt, die Implementierung.
Jetzt müsste man ein neuer Testfall auch zufrieden sein.
Also wenn ich jetzt hier wieder teste, siehe da,
der Balken ist grün. Also wir haben tatsächlich einen Testfall gebaut,
der uns einen Fehler aufgedeckt hat und haben dann erst mal den Test fehlschlagen lassen,
dann den Fehler behoben und am Ende dann den Test erfolgreich durchlaufen lassen.
Das ist quasi so das Mantra der objektorientierten Softwareentwicklung,
was Tests angeht.
So sollte man
das machen
Wenn man mit Tests ernsthaft arbeiten will. Und in der
professionellen Softwareentwicklung setzt sich das inzwischen immer mehr durch.
Vor ein paar Jahren waren wir noch hier so die einsamen
Rufer im Wald, die gesagt haben, ihr müsst testen.
Und inzwischen ist das so, dass also ganz viel Feedback
kommt von Leuten aus der Praxis, die sagen,
ja, ohne Tests läuft es bei uns gar nichts mehr.
Noch eine Sache, was wir jetzt gerade hier gebaut haben, war ein Positivtest.
Wer weiß, können wir noch mal ganz kurz sagen,
was der Unterschied ist zwischen einem Positivtest und einem Negativtest?
Denke ich mal kurz nach. Was war noch mal ein
Positivtest, was war noch mal ein Negativtest, ja?
Positiv Test geht davon aus, dass man,
das haben wir so genutzt,
wie es vorgesehen ist
Und überprüft und putsch aufs Gamblet funktioniert und negativ pressig ist.
Doll fesseln, aber auch auf die Bank russeln übrigens,
das ist auch das Verraumen, wo ich Fehler gab,
so sind wir sinnvoll.
Perfekt, ich hätte es nicht besser sagen können.
Also er sagt, bei einem Positivtest testet man das Programm,
so wie es benutzt werden soll.
Also man hält sich an die Schnittstellenspezifikation und benutzt es genauso,
wie es vorgeschrieben ist und prüft, ob alles funktioniert,
wie es funktionieren soll.
Das sind Positivtests. Damit testet man die Korrektheit der Software.
Und wenn man Negativtests macht, dann testet man bewusst Dinge,
die eigentlich in der Spezifikation nicht vorgesehen sind
In der Spezifikation steht. An dieser Stelle wird ein Parameter
übergeben, eine Referenz, die darf nicht nall sein.
Lieber Klient, hier darfst du nicht nall übergeben.
Dann komme ich daher und sage, oh, oh,
muss mal gucken. Wenn ich jetzt nall übergebe,
geht es vielleicht auch schief, vielleicht sogar so schief,
dass die Implementation irgendwelchen Unfug macht.
Das will man ja hoffentlich nicht, dass da irgendwie,
wenn das Ding falsch benutzt wird, dann gleich die Datenbank abraucht oder ähnliches.
Also testet man das auch und dann testet man negativ.
Das heißt, man testet auf Robustheit.
Ob die Software sich clever verhält, wenn sie auch mal
fehlerhaft benutzt wird
Und ganz wichtig eben, ihr habt bisher nur Positiv-Tests gesehen.
Ihr könnt auch momentan technisch nur Positiv-Tests schreiben. Wenn ihr Negativ-Tests
schreiben wolltet, dann müsstet ihr ja eine Softwareeinheit benutzen,
einen Klienten programmieren, der einen Dienstleister benutzt.
Und wenn der in der Spezifikation steht, aha,
wenn jetzt hier was fehlerhaft passiert,
dann wird die und die Acception geworfen,
dann müsstet ihr ja provozieren, dass diese Acception wirklich ausgelöst wird.
Und dann diese Acception abfangen und dann untersuchen,
ob es die richtige Acception ist.
Sowas könnt ihr noch gar nicht. Also Acceptions auffangen haben
wir noch gar nicht thematisiert,
das kommt alles in
Das lernt ihr also noch. Aber momentan könnt ihr euch merken,
positiv Tests solltet ihr eigentlich immer euch was dazu überlegen.
Negativtests ist dann sozusagen die zweite, zweite Riege.
Okay. So viel, so ein bisschen ein kleiner
Ausflug zu Anfang. Wir sind fast durch, ja,
fast durch mit SE1. Wir haben noch drei Vorlesungen mit der heutigen.
Heute geht es nach wie vor darum, Sammlungen zu implementieren.
Letzte Woche haben wir Listen implementiert, das Listen Interface,
haben wir gesehen in der Benutzung. Positionen sind relevant,
man kann die Position manipulieren, die Reihenfolge ist also Benutzer definiert,
Duplikate sind zugelassen,
das sind Listen
Und wir haben gesehen, mit einer Linklist und mit einer
Arealist kann man das unterschiedlich implementieren, haben unterschiedliche Vorteile,
die beiden Implementationen, wunderbar, letzte Woche.
Jetzt gucken wir uns was anderes an, nämlich wie man Mengen implementiert.
Und das ist das, was wir heute uns angucken,
sind also hier binäre Suchbäume und Hashing.
Und das ist so meine persönliche Lieblingsvorlesung in SE1 und ich hoffe,
ich kann irgendwie rüberbringen, warum ich die so spannend finde.
Okay. Also Sammlungen jetzt implementieren Nummer zwei Mengen,
einmal mit Bäumen und einmal mit Hashverfahren
Wer kennt Movie Dick? Boah, nicht so müde.
Also ich werde mehr kennen, Movie Dick. Okay,
also da gehen so gefühlt zehn Prozent der Hände hoch.
Na gut, kann ja sein,
dass ihr heutzutage sowas gar nicht mehr kennt.
Movie Dick ist ein Klassiker der Literatur,
ist eine Geschichte über einen ziemlich dicken Wal.
Ab der Dicke weiß man gar nicht, aber er ist jedenfalls legendär.
Das ist ein weißer Wal und irgendein Typ hier,
der Captain Airhapp,
der will
diesen Wal fangen
Scheitert tragisch daran, dass er diesen Wal nicht schafft zu fangen.
Ist verfilmt worden hier mit Gregory Pack, mehrfach verfilmt worden.
Ist ein absoluter Klassiker, also wirklich, ist auch ein Voluminö,
ein voluminöses Werk, also ungefähr 670 Seiten.
Klassische englische Literatur, toll. Urheberrecht ist inzwischen abgelaufen,
der gute Herr Melville ist lange tot, 70 Jahre Urheberrecht.
Das heißt, der Text ist ziemlich frei verfügbar.
Was ganz cool ist, weil jetzt können wir nochmal anfangen,
den so ein bisschen digital zu bearbeiten.
Das Werk ist unter anderem so spannend, weil es Eine
unglaublich reiche Sprache hat. Also für so Linguisten,
Literaturwissenschaftler ein gefundenes Fressen, was man da so an Begriffen findet,
der englischen Sprache, sind ungefähr 2010.000 Wörter.
Also ihr könnt euch vorstellen, wenn wir den Text wieder,
den Buchtext als eine ganz lange Liste von Wörtern auffassen,
dann sind das ungefähr 2010.000 Wörter.
Wo natürlich einige Wörter doppelt vorkommen, ist ja klar.
Also die Füllwörter und ähnliches und Eigennamen,
die tauchen öfter auf und so weiter.
So, jetzt kann man eben halt sich als zum Beispiel
als Literaturwissenschaftler interessieren dafür, wie groß ist denn
der Wortschatz
Wie groß ist der Watch? Also wie viele verschiedene Wörter
stecken in diesem Text drin? Das wird bei Linguisten
auch Korpus genannt. Das heißt, das ist ja,
das sind sozusagen die Menge der Wörter in dem Text.
Okay. Ihr habt jetzt alle Möglichkeiten, sowas zu programmieren.
Das ist eigentlich relativ einfach. Also wir nehmen an,
der Text liegt uns digital in einer Textdatei vor.
Dann können wir ja mit einem Java-Programm wortweise,
die in eine Liste einlesen
Also einfach eine Liste von Wörtern des Textes.
Und anschließend können wir dann jedes Wort der Liste in
eine Menge einfügen. Und das kennt ihr schon,
wenn ich so eine Liste nehme und in eine Menge einfüge,
dann fliegen die ganzen Duplikate raus.
Dann frage ich nur noch die Kardinalität der Menge ab und
schon habe ich meine Größe meines, meines Wortschatzes.
Abzüglich gewisser Unschärfen, was so Beugung, das eigene,
desselben Wortes angeht, also Plural mit S und so.
Gut, also ja, es ist nicht ganz,
ganz korrekt, überhaupt nicht.
Aber man kriegt eine Größenordnung. Hier ist der Code dazu
. Den solltet ihr schreiben können
Das sollte jetzt für euch kein großes Problem sein. Also okay,
wie man das Einlesen eines Buches, eine Liste von Wörtern vornimmt,
das findet ihr in einigen unserer Projekte der letzten Wochen.
Das heißt also hier eine Liste von Strings.
Das ist der Buchtext. Und dann habe ich hier
eine Menge von Strings, ein Set von String.
Das sei der Wortschatz. Dann nehme ich ein Hash-Set,
kennt ihr schon, das ist eine Implementation, das ist Interface is Set.
Und dann mache ich einfach hier eine Vorschleife für jedes Wort in den Buchtext, wortschatz.at.
Es gibt noch eine andere Möglichkeit, das kürzer zu schreiben.
Habt ihr schon bei mir in der Vorlesung gesehen,
man könnte auch tatsächlich beim Erzeugen des Hash-Sets Buchtext,
die Referenz auf den Buchtricks, auf die Liste als Konstruktorparameter mit reingeben.
Dann werden auch sofort alle Elemente in die Menge aufgenommen.
Hier ist es ein bisschen expliziter, dass man sieht,
ich füge also alle Wörter in die Menge ein und dann
habe ich am Ende den Wortschatz.
Das ist die Klientensicht. Diese Klienten sich, die müsstet ihr jetzt beherrschen,
also ihr müsstet mit Mengen und Listen sowas ohne Probleme implementieren können.
Okay, das funktioniert ja sehr gut. Was wir jetzt wollen ist,
wir wollen ja hinter die Kulissen gucken,
wir wollen ja verstehen,
wie man
solche Implementationen effizient machen kann
Also wie kann man sowas richtig schlau machen, dass es schnell ist?
Und dass es auch langsam geht, das zeige ich euch
jetzt als erstes. Ich zeige euch als erstes eine Implementation,
einer Menge, die sehr einfach ist.
Und ihr werdet sehen, dass die so einfach ist,
dass sie uns hier ein bisschen Zeit kosten wird.
Also, es gibt ausgefallte Implementationen, die gucken wir uns
heute an, aber ihr müsst sie nicht alle kennen.
Normalerweise benutzt ihr sie ja nur,
aber jetzt wollen wir eben halt unter die Motorhaube gucken.
Und jetzt nehmen wir einfach mal an,
wie hier gemeinsam sollten jetzt eine Implementation für eine Menge bauen.
Für diese Mengenfunktionalität, die insbesondere ja prüfen muss,
ein Element schon drin ist. Und das ist so der größte Knackpunkt.
Wenn ich nicht eine ganz große Menge von Wörtern habe,
also der Wortschatz hat schon irgendwie mehrere tausend Wörter drin.
Jetzt kommt ein weiteres Wort, jetzt muss ich prüfen,
ob es eingefügt werden muss.
Ob es ein Duplikat ist. Was können wir da nur
machen bisher mit dem, was wir gelernt haben?
Naja, wir können das Wort hernehmen und das mit
jedem Wort in der Liste, in der Implementation vergleichen.
Also in dem Wortscha sind ja schon Wörter drin.
Ich muss also eigentlich das neue Wort mit jedem Wort in
dem Wortschatz vergleichen und fragen, ob es gleich ist.
Und erst wenn ich einmal komplett durch bin,
das war nicht gleich mit irgendeinem der Wörter,
dann weiß ich,
es war kein Duplikat
Dann kann ich es erst einfügen. Das ist nachvollziehbar, oder?
Die Logik, dass ich einfach tatsächlich mir alle Wörter angucken muss.
Okay. Dann bauen wir das doch mal. Ich habe
also hier ein Projekt, wir können das hier zumachen.
Das ist so ein bisschen so ähnlich wie das Projekt,
was ihr in den Übungen diese Woche zu sehen bekommt,
so ähnlich.
Das Entscheidende ist hier
dieses Interface Wortschatz
Ja, also hier, Interface Wortschatz, der ist der Text relativ einfach.
Also die Beschreibung, ein Wortschatz ist eine Menge von Wörtern.
Also Wörter im Sinne von Zeichenketten, Wörter aus einem Text.
Und dieses Interface definiert geeignete Operationen auf einer solchen Menge.
Auf Mengen braucht man eben mal sowas wie einfügen, sowas wie entfernen.
Und es kann abgefragt werden, ob ein bestimmtes Wort enthalten ist.
Und ich kann die Anzahl der inhaltenden Wörter abfragen.
Das ist genau die Funktionalität, die ich brauche,
um hier diese Fragen zu beantworten,
die wir uns hier gerade gestellt haben.
Also gibt es hier Operationen, Anzahl der Wörter,
die drin sind, Füge Wort hinzu,
entferne Wort und ist ein bestimmtes Wort enthalten
Okay, das ist das Interface, was wir implementieren wollen.
Und hier haben wir jetzt so zwei Hilfsklassen. Das eine ist diese Worteinleserklasse,
da können wir uns mal ganz kurz ankommen,
dass ihr einfach mal so ein bisschen seht,
wie man sowas machen kann.
Das ist wieder so dieses IO-Datei-Einlesen, was ihr nicht alles unbedingt kennen müsst.
Aber hier könnt ihr am Ende nochmal sehen,
wie dann sozusagen jede Zeile, die da eingelesen wird,
außer Datei verarbeitet wird. Also hier ist ein String,
die ganze Zeile ist ein String und das soll alles,
die Wörter sollen ja in so eine Wortliste eingefügt werden.
Und da wird tatsächlich hier Folgendes gemacht,
Zeile to Lower Case und dann wird ein Split gemacht an
den Zeichen innerhalb der Zeile, die nicht A bis Z sind.
Also wir haben jetzt hier einen englischen Text, deswegen keine Umlaute,
deswegen können wir uns auf A bis Z beschränken.
Alles ist klein geschrieben, haben wir schon dafür gesorgt,
to lower case. Also splitten wir an allen Zeichen,
die keine Buchstaben sind, die also vermutlich nicht zu Wörtern gehören.
Ja, und das ist ja hier genau dieses reguläre Ausdruck für nicht.
Also wenn das hier eine reguläre Menge ist,
die Zeichen von A bis Z, dann ist das sozusagen die negativen Menge.
Okay, und dann wird einfach durch diese dann entstehenden Wörter durchgegangen,
also hier durch dieses String Array, was da erzeugt wird.
Und wenn, das kann natürlich sein,
dass da mehrere Sonderzeichen irgendwie nebeneinander liegen,
dann hat man mehrere Wörter der Länge Null
Und deswegen, if Word längst größer null, dann wird es wirklich hinzugefügt.
Also wir fügen alle Wörter mit mindestens länger eins da ein.
So, also das ist ganz einfach. Und hier in diesem Start-up,
da passiert jetzt nichts besonders Aufregendes, das ist was,
was ihr auch schon mehrfach gesehen habt.
Also hier wird eine Arrailis angelegt für die Wortliste.
Das ist also das, was auch auf der Folie zu sehen war.
Dann wird das Einlesen gestartet. Einlesen aus Datei hier.
Dann wird dieser Mobby 10b-Text genommen. Das ist tatsächlich Mobby
Dick in Digitalform. Kann man sich auch mal angucken.
Mobby 10b, TXT, können wir mal gucken,
dass ich euch jetzt,
dass ich euch hier behumse
Also hier, Mobi 10b, das ist dann hier Chapter One,
Call me Ismael und some hears a go,
never mind how long, precisely, bla, bla, bla.
Das ist tatsächlich mobby dick. Gut. Und
diese Worte werden dann aus dem Text eingelesen in die Wortliste
und dann weiß man zumindest schon mal,
wie viele Wörter der Text lang ist.
Wortliste.size. Und dann, wie lange das gedauert hat,
ist nicht so wichtig. Und jetzt kommt hier dieses,
hier wird also ein Wortschatz übergeben,
also eine Implementation des Interfaces des Wortschatzs,
wo man dann tatsächlich
diese ganzen Wörter einfügt
Also hier in einer Vorschleife,
genau wie auf der Folie eben, jedes Wort wird hinzugefügt.
Das Ganze wird gemessen mit Karen Time, Millisekunden.
Und dann sieht man, wie lange das gedauert hat.
Also wie lange hat es jetzt gedauert, rauszufinden,
wie viele Wörter im Wortschatz sind,
nämlich das ist hier die entscheidende Frage, Wortschatzanzahl Wörter.
Okay. Super, sehr schön. Was habe ich gesagt,
wir können nur bisher Listen implementieren. Die Idee war ja irgendwie,
ich benutze eine intern jetzt in meiner Implementation,
die ich bauen will, eine Liste.
Und durch diese Liste gehe ich immer durch und gucke eben,
ob das einzufügende Wort da schon drin ist
Mehr kann ich momentan nicht. Um das ein bisschen zu verkürzen hier
in der Vorlesung, dass es nicht zu lange dauert,
habe ich hier so eine kleine Hilfsklasse, die nennt sich Wortliste.
Und das ist nichts weiter als eine Liste von Strings.
Hat Operationen, die at Get Remove Size und so weiter heißen.
Das hat den Grund, dass diese Schnittstellenmethoden,
diese Schnittstellen-Operationen genauso heißen wie im Collections Frankwork.
Warum werdet ihr gleich sehen, dadurch kann ich sehr schnell
mehrere Implementationen bauen. So, erstmal baue ich jetzt hier
eine ganz einfache Implementation, nämlich irgendwie Hash,
Quatsch,
Und dann mit einer Liste. Also das ist meine
allereinfachste Implementation, die ich jetzt hier bauen will.
Ich möchte dieses Interface implementieren mit Hilfe einer Liste. Okay,
jetzt mache ich erstmal hier Folgendes. Ich sage Wortschatz-Liste implementiert das Interface,
damit ich das jetzt wirklich machen kann,
muss ich jetzt hier mal eben mir die ganzen Methodenköpfe kopieren,
weil die muss ich ja implementieren, schwuppdiwupp, mache ich das mal eben.
Da haben wir die ganzen Methodenköpfe aus dem Interface.
Die muss ich ja jetzt hier in meiner Klasse implementieren.
Okay, jetzt schmeiße ich hier sämtliche Kommentare weg
Weiß jemand, warum ich die Kommentare wegschmeiße?
Hatte ich mal eine Idee, warum ich die Kommentare wegschmeiße?
Ja. Die stehen ja schon im Interface.
Er sagt das ganz genau richtig und das wäre redundant,
wenn man den nochmal hier stehen hätte.
Es ist tatsächlich so, das wäre Code-Duplikation, wenn sie hier stünden.
Und deswegen nehmen wir sie auf jeden Fall weg.
So, ja, ihr seht jetzt hier,
ich mache aus diesen Methoden Köpfen hier jetzt Mist,
Mist, Mist,
Mist,
Hier mache ich jetzt. So,
jetzt müsste ich das eigentlich schon übersetzbar sein, oder?
Gucken wir mal, übersetzen wir mal. Nee, meckert
der Compiler. Missing Returns, da hat er recht.
Ist ja ein Bulien, der erwartet wird. Returnfalls.
Das ist schon mal das Einfachste, was da geht.
Und Anzahl Wörter, Return 0. So,
jetzt habe ich hier eine syntaktisch korrekte Implementation von dem Interface.
Macht noch Mischt. Ist jetzt wenig spannend bisher.
Jetzt geht's los. Ich benutze intern Diese komische Wortliste,
die ich hier habe. Ich benutze also intern eine Liste von Wörtern,
Wortliste, unter Strichwörtern, nenne ich das Ding mal.
So, dann baue ich mir hier einen Konstruktor,
Public, Wort, Wort, Schatz, Liste.
Und in dem Konstruktor, initialisiere ich hier meine Exemplarvariable,
Wörter ist gleich, New Wort Liste.
Das lässt sich übersetzen. Ich habe jetzt schon mal meine interne Liste,
in der ich jetzt die ganzen Wörter aufbewahren will.
Aber eben halt nur die, die keine Duplikate sind.
Okay? Das heißt, ich habe ja jetzt hier bei Fügewort hinzu,
muss ich als erstes prüfen, ob es schon drin ist.
Sonst würde ich es ja nicht hinzufügen.
Ich will ja in meiner Liste nur Duplikatfreie Wörter drin haben.
Jetzt kann ich hier was machen, was ganz lustig ist.
Da unten ist die Methode, ist enthalten,
die benutze ich jetzt einfach mal.
If not ist enthalten,
dieses Wort
Dann, ja, was mache ich denn dann?
Dann muss ich mir irgendwas einfallen lassen, wie ich das jetzt hinzufüge.
Gut, da gibt es jetzt in dieser Wortliste
eine Operation, die heißt Ed, hinzufügen.
Also kann ich hier einfach sagen, Wörter.at Wort.
Also, wenn das Wort nicht schon enthalten ist,
dann kann ich es hinzufügen. Ich habe jetzt,
das lässt sich übersetzen, immer schlau, zwischendurch mal übersetzen,
immer wieder kleine Übersetzungsschritte machen, um zu sehen,
ob man nicht irgendwo noch was Falsches reingebaut hat.
So, wunderbar
Der Trick liegt natürlich jetzt, in dem ist enthalten.
Also hier, so jetzt, wie mache ich das jetzt?
Ich muss jetzt durch alle Elemente in der Liste
durch und feststellen, ob es schon drin ist.
Ich mache mal Folgendes. Ich deklariere mir eine bulsche Variable,
irgendwie enthalten, lokal hier und sage, der ist mal false.
Und dann gehe ich jetzt durch alle Wörter in der Liste
durch und gucke, ob das Wort,
was ich jetzt gerade einfügen will,
schon drin ist
Also, vor Stringen weh in meinen schon eingefügten Wörtern. Prüfe ich jetzt,
ob das Wort, if Wort, Moment, Wort,
Equals, ganz wichtig, Strings immer nur mit Equals vergleichen, Wort Equals weh. So,
ich könnte es auch vielleicht noch ein bisschen sicherer machen,
machen wir lieber weh, Equals Wort,
ist auch nicht ganz so, weil ich weiß ja,
dass die Wörter, die ich schon drin habe in meiner Liste,
die sind garantiert Nein, ich müsste eigentlich hier prüfen,
ob das ein Altei ist, egal.
Ihr wisst, was zu tun ist. Okay,
und wenn das gleich ist, dann können wir ja hier
sagen, enthalten ist gleich True.
Sobald ich also ein Duplikat gefunden habe, ist es drin.
So, und am Ende schreibe ich hier jetzt nur noch Return enthalten.
Ist das soweit erstmal verständlich? Ich gehe also durch alle schon in meiner Liste enthaltenen Wörter durch,
prüfe, ob das neue Wort drin ist und wenn ich irgendwie feststelle,
oh, das ist schon drin,
dann habe ich halt enthalten,
wird auf True gesetzt und dann wird es
halt nicht hinzugefügt
Da fehlt noch eine Klammer. Ja, hier am Four.
Ganz genau. Vielen Dank. Da fehlt noch die schließende Klammer
. Also, ich muss es ja auch übersetzen können.
Ich mache nochmal eben die Implementation vollständig.
Hier ist noch entferne Wort. Da gibt es tatsächlich hier
bei der Liste die Wirklichkeit zu sagen Remove, Wort.
Ganz einfach. Da wird einfach das erste Auftreten des Wortes
an der Liste entfernt. Wir wissen, dass wir
keine Publikate drin haben, also müsste das funktionieren.
Wunderbar, geht auch. Und hier, Anzahl der Wörter
, auch sehr schön. Es gibt bei unserer Liste,
die wir benutzen, schon die Operation Size
Und schon haben wir eine Implementation einer Menge, die uns tatsächlich Duplikate nicht zulässt.
Cooles Zeug. Jetzt lasse ich mal diesen Test laufen.
Ich erzeuge mir also ein Exemplar dieser Wortschatzliste. Und die
Referenz auf diese Implementation übergebe ich jetzt an diese Methode hier,
Start, die einen Wortschatz bekommt und dann in diesen Wortschatz
die ganzen Wörter aus dem Buchtext einfügt.
Also irgendwie über 200.000 Wörter, ein Füg nach der Implementation,
wie wir es gerade gesehen haben.
Was schätzt ihr, wie lange das dauert? 200.000 Wörter,
ihr müsst euch überlegen, er muss für jedes der 200.000 Wörter,
muss er prüfen, ob es schon in dem bestehenden Wortschatz drin ist.
Ich starte das mal. Und jetzt haben wir Zeit.
Früher habe ich dann in der Vorlesung dann immer angefangen,
hier irgendwie Breakdance zu machen oder irgendwie Pantomime oder so,
bin ich nicht mehr so gut drin,
ich bin ein bisschen steif,
nein.
Wir können mal
Storesi starten
Wir nehmen mal hier Vorlesung 12. Wie lange dauert es,
bis die Größe des Wortschattes ermittelt ist?
Ja, also wir haben jetzt hier mal,
ihr seht, der rechnet noch, der rechnet noch.
So, wir starten mal die Umfrage. Was schätzt ihr denn?
Was schätzt ihr denn? Kauert das jetzt mehrere Minuten,
dauert das mehrere Stunden, mehrere Tage und dauert das ewig
Ja. Weil das dort jetzt, ja, das läuft
jetzt. Ich weiß ungefähr, wie lange es dauert,
aber es kann ja sein, dass auch abbrechen, ja,
dass ich irgendwann sage, ja, es hat keinen Zweck.
Könnt ihr euch ja hinter das Licht führen wollen.
Also verlasst es auch nicht zu sehr darauf,
dass es Antwort A ist. Ja. Ich glaube,
so bei 100 Antworten können wir dann langsam Schluss machen.
Spätestens, wenn er hier durch ist, ja,
falls er hier durch ist. Naja. Das ist was,
was ihr lernen müsst, so educated guessing,
also so, ja, wie kriege ich denn raus,
was das
Okay, also wir haben jetzt hier schon wieder 120 Stimmen,
herrlich. Halt das mal an. Wir gucken mal,
wie so eure Meinung ist. Ja,
also doch ziemlich viele meinen, das dauert mehrere Minuten.
Also es müsste innerhalb dieser Vorlesung noch zu Ende gehen.
Okay, dann gibt es irgendwie ein paar Leute,
die sagen, ja, mehrere Stunden.
Und andere manche sagen, es dauert ewig, toll.
Also ewig ist eine schöne, schöne Zeitangabe.
Ja, mal gucken, was er hier so macht.
Ja,
dauert irgendwie
Aha, gucken wir mal. Oh, erst durch,
juhu. 141.000 Millisekunden, also ungefähr zwei Minuten 20 Sekunden.
Das heißt, die richtige Antwort, mehrere Minuten.
Gut, toll. Nur hat nur zwei Minuten gedauert. Also,
wenn ihr vor 100 Jahren, als der Melville schon da war,
als das Buch schon existierte, wenn ihr da Lektor gewesen wärt,
und ihr hättet das von Hand zählen müssen.
Ich wette, ihr hättet länger gebraucht als zwei Minuten.
Also, das ist schon eine coole Technik hier.
Computer sind geile Geräte, oder
Man kann jetzt hier solche Dinge sagen. Ich kann jetzt sagen, wie groß ist...
Ach ja, übrigens, hier ist das Ergebnis, ja.
16.000 Wörter, 16.000 verschiedene Wörter. Ich weiß nicht,
ob ihr schon mal so euch Gedanken gemacht habt über so Wortschatz von Texten,
aber wenn ihr so einen Spiegel-Text lest, einen längeren,
dann kann man davon ausgehen,
dass es nicht ganz so viele verschiedene Wörter sind.
Ja, cool. Meine erste Frage. Wem ist
eine Optimierung aufgefallen? In meiner Implementierung. Da oben ist
eine Meldung
Mit den Gerichten da. Ich verstehe dich, ich muss ein bisschen lauter sprechen.
Genau, richtig, genau richtig. Er sagt,
was ich ja hier implementiert habe, ist ziemlich dämlich,
das hat er nicht gesagt, also dass ich dämlich bin,
aber also ich sage das jetzt, dass es dämlich war,
habe ich natürlich bewusst gemacht.
In dem Moment, wo ich feststelle, dass das Wort
drin ist, brauche ich ja nicht mehr weiter suchen.
Ich gehe hier stumpf immer durch alle Wörter durch,
auch wenn ich schon beim fünften Wort feststelle, oh,
es ist schon drin, gehe ich trotzdem noch durch alle Wörter durch.
Das ist völlig bescheuert
Das ist eine korrekte Implementation. Sie macht das Richtige,
liefert das richtige Ergebnis. Nur sie ist ineffizient.
Man kann das schlauer machen. Ich kann nämlich hier,
wenn das Wort enthalten ist, sage ich einfach hier,
Return, True. Das heißt, sobald ich das Duplikat,
sobald ich die Aussage treffen kann, es ist ein Duplikat,
was da eingefügt wird, kann ich schon abbrechen.
Ich gehe nicht mehr durch alles durch.
Also so kann ich es machen hier mit Return True,
dann würde ich wahrscheinlich jetzt hier es netter machen,
dann würde ich hier Return Falls machen,
würde diese blöde Bullsche Variable wieder rausschmeißen.
So geht es auch
Okay, jetzt mache ich keinen Storesi,
wie lange das jetzt dauert. Das müsste schneller sein.
Wer mag mir mal ein paar Zahlen an den Kopf werfen?
Was schätzt ihr, wie lange dauert das jetzt?
Wenn ich das implementiere? Was denn? 30 Sekunden.
20 Sekunden. Cool, cool, cool, cool.
Wir schauen mal. Wir nehmen mal so eine Implementation her.
Sagen, mach doch mal. Ja, zack. Und
dann gucken wir mal, was da rauskommt. So. Nee,
nee, das ist nur die benötigte Zeit fürs Einlesen.
Hier errechnet noch. So schnell geht es auch nicht
Ja, also jetzt hier, boah, reicht nicht ganz,
um mir eine Tasse Tee zu kochen,
aber dürfte schon ein bisschen schneller sein.
Aha, okay. 18.000 Sekunden. Da war jemand also
ziemlich gut dran. Also 20 Sekunden hat es jetzt gedauert.
Wow, deutliche Geschwindigkeitssteigerung. Einfach nur dadurch,
dass ich jetzt nicht jedes Mal die ganze Liste durchsuche,
ob ein Duplikat drin ist, sondern sobald ich weiß,
dass es ein Duplikat ist, breche ich ab.
Ganz kleine Optimierung mit einem riesen Effekt. Ihr seht hier
20 Sekunden gegenüber, das ist irgendwie Faktor 10.
Naja, nicht ganz Faktor 10. Ungefähr
Sieben. Cool, cool, cool. Also,
wir haben jetzt was mit einer Liste implementiert.
Das ist das, was wir können.
Das ist etwas, was ziemlich blöd ist. Ja,
also einfügen in Mengen hoher Aufwand,
wenn ein Element in eine Menge eingefügt werden soll,
dann muss die Implementierung der Menge prüfen, auf ein Duplikat drin ist.
Das ist auch bei jedem Einfügen ist so ein Test notwendig.
Wenn wir das mit einer Liste implementieren,
wie wir es gerade gesehen haben,
dann ist der Aufwand für diese Überprüfung O von
Also abhängig von der Anzahl der schon enthaltenen Wörter,
das ist diese Notation, die wir letzte Woche eingeführt haben,
größenordnungsmäßig, hängt das von der Größe der Liste ab.
Also O von N, also linear abhängig von der Anzahl der schon eingefügten Wörtern.
Das ist bei großen Mengen nicht akzeptabel. Wir haben jetzt
hier was gehabt mit 16.000 Wörtern, das ist nicht groß.
Ihr wisst heute in Zeiten von Big Data,
da werden nicht Millionen, da werden Milliarden, da werden Fantastillonen verarbeitet.
Und deswegen ist das einfach nicht sinnvoll. Wir brauchen also
was Schnelleres. Und coolerweise gibt es ja sehr, sehr
raffinierte Implementationen. Dafür ist die Informatiker, die Informatik,
dass wir so, wir haben ja auch was gemacht in
den letzten 40 Jahren
Also dieser Testaufenthaltensein, den wir jetzt hier brauchen,
den wir effizienter haben wollen. Der wird auch als Suche bezeichnet,
linearer Aufwand, blöd, damit wir besser werden können,
wenn wir also schlauer, schneller diese Frage beantworten können,
ob in einer Menge von Wörtern ein Element schon enthalten ist,
brauchen wir Anforderungen an die Elemente.
Wenn die Elemente bestimmte Eigenschaften erfüllen, dann können wir die Aussage schneller treffen.
Und zwar typische Anforderungen an die Elemente sind,
dass sie entweder sortierbar sind oder dass sie kategorisierbar sind.
Ich werde gleich erläutern, was das bedeutet.
Wenn sie sortierbar sind, dann kann man eine binäre
Suche machen
Und das kann man mit einem Suchbauer implementieren. Kategorisierbare Elemente ermöglichen Hash-Verfahren.
Wir gucken uns jetzt noch vor der Pause Bäume an,
als theoretisches Konzept. Das werden wir hier nicht implementieren.
Das werdet ihr auch in den Übungen nicht implementieren.
Baumimplementationen sind ein Riesenthema in der Informatik. Da werdet ihr
noch in diversen Veranstaltungen mit in Kontakt kommen.
Datenbanken sind heutzutage fast alle basierend auf Baumimplementationen.
Da steckt also ganz viel Zeug drin.
Wir gucken uns jetzt mal Bäume an. Einfach mal so
ein allererster Einstieg in Bäume.
Wirklich nur kurzer Überblick
Also ein Baum ist eine Struktur,
in der Knoten miteinander durch gerichtete Kanten verbunden sind.
Also hier so angedeutet, das ist ein Baum,
das Gelbe sind die Knoten, die Striche sind die Kanten.
Gerichtet heißt, es gibt möglicherweise in beide Richtungen die Möglichkeit zu navigieren.
Genau wie bei einer verketteten Liste kann ich auch bei so einem Baum irgendwie sagen,
bei einer WhatsApp gib mir mal die Kinder oder gib mir
das nächste Kind oder ich kann auch bei einem Kind sagen,
gib mir doch mal den Vater oder den Elternknoten besser.
Uns geschlechtsneutral auszudrücken. Also ein Knoten kann beliebig viele Kindknoten haben,
einen Knoten bei einem Baum hat aber maximal einen Vorgängerknoten.
Es gibt also immer
einen Elternknoten
Knoten ohne Vorgänger heißt Wurzel, das ist die Wurzel.
Da haben wir keinen Vorgänger, niemanden drüber, keinen Knoten drüber.
Und die hier unten, das sind die Blätter.
Wir haben keine Kinder. Wollen wir sind rekursiv definiert üblicherweise
. Das heißt also zum Beispiel das hier,
das ist ein Unterbaum oder ein Teilbaum.
Und dann ist das hier die Wurzel eines kleineren Baumes.
Also im Grunde genommen kann man sagen,
dass jeder Baum als Kinder wieder Bäume hat.
So, was wir brauchen, um jetzt zu verstehen,
wie man sowas effizient bauen kann, sind sogenannte binäre Suchbäume.
Erstmal brauchen wir binäre Bäume.
Also was ist ein binäre Baum? Da hat ein Knoten
maximal zwei Kindknoten
Also wenn jeder Knoten,
einsortierbares Element enthält und gilt das im linken Teil,
warum alle kleineren, im rechten Baum alle größeren sind,
dann habe ich tatsächlich einen binären Baum,
der ein binärer Suchbaum ist.
Hier ist ein Beispiel. Ich habe hier mehrere Elemente in
einem Baum. Jetzt möchte ich zum Beispiel feststellen,
ob die zwölf in der Menge, die da gehalten ist,
schon drin ist. Also diese Menge hier ist hier angedeutet,
das ist die Menge der Zahlen, 19 und so weiter.
Und ich möchte jetzt wissen, ob die zwölf drin ist.
Wir sehen das durch Draufschauen, aber bei einer großen Menge
sehen wir es nicht mehr.
Ja, ist klar. Wir müssen das ja Maschinenverarbeitbar machen
. Also wir haben eine Referenz auf den Wurzelknoten.
Das ist üblicherweise so, wenn man eine Baumimplementation hat.
Genau wie bei einer Liste haben wir ja nur die Referenz
auf den Listen anfangen
Halt hier die Referenz auf den Listen, auf den Wurzelknoten. Na gut,
bei einem binären Suchbaum kann ich halt jetzt fragen,
wenn hier die Kleineren sind, dann weiß ich also,
ich suche die zwölf, gucke mir den Wurzelknoten an,
es ist die 15, nö, ist kleiner,
also gehe ich nach links,
also hier zur 13. Dann sehe ich, oh,
zwölf ist kleiner, also gehe ich nochmal nach links,
dann sehe ich, oh die 12 ist größer und will ich nach rechts,
da ist aber nichts, also weiß ich,
dass in diesem Baum die zwölf nicht enthalten ist.
Das ist die Idee eines binären Suchbaums. Dann gibt es so
ein paar Eigenschaften von binären Bäumen, die ganz hilfreich sind,
zu verstehen, warum binäre Suchbäume so super funktionieren.
Also ganz viele Eigenschaften von solchen binären Bäumen beziehen sich auf
die Anzahl der Knoten und auch die Anzahl der Blätter.
Und auf die Höhe eines Baumes. Okay,
die Höhe eines Baumes kann man hier sehen. Also hier
dieser Baum mit nur einem Knoten hat die Höhe Null,
so einer hat Höhe 1, 2,
3. Und dann kann man verschiedene Aussagen über solche Bäume treffen.
Zum Beispiel ein voller binärer Baum,
wie wir ihn hier sehen, also einer der,
wo alle nicht Blattknoten zwei Kinder haben.
Mit Höhe H hat zwei Hochhar-Blätter
Gucken, dieser Baum hat die Höhe 3, 2 hoch 3 ist 8,
also müssten wir hier 8 Bette haben und siehe da, es sind 8 Blätter.
Stimmt. Oder die Zahl der Knoten in einem vollen binären Baum mit höher Haar ist zwei hoch Haar plus 1,
minus 1. Okay, kann man sich auch überlegen,
irgendwie bei Höhe 0 sind es zwei hoch null Knoten,
zwar auch 0 ist 1, also ein Knoten,
wenn ich Höhe 1 habe,
dann habe ich hier 2 hoch 0 plus 2 hoch 1,
also das von dem niedrigeren Baum plus zwei neue und dann entsprechend jeweils auf jeder Ebene mehr,
also zwar auch null plus 2 hoch 1,
kann ich eine Summenbildung machen und dann komme ich irgendwie darauf,
dass es zwei H plus 1, 2 Hochhaar plus 1,
minus 1 ist.
Also in diesem Fall zum Beispiel hier Höhe 3,
das ist also 2 hoch 3 plus 1,
ist 2 hoch 4, das ist 16,
minus 1 ist 15, also sind hier 15 Elemente in dem Knoten,
in dem Baum drin.
Stimmt. Gut. Nächste Eigenschaft, die man braucht,
wenn man so mit Suchbäumen umgehen will,
ist so die Logik vom Traversieren, also das Durchlaufen durch die Elemente.
Also irgendwie linearisieren. Tatsächlich alle Elemente mal besuchen.
Da haben wir bei einer Liste eben auch schon gemacht,
kennen wir auch schon, diesen Begriff des Traversierens.
Und da kann man verschiedene Strategien wählen Und es kommt
ganz darauf an, was man erreichen will.
Da gibt es also einfach nur hier ganz einfach dargestellt,
Breitendurchlauf und tief im Durchlauf, der Breitendurchlauf ist die Idee,
ich gehe den Baum sozusagen schichtweise durch.
Also immer so der Reihe nach von oben nach unten.
Also hier so in dieser Ordnung. Breite vor Tiefe,
links vor rechts. Also wenn ich irgendwie einen Knoten suchen
will, der einen minimalen Abstand hat zur Wurzel.
Manchmal ist das bei bestimmten Anwendungen ganz nützlich. Oder der tiefen Durchlauf,
ich gehe sozusagen Tiefe vor,
Breite gehe also zuerst in die Tiefe und links vor rechts
bei einem sortierten Baum ist das üblicherweise die Reihenfolge tatsächlich,
wie man dann die in sortierter Reihenfolge auch ausgeben kann
Okay. Die Details sind gar nicht so wichtig.
Was ich jetzt hier ganz kurz nochmal eben auf der Folie
habe, ist der Suchalgorithmus. Also eine Beschreibung,
wie ich den jetzt systematisch in so einem binären Suchbaum suchen kann.
Ja, das ist das, was ich eben von Handeimer
in dem Beispielbaum schon gemacht habe.
Also rekursiv definiert folgendermaßen. Ich habe die Referenz auf den Wurzelknoten,
also auf den Baum, ist der Baum leer.
Dann kann ich sagen, ja, das Element ist garantiert nicht drin.
Suche erfolglos. Wenn nein, wenn es nicht leer ist,
dann ist die erste Frage, ist der Wurzelknoten das gesuchte Element?
Ja, erfolgreich. Wenn nein, ist das Element kleiner,
dann suche ich im linken Baum weiter,
sonst suche ich im rechten Baum weiter
Also das ist relativ einfach, die Beschreibung des Algorithmuses,
wie man in einem binären Suchbaum feststellt, ob ein Element enthalten ist.
Und das ist natürlich hier rekursiv, also wird dann wieder sich selbst aufrufen.
Ein Problem haben wir noch, Bäume können entarten.
Das heißt, bei einem gutmütigen Baum ist die Suche ziemlich schnell.
Das ist ein gutmütiger Baum. Das hier ist auch ein
Baum. Ein binärer Suchbaum, da ist die Bedingung,
jeder Knoten darf maximal zwei Kinder haben.
Naja, hier hat er maximal zwei Kinder,
die haben jetzt dummerweise, haben wir alle nur ein Kind.
Das heißt, dieser binäre Baum ist eigentlich eine Entartung,
in dem Sinne,
dass es einfach
eine Liste ist
Baum so aussieht, ist er nicht sehr effizient.
Dann habe ich wieder lineare Suche. Es wird eine entscheidende Bedingung,
die brauchen wir nochmal eben, dann sind wir gleich durch.
Sorry für so viel Theorie, balancierte, binäre Suchbäume.
Also wenn der Baum bei einem balancierten, binären Baum gilt die Eigenschaft,
dass die Höhen seiner beiden Unterbäume sich maximal um eins unterscheiden.
Das gilt für jeden Knoten im Baum,
die Unterbäume dürfen sich maximal in der Höhe um eins unterscheiden.
Dann berechnet sich die Höhe eines solchen Baumes,
logarithmisch aus der Anzahl der Knoten im Baum
Also hier, H ist dann LD von N. Logarithmus,
Duales. Ja, das ist der Logarithmus zur Basis 2.
Sollte für euch ja alles kein Problem sein.
Das ist ja mathematisch total fit. Wenn ich zum Beispiel
1024 Elemente in meinem Baum habe, wie hoch ist dann wohl der Baum?
Ja, 10. Ah, genau, das ist eine Magic
Number der Informatik, 1024, ist 2 hoch 10.
Also Logarithmus, Duales hier und genau umgedreht.
Also 2010 muss 1024 sein. Das heißt,
was heißt das jetzt für unsere Suche?
Ich habe tausend Elemente und der Baum hat nur die Höhe 10.
Und ich habe ja gerade diesen Suchalgorithmus gesehen,
der muss sozusagen auf jeder Ebene nur eine Frage stellen
Das heißt, er muss maximal zehn Vergleiche machen,
um festzustellen, ob es enthalten ist oder nicht.
Das ist ziemlich cool. Statt tausend Vergleichen,
im schlechtesten Fall, wenn es kein Duplikat ist,
muss ich nur noch zehn machen.
Das ist richtig gut. Und das ist tatsächlich das,
was die Baumimplementation im Jabba Collection Framework machen.
Die benutzen so einen balancierten, binären Suchbaum,
setzen aber voraus, dass die Elemente sortierbar sind.
Das ist ein bisschen eine Einschränkung. In dem Anwendungsbeispiel,
was wir hier betrachten, ist es zum Glück so,
dass Strings sortierbar sind.
Strings haben eine Ordnung
Oder sind sortierbar und entsprechend ist das tatsächlich, funktioniert das hier.
Das heißt, was wir jetzt machen können,
wir bauen jetzt mal eben eine weitere Implementation, Wortschatz, Tree.
Das heißt, ich benutze jetzt einfach mal eine Tree-Set-Implementation aus dem Collection-Framework.
Wir bauen jetzt hier keinen eigenen Baum im Himmelswillen.
Das wäre ziemlich viel Mühe. Ich nehme mal eben kurz
die Implementation hier, kopiere mir die rüber.
Ja, die sieht irgendwie mir da wieder genauso aus.
Es muss ja alles genauso implementieren. Ja,
komm her
Zack. So, was muss ich jetzt noch ändern?
Ich muss jetzt hier so ein paar Dinge ändern.
Ich möchte als erstes, möchte ich statt einer Wortliste,
benutze ich jetzt ein Treeset aus dem Collections-Framework.
Also sage ich hier Java, You Till, Set von String.
Eine Menge von Strings benutze ich intern, um meine Wörter abzulegen.
Und ich benutze tatsächlich hier nicht Wortliste,
sondern New Java,
You Till,
Von String. Das müsste schon mal funktionieren,
das müsste ich eigentlich jetzt hier schon fast übersetzen können,
aber es wird noch viel schlagen, genau,
weil hier Wortschatz, Liste der Konstrukte muss natürlich heißen,
wie die Klasse, also Wortschatz, Tree.
Jetzt kann ich das schon mal übersetzen, glaube ich.
Ja, Moment, rutscht das hier raus, ja,
lässt sich übersetzen. Macht aber natürlich noch nicht
so wahnsinnig viel Schlaues. Bei Füge Wort hinzu,
ja, da benutze ich ja jetzt intern ein Set,
das auf einem Tree basiert, da muss ich jetzt hier das,
mit dem Ist-Enthalten gar nicht mehr machen
, kann also hier einfach sagen, ja,
das rücken wir mal eben
ein bisschen ein
Ich füge es einfach hinzu. Die Implementation kümmert sich selber
darum, dass kein Duplikat drin ist.
Das kann die ja schon. Ich benutze jetzt eine,
die es kann. Wörter Remove funktioniert genauso,
ist enthalten, kann ich mir auch komplett sparen.
Da schreibe ich jetzt einfach nur noch Return, Wörter.contains, Wort.
Das kann die Implementation, lässt sich übersetzen und auch wörter.size ist richtig implementiert.
Deswegen habe ich in meiner kleinen Wortliste hier die Operation genauso mit.
Natürlich ging das jetzt sehr schnell mit dem Kopieren.
Das heißt, ich habe jetzt schwuppdiwupp Eine Implementation,
ich habe noch nicht dafür gesorgt,
dass die auch das Interface implementiert, lässt sich übersetzen.
Das Interface, diese Klasse implementiert jetzt diese Interface.
So, jetzt wird es spannend. Oh,
kurz vor der Pause noch so ein kleiner Cliffhanger.
Ja, wahnsinnig aufregend. Was schätzt ihr denn jetzt,
wie lange das dauert? Also hier sind die Zeiten.
Ja. Was schätzt ihr jetzt, wie lange das dauert?
Wer hat mal einen Tipp? Eben hatten wir
20 Sekunden bei einer Listenimplementation
Werft mit Zahlen an den Kopf. Vier Sekunden,
zehn Sekunden. Eine Sekunde. Weniger als eine Sekunde.
Ewig. Sehr gute Antwort. Okay, also ich starte es
jetzt und ihr werdet sehen, das ist richtig schnell.
Ja, 138 Millisekunden. Einfach nur, um den Kontrast
zu zeigen, ja, dass das ist einfach deutlich schneller.
Das sind hier schon einige Welten. Das ist,
also, boah, das Hundertfache ungefähr.
Hundertmal schneller
Finde ich schon relativ beachtlich. Und das mit, sagen wir
, relativ einfachen Mitteln. Naja, wir haben natürlich jetzt
nicht genau gesehen, wie dieser balancierte Suchtree gebaut wird.
Also das konnten wir jetzt nicht sehen. Wunderbar. Das heißt also,
das ist schon mal schlau, dieses ist enthalten mit einer Tree-Implementation,
mit einem binären Suchverfahren, ist ziemlich schnell.
Es geht aber noch schneller. Oha. Und das
sogar ohne die Notwendigkeit, dass die Elemente sortierbar sind.
Weil Tree-Set erfordert das die Elemente sortierbar sind.
Also das ist noch eine Einschränkung und diese Einschränkung
schmeißen wir auch noch raus
Erst nach der Pause, jetzt machen wir fünf Minuten Pause.
Jetzt kommen wir zu Hash-Verfahren. Also meiner Meinung nach
so jetzt auf der Lab, auf der Ebene,
auf der wir uns hier in SE1 bewegen,
so die Königsdisziplin für clevere Datenstruktur.
Es ist auch eine dynamische Datenstruktur, genau wie ein Baum,
eine dynamische Datenstruktur ist, die sich halt in ihrer Größe verändert.
Eben haben mich zwei Kommilitonen in der Pause gefragt,
wie kann das denn anseilen, angehen,
dass dieser Baum so schnell ist?
Weil wenn jetzt da immer was eingefügt wird,
dann ist er doch irgendwann nicht mehr balanciert,
der Baum
Und das ist richtig. Das heißt,
wenn so ein Baum irgendwie droht zu kippen,
sozusagen, wenn die Balance nicht mehr da ist,
dann muss der Baum reorganisiert werden.
Da kommt die komplette Struktur neu aufgebaut werden.
Und da gibt es einfach wahnsinnig clevere Algorithmen.
Die habe ich jetzt hier natürlich gar nicht thematisiert.
Das ist was für eine Lehrveranstaltung wie Algorithmen und Datenstrukturen.
Da werden dann solche Baumalgorithmen intensiver betrachtet. Aber das ist nicht
das Thema hier von SE1. Hier geht eigentlich nur darum,
dass ihr so ein Gefühl dafür kriegt,
wie unterschiedlich schnell Implementierung sein kann und dass man da eben
halt mit ein paar cleveren Kniffen schon ziemlich viel erreichen kann
So, und zu den Bordmitteln von SE1 gehört,
dass ihr auch lernt, wie man Hash-Verfahren implementiert.
Und Hash-Verfahren haben eine ganz einfache Grundidee, die bilden Elemente
auf eine Indexstruktur ab. Also ich habe Elemente,
die in eine Menge eingefügt werden sollen und ich benutze eine Indexstruktur,
eine Indexbasierte Struktur und ich möchte,
dass ein Element unmittelbar sagen kann, an welchem Index es hingehört.
Das heißt, um das ein bisschen anschaulich zu machen,
der Lösungsansatz für ein Hash-Verfahren ist,
statt dass ich eine Liste komplett durchsuche.
Also ich habe hier
eine Liste von Elementen
Und statt sie jetzt, wie das gemacht haben mit unserer naiven Implementierung,
das Ding sequenziell durchzugehen und wirklich jedes Element anzugucken,
mache ich was anderes.
Ich sorge dafür, dass meine ganzen Elemente,
die ich schon in der Liste drin habe oder in meiner Menge besser gesagt,
dass ich die in mehreren Listen organisiere.
Und dann weiß, in welcher ich suchen muss. Zum Beispiel,
wenn ihr eure Küchenschrank aufmacht und die Schublade aufzieht mit dem
Besteck und ihr sucht ein bestimmtes Messer mit ihm in Kartoffeln schneiden könnte,
dann wette ich mit euch,
dass ihr nicht in dem Fach sucht mit den Löffeln
Sondern ihr sucht wahrscheinlich in dem Fach für die scharfen Messer
oder in dem Fach für die Messer oder so.
So ähnlich ist das hier auch. Das ist schon eine Form von Hashen
, was ihr da macht. Ihr sucht, ja,
ihr sucht in einer richtigen, in einer vernünftigen Teilmenge.
Es gibt wahnsinnig viel Besteck. Ihr sucht ein Messer,
der sucht natürlich nur in den Messern. Genauso ist das hier.
Ich habe hier das mal mit so farbigen Elementen angedeutet.
Also, ja, jede Liste, ich habe also mehrere Listen.
Jede Liste enthält die Elemente einer Kategorie. Ja,
also zum Beispiel hier die Quergestreiften, die senkrecht gestreiften,
die Uni, die irgendwie karierten und die bayerisch blau gestreiften
Bayerisch kariert. So, und wenn jetzt so ein neues
Element daherkommt, dann gucke ich mir erst mal an,
was hast du denn für eine Eigenschaft?
Ah, du bist ein gestreiftes Element. Na,
dann gucke ich einfach nur in den gestreiften Elementen.
Das heißt, ich habe eine sehr viel kürzere Liste zu durchsuchen.
Und auch beim Besteckkasten, ich muss eine sehr viel kleinere Menge durchsuchen.
Also jede Liste enthält die Elemente einer Kategorie und jedes Element
kann Auskunft geben, zu welcher Kategorie es gehört.
Also wenn ihr etwas sucht, dann sucht ihr ein Messer,
dann wisst ihr ja, was ihr sucht.
Also ich suche jetzt ein Messer, dann ist die Eigenschaft klar.
Aber auch wenn wir ein Element haben und das einfügen wollen,
dann können wir das Element ja fragen,
zu welcher Kategorie gehörst du denn?
Und dann muss ich
eine deutlich kleinere Menge durchsuchen
Das ist der Lösungsansatz, der Hashing zugrunde liegt.
Im Kern ist die Idee bei einem Hash-Verfahren,
wenn wir das jetzt implementieren, dass man ein Array benutzt.
Also die sogenannte Hash-Tabelle. Und die kann man verstehen als
eine Tabelle von möglichst kurzen Listen. Das heißt also,
ich habe ein Array in einer Zelle dieses Arrays,
ist eine Referenz auf eine Liste von Elementen einer Kategorie.
Und wenn ich jetzt hier so ein Element habe und dann jetzt irgendeine einen Algorithmus anschmeißen kann,
der aus dem Element den richtigen Index ermittelt, der dafür sorgt,
dass sich dann nur noch eine Referenz habe auf eine kürzere Liste.
Je kürzer die Listen sind, desto besser.
Die Menge, die Anzahl der Listen kann riesig sein.
Wir können ja ruhig viele Listen sein, wenn ich in
der richtigen Liste suchen kann, muss ich viel weniger vergleichen.
Das ist die ganze Grundidee von dem Hashing. Und der Trick ist tatsächlich,
dass diese Tabelle hier, wo die ganzen Mengen,
die ganzen Listen drinhängen, diese Listen,
in denen ich suchen will, Diese Tabelle ist ein Array,
wenn ich dann diesen Index aus dem Element berechne,
dann ist das ein Index in einem Array und ein,
ihr wisst,
ein Indexzugriff in einem Array ist effizient.
Das kann ich in O von 1 machen, üblicherweise, geht man davon aus.
Bei genügend kleinen Arrays. Es wird natürlich schwieriger,
wenn man jetzt auf einem Rechner arbeitet mit einem bestimmten Betriebssystem,
wo dann das ganze Array nicht in den Hauptspeicher passt,
dann muss da irgendwie mit das Betriebssystem was nachladen.
Das sind alles so Sachen hinter den Kulissen,
die auch die Informatik schon lange beschäftigen. Also Datenbank,
Indexstrukturen, die beschäftigen sich da sehr intensiv damit.
Das sind so fortgeschrittene Themen, Gucken wir gar nicht drauf.
Wir gehen davon aus, eine Ray passt erstmal den Hauptspeicher
und dann ist einfach ein Zugriff schnell auf die richtige Liste.
Also ich habe das Element, Berechnung des Index,
des Indexes, dann Zugriff und dann in der kurzen Liste suchen.
Und da muss ich dann eventuell linear suchen. Also hier,
je nachdem wie lang die Liste ist, dauert das halt auch ein paar Vergleiche.
Aber ich muss vielleicht hier, seht ihr,
hier muss ich immer nur drei oder vier Vergleiche machen,
um festzustellen, ob es ein Duplikat ist.
Was ich natürlich will, ich möchte möglichst wenig Überläufe haben.
Also wenn diese Listen Hier länger werden, dann nennt man
die Elemente dann auch gerne Überlaufelemente. Oder diese Listen,
wie ich sie hier genannt habe, die werden auch gerne Überlaufbehälter genannt.
Also es ist ein Behälter und der kann überlaufen.
Das heißt also, da können mehrere Elemente rein.
Eigentlich ist die Idee beim Hashverfahren, dass in jedem Behälter,
in jeder solcher Listen, nur ein Element drin ist oder gar keins.
Dann kann ich diese Frage total schnell beantworten. Ich habe den Index, berechne,
greife auf den richtigen Behälter zu und frage an den Behälter,
ist das Element, ist das Element, was da drin ist?
Ist das gleich zu dem, was ich jetzt hier
gerade untersuche oder nicht
Und wenn links drin ist, dann weiß ich schon,
ja, es ist kein Duplikat.
Und wenn das drin ist, muss ich nur den einen Vergleich machen.
Ich muss nur einen Vergleich machen mit einem anderen Element,
um das zu beantworten. Solange ich keine Überläufe habe,
solange in diesen Behältern hier immer nur ein Element drin ist.
Maximal ein Element, entweder keins oder eins. Ja,
da ist eine Frage. Könnte man das nicht auch so ausprobieren.
Also zum Beispiel, da hat man ja relativ viele Überläufungen,
weil der Peter machen konnte, dass man da nicht eigentlich Stadtlisten,
auch das zitabel an Füßen einsetzt.
Okay, er sagt, eine Möglichkeit wäre doch,
wenn man jetzt das ein bisschen optimieren will,
und da hat er natürlich recht, Das hier sind momentan als einfachste Annahme Listen.
Wäre es denn nicht möglich da, dann wieder Trees einzusetzen?
Und natürlich ist das möglich. Natürlich kann man jetzt wieder
in der Implementation eines Hash-Verfahrens die Überlauflisten als Trees implementieren.
Kann man machen. Ist aber möglicherweise ein bisschen zu aufwendig,
wenn das Hash-Verfahren gut ist, sollten hier,
wie gesagt, nicht so viele Elemente drin sein.
Also ein Hash-Verfahren hat eigentlich als Kernidee die Elemente,
möglichst gleichmäßig über diese Listen zu verteilen.
Und zwar idealerweise so, dass überall nur ein Element
drin ist,
maximal ein Element
Dann braucht man auch keine Array, keine Tree-Implementierung dahinter.
Dann ist einfach nur die Frage, ist das eine Element drin oder nicht?
Aber prinzipiell richtig. Okay. Ganz entscheidend, dass so
ein Hash-Verfahren gut funktioniert, ist die Hash-Funktion. Das heißt,
die Hash-Funktion ist diese Funktion,
die wir auch implementieren müssen bei einem Hash-Verfahren,
die von einem Element sagt, hey, was ist denn dein,
wo finde ich denn jetzt Elemente,
die so sind wie du?
In der Tabelle
Und die Hash-Funktion, die ihr üblicherweise kennt, ist tatsächlich Hashcode.
Also wenn ich hier in so einem Bluejay-Projekt,
kann ich zum Beispiel dieses Exemplar hier geerbt von Object,
fragen nach seinem Hashcode.
Ich kann jedes Objekt in Bluejay nach seinem Hashcode fragen.
Das ist ein Int. Ihr wisst, Int hat einen
ziemlich großen Wertebereich von minus zwei Milliarden bis plus zwei Milliarden.
Können also ziemlich viele Werte sein. Das ist genau das,
was hier auch auf der Folie zu sehen ist.
Also hier der Wertebereich der Hash-Funktion kann sehr groß sein
Also von einem Element, und das ist eben in Java,
ist das tatsächlich die Grundidee,
jedes Objekt kann ja so einen Hashcode liefern,
muss es ja liefern können, ist ja sozusagen für alle Objekte definiert.
Und dann muss man aber natürlich hinterher jetzt aus diesem Hashcode
einen Index in der Tabelle bauen. Das heißt,
eure Tabelle ist ja meistens nicht vier Milliarden Elemente groß,
meistens, sondern irgendwie kleiner. Und ich muss also wieder
sozusagen aus diesem Hash-Wert runterkommen auf einen gültigen Index in der Tabelle.
Das heißt, als erstes, die bildet ein Element auf
einen ganz heiligen Wert ab, auf einen Intiger-Wert.
Dieser berechnete Wert bildet dann so eine Art künstliche Kategorie.
Deswegen vorher hatte ich so zur Veranschaulichung so verschiedenfarbige Elemente oder so verschiedene gemusterte Elemente.
Eigentlich haben wir jetzt sowas wie eine künstliche Kategorie geschaffen.
Also alle Elemente mit dem Hashcode 17 gehören in eine Kategorie.
Okay. Und wenn jetzt die Hashfunktion zwei verschiedene Elemente auf
denselben Wert abbildet, dann ist das eine Kollision.
Und eine Kollision tut nicht weh in diesem Fall.
Das ist dann einfach nur, wenn zwei Elemente denselben Hashcode haben,
dann fallen sie halt in denselben Behälter rein und dann
haben wir da halt einen Überlauf
Wie gesagt, es tut nicht weh. Also ein,
zwei, drei Elemente können da ruhig mal drin sein.
Das ist schon in Ordnung. Okay. Und ich muss aber,
wie gesagt, immer, wenn ich so eine Hash-Funktion einsetze,
einmal die Hash-Funktion anwenden,
dann kriege ich einen Wert und dann muss ich das wiederum
abbilden für mein Hash-Verfahren auf einem Index in der Tabelle.
Und das ist nicht so ganz trivial, weil die Hash-Tabelle
lässt ja nur Werte von null bis Größe der Tabelle zu.
Positive Werte. Int hat aber auch einen negativen Wertebereich.
Das heißt, wenn ihr einen Hash-Code von irgendeinem Objekt berechnet,
kann der auch negativ sein.
Ihr könnt also nicht einfach jetzt so ein Modulo machen und
dann irgendwie den als Modulo-Tabellengröße und dann den als Index berechnen.
Das geht nicht ganz. Da gehe ich gleich noch mal
ein bisschen genauer drauf ein. Vielleicht noch mal ein anderes
Beispiel für eine Hash-Funktion. Ich möchte die Monatsnamen in einer Hash-Tabelle
mit zwölf Positionen einfügen. Ich möchte also irgendwie möglichst schnell
auf die Dinger irgendwie zugreifen können. Ich weiß jetzt nicht,
was das für ein Kontext ist, wo das geht,
aber dass ihr einfach versteht, wie so eine Hash-Funktion funktionieren kann.
Ich habe also Character 1 bis Character Car, das ist
der Monatsname als Zeichenkette, also November ist ein N-OV und so weiter.
Und das große N von so einem Zeichen, Das ist die Binärdarstellung.
Man könnte auch einfach sagen, das ist der Codepoint in dem Zeichensatz,
in dem Unicode-Zeichensatz, also die Position im Zeichensatz,
also das N ist zum Beispiel irgendwie, was weiß ich,
80 oder was, keine Ahnung.
Und dann hat man eine Hash-Funktion, H mit M gleich 12,
die bildet einfach jetzt diese Zeichenketten auf in die Cs von 0 bis 11, folgendermaßen.
Ich addiere einfach diese Codepoints auf, also diese Zeichenposition macht
das Ganze Module 12 und dann kriege ich irgendeinen Wert raus.
Und dann sieht man hier, wie das für die Zeichenketten passiert. Und man sieht,
diese Hash-Funktion ist relativ gut für eine Tabelle mit zwölf Einträgen,
erzeugt sie zwei Lehre, Tabellenpositionen und zwei,
wo es zu einer Kollision kommt.
Das ist so, okay, ist das ganz gut.
Das könnte schlimmer sein. Es gibt auch den Begriff der
idealen Hash-Funktionen und das kann man sich vorstellen. Also wenn ich,
was ich sozusagen, wenn ich jetzt weiß,
wie ein Hash-Verfahren implementiert ist,
brauche es immer so am besten Werte von null beginnend und immer sozusagen den Bereich der Tabelle auffüllend.
Also alle Schlüssel, eins zu eins auf unterschiedliche Integra-Werte abbilden,
die fortlaufend sind und bei null beginnen.
Das ist die perfekte Hashfunktion. Die gibt es für viele Anwendungsfälle
nicht. Also wenn jetzt zum Beispiel Zeichenketten hashen wollt,
dann ist es schwierig, eine perfekte Hashfunktion zu finden.
Die müsste nämlich dann sozusagen für jede Zeichenkette,
die es so gibt, und da gibt es eine ganze Menge von,
eine klare Position in einer linearen Auflistung angehen.
Hat man also häufig nicht. Und dieses Hashverfahren hier,
was wir uns jetzt gleich ansehen werden, das funktioniert genau so.
Also das Hash Set, das ihr ja auch schon benutzt habt
als Implementation von Z. Das basiert auf so einem Hash-Verfahren.
Als Basis für die Hersprung sind wir das Ergebnis der Operation Hash-Code verwendet.
Die ist ja in der Klasse Object und damit für alle Objekte definiert.
Und wir haben ja schon euch erzählt, dann immer,
wenn ihr einen eigenen Typ definiert und da Equals redefiniert,
also sagt, zwei Exemplare meiner Klasse sollen als gleich gelten, wenn das und das gilt.
Equals soll True liefern in bestimmten Fällen.
Dann muss auch derselbe Hash-Code rauskommen. Damals habe ich gesagt,
warum das so ist, erkläre ich später.
Jetzt habe ich es erklärt
Das heißt, wenn wir mal annehmen, wir haben zwei Elemente,
also zwei Objekte, zwei Java-Objekte, die sollen laut Equals gleich sein.
Also wenn ich die mit Equals vergleiche, dann kommt True raus.
Dann würde ich ja erwarten, dass wenn ich die jetzt in eine Menge einfüge,
dass das eine dem anderen gegenüber als Duplikat angesehen wird.
Die sollen ja als Duplikate gelten innerhalb der Sammlung.
Wenn ich jetzt eine Hash-Implementation habe und ich nehme mal an,
die beiden Elemente liefern unterschiedliche Hash-Codes,
dann würde ich ja beim Einfügen, das erste Element einfügen,
es würde in irgendeiner der Listen landen mit dem passenden Hashcode,
ja, abhängig von dem passenden Hashcode.
Das andere Element würde aber jetzt in der Liste in einer
anderen Liste landen. Weil es einen anderen Hashcode hat,
werden die gar nicht miteinander verglichen. Das heißt,
das funktioniert einfach nicht. Es darf nicht sein,
dass zwei Elemente, die per Equals gleich sind,
nicht auch den gleichen Hashcode haben.
Dann zerbrechen alle Verfahren,
die auf Hashing basiert sind
Wie heißt die? Hat er mich noch?
Das ist nicht konform mit der Spezifikation einer Menge.
Das Duplikate nicht eingegetragen werden. Ich hoffe,
dass es ein bisschen nachvollziehbarer jetzt,
warum man eben halt diesen Vertrag hat in der Klasse am Object,
Equits und Hashcode müssen immer zueinander passen.
Genau, die Spezifikation von Z würde damit nicht eingehalten.
Ich zeige mal eben ganz schnell, wie man das implementiert,
aber wir machen mal wieder eine neue Klasse hier.
Wort Schatz Hash. Wunderbar, nehmen wir mal her
Schnappen wir uns schnell die Implementation von hier.
Die ist ja schon ziemlich gut. Zack. Dann habe
ich jetzt hier mein Wortschatz Hash. Da muss ich den
Konstrukt noch ein bisschen umbenennen. Wortschatz Hash. Es ist
auch ein Set vom Typ, den ich intern benutze.
Und ich benutze hier jetzt aber statt eines Tree-Sets ein Hash-Set.
Gut, alles andere kann gleich bleiben.
Head remove, contains, size.
Einmal kurz noch dafür sorgen, dass es das Interface implementiert, übersetzen geht.
Hier ist mein Hash Wortschatz. Und... Boah, Tipps.
Wie viel schneller als das Treeset. Wir haben nochmal hier
die Zahlen. Beim Treeset waren es 138 Millisekunden.
Das Gemeine ist, das ist eine ziemlich gute Zeit.
Also ich bin ein bisschen nervös jetzt,
ob das Herr schon wirklich schneller ist.
Also es ist schneller. Ich verspreche das jetzt einfach
mal,
Ja. Also es ist nochmal ungefähr Faktor 3 schneller in diesem Fall.
Ich finde das schon ziemlich beeindruckend. Also dass man tatsächlich mit Hashen
richtig schlaue Dinge machen kann. Das ist also angesichts dieser Äonen,
die wir vorher gewartet haben, es ist das Gleiche.
Es kommen die gleichen Zahlen raus. Es kommen immer
die gleichen Werte raus. Alle Implementationen sind korrekt.
Es sind alles drei korrekte Implementationen des Interfaces Wortschatz.
Wir hatten sogar vorher noch eine vierte, die noch langsamer
war.
Alle waren korrekt
Völlig unterschiedlich effizient. Und wenn ihr manchmal vor so einem Rechner
sitzt und lange wartet, da hat jemand wieder eine Liste benutzt,
wo man vielleicht ein Hash-Verfahren hätte benutzen können.
Keine Ahnung. Also das geht wirklich viel schneller.
Vorsicht ist aber geboten, wenn ihr Hash-Code implementiert,
ihr dürft das niemals abhängig machen von veränderlichen Exemplarvariablen.
Warum ist klar, wenn ich ein Element erst einfüge mit
einem bestimmten Hash-Code und beim zweiten Mal wird das Element nochmal angefühlt und inzwischen hat es sich irgendwie geändert
Der Zustand hat sich geändert, also hat sich der Hashcode geändert,
dann wird wieder der andere Hashcode dafür sorgen,
dass die gar nicht mehr zum Vergleich kommen,
weil einfach in einer anderen Liste geguckt wird,
ob da ein Element drin ist.
Da war eine Frage. Ich habe eine Frage,
wofür gibt es denn dann dieses Dinerset mit dem Trauer,
denn das herrscht deutlich schneller.
Ja, das ist eine sehr gute Frage.
Er fragt, wenn das Hashen so schnell ist,
warum gibt es dann überhaupt so etwas wie eine Baumimplementation?
Wer hat eine Idee, wer hat eine Ahnung,
wer hat eine Meinung dazu
Tatsächlich könnte man jetzt sagen, wow, Hashen ist doch,
das ist doch die Königsdisziplin, warum macht man überhaupt was anderes?
Ja, es hat auch gewisse Nachteile. Ich erkläre erst mal,
warum das auch bestimmte Nachteile gibt. Nehmen wir mal an,
die Hash-Funktion, die verwendet wird, ist schlecht.
Eine gute Hash-Funktion verteilt die Elemente möglichst gleichmäßig über die Hash-Tabelle.
Klar, dass man möglichst kurze Listen hat,
damit man, also die sollten gleichmäßig verteilt werden.
Also wenn ich da irgendwie, sagen wir,
ich habe in meiner Hash-Tabelle irgendwie tausend Zellen,
dann möchte ich ja tausend Listen haben mit ungefähr
einem Element
Jetzt, was wäre jetzt, wenn die Hash-Funktion konstant den
Wert null liefert? Was passiert dann? Was passiert,
wenn die Hash-Funktion immer denselben Wert liefert? Was passiert dann?
Ja? Genau, dann vergleicht man wieder eine Liste.
Dann fallen ja alle Elemente in dieselbe Liste rein.
Und dann habe ich wieder genau den linearen Aufwand.
Das heißt, Hash-Verfahren können ziemlich leicht degenerieren,
wenn die Hash-Funktion schlecht ist.
Das ist sehr stark abhängig von der Güte der Hash-Funktion
Deswegen war davor dieses kleine Blatt mit auf der Folie.
Also die Güte entscheidet schon sehr über die Wirkung.
Und das ist beim Hashen ganz wichtig.
Man hat also verlässlichere, verlässlichere Leistungsdaten bei einem Tree.
Gut, noch eine kleine Anekdote zum Hashing.
Cooles Zeug. Also ich habe gerade gesagt,
Hashcode eines beliebigen Objektes liefert etwas aus dem Wertebereich von Int,
also auch negative Zahlen.
In der Hash-Tabelle darf ich aber
nur positiv indizieren
Das heißt, beim Abbilden des Hash-Wertes auf einen gültigen Index
in der Tabelle muss ich eventuell das negative Vorzeichen entfernen.
Da gibt es verschiedene Möglichkeiten, sind hier auf der Folie genannt.
Man kann auch verwenden, mas.ups. Das ist eine statische
Methode in der Klasse Mars. Mit der kann ich sozusagen
mir den Betrag einer ganzen Zahl geben lassen.
Kann ich immer so kurz mal demonstrieren. Also ich habe
hier mas.ups von minus 5 ist, oh Wunder 5.
Soweit so gut. Jetzt gibt es aber eine Eigenschaft des Zahlen,
der Zahlendarstellung, die in Java verwendet wird, die etwas unangenehm ist.
Es gibt nämlich folgende Zahl. Es gibt die Zahl Integer.max Value.
Das ist eine ziemlich große Zahl. Irgendwo ungefähr zwei Milliarden.
Diese Zahl ist nicht so problematisch wie diese Zahl hier.
Integer Min-Value. Das ist minus zwei Milliarden.
Aber es ist nicht ganz gleich. Ihr seht hier,
da ist ein Unterschied um eins. Das heißt,
die kleinste Zahl im Int-Bereich ist von ihrem Betrag um eins größer als die größte Zahl.
Das ist so eine ganz blöde Eigenschaft, weil wenn ihr Folgendes macht,
ihr macht Intager-Min-Value und ihr lasst euch jetzt davon Mars.abs berechnen,
was kommt da raus? Eigentlich der Betrag einer negativen Zahl,
also irgendwie wahrscheinlich irgendwas mit einer 48 am Ende.
Das Dumme ist, der Zahlenbereich nach den positiven Bereich,
hat so gar nicht so eine große Zahl.
Die größte Zahl ist da hier das Ding mit der 47.
Was kommt da raus? Da kommt dieselbe Zahl wieder raus. Das heißt,
wenn ihr von der kleinsten negativen Zahl im Zweierkompliment,
Mars.ups aufruft, kommt wieder eine negative Zahl raus.
Das heißt, wenn ihr, sagen wir,
relativ einfach in eurer Implementation Mars.ups benutzt, kann es passieren,
dass manche Strings euch trotzdem einen negativen Index liefern.
Und wenn ihr dann mit einem negativen Wert in der Hash-Tabelle indiziert,
wird das ziemlich unangenehm. Es gibt eine Acception,
wisst ihr, ja, Arry Index out of bounds Acception.
Man darf mit einem negativen Index nicht indizieren. Jetzt könntet ihr sagen,
naja, das ist ja so ein Grenzfall,
das ist irgendwie hier Intager-Min-Value, das kommt ja nie vor.
So, und jetzt leider gibt es so verschiedene Wörter,
die haben genau diesen Hashcode.
Also zum Beispiel gibt es ein Wort, das heißt Hornwieseler.
Kennt ihr das? Das ist ein bayerisches Bier. Hab ich
noch nie getrunken? Hornwieseler, es ist total lecker.
Quatsch, gibt es nicht. Es klingt nur wie ein
bayerisches Bier
Wenn ihr da sagt, Hashcoat, dann kommt genau dass
er raus. Das heißt, es kann euch passieren,
wenn ihr ein Hash-Verfahren naiv implementiert und ihr benutzt sozusagen maß.ups,
um das Vorzeichen loszuwerden, macht dann noch ein Module auf die Tabellengröße,
kann da immer noch ein negativer Index bei rauskommen.
Und zwar, wenn irgendwie, das kann monatelang gut gehen,
also Hash-Verfahren funktioniert wunderbar und irgendwann will jemand dieses unschuldige Wort Hochenwieseler verwenden in seinem Text.
Und eure Implementation stürzt grandios ab. Das ist so ein Paradebeispiel für die Informatik,
schafft sich immer mit diesen Grenzfällen so ihre eigenen Probleme.
Und die fallen einem eben halt nur sehr selten auf die Füße.
Also zufällig auch bei den Übungen, die ihr jetzt diese Woche machen dürft,
haben wir auch so einen Text dabei,
wo ihr dann die Wörter verarbeiten sollt.
Und in dem Text steckt zufällig auch das Wort Hochenwieseler.
Dann würden wir also entdecken, wenn ihr nicht so schlau hasht.
Gut. Das nur dazu. Die Hashverfahren im Java
Clection Framework sind außerdem noch ein bisschen schlauer.
Also die passen auch die Tabelle dynamisch an. Also wenn
die Tabelle am Anfang mit einer bestimmten Größe angelegt wird,
sagen wir 100 Zellen, 100 Listen.
Und jetzt werden da Elemente reingefügt und irgendwann sind da irgendwie 60,
70 Elemente drin, dann ist es üblicherweise so,
dass dann gesagt wird, wenn ein bestimmter Füllgrad der Tabelle erreicht ist,
also ein Verhältnis ist zwischen Anzahl Elemente und Anzahl der Listen,
wenn das Verhältnis zu ungünstig wird,
dann wird einfach eine größere Hash-Tabelle angelegt und dann werden alle Elemente wieder in die neue Hash-Tabelle rüber gehasht.
Das ist also ein Rehash gemacht werden.
Genau wie bei dem Tree muss ein Rebalancieren gemacht werden,
muss man auch bei einem Hash-Verfahren rehashen.
Das ist dann wieder
ein bisschen teurer
Und da kann man eben halt sagen, ab welcher,
also mit welcher Anfangskapazität die Tabelle angelegt wird,
das kann man also im Konstrukttaparameter mit angeben.
Und ab welchem Befüllungsgrad neu gehasht wird. Also eine größere Herrschttabelle
angelegt wird. Üblicherweise ist das irgendwo so bei 60,
70 Prozent, also wenn ich so 60,
70 Elemente eingefügt habe in so eine Tabelle mit 100 Einträgen,
dann kann ich davon ausgehen, dass da schon ein paar Überläufe drin sind.
Und da möchte ich eigentlich wieder was Größeres haben.
Gut, hier nochmal der Vergleich zwischen Treeset und Hashset,
also Balanciertabinäre Suchbaum, Einfüge entfernen in Oloc N.
Reorganisation bei Ungleichgewichten durch Knotenumordnung akzeptabel schnell, oft vor,
Hash-Verfahren mit dynamischer Anpassung der der Hash-Tabelle,
einfügen und entfernen in konstanter Zeit.
Naja, Reorganisation nachher erreichen des Load-Faktors durch komplettes Rehashen teuer.
Geschwindigkeit hängt von der Güte der Hash-Funktion ab. Also wenn
die Hash-Funktion nicht gut ist, kann das auch ein bisschen schlechter werden.
Okay, ein paar Storesi-Fragen zum Abschluss.
Welche Ordnung hat der Aufwand für die Suche nach einem Element
in einem balancierten, binären Suchbaum im schlechtesten Fall?
Im schlechtesten Fall
Ist das O von Login? Ist das O von N
? Ist das O von N-My Login oder ist das O
von N-Quadrat?
Im schlechtesten Fall
Okay, entscheidet euch jetzt. 100 Stimmen möchte ich Hanna haben,
ja, wunderbar, vielleicht kommen wir noch auf 120. Gut, reicht.
Antwortverteilung, oha. Ziemlich gleich verteilt, so.
Also mit einem gewissen Schwergewicht auf O von Login.
Und das ist auch die richtige Antwort
Das ist das Coole an einer Baum-Implementation, wenn der Baum balanciert ist,
wenn es ein balancierter, binärer Suchbaum ist.
Es ist tatsächlich so, dann ist die Suche, wo von Login.
Das ist garantiert. Es wird nicht schlechter. Ja,
das ist, wenn wir mal nur ein Event machen,
ich finde, es ist ja doch nicht auf jeden Fall.
Okay, er sagt, was ist denn, wenn man nur ein Element hat,
dann ist es ja doch O von N, aber es ist auch O von 1.
Und das ist konstant. Also natürlich ist das ein Grenzfall,
den man theoretisch betrachten kann, dass das O von N ist,
aber Also das ist ja,
man nimmt das ja üblicherweise für große Zahlen an,
bei der O-Notation, also für relativ große.
Einen habe ich noch. Einen habe ich noch. Welche Ordnung hat
der Aufwand für die Suche nach einem Element beim Hash-Verfahren?
Ein Hash-Verfahren, im schlechtesten Fall. O von 1,
O von Login, O von N oder O
von N
mal Login
Ja. Okay. Das sind so klassische Fragen,
die wir auch gerne in der Klausur stellen.
So, 100 Antworten haben wir reicht.
Schauen wir mal auf die Verteilung
Das ist ein Booster. Im schlechtesten Fall sagt die Mehrheit,
mehr als die Hälfte OVNN, wovon Endlock-End,
das ist schon eine harte Nummer. Also da wird es
richtig schlecht. OVN1, also im schlechtesten Fall OVN1,
das wäre schon ein ziemlich schlechter, cooler, schlechter Fall.
Also die richtige Antwort ist natürlich das hier.
Das ist genau, wenn die Hash-Funktion einen konstanten Wert liefert.
Dann fällt wieder alles in eine Überlaufbehälter rein,
dann wird die listelistelinear durchsucht und dann hat man O von N.
Das war meine Vorlesung zu Hashing, wenn nicht nur Fragen sind,
dann machen wir Schluss für heute.
Vielen Dank