Für meine Eltern Sangeeta und Yogesh

Vorwort

Anfangs war Programmieren für mich einfach nur ein Hobby. Die Grundlagen erlernte ich mit dem Buch Visual Basic 6 für Dummies und ich las weitere Bücher, um mehr zu erfahren. Das Thema ‌Algorithmen war für mich allerdings undurchschaubar. Ich kann mich noch gut daran erinnern, dass ich mir das Inhaltsverzeichnis meines ersten Lehrbuchs zu diesem Thema zu Gemüte führte und dachte: »Endlich werde ich das Ganze mal verstehen!« Der Inhalt war jedoch so kompakt und informationsreich, dass ich die Lektüre nach einigen wenigen Wochen aufgab. Erst als ich auf einen wirklich guten Professor für Algorithmen traf, wurde mir klar, wie einfach und elegant die zugrunde liegenden Ideen tatsächlich sind.

Vor einigen Jahren verfasste ich meinen ersten bebilderten Blogbeitrag. Ich kann am besten lernen, wenn mir der Stoff visuell präsentiert wird, daher gefielen mir die illustrierten Beiträge besonders gut. Seitdem habe ich selbst einige bebilderte Beiträge über funktionale Programmierung, Git, Machine Learning und Nebenläufigkeit geschrieben. Ich war anfangs übrigens nur ein mittelmäßiger Autor. Technische Begriffe zu erklären ist schwierig. Es erfordert viel Zeit, gute Beispiele zu finden und komplizierte Konzepte zu erklären, daher ist es am einfachsten, die schwierigen Dinge unter den Teppich zu kehren. Ich dachte eigentlich, ich würde die Sache ganz gut machen, nachdem sich einer meiner Artikel ziemlich weit verbreitete. Bis ein Kollege zu mir kam und sagte: »Ich habe deinen Artikel gelesen, aber das Thema noch immer nicht verstanden.« Ich musste noch viel über das Schreiben lernen.

Während ich eine Reihe dieser Blogbeiträge verfasste, kam der Manning-Verlag auf mich zu und fragte, ob ich Lust hätte, ein illustriertes Buch zu verfassen. Es stellte sich heraus, dass die Redakteure des Verlags sich hervorragend mit dem Erklären technischer Konzepte auskannten und sie zeigten mir, wie man Wissen vermittelt. Ich habe dieses Buch verfasst, weil mir eine Sache besonders am Herzen lag: Ich wollte ein Buch schreiben, das schwierige technische Themen gut erklärt, ein leicht verständliches Buch über Algorithmen. Meine Fähigkeit zu schreiben hat sich seit meinem ersten Blogbeitrag weiterentwickelt und ich hoffe, dass dieses Buch eine leicht verständliche und informative Lektüre ist.

Kapitel 1:
Einführung in Algorithmen

In diesem Kapitel:

1.1  Einführung

Ein Algorithmus‌ ist eine Reihe von Anweisungen, die eine Aufgabe ausführen. Man könnte eigentlich jeden Codeschnipsel als Algorithmus bezeichnen, aber dieses Buch befasst sich mit den interessanteren Aspekten. Die Algorithmen in diesem Buch habe ich ausgewählt, weil sie schnell sind, interessante Aufgabenstellungen lösen oder beides. Hier sind einige der Highlights:

Ich werde hierfür jeweils einen Algorithmus erläutern und ein Beispiel dafür zeigen. Anschließend betrachten wir die Laufzeit des Algorithmus mithilfe der Landau-Notation (Landau-Symbole). Und schließlich erfährst du, welche anderen Aufgabenstellungen mit demselben Algorithmus gelöst werden können.

1.1.1  Performance

Die gute Nachricht ist, dass Implementierungen der Algorithmen, die in diesem Buch beschrieben werden, sehr wahrscheinlich in deiner Lieblingsprogrammiersprache verfügbar sind. Du musst die Algorithmen also nicht alle selbst schreiben! Allerdings sind diese Implementierungen nutzlos, wenn du deren Vor- und Nachteile nicht verstehst. In diesem Buch lernst du, die Vor- und Nachteile verschiedener Algorithmen miteinander zu vergleichen: Solltest du für eine bestimmte Aufgabe Mergesort oder lieber Quicksort verwenden? Ist ein Array oder eine Liste besser geeignet? Die Verwendung einer anderen Datenstruktur kann einen sehr großen Unterschied ausmachen.

1.1.2  Problemlösungen

Du wirst zudem Verfahren zur Problemlösung für Aufgaben kennenlernen, an die du dich bislang vielleicht nicht herangetraut hast. Einige Beispiele:

Allgemeiner formuliert: Nach der Lektüre dieses Buchs werden dir einige der meisten verbreiteten und für sehr viele Fälle anwendbaren Algorithmen vertraut sein. Mit dem Wissen aus diesem Buch kannst du dich spezielleren Algorithmen für die KI, für Datenbanken usw. zuwenden oder dich noch größeren Herausforderungen stellen.

Erforderliche Kenntnisse

Für die weitere Lektüre des Buchs benötigst du Grundkenntnisse der Algebra‌. Betrachte beispielsweise die folgende Funktion: f(x) = x × 2. Welchen Wert besitzt dann f(5)? Wenn deine Antwort 10 lautet, bist du bereit.

Darüber hinaus ist dieses Kapitel (wie das ganze Buch) leichter verständlich, wenn dir eine Programmiersprache vertraut ist. Die Beispiele in diesem Buch sind in Python geschrieben. Falls du noch keine Programmiersprache kennst und eine erlernen möchtest, solltest du Python wählen – die Sprache ist hervorragend für Anfänger geeignet. Wenn du eine andere dir bekannte Programmiersprache (wie z.B. Ruby) verwenden möchtest, ist das problemlos möglich.

1.2  Binäre Suche

[Bild]Nehmen wir an, du suchst in einem Telefonbuch nach einer Person (wie altmodisch das klingt!). Der Name beginnt mit K. Du könntest nun am Anfang des Telefonbuchs loslegen und so lange blättern, bis du zum Buchstaben K gelangst. Wahrscheinlich wirst du mit der Suche jedoch eher in der Mitte anfangen, weil du weißt, dass sich die Einträge mit K ungefähr in der Mitte des Telefonbuchs befinden.

Oder du stellst dir vor, dass du einen Begriff, der mit dem Buchstaben O beginnt, in einem Wörterbuch suchst. Auch in diesem Fall wirst du mit der Suche in der Nähe der Mitte beginnen.

Und nun stelle dir vor, dass du dich bei Facebook anmeldest. Facebook muss dann überprüfen, ob du ein Konto besitzt und dementsprechend in einer Datenbank nach deinem Namen suchen. Nehmen wir an, dein Username lautet karlmageddon. Facebook könnte nun beim Buchstaben A mit der Suche anfangen – allerdings ist es sinnvoller, irgendwo in der Mitte anzufangen.

[Bild]Bei dieser Aufgabe handelt es sich um eine Suche. Zur Lösung solcher Aufgaben kommt stets der gleiche Algorithmus zum Einsatz: die binäre Suche.‌‌

Die binäre Suche ist ein Algorithmus, dessen Eingabe aus einer sortierten Liste von Elementen besteht. (Ich erkläre später, warum die Liste sortiert sein muss.) Wenn das gesuchte Element in dieser Liste enthalten ist, liefert die binäre Suche die Position zurück, an der es sich befindet. Andernfalls gibt die binäre Suche den Wert null zurück.

Zum Beispiel:

[Bild]

Hier ist ein Beispiel für die Funktionsweise der binären Suche. Ich denke an eine Zahl zwischen 1 und 100.

[Bild]Du musst nun meine Zahl mit möglichst wenigen Versuchen erraten. Ich sage dir jeweils, ob die geratene Zahl zu groß, zu klein oder richtig ist.

Nehmen wir an, du nennst die Zahlen der Reihe nach: 1, 2, 3, 4, ... Das sähe dann folgendermaßen aus:

[Bild]

Hierbei handelt es sich um eine einfache Suche‌‌ (vielleicht wäre eintönige Suche in diesem Fall eine passendere Bezeichnung). Mit jedem Versuch schließt du nur eine einzige Zahl aus. Wenn ich an die Zahl 99 gedacht hätte, würdest du 99 Versuche benötigen, um meine Zahl zu erraten!

1.2.1  Eine bessere Suchmethode

Ein besseres Verfahren ist das folgende: Fang mit der Zahl 50 an.

[Bild]

Diese ist zu klein, allerdings hast du soeben die Hälfte aller Zahlen ausgeschlossen! Du weißt nun, dass alle Zahlen von 1 bis 50 zu klein sind. Nächster Versuch: 75.

[Bild]

Zu groß, aber du hast wieder die Hälfte der verbliebenen Zahlen ausgeschlossen! Bei einer binären Suche nennst du die Zahl in der Mitte und schließt dadurch jeweils die Hälfte der noch vorhandenen Zahlen aus. Nun ist 63 an der Reihe (die Mitte zwischen 50 und 75).

[Bild]

So funktioniert die binäre Suche. Du hast soeben deinen ersten Algorithmus erlernt! So viele Zahlen kannst du mit jedem Rateversuch ausschließen:

[Bild]

An welche Zahl ich denke, spielt keine Rolle: Du benötigst höchstens 7 Versuche, um sie zu erraten, weil bei jedem Rateversuch so viele Zahlen ausgeschlossen werden.

Nehmen wir wieder an, du suchst nach einem Begriff in einem Wörterbuch, das 240.000 Einträge enthält. Was meinst du, wie viele Schritte für eine Suche im Worst Case, also im ungünstigsten Fall, nötig sind?

[Bild]

Bei der einfachen Suche können 240.000 Schritte notwendig sein, sofern sich das gesuchte Wort ganz am Ende des Wörterbuchs befindet. Bei der binären Suche hingegen wird bei jedem Schritt die Anzahl der verbliebenen Wörter halbiert, bis schließlich nur noch ein Wort übrig ist.

[Bild]

Bei der binären Suche sind also 18 Schritte erforderlich – ein riesiger Unterschied! Verallgemeinert bedeutet das: Bei einer Liste der Länge n benötigt die binäre Suche im Worst Case log2 n Schritte, bei einer einfachen Suche sind hingegen n Schritte erforderlich.

Logarithmen

Vielleicht erinnerst du dich nicht mehr daran, was Logarithmen‌ sind, aber du weißt vermutlich noch, was Exponentialfunktionen‌ sind. Der Ausdruck log10 100 entspricht der Frage: »Wie viele Zehnen muss man miteinander multiplizieren, um 100 zu erhalten?«. Die Antwort lautet 2: 10 × 10 = 100. Also ist log10 100 = 2. Logarithmen sind die Umkehrfunktionen‌ von Exponentialfunktionen.

Wenn es in diesem Buch um die Laufzeit und die Landau-Notation (die in Kürze erklärt wird) geht, bedeutet log stets log2. Wenn du für die Suche nach einem Element eine einfache Suche verwendest, musst du im Worst Case jedes einzelne Element überprüfen. Bei einer Liste von 8 Zahlen musst du höchstens 8 Zahlen überprüfen. Bei einer binären Suche musst du im Worst Case log n Elemente überprüfen. Für eine Liste mit 8 Elementen gilt log 8 == 3, denn 23 == 8. Du musst also höchstens 3 Zahlen überprüfen (und dann kannst du mit dem 4. Versuch das richtige Ergebnis nennen). Für eine Liste mit 1.024 Elementen gilt log 1.024 == 10, denn 210 == 1.024. Bei einer Liste von 1.024 Zahlen musst du also höchstens 10 Zahlen überprüfen.

[Bild]

Hinweis

In diesem Buch geht es des Öfteren um die logarithmische Laufzeit‌, deshalb sollte dir das Konzept von Logarithmen vertraut sein. Sollte dies nicht der Fall sein, findest du bei der Khan Academy (https://www.khanacademy.org) ein anschauliches englisches Video, das dieses Konzept verdeutlicht.

Hinweis

Die binäre Suche funktioniert nur dann, wenn die zu durchsuchende Liste sortiert ist. Die Namen in einem Telefonbuch sind beispielsweise alphabetisch sortiert. Hier kannst du also für die Suche nach einem Namen eine binäre Suche verwenden. Wie sähe es aus, wenn die Namen nicht sortiert wären?

Sehen wir uns doch einmal an, wie man eine binäre Suche in Python programmiert. Der Beispielcode verwendet Arrays. Falls du nicht weißt, wie Arrays‌ funktionieren, keine Sorge, sie werden im nächsten Kapitel erklärt. Du brauchst nur zu wissen, dass sie eine Sequenz von Elementen in einer Reihe aufeinanderfolgender Behälter speichern können, deren Gesamtheit als Array bezeichnet wird. Die Behälter werden, angefangen bei 0, durchnummeriert: Der erste befindet sich an der Position #0, der zweite ist #1, der dritte #2 usw.

Die Funktion binary_search‌ nimmt ein sortiertes Array und ein Objekt entgegen. Wenn das Objekt in diesem Array enthalten ist, liefert die Funktion dessen Position zurück. Du führst darüber Buch, welcher Teil des Arrays zu durchsuchen ist. Anfangs handelt es sich um das gesamte Array:

low = 0
high = len(list) - 1

[Bild]

Du überprüfst jeweils das mittlere Element:

mid = (low + high) // 2 [Bild]
guess = list[mid]

[Bild] Der Wert der mid-Funktion wird von Python automatisch abgerundet, sofern (low + high) keine gerade Zahl ist.

Sollte der geratene Wert zu klein sein, aktualisierst du low dementsprechend:

if guess < item:
  low = mid + 1

[Bild]

Und sollte der geratene Wert zu groß sein, aktualisierst du high. Hier ist der vollständige Code:

def binary_search(list, item):
  low = 0 [Bild]
  high = len(list)—1 [Bild]

  while low <= high: [Bild]
    mid = (low + high)  //2 [Bild]
    guess =  list[mid]
    if guess == item: [Bild]
      return mid
    if guess > item: [Bild]
      high = mid - 1
    else: [Bild]
      low = mid + 1
  return None [Bild]

my_list = [1, 3, 5, 7, 9] [Bild]

print binary_search(my_list, 3) # => 1 [Bild]
print binary_search(my_list, -1) # => None [Bild]

[Bild] low und high führen darüber Buch, welcher Teil der Liste durchsucht wird.

[Bild] Solange der Suchbereich mehr als ein Element umfasst ...

[Bild] ... wird das mittlere Element überprüft.

[Bild] Das gesuchte Objekt wurde gefunden.

[Bild] Der geratene Wert war zu groß.

[Bild] Der geratene Wert war zu klein.

[Bild] Das gesuchte Objekt ist in der Liste nicht enthalten.

[Bild] Testen der Funktion.

[Bild] Denk daran, dass die Nummerierung der Listenelemente bei 0 beginnt. Das zweite Element hat den Index 1.

[Bild] »None« bedeutet in Python nil. Es zeigt an, dass ein Objekt nicht gefunden wurde.

[Bild]Übungen

1.1 Eine sortierte Liste enthät 128 Namen. Du durchsuchst sie mit einer binären Suche. Wie viele Schritte sind dafür maximal erforderlich?

1.2 Nehmen wir an, du verdoppelst die Größe der Liste. Wie viele Schritte sind nun maximal erforderlich?

1.2.2  Laufzeit

[Bild]Bei allen vorgestellten Algorithmen werde ich die Laufzeit erklären. Ob nun die Laufzeit oder aber der Speicherplatzbedarf optimiert werden soll: Im Allgemeinen möchte man den effizientesten Algorithmus auswählen.

Zurück zur binären Suche. Wie viel Zeit sparst du durch ihre Verwendung? Beim ersten Ansatz wurden alle Zahlen der Reihe nach überprüft. Wenn die Liste 100 Zahlen enthält, sind bis zu 100 Rateversuche erforderlich. Wenn die Liste 4 Milliarden Zahlen enthält, sind bis zu 4 Milliarden Rateversuche notwendig. Die maximale Anzahl der Rateversuche entspricht also der Größe der Liste. Man bezeichnet das als lineare Laufzeit.‌‌

Bei der binären Suche verhält es sich anders. Wenn die Liste 100 Objekte enthält, sind höchstens 7 Rateversuche erforderlich. Enthält die Liste 4 Milliarden Objekte, sind höchstens 32 Rateversuche notwendig. Ziemlich leistungsstark, oder? Die binäre Suche benötigt eine logarithmische Laufzeit.‌‌ Die folgende Tabelle fasst unsere Ergebnisse zusammen.

[Bild]

1.3  Landau-Notation

Die Landau-Notation (engl. Big-O-Notation, der Begriff Landau-Symbole ist ebenfalls gebräuchlich)‌‌ ist eine spezielle Schreibweise, die angibt, wie schnell ein Algorithmus ist. Warum ist das wichtig? Du wirst feststellen, dass du häufig Algorithmen verwendest, die andere Leute entwickelt haben. In diesem Fall ist es praktisch zu wissen, wie schnell oder langsam diese Algorithmen sind. In diesem Abschnitt werde ich dir zeigen, was die Landau-Notation bedeutet und eine Liste der gängigsten Laufzeiten der Algorithmen präsentieren, die Landau-Symbole verwenden.

1.3.1  Die Laufzeiten von Algorithmen nehmen unterschiedlich schnell zu

[Bild]Bob programmiert einen Suchalgorithmus für die NASA. Sein Algorithmus kommt zum Einsatz, wenn eine Rakete auf dem Mond landen soll und kann den Landeplatz berechnen.

Hierbei handelt es sich um ein Beispiel dafür, dass die Laufzeiten zweier Algorithmen auf unterschiedliche Weise zunehmen können. Bob muss sich zwischen einer einfachen Suche und einer binären Suche entscheiden. Der Algorithmus muss sowohl schnell sein als auch fehlerlos funktionieren. Einerseits ist die binäre Suche schneller. Bob hat nämlich nur 10 Sekunden Zeit, um den Landeplatz zu ermitteln – denn sonst kommt die Rakete vom Kurs ab. Andererseits lässt sich die einfache Suche leichter programmieren. Und Bob möchte wirklich nicht, dass der Code zum Landen einer Rakete Bugs enthält! Vorsichtshalber will Bob die Laufzeiten beider Algorithmen messen, wenn er eine Liste verwendet, die 100 Elemente enthält.

Nehmen wir an, das Überprüfen eines Elements dauert eine Millisekunde (ms). Bei der einfachen Suche muss Bob 100 Elemente überprüfen, also dauert das Ausführen der Suche 100 ms. Bei der binären Suche muss er hingegen lediglich 7 Elemente überprüfen. log2 100 ist ungefähr 7, also dauert die Suche 7 ms. Realistisch betrachtet wird die Liste tatsächlich eher eine Milliarde Elemente enthalten. Wie lange würde dann die einfache Suche dauern? Und wie lange die binäre Suche? Vergewissere dich, dass du diese beiden Fragen beantworten kannst, bevor du weiterliest.

[Bild]

Bob führt eine binäre Suche mit einer Liste aus, die eine Milliarde Elemente enthält. Sie dauert 30 ms (log2 1.000.000.000 ist ungefähr 30). »30 ms!«, geht ihm durch Kopf. »Die binäre Suche ist rund 15 Mal schneller als die einfache Suche, denn bei 100 Elementen dauerte die einfache Suche 100 ms und die binäre Suche 7 ms. Also sollte die einfache Suche 30 × 15 = 450 ms dauern, richtig? Das ist deutlich unter den zulässigen 10 Sekunden.« Bob entschließt sich also dazu, die einfache Suche zu verwenden. Aber ist das die richtige Entscheidung?

Nein, denn wie sich zeigen wird, liegt Bob falsch. Völlig falsch. Die Laufzeit der einfachen Suche beträgt bei einer Milliarden Objekte eine Milliarde ms – das sind mehr als 11 Tage! Das Problem besteht hier darin, dass die Laufzeiten für binäre und einfache Suche nicht auf dieselbe Weise zunehmen.

[Bild]

Wenn die Anzahl der Objekte zunimmt, benötigt die binäre Suche zur Ausführung etwas mehr Zeit. Die einfache Suche jedoch benötigt sehr viel mehr Zeit zur Ausführung. Deshalb wird die binäre Suche sehr viel schneller als die einfache Suche, wenn die Zahlenliste größer wird. Bob dachte, dass die binäre Suche 15 Mal schneller als die einfache Suche ist, aber das stimmt nicht. Wenn die Liste eine Milliarden Objekte enthält, ist die binäre Suche rund 33 Millionen Mal schneller. Aus diesem Grund reicht es nicht aus zu wissen, wie lange ein Algorithmus zur Ausführung benötigt – man muss auch wissen, wie die Laufzeit anwächst, wenn die Größe der Liste zunimmt. Hier kommt die Landau-Notation ins Spiel.

Die Landau-Symbole geben an, wie schnell ein Algorithmus ist. Betrachte beispielsweise eine Liste der Größe n. Bei der einfachen Suche muss jedes einzelne Element überprüft werden, daher sind n Operationen erforderlich. Für diese Laufzeit wird das Landau-Symbol O(n) verwendet. Aber wo sind die Sekunden angegeben? Es gibt keine – die Geschwindigkeit wird nicht in Sekunden angegeben. Die Landau-Notation ermöglicht es, die Anzahl der Operationen zu vergleichen. Sie teilt dir mit, wie schnell die Anzahl der Operationen des Algorithmus anwächst.

[Bild]

Hier ist ein weiteres Beispiel: Die binäre Suche benötigt log n Operationen, um eine Liste der Größe n zu überprüfen. Wie sieht das als Landau-Notation aus? Hierfür schreibt man O(log n). Im Allgemeinen notiert man ein Landau-Symbol folgendermaßen:

[Bild]

Du kannst daran ablesen, wie viele Operationen der Algorithmus ausführen wird. Im Englischen spricht man auch von der Big-O-Notation, weil der Anzahl der Operationen ein großes »O« vorangestellt wird. (Klingt wie ein Scherz, ist aber tatsächlich wahr!)

Betrachten wir einige Beispiele. Kannst du die Laufzeiten der Algorithmen herausfinden?

1.3.2  Visualisierung verschiedener Laufzeiten

Zunächst ein praktisches Beispiel, das du mit ein paar Blättern Papier und einem Bleistift leicht nachvollziehen kannst. Deine Aufgabe besteht darin, ein Gitter zu zeichnen, das aus 16 Kästchen besteht.

[Bild]

Algorithmus 1

Eine Möglichkeit besteht darin, der Reihe nach 16 einzelne Kästchen zu zeichnen. Wie du weißt, gibt die Landau-Notation die Anzahl der Operationen an. In diesem Beispiel stellt das Zeichnen eines Kästchens eine Operation dar. Und du musst 16 Kästchen zeichnen. Wie viele Operationen sind also erforderlich, wenn du die Kästchen einzeln zeichnest?

[Bild]

Zum Zeichnen von 16 Kästchen sind 16 Schritte notwendig. Welche Laufzeit benötigt dieser Algorithmus demzufolge?

Algorithmus 2

Probiere nun den folgenden Algorithmus aus. Falte das Papierblatt.

[Bild]

Bei diesem Beispiel ist jedes Falten des Papierblatts eine Operation. Du hast soeben also zwei Kästchen mit nur einer Operation erstellt!

Falte das Papierblatt ein zweites, drittes und viertes Mal.

[Bild]

Falte das Papierblatt nach der vierten Faltung wieder auf. Und siehe da – ein tadelloses Gitter. Mit jeder Faltung verdoppelst du die Anzahl der Kästchen. Du hast also mit 4 Operationen 16 Kästchen erstellt!

[Bild]

Du kannst die Anzahl der Kästchen mit jeder Faltung verdoppeln, also kannst du mit 4 Schritten 16 Kästchen »zeichnen«. Welche Laufzeit benötigt dieser Algorithmus? Lege die Laufzeiten für die beiden Algorithmen fest, bevor du weitermachst.

Antworten: Algorithmus 1 benötigt die Laufzeit O(n) und Algorithmus 2 die Laufzeit O(log n).

1.3.3  Die Landau-Notation beschreibt die Laufzeit im Worst Case

Nehmen wir an, du verwendest eine einfache Suche, um in einem Telefonbuch nach einer Person zu suchen. Wie du weißt, benötigt die einfache Suche die Laufzeit O(n), was im Worst Case bedeutet, dass du sämtliche Einträge deines Telefonbuchs überprüfen musst. Im vorliegenden Fall suchst du nach meinem Namen »Adit«. Hierbei handelt es sich um den ersten Eintrag in deinem Telefonbuch. Du musst also gar nicht alle Einträge überprüfen – du hast das Gesuchte schon beim ersten Versuch gefunden. Heißt das nun, dass dieser Algorithmus die Laufzeit O(n) benötigt? Oder sogar nur O(1), weil du die Person beim ersten Versuch gefunden hast?

Die einfache Suche benötigt nach wie vor die Laufzeit O(n). In diesem Fall hast du zwar sofort gefunden, was du gesucht hast, aber hierbei handelt es sich um den günstigsten Fall. Die Landau-Notation beschreibt jedoch den Worst Case. Du kannst also sagen, dass du im Worst Case sämtliche Einträge im Telefonbuch einmal überprüfen musst – und das entspricht der benötigten Laufzeit O(n). Dabei handelt es sich also um eine Art Zusicherung (eine obere Schranke für die Laufzeit) – du kannst dir sicher sein, dass die einfache Suche niemals langsamer als O(n) sein wird.

Hinweis

Neben der Laufzeit im Worst Case, also im ungünstigsten Fall, spielt auch die Laufzeit im Average Case, also im durchschnittlichen Fall, eine wichtige Rolle. Diese sogenannten Zeitkomplexitäten (der Worst Case und der Average Case) werden einander in Kapitel 4 gegenübergestellt.

1.3.4  Typische Laufzeiten gebräuchlicher Algorithmen

Nachstehend sind fünf Laufzeiten (sortiert nach abnehmender Geschwindigkeit) aufgeführt, die dir häufig begegnen werden:

Nehmen wir an, du zeichnest wieder die 16 Kästchen und du kannst dir dafür einen von 5 verschiedenen Algorithmen aussuchen. Wenn du den ersten Algorithmus verwendest, wirst du eine Laufzeit von O(log n) benötigen, um die Kästchen zu zeichnen. Du kannst pro Sekunde 10 Operationen ausführen. Bei einer Laufzeit von O(log n) benötigst du 4 Operationen, um 16 Kästchen zu erzeugen (log 16 = 4). Du benötigst also 0,4 Sekunden zum Zeichnen des Gitters. Und wenn du 1.024 Kästchen zeichnen müsstest? Zum Zeichnen von 1.024 Kästchen sind log 1.024 = 10 Operationen bzw. 1 Sekunde Zeit erforderlich. Diese Angaben gelten für den ersten Algorithmus.

Der zweite Algorithmus ist langsamer: Er benötigt eine Laufzeit von O(n). Zum Zeichnen von 16 Kästchen sind 16 Operationen notwendig und zum Zeichnen von 1.024 Kästchen sind 1.024 Operationen nötig. Wie vielen Sekunden entspricht das?

Der folgenden Tabelle kannst du entnehmen, wie lange es dauert, ein Gitter mit einem der anderen Algorithmen zu zeichnen (sortiert nach zunehmender Laufzeit):

[Bild]

Es gibt noch andere Laufzeiten, aber diese fünf sind die gebräuchlichsten.

Diese Darstellung ist eine Vereinfachung. Tatsächlich kannst du eine bestimmte Laufzeit nicht so fein säuberlich in eine bestimmte Anzahl von Operationen umwandeln, aber fürs Erste ist diese Betrachtungsweise ausreichend. Nachdem du einige weitere Algorithmen kennengelernt hast, werden wir in Kapitel 4 auf die Landau-Notation zurückkommen. Du solltest dir an dieser Stelle die folgenden Punkte merken:

[Bild]Übungen

Benenne die Laufzeit der nachstehenden Fälle in Form der Landau-Notation.

1.3 Du möchtest die Telefonnummer einer Person anhand ihres Namens in einem Telefonbuch finden.

1.4 Du möchtest den Namen einer Person anhand ihrer Telefonnummer in einem Telefonbuch finden. (Hinweis: Du musst das gesamte Telefonbuch durchsuchen!)

1.5 Du möchtest die Telefonnummern sämtlicher Personen im Telefonbuch einlesen.

1.6 Du möchtest nur die Telefonnummern der Personen lesen, deren Name mit A beginnt. (Diese Aufgabe ist knifflig! Hier kommen Konzepte zum Einsatz, auf die wir in Kapitel 4 ausführlicher eingehen. Wenn du die Antwort liest, wirst du womöglich überrascht sein!)

1.3.5  Das Problem des Handlungsreisenden

Nach der Lektüre des letzten Abschnitt wirst du dir vielleicht gedacht haben: »Das wird niemals vorkommen, dass ich auf eine Algorithmus stoße, der eine Laufzeit von O(n!) benötigt«. Ich werde versuchen, dir das Gegenteil zu beweisen. Betrachten wir ein Beispiel für einen Algorithmus mit wirklich miserabler Laufzeit. Hierbei handelt es sich um ein sehr bekanntes Problem aus der Informatik, weil die Anzahl der Operationen haarsträubend schnell zunimmt und einige sehr kluge Köpfe denken, dass es keine bessere Lösung gibt. Es wird als das Problem des Handlungsreisenden bezeichnet.‌

[Bild]

Ein Handlungsreisender muss fünf verschiedene Städte besuchen.

[Bild]

Der Handlungsreisende, den ich Opus nenne, möchte diese fünf Städte besuchen, dabei aber nur die minimal nötige Strecke zurücklegen. Mit dieser Möglichkeit können wir das Ziel erreichen: Betrachte alle möglichen Reihenfolgen, in der er die verschiedenen Städte aufsuchen kann:

[Bild]

Opus summiert die jeweils zurückgelegten Strecken und wählt die kürzeste Reiseroute aus. Bei 5 Städten gibt es 120 Permutationen, es sind also 120 Operationen erforderlich, um diese Aufgabe für 5 Städte zu lösen. Bei 6 Städten sind 720 Operationen nötig (es gibt 720 Permutationen). Bei 7 Städten sind es schon 5.040 Operationen!

Bei n Städten sind n! (n Fakultät)‌ Operationen nötig, um das Ergebnis zu berechnen. Der Algorithmus benötigt also eine Laufzeit von O(n!) – eine faktorielle Laufzeit. ‌‌Wenn man von den ersten paar Zahlen absieht, ist eine enorme Anzahl von Operationen erforderlich. Wenn du es mit mehr als 100 Städten zu tun hast, ist es unmöglich, das Ergebnis zu berechnen. Bevor das Resultat feststeht, wird unsere Sonne schon zu einem Roten Riesen geworden sein.

[Bild]

Der Algorithmus ist ja wirklich fürchterlich. Sollte Opus nicht lieber einen anderen verwenden? Das kann er aber leider nicht, denn hierbei handelt es sich um eines der ungelösten Probleme der Informatik. Es gibt keinen schnelleren Algorithmus für diese Aufgabe und viele kluge Köpfe halten es für unmöglich, einen besseren Algorithmus für diese Aufgabe zu finden. Wir können bestenfalls eine Näherungslösung berechnen. Mehr dazu in Kapitel 10.

Ein abschließender Hinweis: Erfahrene Leser sollten einen Blick auf binäre Suchbäume werfen! Im letzten Kapitel werden diese kurz beschrieben.