2. Suchen und Sortieren
2.1 Binäre Suche
Dieses Teilkapitel überschneidet sich stark mit dem aus der Vorlesung Theoretischen Informatik. Dennoch habe ich es hierhin kopiert, da ja nicht gararntiert ist, dass alle Teilnehmer und Teilnehmerinnen dieses Kurses auch in TI waren.
Erinnern Sie sich noch an das hier?

Vielleicht sind Sie ja dafür tatsächlich zu jung. Es ist ein Telefonbuch. Wenn man eine Telefonnummer herausfinden will, zum Beispiel vom Kfz-Mechaniker Windisch aus Freudenberg, dann schaut man im Telefonbuch unter "W" nach und blättert. Und heute? Da benutzt man immer noch ein Telefonbuch. Dies ist dann aber elektronisch, und das Blättern tun auch nicht Sie, sondern ein Rechner (oder viele). Sie können zum Beispiel Das Telefonbuch verwenden oder eine Suchmaschine Ihrer Wahl.
Und ob Sie eine Telefonnummer suchen oder etwas ganz Anderes, ein Großteil von dem, was Rechner erledigen müssen, beinhaltet, irgendwo Inhalte aus einer riesigen Datenmenge rauszufischen. Grund genug, diesem Thema ein paar Vorlesungsstunden zu widmen.
Um konkret zu werden: in der Datei englishVocabulary.txt finden Sie eine Liste von über 50.000 englischen Wörtern. Schauen Sie nach. Fällt Ihnen etwas auf? Die Wörter sind aphabetisch sortiert. Das sollte uns bei der Suche helfen. Spielen Sie das folgende Spiel, in dem Sie auf den Screenshot klicken:
Können Sie das gesuchte Wort mit höchstens sieben Klicks finden? Wahrscheinlich haben Sie rausgefunden, dass Sie als erstes irgendwo in der Mitte klicken sollten. Falls das dortige Wort "zu groß" ist, suchen Sie in der ersten Hälfte weiter; falls es zu klein ist, suchen Sie in der zweiten Hälfte weiter.
Abstraktion
Versuchen wir nun, die Aufgabenstellung und das Prinzip der binären Suche
genauer zu verstehen. Wir haben ein Array
Des Weiteren haben wir einen Suchwert
Gegeben ein sortiertes Array
binarySearch(Array A, key x): l := -1, u := m while u - l > 1: m := (l+u)/2 if A[l] <= x then l = m else u = m return m
Die Funktion binarySearch(A,x)
gibt den größten Index
binarySearch
den Wert binarySearch
den Wert binarySearch
müsste laut unseren vorherigen Angaben also
sowohl binarySearch
im schlimmsten Falle durchläuft, in Abhängigkeit von
Behauptung.
Beweis.
Falls
Falls nun
Setzen wir
Wieviele Schritte braucht es, bis aus binarySearch
genau
binarySearch
auf einem
Array aus Der genaue Ausdruck
Also zum Beispiel
Implementierung in Python
Schreiben wir nun eine kleine Funktion in Python, die binäre Suche implementiert.
def binarySearch (data, x):
lower = -1
upper = len(data)
while (upper - lower > 1):
lookup = (upper + lower) // 2
if (data[lookup] <= x):
lower = lookup
else:
upper = lookup
# end if
return lower
Python-Programme sind oft sehr kurz, weil Sie im Gegensatz zu Elm oder Java keine großen Typendeklarationen machen müssen und Fehlerbehandlung gänzlich ausblenden können. Daher eignet sich Python wunderbar, um Algorithmen quick and dirty zu implementieren. Verwenden Sie aber Python bitte nicht, um die Steuerungssoftware eines Atomkraftwerks zu schreiben!
Übungsaufgabe 2.1.1
Kopieren Sie den Python-Code in eine Datei binarySearch.py
und speichern Sie die Datei in einem sinnvollen Verzeichnis, beispielsweise
home/AuK/code/binarySearch/
. Speichern Sie auch
englishVocabulary.py dort ab.
Fügen Sie die folgende Zeile an den Anfang der Datei binarySearch.py
ein:
from englishVocabulary import wordlist_sorted, wordlist_unsorted
Öffnen Sie eine Konsole und starten Sie das Python-Programm im Repl-Modus:
~/AuK/code/binarySearch $ python3 -i binarySearch
binarySearch(wordlist_sorted, "yak")
57885
wordlist_sorted[57885]
'yak'
Geben Sie per Hand ein sortiertes Array ein, also
beispielsweise data = [1,4,9,16,25]
und spielen damit rum.
Testen Sie Grenzfälle wie Suchwörter, die kleiner sind als alle Einträge oder solche, die größer sind als alle. Was geschieht bei leeren Arrays?
Übungsaufgabe 2.1.2
Schreiben Sie eine Funktion linearSearch
, die
das Array in linearer Suche durchgeht und dadurch auch auf
unsortierten Arrays funktioniert.
Testen Sie sie auf der unsortierten Wortliste
wordlist_unsorted
, die ebenfalls
englishVocabulary.py definiert ist.
Laufzeit mesen
Mit Python können Sie recht einfach die Laufzeit Ihrer Funktionen messen.
Um das Ihnen noch leichter zu machen, habe ich ein
kleines Modul timeMyPrograms.py geschrieben. Kopieren
Sie die Datei in Ihren Verzeichnis .../code/binarySearch
und fügen Sie oben
in binarySearch.py
die Code-Zeile
from timeMyPrograms import *
ein. Dann können Sie die Laufzeit wie folgt messen:
~/AuK/code/binarySearch $ python3 -i binarySearch
binarySearch(wordlist_sorted, "processor")
39136
measure_alg(binarySearch, wordlist_sorted, "processor")
1.2775002687703818e-05
Ganz allgemein können Sie mit
measure_alg(alg, p_1, p_2, ..., p_n)
die Laufzeit
des Aufrufs von alg(p_1, p_2, ..., p_n)
empirisch messen.
Beachten Sie, dass die Zahl 1.2775002687703818e-05
die Floating-Point-Schreibweise ist für
Übungsaufgabe 2.1.3
Messen Sie empirisch die Laufzeit Ihrer Implementierungen von
binarySearch
und linearSearch
. Um welchen
Faktor unterscheiden sie sich?
Implizite binäre Suche
Binäre Suche taucht auch oft auf, ohne dass explizit ein Array vorliegt. Beispielsweise, wenn wir die ganzzahlige Wurzel einer Zahl berechnen wollen:
Wir wollen hier explizit nicht auf
die bereits vorhandene Bibliotheksfunktion math.sqrt
zurückgreifen. Diese arbeitet
insbesondere mit Floating-Point-Zahlen und liefert daher auch schnell falsche Ergebnisse:
~/AuK/code/binarySearch $ python3 -i binarySearch
def sqrtInt(n): return math.floor(math.sqrt(n))
sqrtInt(16):
4
n = 3**40
sqrt(n*n) - n
-33 # Ouch!
Wir müssen es also selbst implementieren. Denken wir nach:
def intSqrt(n):
k = 0
while (k*k <= n):
k = k+1
return k-1
Übungsaufgabe 2.1.4
Experimentieren Sie mit der Funktion intSqrt
. Wie weit kommen Sie
mit vertretbarer Laufzeit? Was ist der Zusammenhang zwischen
intSqrt
?
Was ist die Größe einer ganzen Zahl?
Die Laufzeit von intSqrt(n)
ist ungefähr proportional
zu der Anzahl der Schleifendurchläufe und somit zu
intSqrt
bräuchte
einen Prozessortakt, und darüberhinaus ist alles perfekt
parallelisierbar (also auf die 12 CPU-Kerne) verteilbar, so
dass das Macbook pro Sekunde
In diesem (unrealistisch optimistischen) Szenario würde
intSqrt(2**80)
also intSqrt(2**100)
bereits tausendmal so lange, also fast einen halben Tag.
Ist das gerechtfertigt? Ist 2**100
eine "große" Zahl? Fragen wir Python:
2**100
1267650600228229401496703205376
Das passt wunderbar auf eine Zeile. Gemessen an der Anzahl an Stellen, die wir
brauchen, um diese Zahl zu repräsentieren, ist sie recht klein.
Im Allgemeinen bemisst man die Größe einer Zahl (im Kontext von Algorithmen) als
ihre Bitlänge, also
die Anzahl der Bits in ihrer Binärdarstellung. Wie bereits weiter oben diskutiert hat die Zahl
Anders herum gesagt: eine intSqrt(n)
benötigt also mindestens
Schleifendurchläufe. Somit ist die Laufzeit exponentiell in der
Bitlänge
Quadratwurzel mit binärer Suche
Wenn wir ein Array mit den Quadratzahlen
~/AuK/code/binarySearch $ python3 -i binarySearch.py
array = [0, 1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
binarySearch(array, 48)
6
Wir können also mittels binärer Suche die Quadratwurzel berechnen.
Natürlich müssen (dürfen) wir das Array
if (data[lookup] <= x):
durch einen Funktionsaufruf ersetzen.
Schreiben Sie in Python eine Funktion
intSqrt (n)
, die die Funktion
Übungsaufgabe 2.1.6
Messen Sie die Laufzeit Ihres Algorithmus mit
measure_alg (intSqrt, 2**100)
, beispielsweise.
Legen Sie eine Tabelle an: wie entwickelt sich die empirisch gemessene Laufzeit mit der Bitlänge der Eingabezahl? Tip: Sie müssen schon sehr große Zahlen eingaben (tausendstellige, zehntausendstellige), um die Laufzeit zu "spüren".
Wenn Sie die Bitlänge verdoppeln, wie verändert sich die Laufzeit?
Untere Schranken
Ich habe Ihnen einen speziellen Algorithmus vorgestellt, der
das gegenwärtige Suchareal in jedem Schritt halbiert. Ist dies optimal?
Versuchen Sie zu beweisen, dass man im Schlimmsten Fall auch
-
Formulieren Sie in Gedanken die binäre Suche als Spiel . Bob kennt das Array
. In jedem Schritt fragt Alice einen index an und Bob antwortet mit . Alice will möglichst schnell den Wert finden, und Bob will dies verzögern. -
Tun Sie so, als wäre Bob ein adaptiver Gegner. Das heißt, er kann sich das
Array
Schritt für Schritt während des Spiels ausdenken. Er muss aber immer konsistent antworten, d.h. wenn Alice sowohl die Indizes angefragt hat, müssen Bobs Antworten die Bedingung erfüllen. -
Zeigen Sie, dass Sie den adaptiven Bob durch einen nicht-adaptiven Bob ersetzen können
(also durch
ein Array
, dass von vornherein feststeht), so dass Alice dennoch nicht schneller zum Ziel kommt (hier ist es wichtig, dass Alice deterministisch ist, also keinen Zufall verwenden darf).