So, dann können wir, glaube ich, beginnen.
Wir haben in der letzten Vorlesung als abschließendes oder vielleicht auch
einleitendes Beispiel zur Komplexitätsanalyse. Können Sie mich hören, ja
, oder? Zur Komplexitätsanalyse, das Maximum Sub-Array-Problem betrachtet.
Das Problem können Sie hier nochmal sehen. Es geht um
folgende Fragestellung. Es ist also ein Array gegeben mit Zahlenwerten
. Da sehen Sie ein Beispiel mit zehn Zahlen
Und die Frage war, welcher Teil des Arrays,
also welcher zusammenhängende Teil des Arrays,
hat die maximale Summe, ja,
also wir können an irgendeiner Stelle hier beginnen und zählen,
da bist du in einer anderen Stelle und addieren dann die Werte,
die da sind und dann suchen wir den Teilbereich des Arrays mit der maximalen Summen.
Ja, Beispiele für so ein Problem gibt es vielfältige,
wenn die Zeichenketten vergleichen, suchen sie vielleicht einen großmöglichen übereinstimmenden Bereich,
das würde so aussehen, da wäre ja jede Position,
eine Zeichenkette hätte eine Bewertung,
wie gut die beiden Positionen übereinstimmen und dann suchen sie sich die größte Beeinstimmung.
Sowas könnte man damit machen. Letzte Vorlesung habe ich von Börsenkursen gesprochen,
die auf- und absteigen und man sucht die Spanne mit dem maximalen Gewinn.
Also viele Anwendungen für so ein einfaches Problem. So,
dafür haben wir uns in der letzten Vorlesung schon drei Algorithmen angeschaut.
Die werde ich jetzt auch nicht alle im Detail wiederholen.
Das haben wir schon gemacht. Aber was wir nicht mehr geschafft haben,
das wollte ich Ihnen dieses eine Mal mal zeigen,
ist wirklich die Konsequenzen der verschiedenen Algorithmen anzuschauen
Also wir haben den ersten Algorithmus, der erste Algorithmus funktionierte so,
es ist der naheliegende Weg, wie man das berechnen kann.
Man hat zwei Vorschleifen, betrachtet alle möglichen Anfangs- und Endpunkte,
dieser Teilsumme addiert die Werte und bildet darüber das Maximum.
Ja, dann hat man einen Algorithmus mit Laufzeit O
von N hoch 3, also drei geschachtete Vorschleifen,
die im ungünstigsten Fall jeweils in Schleifendurchläufe haben.
So, die zweite Variante, die wir gesehen haben,
war ein Divide-and-Konka-Verfahren
Sehen Sie hier nochmal, da haben wir uns überlegt,
dass man das Array in der Mitte teilen kann und dass
diese maximale Teilsumme entweder auf dieser linken Seite liegt oder auf
der rechten Seite liegt oder sie geht über diesen Schnitt hinweg.
Das sind die drei Möglichkeiten, die wir haben.
Den Linke Teilsumme und die Rechte, maximale Teilsumme können wir
jeweils rekusiv bestimmen in den Verfahren. Und um dann zu berechnen,
wie eine maximale Teilsumme über diesen Schnitt liegt,
haben wir dann ausgehend von dem Schnitt,
von dem Schnitt die Teilsumme nach links und nach rechts bestimmt,
mit diesen Funktionen hier RTS links und RTS rechts und die
beiden Werte haben wir dann addiert
Ja, und dann haben wir über diese drei Werte maximiert.
Das ist das Devide-in-Konkra-Verfahren gewesen. Das ist mir auch
nicht mehr genau anschauen. Und die Laufzeit,
die da herauskam, war, durch Anwendung des Mastertheoriens herausgefunden,
ist das ein Algorithmus mit Laufzeit, N-Log-N, also O von N-Log-N.
So, und das dritte Verfahren,
was wir dann gesehen haben, ist das Scanline-Verfahren,
das Scanline-Verfahren ging quasi mit dieser Scanline von links nach rechts
über das Array und hat immer zwei Werte aktualisiert.
Das eine ist die maximale Teilsumme, die in diesem Bereich
des Arrays schon vorhanden ist. Und das zweite,
was dieses, diese Funktion aktualisiert gehalten hat,
ist eine Variable, die die maximale Teilsumme am rechten Rand,
also an der Scanline hier gespeichert hat.
Und damit konnten wir immer berechnen, was passiert,
wenn die Scanler sich um einfällt, weiter nach rechts bewegt.
Ja, dann ist nämlich entweder die neue maximale Teilsumme,
die enthält dann entweder dieses Element, oh,
das ist der alte Wert, den wir vorher hatten.
So, und dann konnten wir mit einem Durchlauf,
hier ist der Algorithmus, diese maximale Teilsumme berechnen.
Dieser Algorithmus hat Laufzeit O von N. Also wir haben
drei Verfahren, O von N noch drei,
O von N-Log N und O
Die Abschätzung, die wir gemacht haben, sind Laufzeitschranken.
Diese Schranken sind sehr grob. Wir haben alle Konstanten vernachlässigt.
Wir haben nur das Wachstum der Funktion gegen unendlich angeschaut.
Okay, so, wir werden das in der Vorlesung nicht so häufig machen.
Also genau genommen ist das jetzt ein Sonderfall,
weil ich ihn einmal tatsächlich in der Praxis zeigen will,
ob das jetzt wirklich relevant ist.
Für so ein einfaches Problem, ja.
Deswegen habe ich hier ein kleines C-Programm. Wir von Ihnen
kein C-Programmieren. Ah, okay. Also, für Sie
hätten Sie es auch in Java schreiben können.
Ich wahrscheinlich eher von der alten Schule,
habe das
in C geschrieben
Und habe hier so eine Teilsumme-Programm geschrieben. Also C und Java
sind sich ja sehr ähnlich. Verstehen Sie wahrscheinlich auch so.
Es ist ein Code, da sind alle drei Algorithmen drin.
Die kann man hier mit den Diversein- und Ausschalten.
Und geh mal hier ans Ende. Also hier ist der
erste Algorithmus hier oben. Ja, die Blutforce-Suche.
Sie sehen also hier diese zwei Vorschleifen,
die die Ober- und die Untergrenzen durchprobieren.
Und dann sehen Sie hier die dritte Vorschleife,
die dann jeweils die Summe berechnet,
in dieser Variablen Current
Steht für Carren tome und hier wird das immer aufaddiert und dann wird hier
das Maximum gebildet aus der bisher maximal gefundenen Summe und der aktuellen Sommer.
Das ist ja der erste Algorithmus, der zweite Algorithmus ist
hier rekursiv beschrieben, ist dann die beide in Konka-Verfahren.
Also hier wird der aufgerufen, rekursive Berechnung dieser Teilsumme,
steht da jetzt weiter oben. Hier ist der,
da sehen Sie also, wenn linke und rechte Grenze gleich sind,
dann ist das Maximum entweder der Wert an der Stelle oder es ist null, ist die maximale Teilsumme.
Anderenfalls suchen wir die Stelle, an der wir das Array teilen,
das ist die Litte, L plus eher halbe,
dann berechnen wir die maximale Teilsumme im rechten Teil, nee,
im linken Teil und dann im rechten Teil und willen jeweils das Maximum darüber und dann rufen wir unsere Randteilsummenfunktion aus,
die steht da oben drüber,
sieht genauso aus wie im Skript und addieren die beiden Werte und maximieren da darüber auch nochmal.
Das ist ja der zweite Algorithmus gewesen und der dritte,
der dritte, der war ja ganz unten,
der dritte Algorithmus Scanline-Prinzip, eine Vorschleife hier,
die geht über die Array-Länge hinweg und wilde Zeit immer encoverent ass.
Also das war diese Randteilrandsumme und maximiert dann darüber und speichert das Ergebnis in Max.
Okay, so, zwei Algorithmen sind ausgeschaltet,
nur der erste ist an und wir können mal gucken, was passiert.
So, mein Verfahren erzeugt einfach eine Zufallzahlenfolge.
Momentan ist das eingestellt auf 20 Zahlen. Hier sehen Sie
die Zahlenfolge und der Algorithmus eins berechnet als Ergebnis
5055. Ich habe jetzt nicht ausgegeben, von wo bis wo
das ist, aber ist jetzt auch nicht
so wichtig
Und sie sehen, dass das relativ schnell geht.
Also wir können das auch noch mal machen. Das heißt,
für Summen oder für Arrays dieser Länge spielt es alles wahrscheinlich
keine große Rolle. So, aber die Frage ist natürlich,
was passiert, wenn die Array-Länge größer wird,
könnt ihr was anderes einteilen, steht hier.
Also steht 20, hört zum Beispiel 200 eintragen und gucken,
was dann passiert.
Die Frage ist, sehen wir jetzt schon eine
signifikante Verzögerung oder nicht
Habe ich auch nicht ausprobiert. Nö, es geht immer
noch. Also es spielt eigentlich scheinbar keine große Rolle.
Auch hier kriegen sie ein Ergebnis noch innerhalb kürzester Zeit.
Wir machen mal 3000. Ich weiß gar nicht,
ob er da ist. Letzte Vorlesung war jemand da,
der hat gesagt, so im tausender Bereich wird es langsam schmerzhaft.
Da müssten wir langsam etwas länger warten.
Und ist er da? Da sitzen sie. Okay.
Und ich kann Ihnen sagen,
da haben Sie
Recht gehabt
Ja, also er sehen schon,
die Zahlfolge habe ich dann nicht mehr ausgegeben,
das hätte hier den ganzen Bildschirm nur Folge hustet,
wenn die Zahlenfolge so lang ist.
Und sie sehen also, wenn die Folge lang ist,
jetzt sind wir bei 3000, da können wir halt schon ein bisschen warten.
Und ich glaube, dass das noch, ja,
so in der Größenordnung von einer Minute,
also ich habe jetzt nicht auf die Uhr geguckt,
aber es ist so in dem Bereich.
Und ich kann Ihnen sagen, 6000 ist,
warten wir hier nicht mehr ab, ja, nicht in der Vorlesung.
Also es ist dieses Wachstum, was wir mit einer Woche
3 sehen. Ja, Sie haben eine Frage
Okay. Kann ich natürlich machen. Wenn Sie es noch wissen wollen,
dann haben Sie natürlich recht, das können wir noch angucken.
Jetzt weiß ich aber nicht, was ich Ihnen jetzt eine
Minute lang erzählen soll. Okay, jetzt die Frage ist jetzt,
was passiert mit den anderen beiden Algorithmen?
Immer etwas ungeduldig. Wir machen das gleich nochmal mit Time.
Ich mache mal Folgendes. Nein. Ich schalte die anderen
beiden jetzt auch mal ein
So, jetzt berechnen wir das Ergebnis dreimal. Und die
Ergebnisse, also die Algorithmen werden in dieser Reihenfolge aufgerufen.
Richtig. Danke. Da war's. So,
und dann sehen wir jetzt allerdings auch nur die Gesamtzeit.
Aber sie sehen jetzt an der, wenn die,
also Resultate berechnet sind, dann werden die ja erscheinen,
dann sehen sie schon den Unterschied, wie lange das dauert.
Also das erste dauert ja ungefähr eine Minute, haben wir jetzt gesehen.
Der N-Log-N-Algorithmus sollte schon schneller sein. Ob sie jetzt schon
einen Unterschied sehen zwischen dem N-Log-N und dem O von N-Verfahren
, ist natürlich die Frage.
Ich glaube, das sieht man bei 3000
noch nicht
Und so, ja, jetzt haben sie gesehen,
also 24 Sekunden waren das. Bin auch ganz stolz,
kam dreimal das Gleiche raus. Ja, und sie haben gesehen,
also ein ganz großer Teil dieser Laufzeit gegen zur Berechnung des
ersten Verfahrens, was NO3 Laufzeitbedarf hatte.
Die anderen beiden Resultate waren faktisch sofort da. Okay,
also wie gesagt, das jetzt noch auszuprobieren mit 4.000
noch nicht so viel Sinn.
Und auch das hier mal weg. Jetzt haben wir noch
die beiden schnelleren Verfahren im Rennen.
Jetzt können wir mal
was Größeres ausprobieren
Und gucken, was passiert, weiß ich jetzt auch nicht
so genau. Ja, also sie sehen,
da ist jetzt die Diskrepanz nicht mehr so groß im Moment.
Also da müssen die N-Werte erstmal größer werden,
damit man erstmal den Unterschied wieder sieht.
Was glauben Sie denn, wie groß ich das hier machen muss?
Obst? Aha. So zum Beispiel Also Sie sehen,
das geht immer noch ganz gut. Einen Versuch machen wir
noch. Wir haben hier mal noch eine Null da reinschreiben.
Jetzt haben wir 500.000. Das ist immer noch flott. So,
und irgendwann sind wir dann an dem Punkt, dass wir ein Speicher-Problem kriegen.
Also irgendwie vier Millionen mehr können wir dann nicht machen.
Dann irgendwann Geht das mit dem Speicher nicht mehr so einfach?
Da müsste man ein paar Tricks machen, da hatte ich keine Lust.
Das sind jetzt zwei Millionen. Zwei Millionen, das müsste
noch gehen. Und wir können mal sehen. Also hier
sehen sie so langsam eine zeitliche Verzögerung zwischen dem zweiten und
dem dritten Verfahren. Wenn sie jetzt noch weiter hochgehen würden,
dann würden sie noch natürlich auch einen signifikanteren Unterschied sehen.
Okay, also der Grund, warum ich das mache,
also ich werde jetzt nicht jeden Algorithmus,
den wir hier irgendwie vorstellen, irgendwie laufen lassen.
Also das dürfen sie gerne machen. Die dürfen das
gerne als Programmierender laufen lassen. Aber der Grund,
warum ich es ihnen einmal zeigen wollte, ist,
Man hat beim Okalkühl schnell den Eindruck, ja,
da wird so viel abgeschätzt.
Und was da rauskommt, wen interessiert denn das noch?
Ja, und an diesem einfachen Beispiel sieht man schon,
dass es nicht so ist. Ja,
diese Laufzeitklassen sind wirklich entscheidend für die Qualität der Algorithmen und die
wirken sich auf die praktische Laufzeit auch massiv aus.
Ja, okay, so, das war's noch dazu.
Und damit können wir fortschreiten. Und das zweite Kapitel gehen
Achso, vielleicht ist das mal gerade ein guter Zeitpunkt zu sagen,
ich weiß nicht, kriegen Sie alle immer die Stinen-Nachrichten,
die ich darüber versende, wenn ich darüber eine E-Mail schreibe.
Also Sie finden inzwischen die Folien der ersten vier Kapitel.
Können Sie sich da angucken, weil Sie da runterladen.
Ich habe auf der Basis eines Gesprächs mit einem von Ihnen,
sind die jetzt so formatiert, dass auf jeder DIN A4-Seite nur eine Folie ist.
Das ist dann Tablet-konform. Und wenn Sie drucken wollen,
was vielleicht die allermeisten nicht mehr machen, können sie einfach relativ einfach herunterskalieren,
indem sie einfach vier Blätter auf eine drucken und so.
Also andersrum ist es halt schwieriger,
deswegen habe ich jetzt das so gemacht, also wie gesagt,
nur eine Seite auf einer Folie, wenn sie drucken,
eine Folie auf einer Seite, wenn sie drucken,
müssen sie es halt selber ein bisschen runterskalieren,
sonst können sie sich das so angucken.
So, okay, also wir haben die Grundlagen gelegt,
wir wissen, was wir zu tun haben,
um Algorithmen bewerten zu können, um ihre Korrektheit zu analysieren
und natürlich auch um sie bezüglich der Performanz zu bewerten.
Und bevor wir detaillierter weitermachen mit den Algorithmen,
müssen wir ein bisschen die Grundlagen legen für die Datenstrukturen.
Das ist ein Bereich, der vielen von Ihnen schon geläufig ist,
dadurch, dass Sie sich mit Programmierung auseinandersetzen,
hier ist der Bezug zur Programmierung schon relativ stark da.
Trotzdem der Vollständigkeit halber werden wir das hier einmal durchgehen.
Wir werden einfach die ganz grundlegenden Datenstrukturen uns anschauen,
weil wir die nachher brauchen,
um später weiterführende und komplexere Datenstrukturen zu entwickeln und zu analysieren.
Also wir werden Kurz Stapel und Warteschlangen sehen,
Listen und Bäume und im weiteren Teil der Vorlesung werden wir
uns noch mit den Warteschlangen etwas intensiver auseinandersetzen und entsprechende Prioritätswarteschlangen entwickeln.
Und bei den Bäumen natürlich ganz intensiv mit balancierten Suchbäumen noch befassen.
Heute wird es erst mal relativ einfach und wir reden erst mal
über diese Grundlagen der Datenstrukturen. Wenn Sie heute programmieren,
dann haben Sie in jeder Programmiersprache eine ganze Reihe von
erst mal ganz grundlegenden Datentypen zur Verfügung.
Die sind erst mal da. Sowas wie Integer und Gleitpunktzahlen,
Reel oder Double und so,
das haben Sie zur Verfügung
Und auf der Basis dieser Datentypen modellieren sie die Daten,
die sie zur Verfügung haben, die sie beschreiben wollen im Programm.
Dazu brauchen sie eine Beschreibungsform. Und bei der Programmierung ist
es heute eigentlich Standard geworden. In der objektorientierten Programmierung,
dass wir Objekte mit ihren Operationen, die wir auf ihnen ausführen,
zusammenfassen zu entsprechenden Klassen oder Modulen.
Dieses Konzept ist in der Theorie auch relativ stark verankert.
Dort redet man von abstrakten Datentypen und man meint damit nichts weiter als die Vereinigung der,
also die Beschreibung der Objekte, die wir verwenden,
um etwas zu klassifizieren,
gemeinsam mit den Operationen auf diesen Objekten
Ja, das sind abstrakte Datentypen. Die spielen in der Modellierung eine ganz starke Rolle.
Ich werde das hier nicht so in den Fokus rücken,
weil wir uns ja mehr mit dem Komplexitätsfragen von Algorithmen und Datenstrukturen auseinandersetzen wollen.
Ich wollte hier nur am Anfang einmal sagen,
dass das die Grundlage von Datentypen ist und dass man sich auch sehr intensiv damit beschäftigen kann,
dass formal so sauber auch zu beschreiben.
Wir werden das hier nur ganz oberflächlich ankratzen. So,
für uns ein Beispiel hier, ein Datum, wir wollen ein Datum beschreiben,
ein Datum hätte vielleicht zwei verschiedene oder eine klasse Date,
hätte zwei verschiedene Objekte.
Einmal das Datum, was aus seinem Tag ein Modell im Jahr besteht,
Objekt, was für die Anzahl von Tagen besteht.
Und dazu hätten wir eine Reihe von Operationen und die Operationen
würden wir dann entsprechend für diese Objekte definieren.
Da gibt es zum Beispiel sowas wie eine Addition,
die könnte so aussehen, dass wir zu einem Datum,
eine Anzahl von Tagen addieren, ein neues Datum erhalten.
Es könnte auch eine Subtraktion geben und da muss man schon aufpassen,
weil der Begriff der Subtraktion jetzt mehrdeutig ist und man könnte überlegen,
was genau gemeint ist.
Also eine Option wäre zu sagen, ist die Umkehrung der Addition.
Also ein Datum minus einer gewissen Anzahl von Tagen ergibt ein anderes Datum,
aber eine Subtraktion könnte auch bedeuten,
dass die Differenz von zwei Daten berechnet wird und damit
eine Anzahl von Tagen als Resultate rauskommt
Dann gibt es manche Operationen, die in andere Objektklassen verweisen.
Das wäre sowas wie eine Funktion, ist Feiertag.
Damit würde für ein Datum ein Bulscher Wert berechnet,
also wahr oder falsch. Und man verlässt sozusagen in diesem Augenblick
die Klasse und geht quasi in einen anderen Datentyp hinein.
Solche Datentypen müssen entworfen werden. Und wie gesagt,
das hat sehr viel mit Software Engineering zu tun, das richtig gut zu machen.
Aber es hat eben auch was mit Algorithmen und Komplexität zu tun.
Denn die Datentypen entscheiden ganz maßgeblich, schnell die Algorithmen nachher sind,
die diese Datenstrukturen am Ende verwenden werden.
So, wie geht man generell vor? Also häufig ist es eine gewisse konstruktive Methode,
man definiert die Objekte, bestehend aus den elementaren Datentypen und die Operationen,
die der Algorithmus benötigt und man überlegt sich bei diesen Operationen,
die Operationen selber sind ja Algorithmen, die da laufen.
Überlegt man sich, welche von denen braucht man häufig,
welche braucht man seltener und entsprechend versucht man die Datenstruktur so zu entwerfen,
dass die Operationen effizient ausgeführt werden können.
Mit der geweiteren Spezifikation all dieser Dinge werden wir uns hier nicht Nicht herumschlagen. Gut,
vielleicht noch ganz kurz zusammengefasst, aber das wissen Sie alle,
elementare Typen, die eigentlich immer zur Verfügung stehen,
um Daten zu modellieren, sind eben ganze Zahlen,
Begleitpunktzahlen, Wahlswerte, Zeichen.
Wobei, wenn wir Zeichen ein bisschen aufpassen muss,
weil ein String, eine Zeichenkette,
schon ein zusammengesetzter Datentyp ist, der als Array modelliert wird.
Strukturierungsmethoden kennen Sie auch alle aus der Programmierung.
Wir haben Arrays, mit dem wir einfach über einen Index,
eine ganze Ei von Objekten zusammenfassen können.
Wir haben Strukturen, wo wir Opera,
wo wir Datentypen unterschiedliche Art zusammenfassen können
Noch Innomerationen, um neue Typen zu erzeugen.
Unions vergessen wir mal, die brauchen wir sehr selten.
Was noch ganz wichtig ist, ist,
dass es Referenzen gibt, also Verweise auf andere Daten,
die natürlich quasi wie ein elementarer Datentyp behandelt werden.
Okay, so, das sozusagen als Grundlage. Und nun,
ja, die ersten elementaren Datenstrukturen, die wichtigsten,
die es so gibt. Und das erste,
was man da so nennen kann, ist ein sogenannter Stapel.
Die Datenstruktur wird immer definiert über die Art der Operation,
die ich ausführen möchte, auf diesem auf diesen Objekten.
Ein Stapel arbeitet nach einem Lifo-Prinzip. Lifo ist die Abkürzung für Last-in,
First Out. Das bedeutet, sie haben eine Möglichkeit,
Daten zu speichern. Und sie können aber nicht willkürlich auf
die Daten zugreifen, sondern nur in einer speziellen Art und Weise.
Und diese spezielle Art und Weise ist durch diesen Begriff Lifo gekennzeichnet.
Das heißt, sie können auf das Objekt zugreifen,
was sie als Letztes in diese Datenstruktur eingegeben haben, gespeichert haben.
Okay, so ein Stapel hat also typischerweise drei Operationen.
Sie können mit der Head-Operation auf den auf das oberste Element des Stapels schauen,
auf das Letzt eingefügteste Element,
mit einer Push-Operation können sie ein Element oben auf den Stapel legen,
mit einer Pop-Operation, das oberste Element vom Stapel entfernen.
Sieht an, also so aus, hier ist der Stapel,
Head von S gibt ihnen das oberste Element,
das wäre dieses Y mit einem Push,
legen sie einen X oben auf den Stapel drauf,
dann haben sie einen neuen Stapel,
der ist um ein Element höher und dann haben sie eine Pop-Operation,
die nimmt das oberste Objekt wieder herunter.
Das ist dann weg und danach haben sie Zugriff auf das Nächstliegende Element.
Das wäre dann wieder das Y hier. So,
das ist erstmal das,
was wir brauchen und dann stellt sich natürlich die Frage,
wie implementiert man sowas
Vielleicht sollte man auch noch mal sagen,
dass der Stapel wohl auch bezüglich der Betriebssysteme einer der grundlegendsten Datenstrukturen ist,
die wir haben, weil die Ausführung von Computern basiert immer auf Stapeln,
auf zwei Stapeln.
Einmal die Funktionsaufrufe werden in einem Stapel organisiert,
wenn sie also Funktionen aufrufen innerhalb von Funktionen,
werden Speicherbereiche in einem Stapel organisiert.
Und wenn sie den weiteren Speicherbereich betrachten,
wo sie freien Speicher allokieren können für Objekte,
dann ist es nicht wirklich ein Stapel,
aber da werden ähnliche Konzepte verwendet
So, wie macht man, wie kann man sowas machen?
Die einfachste Art und Weise, das zu machen,
ist die Realisierung mit einem Array. Wenn man so einen Array hat,
dann muss man die maximale Anzahl von Elementen irgendwie vorgeben.
Array ist immer, hat immer statische Größe. Und dann kann man
einer zusätzlichen Variablen auf das oberste Element des Stapels zugreifen.
Hier ist dann die Realisierung von jetzt vier Funktionen.
Eine Funktion ist dazugekommen, ist die Funktion Empty oder Empty Stack,
die überprüft, ob der Stapler ein Element enthält oder nicht.
So, die Funktionen sehen wie folgt aus. Also Empty Stack guckt nur,
ob die Variable top hier null ist,
wenn die null ist, dann ist der Stapel leer,
hält keine Elemente und anderenfalls ist der nicht leer und es wird Holz zurückgegeben.
Ein Push-Operation liegt jetzt ein Element auf den Stapel und zwar in
die nächste freie Speicherzelle des Arrays und merkt sich diese Position halt in der Variablen Top,
also Push von S,6, würde also den Wert X auf den Stapel legen,
wenn der Stapel bereits die maximale Anzahl Elemente enthält,
dann würde es eine Fehlermeldung geben.
Anderenfalls würde die Variable Top um 1 vergrößert,
weil der Stapel jetzt ein Element mehr enthält und in
dem Array S würde an dieser Position der Wert X gespeichert
Die Pop-Operation nimmt ein Element runter. Wenn der Stapel zu
diesem Zeitpunkt leer ist, können wir nichts mehr runternehmen.
Da gibt es eine Fehlermeldung. Anderenfalls würde diese Top-Variable
um eins verkleinert. Und dann müssen wir aber,
wir wollen dann auch den Wert zurückgeben,
den dieses Element hatte, was wir vom Stapel herunternehmen.
Das können wir noch machen. Das steht ja noch in
dem Array drin. Deswegen geben wir das zurück,
indem wir auf das ROS zugreifen, mit der Position Top plus eins.
Also weil wir haben das schon um eins dekrementiert.
Ja, und Head gibt einfach nur aus,
wenn der Stapel leer ist, dann sollte man eine Fehlermeldung ausgeben.
Frage, ob das unbedingt unter ist
Sein muss. Also es ist ja nicht,
also der Stapel ist leer und wir haben natürlich keinen Kopfelement,
auf das wir zugreifen können und anderenfalls nehmen wir das vorderste Element des Stapels.
So, das ist wirklich eine ganz grundlegende, einfache Datenstruktur.
Es zeigt aber schon sehr schön, so Prinzipien von Datenstrukturen,
weil man könnte sich jetzt ja fragen, ja warum macht man das alles?
Man nimmt einfach so ein Array und speichert das da drin.
Ja, fertig aus. Der Grund,
warum man sowas tut, ist, wenn sie sicherstellen,
dass sie nur mit diesen Funktionen auf diesen Stack zugreifen,
auf die Datenstruktur zugreifen, Können Sie für einen,
der diese Datenstruktur benutzt, eine Struktur garantieren?
Sie wissen, dass die entsprechenden Eigenschaften, die der Stapel
haben soll, auf alle Fälle immer erfüllt sind.
Das ist der Grund, warum man Datenstrukturen über einen festen Funktionssatz
definiert und denen dann entsprechend anwendet. Okay, so,
für so eine Datenstruktur, so spannend ist sie ja nicht,
braucht man auch mal eine Anwendung.
Hier ist eine ganz schöne Anwendung, wo man so einen
Stapel verwenden kann. Und zwar ist das Problem Folgendes,
was wir lösen wollen
Wir haben einen sogenannten, einen Bracket-String, also wir haben
eine Zeichenkette und wir haben aus der Zeichenkette alles rausgelöscht, außer Klammern.
Wir haben also nur öffnende oder schließende Klammern her.
Und die Aufgabe, die wir haben, ist,
wir wollen diese Klammerstruktur analysieren und gucken,
ob die den so mathematischen Maßstäben gerecht wird.
Und solche mathematischen Maßstäbe wären eben,
also erstmal muss ich natürlich genauso viele schließende Klammern haben wie öffnende Klammern,
die müssen ja immer in Paaren auftreten.
Und das zweite Kriterium ist natürlich,
dass sich zu jeder schließenden Klammer immer eine passende,
öffnende Klammer auch finden und zuordnen muss.
Ja, die sind ja geschachtelt, Klammerpaare.
Und die Frage ist, wie kann ich das überprüfen?
Also wie kann ich jetzt diese Zeichenkette nehmen mit Klammern und dann überprüfen,
ob das ein gültiger Klammerausdruck ist, ja?
Und ja, bei der Gelegenheit möchte ich auch noch ein anderes Problem lösen.
Ich wüsste gerne zu jeder, zu jeder Klammer,
welches die zugehörige Klammer ist, also bei einer schließenden Klammer.
Hier zum Beispiel wüsste ich gerne, an welcher Position steht
denn die zugehörige öffnende Klammer?
Das wäre dann also für die schließende Klammernposition 8,
wer ist die öffnende Klammer in Position 7
Oder für die jetzt zum Beispiel, nee, die ist langweilig,
die hier, also an elf haben wir eine schließende Klammer,
hier steht eine Sechs,
das bedeutet, die entsprechende öffnende Klammer zu der Klammer,
ja eine Position 11, steht an Position 6,
also hier.
Und dann sehen sie, dass es hier auch korrespondiert,
öffnende Klammer an der Position 6 gehört zu der schließenden Klammer in der Position 11.
Also das würden wir gerne berechnen. Wir würden gerne wissen
, welche Klammern gehören zusammen. Das ist diese Ausgabe,
dieses Array hier. Und wir würden gerne berechnen,
ob der Klammerausdruck im Ganzen sinnvoll ist.
So, was haben das jetzt alles mit Stacks zu tun
oder mit den Stapeln,
die wir gesehen haben
Stapel eignen sich in hervorragender Art und Weise,
genau dieses Problem zu lösen, weil wir immer wissen,
wenn wir eine schließende Klammer sehen, dann gehört dazu,
die letzte geöffnete Klammer,
die noch nicht von einer anschließenden Klammer geschlossen wurde.
Also die Idee wäre ja zu sagen, na gut,
wir müssen immer auf die letzte offene,
noch nicht geschlossene Klammer zugreifen können und das machen wir einfach mit einem Stapel.
Wir merken uns einfach die Position,
wo die öffnenden Klammern stehen und wenn eine schließende Klammer ist,
nehmen wir einfach das oberste Element von dem Stapel,
wir müssen die beiden bilden ein Paar ja,
und können dann entsprechend die Ausgabe produzieren.
Und dann nehmen wir diese öffnende Klammer von dem Stapel herunter,
wissen, die hat ja schon eine schließende Klammer gefunden.
Und wenn dann irgendwann wieder eine schließende Klammer kommt, dann wissen wir,
dass wir das entsprechend oberste Element des Stapels wieder verwenden können.
Damit können wir das Problem in hervorragender Art und Weise lösen. So,
das ist die Idee und hier ist auch schon der Algorithmus unter der Annahme,
dass wir unsere Stapel-Datenstruktur, unseren Stack haben, ja,
würde also befolgt funktionieren.
Wir haben also, der Algorithmus heißt bei mir Brackets,
der hat also diese Zeichenkette, ist also ein Array,
Zeichen von eins bis irgendeine Größe.
Anzahl der Länge der Zeichenkette. Und dann haben wir als
Ausgabe hier diese Per-Number, also sozusagen die zugeordneten Klammer-Symbole.
Und das ist auch ein Array gleicher Größe,
aber jetzt auf Integer, also von ganzen Zahlen,
wo jeweils die Position der entsprechenden schließenden Klammer oder öffnenden Klammer steht.
Okay, der Algorithmus funktioniert dann wie folgt.
Wir beginnen also initialisieren irgendwie unseren Stack. Diese Init-Funktion habe
ich ihnen gar nicht gezeigt
Überlegen, wir müssen irgendwie dafür sorgen, dass sie alle Variablen initialisiert sind.
Ja, heutzutage, objektorientierten Programmierung funktioniert das über einen Konstruktor.
Dann haben sie entsprechend dann so einen Typ. So,
dann würden sie in dieser Vorschleife die Zeichenkette durchlaufen und an der Position P. Mal gucken,
was ist denn da für ein Symbol und entsprechend die notwendigen Berechnungen durchführen.
Das machen sie Positionen für Positionen in dieser Vorschleife.
Da ist noch ein Du. Es wird jetzt noch häufiger passieren,
weil ich ja die ganzen Pseudo kurz umformuliert habe,
da habe ich für Sicherheit häufiger noch das ein oder andere oder du,
weil die gibt es ja jetzt nicht mehr, stehen.
Also wenn dir noch was auffällt, was mir nicht auffällt, dann melden sie sich.
Also da ist noch das Du, das muss natürlich weg. So,
wenn an der Position P jetzt eine öffnende Klammer steht,
dann wissen wir, dass wir uns diese Position P auf dem Stack merken müssen.
Das machen wir durch eine Push-Operation, also hoch in die Push-Operation
auf dem Stack auf, speichern da die Position P,
als eine Position, wo eine öffnende Klammer ist.
Anderenfalls, wir gehen mal davon aus,
dass in dem Zeichen, hätte keine anderen Symbole sind,
also es gibt nur öffnende, schließende Klammer,
dann muss es eine schließende Klammer sein,
prüfen wir erstmal, unser Stack leer ist.
Weil wenn der Stack leer ist, dann haben wir
ein Problem. Weil jetzt haben wir eine schließende Klammer,
aber wir haben gar keine öffnende Klammer dafür mehr,
weil alle, die offen waren, haben wir schon geschlossen.
Da müssen wir eine Fehlermeldung ausgehen. Dann ist der die
Zeichenkette offensichtlich nicht in Ordnung. Also kein mathematisch-gültiger Klammerausdruck.
Wenn das nicht so ist, dann wissen wir,
dass an der Position Head von dem Stack die Position steht
von der öffnenden Klammer, die zu dieser schließenden Klammer gehört.
Wir würden dann also in unserem Ergebnis Array per Number
an der Position P,
hier diese Position speichern
Da müssen wir natürlich an dieser Position hier auch die Position P speichern.
Also hier merken wir uns für die schließende Klammer an der Position P.
Wo ist denn die öffnende Klammer? Und hier merken wir
uns für die Position der öffnenden Klammer, wo ist die entsprechende schließende Klammer?
Okay, dann haben wir quasi für diese öffnende Klammer eine Schließende gefunden.
Wir nehmen die vom Stack her runter mit dieser Pop-Operation.
Und das war schon alles. Und am Ende müssen wir noch einen Test machen,
denn wenn wir am Ende keinen leeren Stack haben,
dann gibt es noch eine öffnende Klammer,
für die es keine schließende Klammer gab.
Und deswegen würden wir
eine Fehlermeldung ausgeben
Und würden sagen, hier fehlt also eine schließende Klammer.
Anderenfalls ist alles gut, wir haben also genau alles in Paaren eingeteilt und
das Format wäre also in Ordnung, das könnten wir zurückgeben.
Ja, ganz einfacher Algorithmus für dieses Problem und der soll einfach
nur zeigen, wie einfache Datenstrukturen in Algorithmen eingesetzt werden können.
Und wenn man ein Grundverständnis davon hat, wie die Datenstruktur funktioniert,
natürlich auch ganz massiv zum Verständnis beitragen.
Stellen Sie sich vor, hier würden jetzt Arrays mit Array-Positionen stehen,
dann wäre es viel schwieriger nachzuvollziehen,
was in diesen Operationen genau eigentlich passiert und warum man das
eigentlich macht
Ja. Wir haben natürlich auch noch einen anderen großen Vorteil,
die Verwendung der Datenstrukturen, so wie wir das hier sehen.
Es könnte ja sein, dass sich die Anforderungen an diese Datenstruktur
leicht ändern. Zum Beispiel haben wir so große Klammerausdrücke,
dass wir das Ganze nicht mehr durch eine Konstante vorgeben können.
Also wir können da nicht mehr sagen, es gibt hier
so einen Max-E-Wert, sondern das wissen wir nicht.
Wir müssen vielleicht dynamisch entscheiden, wie groß dieses Array sein muss.
Ja, wenn das so wäre,
müssten wir Veränderungen vornehmen an der Datenstruktur,
aber durch diese Kapselung müssten wir keinerlei Änderungen an unserem
Algorithmus vornehmen
Was noch viel wichtiger ist, ist,
es kommt jetzt bei dieser Datenstruktur nicht vor,
weil die viel zu einfach ist,
aber wenn wir komplexere Datenstrukturen haben, dass es natürlich sein kann,
dass wir eine effizientere Implementierung oder eine effizientere Algorithmen für die Operation der Datenstruktur finden.
Und die könnten wir dann einsetzen. Das hätte Einfluss auf
die Effizienz des Algorithmus als Ganzes. Und dementsprechend könnten wir
ebenfalls ohne Änderung des Algorithmus selbst hier auch die Laufzeit nochmal verbessern.
Wir hätten also eine strikte Trennung von den Operationen auf
der Datenstruktur und dem Hauptalgorithmus,
der hier den Ablauf zeigt
Ja, okay, das war ein einfaches Beispiel für Stapel.
Ja, hier können Sie ja auch noch mal sehen,
wie es funktioniert, wenn Sie es noch nicht glauben,
dass es richtig ist. Also hier ist ein Beispiel,
das ist ein Ausdruck mit zwölf Zeichen öffnen und schließende Klammern,
der die Position P, die schreitet hier so langsam voran.
Wir sind an dieser Position drei hier, das ist die erste,
schließende Klammer, zu dem Zeitpunkt hätten wir oft den Stack,
der wechselt hier von unten nach oben,
bereits die Zahlen eins und zwei gespeichert.
Also eins und zwei, weil das waren öffnende Klammern.
Wir sind an der Position drei. An dieser Position würden wir
auf den Also auf die oberste Position des Stacks zugreifen und
würden feststellen, die Zugehörige öffnende Klammer ist an Position 2.
So, dann würden wir hier die 2 hinschreiben, hier entsprechend die 3.
Dann würden wir weiter marschieren, hier hätten wir eine öffnende Klammer,
die würde dann oben auf den Stack liegen und wir werden an dieser schließenden Klammer angekommen.
Wir würden also dann hier das Paar 4, 5 hinschreiben.
Die P-Position läuft weiter, wir haben eine öffnende,
wir haben eine Öffnung, wir haben eine Schließen,
da passiert das Gleiche wie hier, das habe ich jetzt mal übersprungen,
steht dann 8 und 7 und dann sind wir in dieser Position angekommen.
Der Stack hätte jetzt die Elemente 1, 6 und 9,
an der Position 10 würden wir also die passende Klammer finden an der Position 9,
würden das entsprechend zuordnen und dann würden wir ans Ende laufen und irgendwann würden wir dann hier auch die schließenden Klammern,
die letzten zuordnen.
Der Stack wäre am Ende leer und wir hätten das Ergebnis entsprechend berechnet. Okay,
so, soweit zur, zum Stapel, ja,
wenn es die Möglichkeit gibt, auf das zuletzt gespeicherte Element zuzugreifen,
dann muss es natürlich auch eine Option geben,
auf das zuerst gespeicherte Element zuzugreifen.
Nächste elementare Datenstruktur, Das ist eine Datenstruktur, die nach
dem FIFO-Prinzip arbeitet. FIFO steht für First-in-First-Out, also das,
was zuerst reingekommen ist, kommt auch zuerst wieder raus.
Und das speichert quasi in der Reihenfolge,
in der Sachen entsprechend in die Datenstruktur hineingelegt wurden.
Deswegen heißt das auch Schlange, also die Kurzform für Warteschlange.
Sie speichern Daten und irgendwann werden die prozessiert und
dann geht es an der Stelle entsprechend weiter.
Für die Schlange haben wir wiederum drei Funktionen,
wir können wieder auf den Kopf zugreifen der Schlange
Das ist das vorderste Element, das, was als nächstes prozessiert werden kann.
Und wir können eben auch nur auf dieses zugreifen.
So, und dann haben wir zwei Funktionen.
Das eine, mein Element, in die Schlange hineinzugeben.
Das wäre also das Gleiche, wie unsere Push-Operation im Stack.
Das ist die Ancue-Operation, mit der fügen wir,
also ein Element X, am Ende der Schlange an.
Und dann haben wir eine DQ-Operation, die macht das Gegenteil,
sie nimmt ein Element vom Stack, Entschuldigung, von der Schlange.
Allerdings nicht das Letzte, sondern das vorderste Element.
Das macht DQ, also entfernt das vorderste Element der Schlange
Okay, also ich sehe jetzt so aus,
wir haben also hier unsere Schlange, Head von
Es zeigt auf das vorderste Element. Wir machen eine Ancue-Operation,
dann wird, also hier stehen weitere Elemente,
also hier steht irgendwas anderes drin an den Stellen,
da stehen nur keine Buchstaben.
Da würde das X hinten angefügt an die Schlange und mit
einer DQ würde Operation, würde das vorderste Element halt entfernt.
Okay, auch hier stellt sich natürlich die Frage,
wie kann man diese Operation effizient implementieren?
Und es ist jetzt nicht ganz so einfach wie beim Stapel,
weil jetzt hat man so ein bisschen ein Problem,
weil wenn man jetzt einen Array verwendet und immerhin hinten was anfügt und vorne was wegnimmt,
dann schiebt man sozusagen die gespeicherten Daten so langsam durch das Array durch.
Und auch wenn immer nur eine feste Menge von Informationen in dem Array drinsteht,
gehen die Indizes quasi immer weiter nach hinten.
Die werden immer größer. Deswegen muss man sich überlegen,
wie man den Speicherplatz, das Array optimal ausnutzen kann, bei der Realisierung.
Und das macht man, indem man quasi zyklisch,
also das Array als einen zyklischen Speicher betrachtet,
indem man also zyklisch herumlaufen kann.
Also eine Möglichkeit, so eine Schlange zu realisieren,
wäre also die folgende.
Wir haben wieder
einen Array
Das ist eine maximale Anzahl von Elementen hier angibt, die wir speichern können.
Wir haben zwei Funktionen, die heißen Head und Rear,
also Head ist das vorderste Element der Schlange und Rear ist,
ja, wie sie wollen, das letzte Element der Schlange oder,
das ist die Art und Weise,
wie wir das hier machen werden, das erste Element,
was nicht mehr belegt ist, ja,
also das Ende der Schlange sozusagen.
So, wir machen eine Implementierung hier,
indem wir bei der Position null anfangen,
das hat den großen Vorteil,
dass wir mit der Mololo-Operation relativ einfach arbeiten können und dann
hier zyklisch bei der Addition immer in einem Array herumlaufen
zu können
Gut, wie sähe das jetzt aus? Also die Initialisierungsfunktion wäre halt
so, dass Head und Rear auf 1 gesagt wird.
Ja, also beide Positionen zeigen auf die Array-Position 1.
Ich frage mich gerade, warum, die können sich auch auf Null zeigen.
Also gut, fangen wir über 1, ist egal.
Genau, auf 1 zeigen. Wir könnten noch auf Null zeigen,
könnt noch auf 5 zeigen, wenn sie wollen.
Also sie müssen auf das gleiche Element zeigen. Das ist das Entscheidende,
weil daran können wir erkennen, ob die Schlange leer ist.
Also wenn Head und Rear auf das gleiche Element zeigen,
Head würde auf das erste belegte Element zeigen,
Rear auf das erste freie Element, das Gleiche ist,
dann wissen wir, diese Schlange ist leer.
Das ist für uns das Signal, keine Elemente in der
Schlange gespeichert und damit können wir die MTQ-Operation hier realisieren.
Haben wir also, wir gucken einfach, ob die Head- und
die Video-Position die gleiche ist, dann gehen wir True zurück.
Andernfalls False. So, was machen wir jetzt,
wenn wir dort etwas speichern wollen? Das ist die Ancue-Operation,
damit würden wir etwas Zusätzliches in der Datenstruktur ablegen.
Das wäre jetzt das Objekt X hier. Als erstes mal
müssen wir gucken, ob wir einen Überlauf haben.
Also ob wir gegebenenfalls zu viele Elemente in der in unserer Schlange speichern wollen.
Das ist jetzt nicht mehr so trivial. Wir können nicht einfach gucken,
ob wir bei Max E angekommen sind, also der Array-Größe,
weil wir haben ja gegebenenfalls ihr vorne noch Platz, um was zu speichern.
Deswegen machen wir Folgendes. Wir nehmen die letzte Position,
rechnen eins drauf. Wir würden sagen, okay,
das ist dann die nächste Position,
wenn wir das gespeichert haben und rechnen das Modulo der Größe des Arrays.
Und wenn das der Head-Position entspricht, dann wissen wir,
dass die Real-Position nach Einfügen auf die Head-Position zeigt und dann hätten wir quasi einen Überlauf.
Daran erkennen wir den. Dann beißt sich wortwörtlich die Katze
Die Schlange in den Schwanz. Okay, so. Anderenfalls,
anderenfalls, würden wir jetzt an dieser Position kurier das Element speichern und kurier,
also die Position, wo das Ende der Schlange gespalten ist, um eins vergrößern.
Wir würden das jetzt an dieser Position hier,
wenn das X speichern und die Position,
also die Variable will dann auf das nächste Element sein,
auf das achte Element.
Und das funktioniert auch für den Fall,
dass die Schlange hier über diese Array-Grenze hinausgewachsen ist,
wieder in den vorderen Teil des Arrays hinein.
Und sie sehen, wenn wir jetzt also weitere Elemente speichern,
würde das Kurier so langsam weiter nach rechts wandern,
wenn wir Elemente aus der Schlange herausnehmen, wandert das Head weiter nach rechts.
Und wenn wir nichts mehr herausnehmen und nur noch wieder einfügen,
werden wir irgendwann dahin kommen,
dass das Kurier wieder auf das erste Element zeigt, auf das Head-Element.
Und dann hätten wir den Überlauf, dann wäre die Schlange voll.
So, herausnehmen, geht dann einfach genauso. Ja,
also wenn wir etwas herausnehmen, dann machen wir das, entnehmen wir das erste Element.
Wir gucken, ob Head und Reel gleich ist.
Hier hätte man auch die Funktionen empty verwenden können.
Das ist ja die gleiche Abfrage, habe ich hier nicht gemacht.
Also wenn Head und Reel gleich ist, dann wissen wir,
da ist kein Element in der lange drin,
dann gibt es eine Fehlermeldung. Anderenfalls wissen wir das Element,
was wir als Rückgabewährt haben, das Element,
was da gespeichert ist am Kopf der Schlange, ist also das,
was in dem RAQ an der Position Head steht und die Head-Position hier,
die wird um eins nach rechts geschoben.
Wiederum Modulo der Größe des Arrays. Und dann wandert das
eine Position weiter und dadurch ist das Element,
das steht jetzt zwar noch drin,
aber es wird von uns nicht mehr betrachtet als Teil der Schlange.
Okay. Gut, das ist ein zweites Beispiel für eine
Datenstruktur. Ich habe mir jetzt mal das Beispiel verkiffen,
also wir haben ja eins beim Stapel gesehen,
also für diese Schlangen können natürlich auch Beispiele finden
Wo die eben als Datenstruktur genau die richtige sind,
um das zu bearbeiten. Wir werden,
sobald wir sortieren, uns mit Schlangen noch auseinandersetzen,
allerdings nicht mit diesen normalen Schlangen, sondern mit sogenannten Prioritätswarteschlangen.
Also mit Warteschlangen, die die Möglichkeit haben, das Objekt,
Prioritäten zugeordnet werden und dann die Objekte in dieser Priorität aus
der Schlange herausgenommen werden können.
Gut, so, Listen,
ja, die möchte ich hier einfach nur der Vollständigkeitshalber erwähnen
Listen sind endliche Folgen von Elementen aus einem Grundtyp.
Und die Elemente haben bei Listen immer eine Ordnung.
Also bisher hat sich noch alles an wie ein Array.
Ja, es ist auch relativ ähnlich. Es ist ja
die abstrakte Betrachtungsweise. Ein Array ist eine Möglichkeit,
seine Liste wiederum zu realisieren. Und der Grundtyp der Objekte,
die wir speichern, ist für uns jetzt von Untergeordneter Bedeutung,
genauso wie bei den Stapeln und den Warteschlangen auch in Schlangen.
Also wir wollen einfach nur eine Liste von Objekten haben.
Aber es ist, was schon wichtig ist,
ist,
die haben eine Reihenfolge
So, die Grundoperation ist sowas wie einfügen. Wir wollen
eine beliebige Position einfügen können, also an der Position P.
Wenn wir also die Liste A1 bis ein,
das war die Liste vorher, an der Position P einfügen,
hätten wir danach die Liste,
die eine Position 1 bis P das Gleiche stehen hat,
dann kommt das neu eingefügte Element X und dann kommt der Rest.
Wir können auch irgendein beliebiges Element löschen und wenn wir ein Objekt löschen,
dann hätten wir das Objekt P,
das würde hier gelöscht und die neue Liste geht dann hier bis P minus 1,
die überspringt dann das P-Element,
das ist jetzt aus der Liste heraus,
geht dann von P
plus 1 bis
Dann würden wir gerne auch suchen können. Also wir suchen
dann ein Element X oder wir suchen die Position,
wo X gespeichert ist. Ja,
gehen dadurch das Array durch und werden in der Position AP,
dann das X steht, wer die Rückgabe wert P.
Und wir wollen natürlich auf einen beliebigen Wert zugreifen können.
Und das können wir natürlich so auch machen.
Ja. Okay, das sind die typischen Operationen von Listen.
Es gibt noch weiterführende Operationen, zum Beispiel Faketten.
Das ist eine Operation, die auf Listen häufig vorkommt.
Das heißt, ich habe zwei Listen, ich möchte eine
lange Liste daraus machen. Das machen wir,
das beschreibt man durch so einen Conkett-Operator,
also durch so zwei senkrechte Striche
Hier. Wir wollen natürlich wissen, ob die Liste leer ist.
Wir wollen wissen, wie viele Elemente darin gespeichert sind.
Wir wollen vielleicht sowas machen wie Anhängen.
Anhängen kann man natürlich realisieren durch Einfügen. Das ist also
ans Ende der Liste, eine RWR X eingefügt wird.
Wir haben ab und zu so Sachen wie Kopf oder Rest,
also wo wir sagen wollen, was ist denn das erste Element der Liste?
Was ist denn alles außer das erste Element der Liste?
Und dann würden wir entsprechend hier die entsprechende Liste bekommen. Und dann,
ja, haben wir noch eine andere Art von entfernen,
nämlich entfernen nicht über einen Index, sondern über einen Wert.
Das entfernen zwei. Dann würden wir also erstmal nach
dem Element suchen. Dann kennen wir die Position,
würden es dann aus der Liste löschen.
Ja, das ist ein guter Punkt. Nee,
man geht nicht davon aus, dass sie alle verschieden sind.
Und da haben sie einen guten Punkt. Das heißt,
bei der Operation suchen könnte es dann mehrere Antworten geben.
Das meinen sie. Und dann müsste man streng genommen noch spezifizieren,
was diese Suchoperation genau macht,
dass sie dann also das erste Element zum Beispiel,
das erste Element findet,
welches also diesen Wert hat in dem Array.
Ja, da haben sie recht.
Ja, hier sind Punkte. Also Listen sind so allgemein,
die treten in so vielen Facetten auf.
Das ist auch immer wieder spezifische Funktionen gibt auf solchen Listen.
Aber es lohnt sich trotzdem mal einmal reinzuschauen und zu überlegen,
wie solche Listen eigentlich realisiert werden. Die eine Variante,
sowas zu machen, ist natürlich wieder mit einem Array.
Das ist das, was erstmal das naheliegendste ist.
Man hat wieder einen Array, da schreibt man das alles rein.
Das wäre eine sequenzielle Speicherung. Und für einige der Operationen
hat das auch eine Menge Vorteile. Also wenn sie zum Beispiel
die Operation haben, gib mir den Wert des vierten Listen-Elements.
Das ist ja super, da können sie mit dem Array
direkt über den Index hier das Element zu greifen und dann die Ausgabe produzieren.
Es hat aber auch eine Menge Nachteile. Also wenn sie einfügen,
ist das sehr kostenintensiv, weil wenn sie einfügen, dann müssen sie ja,
wenn sie jetzt zum Beispiel an der Position hier zwischen drei und vier einfügen wollen,
dann müssen sie ja alle Elemente hier um eine Position nach rechts schieben,
damit sie in dem Array Platz haben,
um an dieser Stelle etwas zu speichern.
Die zweite Variante, sowas zu machen,
ist durch eine verkettete Speicherung, was man da macht,
ist, man sagt, okay, ich habe immer so Datenobjekte,
die bestehen aus dem Element selber, was gespeichert werden soll,
also hier das Array Element A1 und aus einem Zeiger,
der ist hier durch diesen Krieg oder ist das O markiert?
Und der Zeiger hat einfach eine Adresse auf das nächste Element in der Liste.
Also von A1 zeigen wir dann auch das Element A2 und dann
ist da hier der Zeiger auf das Element A3 und von hier
ist der Zeiger auf das Element A4.
Diese Nomenklatur hat den großen Vorteil, dass sie relativ schnell einfügen können,
vorausgesetzt, sie sind schon an der richtigen Position,
weil sie hier die Zeigerstruktur manipulieren können und ein Element einfügen können.
Ja, hat aber den Nachteil,
dass wenn sie ein Element suchen,
müssen sie sequenziell durch diese Liste durchgehen,
bis sie das Element tatsächlich gefunden haben.
Na gut,
das müssen sie oben auch
Aber wenn sie die Position schon kennt,
wenn sie sagen, ich will das vierte Element haben,
können sie hier in konstanter Zeit darauf zugreifen.
Hier bauen sie lineare Zeit, um diese Zeigerkette zu folgen,
um an die entsprechende Position zu kommen.
Diese Arten von Listen durch diese Verkettung von Elementen werden an vielen Stellen eingesetzt.
Gerade da, wo man einfügen muss und so ein Verschieben
sehr teuer ist. Also wenn sie zum Beispiel mal überlegen,
wie Daten auf Festplatten organisiert sind, dann ist die Festplatte,
ist das alles schöne Blöcke eingeteilt,
weil man weiß ja nicht,
wie groß die Dateien sind oder eine Datei besteht nur aus
vielen Blöcken
Und dann müssen sie, wenn sie eine Datei einfügen,
brauchen sie halt freie Blöcke, wo sie das reinschreiben können,
wenn sie eine Datei verlängern, müssen sie dann da noch was dranhängen.
Und sowas macht man natürlich mit Listen von Blöcken,
wo sie am Ende eines jeden Blogs ein Verweis haben auf das nächste,
den nächsten Blog. Was man hier auch sieht,
ist ein ganz typisches Szenario, was man bei Datenstrukturen hat.
Man hat verschiedene Möglichkeiten, eine Datenstruktur zu realisieren,
hier als Array oder als so eine Verkettung.
Und die haben Implikationen für die Laufzeit. Häufig ist es nicht so,
dass es eine Realisierung gibt,
wo alle Operationen ganz effizient sind Häufig hat man den Fall,
dass man so oder so machen kann.
Und in der einen Variante ist dann vielleicht die Zugriffsfunktion ganz schnell,
aber die Einfügeoperation langsam und in der anderen Variante ist dann
vielleicht die Einfügeoperation schnell, aber die Zugriffsfunktionen langsam.
Und hier sieht man schon, der Entwurf von der Implementierung
einer solchen Datenstruktur ist nicht unabhängig von der Anwendung.
Also wir müssen eigentlich erstmal wissen, wofür brauchen wir die Datenstruktur,
wie häufig treten in meiner Anwendung die verschiedenen Operationen auf.
Und dann können sie entscheiden, welche Datenstruktur sie verwenden.
Es ist eben
ein großer Unterschied
Wenn sie irgendwie, keine Ahnung, im großen Buchkatalog haben,
ja, wo sie einmal im Monat zehn neue Bücher einfügen,
aber fünfmal pro Sekunde darauf zugreifen, weil sie irgendwas nachschauen,
ja, oder sie haben eine andere, da eine andere Anwendung,
wo sie kontinuierlich einfügen und in seltenen Fällen einfach anfragen,
um dann die richtige Antwort zu bekommen.
Ja, das muss man eben entsprechend berücksichtigen. Okay,
also, wenn man die Sache über einen Array macht,
also sequenzielle Speicherung, dann kann man sich schnell überlegen,
wie viel diese Operation hier benötigen, also ein Zugriff auf ein Element,
geht in konstanter Zeit, O von 1.
Wenn Sie wissen, ich will das Peter-Element haben, geht das in konstanter Zeit.
Das ist der große Vorteil der Array-Implementierung. Suchen,
einfügen, entfernen und verketten, also zwei Listen aneinanderhängen,
benötigen alle im ungünstigsten Fall lineare Zeit,
weil sie durch das Array durchlaufen müssen,
um die Position zu finden, wo das Element steht,
beim Suchen,
beim Einfügen müssen sie im schlimmsten Fall mehr als die Hälfte der Elemente um ein Element verschieben,
um das einfügen zu können.
Und beim Entfernen das Gleiche, das Array muss dann geschlossen sein
und es funktioniert das ja mit dem indizierten Zugriff nicht mehr.
Und beim Verketten müssen sie es alles kopieren. Okay,
der Vollständigkeit halber, so würde das aussehen,
wenn man das implementiert oder wenn man im Pseudo-Code dafür angebt,
wenn man jetzt also die Liste, die ich mit einem Array,
das wäre ja zum Beispiel die Suchfunktion,
man würde an der Position null anfangen,
häufig verwendet man ausgezeichnete Positionen einfach zur Markierung.
Also hier würde die Nullposition nicht kein Array-Element enthalten, da steht nur eine Null drin.
Man würde dann an der Position null das X hineinschreiben,
die Position würde jetzt bei der Suche von hinten beginnen,
also hier an der hinteren Position und solange die Position ungleich dem X ist,
würden wir die Position um eins dekrementieren,
weil wir an diese Position am Anfang das X reingeschrieben haben,
wüssten wir, dass spätestens am Anfang des Arrays hier.
Schluss wäre an der Position null und dann könnten wir
das Ergebnis zurückgeben. Das ist eine Suchfunktion,
die gemäß ihres Kommentars eine spezielle Suche ausführt,
nämlich bei gleichen Elementen würde sie jeweils das letzte Element,
was den entsprechenden Wert hat,
ausgeben,
bei dieser Implementierung
Okay, wenn Sie einfügen wollen, dann müssen Sie Platz schaffen.
Das sehe ich dann zum Beispiel so aus. Also Sie prüfen erst mal,
ob das Array voll ist, also ob Sie da überhaupt noch
ein Listenelement aufnehmen können, das gibt es eine Fehlermeldung.
Anderenfalls, jetzt müssen Sie erst mal schauen,
ob die Position, wo Sie einfügen wollen, gültig ist.
Also wenn die Position hinter dem letzten Array-Element ist,
also mehr als eine Position hinter dem letzten Array-Element,
oder wenn der Wert kleiner ist als eins,
dann können Sie da nicht einfügen,
dann ist das also eine fehlerhafte Eingabe.
Anderenfalls verschieben Sie von der Endposition,
auch hier ist auch noch ein Fehler,
das muss L-Puccis heißen
Von der Endposition aus rückwärts immer das Element um eine Position nach hinten. Ja,
also hier würde man anfangen, das ist die Endposition,
man schiebt es immer ein Element nach hinten,
bis man an der Position P angekommen ist hier.
Und an der Position kann man an das X speichern.
Und dann wäre das die neue Variante des Arrays
und das Attribut Size würde um eins vergrößert.
Also auch alles relativ einfach. Löschen können wir dann genauso
. Beim Löschen gehen wir an diese Position P und von
P aus verschieben wir alle Elemente eine Position nach vorne,
indem wir einfach den Wert von der Position Post,
also es fängt ja mit P an auf den Wert der dahinter liegenden Speicherzelle setzen.
Dadurch rutscht das alles um eins nach vorne.
Okay, hier ist scheinbar richtig. Okay. Okay,
viel spannender ist natürlich die Art und Weise, wie wir das Ganze machen,
wenn wir Zeigerstrukturen verwenden. Das könnte dann so aussehen,
die Datenstruktur würde dann aus so Knoten bestehen,
also Listen, Knoten, das wäre also ein Listenelement,
X und X hätte dann zwei Attribute,
das ist auch Attribut Key, da steht also der Datenwert drin,
der gespeichert werden soll.
Und das Attribut Next, das ist eben der Zeiger auf
das nächste Listenelement
Ja. Und wenn das so ist, dann bräuchte man nur noch
ein Attribut für die Liste als Ganzes, nämlich den Zeiger auf das erste Listen-Element.
Also das wäre so eine verkettete Liste,
die sieht so aus und für so ein Element,
hier haben wir also zwei Attribute, Key und Next,
ne, Next der Verweis auf das nächste Listen-Element.
Im Speicher liegen die natürlich nicht so, also im Speicher können die an beliebigen Positionen liegen.
Das ist ja gerade der Vorteil dieser verketteten Darstellung. Okay,
wenn man diese Repräsentation hat, dann muss man sich überlegen,
wie kann man jetzt hier zum Beispiel etwas suchen,
suchen ist einfach, statt dieser Vorschleife,
die einfach die Position durchgeht, brauchen wir jetzt eine Wildsteife,
immer prüft, ob zum Beispiel,
dass der Element dem Gesuchten entspricht oder irgendwas anderes damit macht.
Und von der Position P zur Position PNEX geht.
Einfach in dem P danach auf das nächste Element verweist.
Wenn wir zum Beispiel dann hinter die Position P einfügen würden,
würden wir uns ein neues Element, einen neuen Knoten erzeugen,
das Attribut Key würde dann auf das X gesetzt.
Und jetzt müssen wir die Zeigerstruktur so ändern,
dass das neue Element in dieser Liste zwischen dem Element P und dem alten Element PPlus 1 ist,
wo A und B mal gespeichert waren.
Das macht man
Indem man von dem neuen Element den Next,
also den Verweis auf das nächste Element, das ist der,
hier auf das nächste Element zeigen lässt und in der anderen Operation,
und der zweite Schritt ist dann,
dass man von dem P-Element den Verweis auf das neue Element N zeigt.
Das ist natürlich wichtig, dass man das in dieser Reihenfolge tut.
Wenn es in der anderen Reihenfolge tun, dann funktioniert das nicht. So,
wenn Sie entfernen wollen, können Sie das auch machen,
dann merken Sie sich das Element, was hinter P folgt.
Sie setzen einfach den Verweis um auf das übernächste Element
und dann können Sie
das Element löschen
Sie sehen schon, diese Einfüge und Löschoperationen gehen offensichtlich in konstanter Zeit.
Im Gegensatz zu der Realisierung der Liste über einen Array.
Da müssten sie jedes Mal verschieben. Das kostete immer linear viel Zeit.
Und konstant viel Zeit oder linear viel Zeit ist bei ein
paar Millionen Listenelementen oder ein paar Milliarden Listenelemente ein entscheidendes Argument.
Okay, diese Art der Liste hat einen Nachteil.
Sie haben keine Chance, rückwärts zu laufen.
Sie können also von der Position, wie sie sind,
nie nach vorne gucken
Weil sie ja nur die Verkettung in einer Richtung haben.
Sie können immer zum nächsten Element laufen, nie zum vorherigen Element.
Deswegen gibt es neben den sogenannten einfach verketteten Listen auch die doppelt verketteten Listen.
Doppelt verkettete Listen haben einfach eine zusätzliche Zeigerfolge,
die quasi immer auf das vorherige Element verweist.
Sehe also so aus, also wieder so ein Listenelement als ein Knoten.
Wir haben den Schlüssel, der gespeichert wird, also hier ist A3,
in dem Fall, ist also das Attribut Key.
Und dann haben wir zwei Verweise,
Previews und Next und Next ist das Gleiche wie vorher.
Es zeigt also auf das nächste Element der Liste,
Previews zeigt auf das vorherige Element der Liste
So würde eine doppelt verkettete Liste aussehen. Wir hätten dann
zwei zusätzliche Attribute für die Liste selber. Head verweist auch
das erste Element der Liste und Rear auf das letzte Element lässt.
Dann können wir die Liste sowohl von hinten nach vorne als
auch von vorne, nach hinten entsprechend durchlaufen.
Siehste, kann man sich auch überlegen, kann auch Listen zyklisch verketten.
Könnt natürlich jetzt auch hier das letzte Element wieder auf das
erste zeigen lassen. Diese Arten von Listen werden auch verwendet.
Es gibt sogar eine ganz berühmte Datenstruktur,
wo diese zyklisch verketteten Listen nachher eine ganz wichtige Rolle
spielen.
Das sind sogenannte Fibunachi-Heeps
Können sie im Hauptstudium lernen, wenn sie wollen. So,
also wenn wir diese doppelte Verkettung haben,
dann müssen wir jetzt ein bisschen mehr, ja,
man sagt dann immer im Jargon Bookkeeping, also so Buchhaltung machen,
also müssen mehr von diesen Zeigern korrigieren,
damit die Liste immer wieder stimmt.
Also durchlaufen können wir die Liste so wie vorher auch.
Natürlich jetzt auch in umgekehrter Reihenfolge, wenn wir jetzt ein Element einfügen wollen,
hinter eine Position P, müssen wir vier von diesen Zeigern verändern. Also hier ist das Element, worauf wir zeigen hier, das ist P. Und was wir machen ist,
wir erzeugen also ein neues Element,
wir setzen die beiden Zeiger des neuen Elements,
die setzen wir zuerst, ja, es ist das neue Element,
wir setzen also den Next-Zeiger auf p.next, also auf das Element,
der zeigt ja hier, ist der erste,
der zweite ist, dass der Previews-Zeiger von dem neuen Element,
das ist der, auf das P selber verweist, ne,
auf das Element und dann müssen wir die Rückverkettung machen,
das machen wir einfach, indem wir sagen,
also von N aus next und davon der Zeiger,
Previews zeigt auf das Element N selber und andersrum genauso von N,
Previews und dann auf dann das Element next,
zeigt wieder auf das Element N selber
Und dann haben sie quasi diese vier Zeiger erzeugt.
Die beiden haben sie damit überschrieben und haben ein Element in die Liste eingefügt.
Entfernen geht genauso. Also wenn sie einfach entfernen wollen,
dann können sie Kuh, also wenn Kuh entfernt werden soll,
Punkt Previews.nex auf Q.nex zeigen lassen und next.previews of Previews.
Und damit haben sie quasi das Kuh aus der Liste
automatisch entfernt und können dann das Kuh löschen. Okay,
ich finde es wichtig,
dass sie es nochmal gesehen haben im Leben,
wenn sie heute programmieren, wenn sie den Java programmieren,
haben sie
so viele grundlegende Klassen ja schon zur Verfügung
Also sie haben eine Listenklasse. Sie sind nicht mehr derjenige,
der sich darüber Gedanken machen muss, in welcher Art und Weise
man die Zeiger da umbiegen muss, damit das funktioniert.
Sie verwenden einfach diese Klasse. Trotzdem, also interhalb dieser
Klasse verbirgt sich natürlich etwas. Und was sich darunter verbirgt
, ist, was? Ja. Okay, so,
und hier kann man jetzt natürlich auch wieder komplexitätstechnisch analysieren,
was bedeutet denn diese andere Realisierung der Liste für die einzelnen Operationen?
Und man sieht, der Zugriff, der ja vorher in konstanter Zeit geht,
wenn man an der Position P will, der ist ungünstiger geworden.
Das kostet das O von Endzeit. Wir müssen durch die
Liste durchlaufen, bis wir an dieser Position angeladen sind.
Allerdings, bei den allermeisten Anwendungen, wo wir Listen verwenden,
sind wir in der Regel schon an irgendeiner Position und wollen an der etwas tun.
Und immer, wenn die Anwendung der Liste so ist,
dann müssen wir da nicht mehr hinlaufen, sondern wir sind an der Stelle.
Und dann sind die Operationen schnell. Also suchen dauert natürlich
auch noch O von N, das hat sich nicht geändert,
wir müssen da linear durchgehen.
Und einfügen, entfernen kann entsprechend in konstanter Zeit gehen,
wenn sie quasi den Zeiger schon auf der richtigen Position haben,
wo sie einfügen oder löschen wollen.
Verketten wird es auf alle Fälle in konstanter Zeit,
weil sie die Lüssen einfach nur aneinander hängen müssen.
Wenn sie das signifikant verbessern wollen, was sie hier sehen,
also diese Laufzeiten hier, dann brauchen sie sogenannte Datenstrukturen für Wörterbücher.
Also wenn sie schnell einfügen, löschen und suchen möchten,
das sind klassische Wörterbücher.
Und diese Wörterbuchwörterbuchdatenstrukturen, die schauen wir uns im vierten Kapitel
an.
Okay, so
Vielleicht nochmal einmal der Vollständigkeit halber. Was passiert denn jetzt,
wenn wir direkt am Anfang oder am Ende der Liste sind,
dann ist es natürlich jetzt nicht so ganz so einfach,
wie das da steht,
dann müssen wir uns nochmal Gedanken darüber machen,
dass natürlich auch die Attribute Head und Rear entsprechend korrigiert werden müssen.
Ich glaube, durch diesen Pseudo-Code müssen wir jetzt nicht im Ganzen durchgehen,
das können sie, wenn sie es nicht schon kennen,
mal zu Hause in Ruhe machen.
Sie sehen, dass einfach spezielle Vorkehrungen dafür getroffen werden,
dass wir am Listen anfangen oder am Listen Ende einfügen.
Diese Tatsache, dass am Anfang und am Ende von Listen
immer so ein Sonderfall ist, Das ist ziemlich schmerzhaft,
wenn man solche Codes aufschreibt. Jedes Mal muss man dann,
das ist viel länger als das, was wir eben hatten,
nur wegen diesen Sonderfällen vom Anfang und vom Ende.
Deswegen gibt es eine Strategie, die werden wir auch später nochmal intensiver sehen,
sogenannte Wächter einzusetzen. Also die Listen ein Element länger zu machen,
sodass sie quasi nie leer sind, also immer mindestens ein Element enthalten.
Und dadurch kommt die Situation eigentlich nicht zustande,
dass diese Sonderfälle auftreten, weil man einfach nie,
man fügt nie ganz am Anfang ein,
weil da immer so ein Wächter ist,
also ein Element,
was immer vorhanden ist und man löscht nie das
letzte Element und deswegen treten die Sonderfälle nicht auf
Ja, hier sehen Sie an einem Bild, wie das mit Wächtern aussieht.
Also hier ist diese Liste. Wir haben ein Element,
das ist der Wächter, das ist so ein grauer Knoten hier.
Da steht auch kein Daten, da stehen keine Daten drin,
bewusst stehen da keine Daten drin.
Der Wächter ist nicht dazu da, um Daten zu speichern,
sondern um einen ausgezeichneten Knoten zu haben,
der immer in der Struktur vorhanden ist.
Und eine leere Liste ist dann nicht einfach so ein Verweis auf nicht,
sondern immer ein Verweis auf den Wächter.
Also hier ist so ein Wächterelement.
Also es ist sozusagen wie das erste Element einer Liste.
Und wenn Sie dann einfügen oder löschen,
dann machen Sie das in den Listenelementen,
die stehen alle dahinter
Ja, und wenn sie alle Listenelemente gelöscht haben,
bleibt immer noch der Wächter bestehen.
Und die Suche hier, hier sie realisiert,
entsprechend mit Wächter, wo sie genau diese Struktur entsprechend ausnutzen.
Auch das Einfügen ist jetzt viel einfacher geworden. Das ist
eine einfüge Operation und die Löschoperation, wenn Wächter vorhanden sind.
Ja, das war die Einfügeoperation, wenn keine Wächter vorhanden sind.
Also es hat nichts mit Effizienz zu tun,
das ist nicht schneller in der Ausführung in irgendeiner Art und Weise.
Ja, es hat auch nicht so viel mit Speicherplatz zu tun,
vielleicht ein ganz klein bisschen langsamer, aber es ist irrelevant.
Und das andere hat auch, vielleicht ein ganz klein bisschen
mehr Speicherbedarf, weil sie eben diesen Wächter speichern müssen.
Das Entscheidende ist hier, die Einfachheit der Codes,
um das Ganze zu realisieren und zu beschreiben.
Gut. So, und als Finale Struktur,
die ich Ihnen noch zeigen möchte, das sind die Bäume.
Also Bäume unterscheiden sich von Listen eigentlich nur durch die Anzahl
der Nachfolger, die Sie haben.
Ja, also wenn Sie in der Liste hatten Sie
immer eine Nachfolger,
das nächste Listenelement
Wenn sie das jetzt erhöhen und zu sagen, sie haben
immer zwei Nachfolger, haben sie automatisch einen sogenannten Baum.
Bäume sind ihnen garantiert schon über den Weg gelaufen in der Informatik.
Das kann man faktisch nicht vermeiden. Die Nomenklatur,
die man da verwendet, ist, man hat wieder Knoten,
das ist also ein so ein Element in dem Baum.
Diese Knoten hatten wir eben schon als ein Element der Liste.
Wir sprechen von direkten Vorgängern und Nachfolgern. Das ist also
ein vorheriges Element im Baum. Beachten Sie immer,
dass ein Vorgänger immer eindeutig ist,
also in einem Baum hat jedes Element einen eindeutigen Vorgänger,
aber der Nachfolger ist nicht eindeutig.
Es gibt also potenziell mehrere Nachfolger.
Dadurch kommt die Baumstruktur
Zustande. Die Begriffe, die da verwendet werden,
im Pseudo-Codes und so, die sie sehen, ja,
also so in älteren Büchern haben sie noch so eine Normanklatur mit Vater und Sohn,
ja, das ist heute natürlich überhaupt nicht mehr legitim.
Also heute steht da sowas wie älter und Kind,
wo man sich im Deutschen immer so ein bisschen schwer mittut,
weil man da immer nur den Plural von kennt.
Im Englischen ist es ein bisschen einfacher,
steht da Parent und Child, also mit Parent ist sozusagen
immer der eindeutige Vorgänger mit Child immer der Nachfolger gemeint.
Vorfahren sind alle Knoten auf dem Weg zur Wurzel.
Die Wurzel ist der Ausgangsknoten, quasi das erste Element des Baums,
Nachfahren sind alle Knoten auf dem Weg zu einem Blatt.
Die Blätter sind die untersten Knoten
Eines Baums. Die Bäume heißen geordnet,
wenn die Nachfolger eine feste Reihenfolge haben,
also wir können von links nach rechts durch die Liste durchlaufen.
Der Wurzel ist der, die Wurzel ist der
eine Knoten ohne Vorgänger, also der Startknoten sozusagen.
Die inneren Knoten sind alle die, die Nachfolger haben.
Blätter sind die, die keine Nachfolger mehr haben.
Mit der Ordnung bezeichnen wir die Anzahl direkter Nachfolger eines Knotens,
also wie viele Kinder, wenn sie so wollen,
einen Knoten hat. Und wir reden noch von Pfaden,
ein Pfad ist einfach eine Sequenz von solchen Knoten,
die jeweils in einer Vorgänger-Nachfolgerbeziehung stehen.
Das heißt, VI ist immer der Vorgänger von VI
minus 1. Dann haben wir einen Pfad in dem Baum
Okay, so kann so ein Baum aussehen.
Also nur schematisch dargestellt. Das sind alles hier verschiedene Knoten
dieses Baums. Wir balen Bäume immer von der Wurzel
oben zu den Blättern unten, also in einer unbiologischen Reihenrichtung.
Das heißt, oben haben wir mal den ausgezeichneten Wurzelknoten,
wenn es ihn Ben gibt.
Und dann geht es hier entsprechend runter. Das sind immer
die Nachfolger hier und entsprechend haben wir innere Knoten.
Das sind die, die Nachfolger haben. Wir haben Blätter
, das sind Knoten, die keine Nachfolger haben.
Manchmal beschreiben wir auch komplette Teilbäume, dann verwenden wir
solche Dreiecke
Diese Art von Dominiklatur werden wir ganz intensiv noch benutzen,
immer dann, wenn es um Suchbäume geht.
Also bei unseren entsprechenden Datenstrukturen dort. Ja,
bei solchen Bäumen ist natürlich von großem Interesse,
wie hoch die sind, also wie lang diese Pfade sein können,
weil wenn die zu suchen verwendet werden,
ist das natürlich das Entscheidende hier, wie viele Knoten muss man betrachten.
Deswegen definiert man die Höhe eines Baums,
die Höhe eines Baums ist wie folgt definiert,
die Höhe eines Blattes ist null,
ja, also die eckigen Kästen,
hier sind die Blätter,
die haben die Höhe null und die Höhe eines inneren Knotens
ist die maximale Höhe seiner Teilbäume,
die da drunter hängen,
plus eins
Und dann können sie sehen,
dieser Baum hat einen insgesamt eine Höhe von vier.
Also die Wurzel hat eine Höhe von vier.
Die haben Höhe null. Der hat Höhe eins,
Höhe zwei Höhe, drei Höhe vier.
Und so, ja. Kann auch andersherum betrachten.
Das Ganze in Ebenen einteilen. Das macht man dann meistens
von der Wurzel rückwärts. Also Ebene, I,
also Ebene null sind alle Knoten auf der Ebene der Wurzel.
Das ist natürlich nur einer, darf nur einer sein,
das ist kein Baum. Und auf Ebene eins dann
die Kinder der Wurzel auf Ebene zwei quasi,
die Kinder der Kinder der Wurzel und so weiter
Okay, so, bei Bäumen müssen wir uns
ein bisschen mit den Zahlen auseinandersetzen, die dort vorkommen.
Also wie groß können diese Bäume sein? Weil Listen ist sowas alles noch ganz einfach,
weil Bäumen ist das das erste Mal, wo es ein bisschen schwieriger wird,
überhaupt zu wissen, wie groß die so werden können.
Also das Erste, was man feststellt,
man guckt hier auf die Niveaus und kann dann zählen,
wie viele Knoten kommen davor. Das Problem ist,
das hängt natürlich davon ab, wie der Baum genau aussieht.
Also dieser hier hätte jetzt auf dieser Ebene 2,
hetter 4, hätte er wieder 2.
Das liegt natürlich daran, der hat Blätter auf unterschiedlichem Niveau.
Also hier gibt es sowohl Blätter auf Niveau 2 als auch Blätter auf Niveau
4. Wenn wir aber einen vollständigen Binär,
warum haben der Höhe Haar, vollständiger,
als wir haben bis zur Höhe Haar oder bis zum Niveau Haar quasi alle Knoten,
die wir einfügen können, die da sein können.
Das ist also die maximale Anzahl Knoten,
die man mit dieser Höhe realisieren kann, wie näherbaum bedeutet,
jeder Knoten hat genau zwei Nachfolger, ja, also hier ein, zwei.
Und wenn wir das haben, dann können wir genau sagen,
wie viele Knoten, auf welcher Ebene auftreten.
Das sehen Sie hier schon an dem Schema,
das ist auch gar nicht schwer. Da sieht man dann,
dass für die Knoten auf der Ebene I, genau zwei Hochnie-I-Knoten auftreten,
das ist die Ebene 0-1er, zwei,
vier, dann nach dem 8, 16 und so weiter.
Die Gesamtzahl der Knoten ist dann die Summe darüber.
Es wäre also die Summe von Igarch 0 bis H,
also bis zur Höhe, 2 hoch I.
Und dann wissen wir, was das ist.
Können Sie ausrechnen über die geometrische Reihe 2 hoch H plus
1, minus 1. Und wenn man diese Formel jetzt mal umdreht und überlegt sich mal angenommen,
ich habe so K viele Elemente,
die ich irgendwie in so einem Baum schweiche,
welche Höhe benötige ich denn dann, können Sie ja sagen,
okay, dann drehe ich das um und rechne mal die Höhe.
Und die Höhe wäre dann, der Logorithmus zur Basis 2
von Betrug Vertrag von K plus 1, halbe.
Das formen sie um, also das auf die andere Seite,
ist K plus 1. Das halbe kommt von dem H plus 1
hier. H plus 1 wäre ja zweimal 2 UH.
Dann haben sie die halbe und dann der Logorithmus zur Bades 2 davon.
Das können sie umformen. Sieht dann so aus,
Logorithmus zu Bades 2 vom Betrag von K plus 1, minus 1.
Und das ist im Okalkül locker. Also wir können sogar sagen,
es ist Tetterlocker, also das oben und unten
beschränkt die Höhe durch den Logorithmus der Anzahl der Knoten,
die wir haben
Warum ist das wichtig? Das ist deshalb wichtig,
weil wir jetzt sehen, dass das Verhältnis zwischen Knoten und Höhe
logorithmisch ist und wir damit eine Aussage machen können,
wie viel Zeit wir potenziell brauchen, um Daten in einem Baum zu finden.
In einer Liste hatten wir die Situation, dass wir,
wenn wir K-Elemente speichern wollten, wir auch eine Liste der
Länge K brauchten und im ungünstigsten Fall mussten wir die Listenelemente durchgehen.
Dann hat das O von K viel Zeit gekostet.
In einem Baum ist das Verhältnis von Höhe zu Anzahl Knoten logorithmisch. Das bedeutet,
wenn ich viele Betrag von Carviele Elemente speichern will hier und
ich betrachte die Wege, um diese Elemente zu finden,
ja, dann haben die nur lugerithmische Länge.
Setzt natürlich voraus, dass sich entsprechend einen geschickten Algorithmus habe,
der mir sagt, welchen Weg im Baum ich langlaufen muss.
Das machen genau die Suchbäume. Die haben genau diese Strategie
und sind in der Lage dann in lugerithmische Zeit die Objekte wieder zu finden.
Ja, vielleicht noch einen Punkt. Wir haben ja bisher bei
den anderen Datenstrukturen immer genau überlegt, wie man die realisiert.
Das könnte man hier auch machen,
also bei einem Binärbaum ist das eigentlich
ganz einfach
Da hat man dann, man erweitert quasi die Liste, um ein weiteres Element.
Wir haben in der Regel geordnete Bäume. Das heißt,
für uns ist das linke Kind oder das erste Kind ein anderes wie das zweite.
Ja, die haben eine Reihenfolge, die haben eine Bedeutung,
ob es das erste oder das zweite ist.
Deswegen redet man typischerweise auch von zwei Nachfolgern,
von Left und Ride, so entsprechend der Darstellung.
Deswegen hat so ein Element hier des Baumes immer entsprechend jetzt drei Attribute,
den Wert, also irgendwie, also man hat vier Attribute,
man hat den Wert und noch drei Verweise, drei Zeiger.
Der erste Zeiger ist ein Zeiger auf den Vorgänger,
auf den älter Knoten von X, ja,
und X.x.left und X.right sind die beiden Zeiger auf die Nachfolger, ja, hier.
Und hier sehen Sie das, ne, also hier,
wenn Sie diesen Knoten betrachten, X,
da steht irgendwo der Wert X und dann haben Sie die drei Zeiger X.p,
zeigt auf den Vorgänger, X.left auf den ersten und X.right auf das zweite Kind.
Ja, so kann man das darstellen. Das ist bei einem binären Baum, ja,
da ist es okay, da habe ich also diese zwei Nachfolger,
aber es ist, wenn ich jetzt keinen binären Baum habe,
wenn jetzt mein Knoten fünf Nachfolger hat,
dann sagen Sie, okay,
dann nehme ich halt einen Array der Länge fünf und dann habe ich halt fünf Kinder,
Aber was ist, wenn ihr Baum eine dynamische Anzahl von Kindern hat?
Wenn sie mal zwei haben, mal fünf,
mal zehn, ja, was machen sie dann?
Ja, dann könnten sie natürlich sagen,
ich mache hier mal einen Array mit so viel Platz,
dass auch die größte Anzahl von Kindern da reinpasst.
Das ist manchmal schwierig, das vorher abzuschätzen, ist nicht so gut.
Sie können natürlich sagen, dann nehme ich halt eine Liste
und in der Liste verweise ich die Zeiger auf die Nachfolger.
Wir haben ja schon solche Datenstrukturen, die man dynamisch erweitern kann,
wie eine Liste, würde auch gehen.
Aber dann wird es kompliziert. Dann hätte man hier an
diesem Knoten jeweils eine Liste stehen,
also wieder eine eigene Datenstruktur in diesem Knoten
Und die Art und Weise, wie man das dann repräsentiert,
ist die folgende. Die Darstellung für Bäume mit einer diversen
unterschiedlichen Anzahl von Nachfolgern ist eine sogenannte Childright Zibbling Repräsentation.
Was man also macht, ist, ich habe wieder nur
an jedem Knoten zwei oder drei Zeiger, drei Verweise.
Ich verweise wieder auf den Elternknoten. Ja, das macht x.p.
Und dann habe ich einen Verweis auf das linke Kind,
also sozusagen auf das erste Kind. Und das ist gleichzeitig
eigentlich der Kopf der Liste von Kindern an dich habe.
Und die baue ich jetzt direkt ein, indem ich einfach sage,
dieses Kind, das hat ein Ride Sibbling,
also einen rechten Bruder, wenn sie so wollen.
Und auf den verweise ich. Und dann sieht die Struktur
wie folgt aus. Wir haben also hier einen Knoten,
der Knoten hätte drei,
drei Kinder und wir verweisen nur auf das erste Kind und von hier
haben wir eine Liste über diese Ride Sibling Pointer,
um zum zweiten, zum dritten,
zum vierten Kind und so weiter zu kommen.
Die verweisen immer auf den gleichen Elternknoten.
Und von hier sieht das dann genauso aus.
Das ist die Struktur, die verwendet wird. Immer dann,
wenn wir Bäume repräsentieren müssen ohne dass wir den Grad kennen.
Den Grad der Knoten. Kommt in dieser Vorlesung,
wenn wir mal überlegen, ich glaube,
es kommt zu dieser Vorlesung noch nicht vor.
Also diese Art von Datenstrukturen, also diese Bäume mit unbeschränktem Grad,
braucht man in komplexeren Datenstrukturen, wenn es wirklich darum geht,
so das letzte Quäntchen aus so einer Datenstruktur rauszupressen an Effizienz.
Ja, auch bei Fibunachi-Bäumen etc., bei Fibunachi-Schlangen,
da muss man sowas machen. Okay, so,
das war quasi ein grundlegender Einstieg in das,
was wir an Datenstrukturen verwenden.
Das war heute hoffentlich alles sehr, sehr einfach für sie.
Soll ja auch ab und zu mal ein bisschen leichter sein.
Und damit haben wir wirklich die Grundlagen gelegt und wir werden
dann nächste Woche anfangen mit Sortieren und dann wird es erstmal
wieder algorithmischer.
Gut,
das war's für heute