Einführung in Suchalgorithmen: Version mit Programmierung
Übersicht
Auf einen Blick
Schlüsselbegriffe: Algorithmus, Komplexitätsordnung, Big-O-Notation, lineare Suche, binäre Suche, Probeteilung, Kryptographie
Altersgruppe: 16–18 Jahre
Vorkenntnisse/Fertigkeiten: grundlegende Python-Kenntnisse
Zeitumfang: 80 Minuten
Autor: Sam Robbins (UK)
Unterrichtsplan
| Zeit in Minuten | Unterrichtsphase | Aktivität | Material |
|---|---|---|---|
| 0–20 | Aufgabe 1 | Schüler*innen entwickeln und programmieren einen Suchalgorithmus zur Lösung der Aufgabe. | Arbeitsblatt, Python-Interpreter (oder Online-Emulator) |
| 20–40 | Aufgabe 2a | Schüler*innen entwickeln und programmieren einen Suchalgorithmus zur Lösung der Aufgabe. | Arbeitsblatt, Python-Interpreter (oder Online-Emulator) |
| 40–60 | Aufgabe 2b | Schüler*innen entwickeln und programmieren einen Suchalgorithmus zur Lösung der Aufgabe. | Arbeitsblatt, Python-Interpreter (oder Online-Emulator) |
| 60–80 | Aufgabe 3 | Schüler*innen entwickeln und programmieren einen Suchalgorithmus zur Lösung der Aufgabe und beantworten zusätzlich die beiden gestellten Fragen. | Arbeitsblatt, Python-Interpreter (oder Online-Emulator) |
Aufgaben und Hinweise für Lehrkräfte
Diese Unterrichtsreihe beschäftigt sich mit der Frage, wie Computer Daten durchsuchen. Suchalgorithmen gehören zu den wichtigsten Werkzeugen der Informatik und sind grundlegend dafür, wie das World Wide Web funktioniert. Bei klassischen Computern gibt es eine Reihe gut bekannter Algorithmen, mit denen Suchvorgänge durchgeführt werden können. Über viele Jahre ging man davon aus, dass die Effizienz dieser Algorithmen eine feste Grenze dafür setzt, wie schnell Computer Daten finden können. Mit dem Aufkommen von Quantencomputern könnte sich das ändern: Neue Quantenalgorithmen versprechen in mehreren Bereichen eine deutlich schnellere Suche.
In dieser Stunde behandeln wir die Grundlagen von drei klassischen Suchverfahren. Im weiteren Verlauf des Kurses greifen wir diese Algorithmen wieder auf und untersuchen, wie Quantencomputer ihre Leistungsfähigkeit in jedem dieser Fälle verbessern können.
Arbeitsblatt
Die folgenden Aufgaben können Sie als Arbeitsblatt in pdf oder docx herunterladen.
Die Aufgaben finden Sie hier auch als interaktives Arbeitsblatt mit einer eingebetteten Python-Umgebung.
Szenario 1: Suchen in unstrukturierten Datenfeldern
Stellen Sie sich vor, wir haben eine Liste mit acht Namen, aber die Liste ist nicht sortiert. Sie möchten herausfinden, an welcher Stelle ein bestimmter Name in dieser Liste vorkommt. Hier ist die Namensliste:
| Adriano | Birte | Edouard | Elena | Florian | Inez | Niamh | Oliver |
Leider kann ein Computer das gesamte Datenfeld nicht gleichzeitig durchsuchen. Er kann immer nur ein Element nach dem anderen betrachten und feststellen, ob der Eintrag an dieser Stelle mit dem gesuchten Namen übereinstimmt oder nicht.
Aufgabe 1
Der folgende Python-Code zeigt, wie das oben dargestellte Namens-Array erzeugt und zufällig gemischt wird:
import random namesArray = [ "Adriano", "Birte", "Edouard", "Elena", "Florian", "Inez", "Niamh", "Oliver" ] random.shuffle(namesArray) nameToFind = "Elena"
Ergänzen Sie diesen Code um eine Suchroutine, die im gemischten Array nach dem Namen „Elena“ sucht. Welche Strategie verwenden Sie, um das richtige Element zu finden? Führen Sie das Programm zehnmal aus. Wie viele Versuche benötigen Sie maximal, und wie viele im Durchschnitt?
Vermutlich sind Sie so vorgegangen, dass Sie beim ersten Element begonnen und dann jedes Element der Reihe nach geprüft haben, bis Sie den gesuchten Namen gefunden haben. Sie hätten auch zufällig Karten auswählen können (mit der Bedingung, dass Sie keine Karte zweimal wählen). Im Durchschnitt wäre das jedoch nicht besser.
In beiden Fällen liegt die maximale Anzahl an Versuchen bei 8, und der Durchschnitt liegt über viele Durchgänge hinweg bei 4,5 Versuchen. Die Strategie, jedes Element eines Feldes nacheinander zu prüfen, bis eine Übereinstimmung gefunden wird, nennt man lineare Suche. Im ungünstigsten Fall entspricht die Anzahl der Vergleiche der Größe des Datenfeldes, das durchsucht wird. Man sagt, die Komplexität des Algorithmus ist O(n) (Big-O-Notation), weil die Anzahl der nötigen Vergleiche bei einem Datenfeld mit n Elementen in der Größenordnung von n liegt.
Bei klassischen Computern gibt es keine Möglichkeit, ein unsortiertes Datenfeld schneller als mit dieser Vorgehensweise zu durchsuchen. Mit einem Quantencomputer kann man eine solche Suche in O(√n) Schritten durchführen. Diese Methode, bekannt als Grover-Algorithmus, behandeln wir in Stunde 4.
Sobald die Schüler*innen ihren Code geschrieben haben, stellt die Lehrkraft die lineare Suche vor (das heißt: Die Datenelemente werden der Reihe nach geprüft, bis das gesuchte Element gefunden ist oder alle Elemente des Datenfeldes überprüft wurden) und erläutert, dass dies das beste klassische Verfahren zur Suche in unsortierten Datenfeldern ist. Die lineare Suche hat die Komplexität O(n), während das entsprechende Quantenverfahren (der Grover-Algorithmus, der in Stunde 4 behandelt wird) die Komplexität O(√n) hat.
Beispielcode:
import random namesArray = [ "Adriano", "Birte", "Edouard", "Elena", "Florian", "Inez", "Niamh", "Oliver" ] random.shuffle(namesArray) nameToFind = "Elena" numGuesses = 1 i = 0 while namesArray[i] != nameToFind: i += 1 numGuesses += 1 print("The shuffled array was:") print(namesArray) print( nameToFind + " found at position " + str(i + 1) + " in " + str(numGuesses) + " guesses" )
Szenario 2: Suchen in strukturierten Datenfeldern
Aufgabe 2a: Suche in einem geordneten Feld
Der folgende Python-Code zeigt, wie das zu durchsuchende Array angelegt wird:
namesArray = ["Adriano", "Birte", "Edouard", "Elena", "Florian", "Inez", "Niamh", "Oliver"] nameToFind = "Inez"
Ergänzen Sie den Code so, dass er im Array nach dem Namen „Inez“ sucht. Wenn Sie ein Element überprüfen, können Sie dieses Mal nicht nur feststellen, ob es mit dem gesuchten Namen übereinstimmt, sondern auch, ob es im Alphabet vor oder nach dem gesuchten Namen steht. Welche Strategie verwenden Sie, um den richtigen Eintrag zu finden? Testen Sie Ihr Vorgehen mit allen Namen aus dem Array. Wie viele Versuche brauchen Sie höchstens, und wie viele brauchen Sie im Durchschnitt?
Die beste Strategie ist, zunächst das mittlere Element des Datenfeldes zu prüfen. Wenn Sie eine Übereinstimmung finden, sind Sie fertig. Ist der gesuchte Name im Alphabet weiter hinten als der geprüfte Name, gehen Sie zum Mittelpunkt der oberen Hälfte des Datenfeldes und prüfen erneut. Ist er im Alphabet weiter vorne, gehen Sie zum Mittelpunkt der unteren Hälfte und prüfen erneut. Wiederholen Sie dieses Vorgehen, bis Sie den Namen gefunden haben.
Je nachdem, wie Sie bei der Bestimmung der Mittelpunkte gerundet haben, sollten Sie „Inez“ nach zwei oder drei Versuchen gefunden haben. Die maximale Anzahl an Versuchen beträgt vier, und der Durchschnitt über alle Namen liegt bei Anwendung dieser Strategie bei 2,625.
Dieses Verfahren nennt man binäre Suche. Die Kenntnis der Struktur des Datenfeldes macht diese Suche deutlich schneller als eine lineare Suche. Im ungünstigsten Fall hängt die Anzahl der Vergleiche mit dem Logarithmus zur Basis 2 der Anzahl der Elemente zusammen. Im Beispiel oben gibt es 8 Elemente: log2(8) = 3. Wenn es 1024 Elemente gäbe, wären höchstens 11 Vergleiche nötig log2(1024) = 10. Man sagt, die Komplexität des Algorithmus ist O(log(n)) (Big-O-Notation), weil die Anzahl der nötigen Vergleiche bei einem Datenfeld mit n Elementen in der Größenordnung von log(n) liegt.
Sobald die Schüler*innen ihren Code geschrieben haben, stellt die Lehrkraft die binäre Suche vor (das heißt: Es wird stets das mittlere Element des Datenfeldes gewählt; je nach Ergebnis des Vergleichs wird die Suche anschließend in der unteren oder oberen Hälfte des Datenfeldes fortgesetzt) und erläutert, dass dies das beste klassische Verfahren zur Suche in sortierten Datenfeldern ist. Die binäre Suche hat die Komplexität O(log(n)).
Beispielcode:
namesArray = ["Adriano", "Birte", "Edouard", "Elena", "Florian", "Inez", "Niamh", "Oliver"] nameToFind = "Inez" numGuesses = 1 low = 0 high = len(namesArray) - 1 mid = int((low + high) / 2) while namesArray[mid] != nameToFind: if nameToFind > namesArraylow = mid + 1 else: high = mid - 1 mid = int((low + high) / 2) numGuesses += 1 print(nameToFind + " found at position " + str(mid + 1) + " in " + str(numGuesses) + " guesses")
Aufgabe 2b: Suche in einem Datenfeld mit Elementen, die durch Merkmale beschrieben sind
Stellen Sie sich vor, unsere Figuren haben nun die folgenden Gesichtseigenschaften:
Das Ziel ist nun, eine Figur aus der Menge zu bestimmen, indem Sie Fragen zu ihrem Aussehen stellen. Jede Frage darf jedoch nur mit „Ja“ oder „Nein“ beantwortet werden. Welche Fragen stellen Sie? Wie viele Versuche benötigen Sie, um die Figur zu finden?
Der folgende Python-Code zeigt, wie das zu durchsuchende Array angelegt wird:
namesArray = [
["Adriano", "Blond", "Blue", "No Glasses"],
["Birte", "Blond", "Blue", "Glasses"],
["Edouard", "Blond", "Brown", "No Glasses"],
["Elena", "Blond", "Brown", "Glasses"],
["Florian", "Brown", "Blue", "No Glasses"],
["Inez", "Brown", "Blue", "Glasses"],
["Niamh", "Brown", "Brown", "No Glasses"],
["Oliver", "Brown", "Brown", "Glasses"]
]
Ergänzen Sie den Code so, dass er anhand der Merkmale nach einer Person sucht. Wie viele Versuche sind nötig, um die Figur zu bestimmen?
Vermutlich haben Sie erkannt, dass Sie durch das Abfragen einer von zwei Möglichkeiten pro Merkmal jedes Mal etwa die Hälfte der Kandidaten ausschließen konnten. Da es acht Figuren gab, reichten drei Fragen, um die Figur jedes Mal eindeutig zu bestimmen (die Figuren wurden so ausgewählt, dass sie alle unterschiedlichen Kombinationen der drei binären Merkmale darstellen, sodass am Ende keine Mehrdeutigkeit entstehen kann).
Das Ergebnis ist in allen Fällen: drei Versuche. Das ist eine Variante der binären Suche aus Aufgabe 2a. Entsprechend liegt die Komplexität des Algorithmus wieder bei O(log(n)) (wobei man in diesem Fall bei jeder Suche genau log2(n) Fragen benötigt).
Im Quantencomputing hat genau diese Problemstruktur eine bemerkenswerte Lösung: Die gesuchte Antwort kann mit einem einzigen Durchlauf gefunden werden, unabhängig davon, wie groß n ist. Dieses Problem ist als Bernstein-Vazirani-Problem bekannt, und wir werden in Stunde 3 sehen, wie der zugehörige Algorithmus funktioniert.
Sobald die Schüler*innen ihren Code geschrieben haben, erläutert die Lehrkraft, dass die richtige Strategie darin besteht, jeweils genau ein Merkmal abzufragen (z. B. „Trägt die Person eine Brille?“). Dadurch wird bei jeder Abfrage etwa die Hälfte der Figuren ausgeschlossen, sodass die richtige Figur bei acht Möglichkeiten immer mit drei Abfragen bestimmt werden kann. Dies ist eine Variante der binären Suche, da sich der Suchraum bei jedem Schritt erneut halbieren lässt. Dieses Beispiel wurde gewählt, weil es äquivalent zu einem speziellen Suchproblem ist, dem Bernstein-Vazirani-Problem, bei dem der Quantenalgorithmus die Komplexität O(1) hat. Dieser Algorithmus wird in Stunde 3 behandelt.
Beispielcode:
namesArray = [
["Adriano", "Blond", "Blue", "No Glasses"],
["Birte", "Blond", "Blue", "Glasses"],
["Edouard", "Blond", "Brown", "No Glasses"],
["Elena", "Blond", "Brown", "Glasses"],
["Florian", "Brown", "Blue", "No Glasses"],
["Inez", "Brown", "Blue", "Glasses"],
["Niamh", "Brown", "Brown", "No Glasses"],
["Oliver", "Brown", "Brown", "Glasses"]
]
hairColour = "Brown"
eyeColour = "Brown"
wearsGlasses = "No Glasses"
possibleCandidates = [
True, True, True, True,
True, True, True, True
]
for i in range(0, len(namesArray)):
if namesArray[i][1] != hairColour:
possibleCandidates[i] = False
if namesArray[i][2] != eyeColour:
possibleCandidates[i] = False
if namesArray[i][3] != wearsGlasses:
possibleCandidates[i] = False
i = 0
while possibleCandidates[i] == False:
i += 1
print(
"The person with these characteristics is " +
namesArray[i][0]
)
Szenario 3: Mathematische Suchaufgaben
Suchalgorithmen werden für viele unterschiedliche mathematische Aufgaben benötigt, zum Beispiel um eine Quadratwurzel zu berechnen oder die Lösung einer Gleichung zu finden. Eine spezielle Aufgabe besteht darin, die Primfaktoren einer großen Zahl zu bestimmen.
Aufgabe 3
Der folgende Python-Code zeigt, wie ein Array mit Primzahlen zum Testen sowie eine Funktion zur Überprüfung angelegt wird, ob eine Zahl eine Primzahl aus dieser Liste ist:
primesArray = [
2, 3, 5, 7,
11, 13, 17, 19,
23, 29, 31, 37,
41, 43, 47, 53,
59, 61, 67, 71,
73, 79, 83, 89,
97
]
numberToFactorise = 8192
primeFactorArray = []
totalCalcs = 0
def inPrimesArray(x, primesArray):
isPrime = False
for i in
range(0, len(primesArray)):
if x == primesArray[i]:
isPrime = True
return isPrime
Ergänzen Sie den Code so, dass er alle Primfaktoren der folgenden beiden Zahlen bestimmt: (a) 8192, (b) 6557. Welche Strategie verwenden Sie dabei? Wie viele Rechenschritte sind nötig, um alle Primfaktoren zu finden?
Die richtige Strategie besteht darin, jede Primzahl der Reihe nach zu testen. Wenn die Zahl nicht durch die Primzahl teilbar ist, nimmt man die nächste Primzahl in der Liste. Ist sie teilbar, berechnet man den Quotienten und prüft dann erneut, ob dieser Quotient durch die Primzahlen in der Liste teilbar ist (wieder beginnend von vorne).
Diese Strategie nennt man Probeteilung (trial division). Bei einer Zahl wie 8192, die viele kleine Primfaktoren besitzt, kann man die Primfaktorzerlegung relativ schnell durchführen (in diesem Fall mit 12 Rechenschritten). Wenn eine Zahl jedoch nur zwei Primfaktoren hat, die zudem ähnlich groß sind, ist das Verfahren deutlich langsamer. Im Fall von 6557 (=79·89) benötigt man 22 Rechenschritte, um die Faktoren zu finden.
Würde man eine Zahl konstruieren, die das Produkt aus zwei Primzahlen ist, die zum Beispiel jeweils 600 Stellen haben, könnte sie nicht einmal ein Supercomputer faktorisieren. Der RSA-Algorithmus, der weltweit zur Verschlüsselung im Bankwesen und im Internet eingesetzt wird, stützt seine Sicherheit genau auf diese Tatsache. Bei klassischen Computern gilt dieses Verschlüsselungsverfahren daher als praktisch nicht zu knacken.
Quantencomputing könnte jedoch Wege eröffnen, diese und andere Verfahren zu brechen, die bislang als unknackbar galten. In Stunde 5 und 6 lernen Sie das RSA-Verschlüsselungssystem kennen und sehen, wie der Shor-Algorithmus einen möglichen Weg bietet, wie ein Quantencomputer RSA angreifen könnte.
Sobald die Schüler*innen ihren Code geschrieben haben, bestätigt die Lehrkraft die Ergebnisse: Für 8192 werden 12 Rechenschritte benötigt, für 6557 22 Rechenschritte. Ziel dieser Aufgabe ist es zu verdeutlichen, dass die Primfaktorzerlegung großer Zahlen rechnerisch aufwendig ist, insbesondere dann, wenn eine Zahl das Produkt zweier großer Primzahlen ist. Der RSA-Algorithmus, auf dem ein großer Teil der Internetverschlüsselung basiert, stützt seine Sicherheit auf die Schwierigkeit, große Zahlen zu faktorisieren. In Stunde 5 wird der RSA-Algorithmus behandelt, und in Stunde 6 wird gezeigt, wie ein Quantenalgorithmus – der Shor-Algorithmus – dies eines Tages möglicherweise brechen könnte.
Beispielcode:
primesArray = [
2, 3, 5, 7,
11, 13, 17, 19,
23, 29, 31, 37,
41, 43, 47, 53,
59, 61, 67, 71,
73, 79, 83, 89,
97
]
numberToFactorise = 8192
primeFactorArray = []
totalCalcs = 0
def inPrimesArray(x, primesArray):
isPrime = False
for i in
range(0, len(primesArray)):
if x == primesArray[i]:
isPrime = True
return isPrime
while inPrimesArray(numberToFactorise, primesArray) == False:
i = 0
totalCalcs += 1
while numberToFactorise % primesArray[i] != 0:
i += 1
totalCalcs += 1
primeFactorArray.append(primesArray[i])
numberToFactorise = numberToFactorise / primesArray[i]
primeFactorArray.append(int(numberToFactorise))
print("The prime factors array is ")
print(primeFactorArray)
print(
"The number of calculations required was " +
str(totalCalcs)
)
Der Schlüssel zum Quanten-Vorteil: Superposition
Quantencomputing bietet einen grundlegend anderen Ansatz als die oben gezeigten klassischen „Trial-and-Error“-Verfahren. Die Quantenalgorithmen, die im weiteren Verlauf dieses Kurses behandelt werden, beruhen darauf, mithilfe von Hadamard-Gattern Superpositionszustände zu erzeugen und am Ende wieder zurückzuführen. Bevor Sie mit dem Kurs fortfahren, sollten Sie daher sicherstellen, dass Sie die Grundlagen dazu verstehen, wie Quantengatter funktionieren.
Superposition ist ein Werkzeug, mit dem man viele mögliche Eingabefälle gleichzeitig in einem Zustand darstellen kann. Das bedeutet jedoch nicht automatisch, dass man das Suchergebnis immer „in einem Schritt“ erhält. In manchen Fällen zwingt uns die Quantenphysik zu einem Kompromiss: Je nachdem, was wir messen, verlieren wir Information über andere Aspekte des Zustands. In anderen Fällen liefert ein Quantenalgorithmus die Antwort nicht mit 100% Sicherheit, aber der Wahrscheinlichkeitsvorteil kann bereits ausreichen, um gegenüber klassischen Verfahren einen großen Gewinn zu erzielen. Quantencomputing erfordert viel Kreativität. Im weiteren Verlauf dieses Kurses lernen Sie einige bemerkenswerte und elegante Tricks kennen, die grundlegend verändern, wie wir in Zukunft nach Daten suchen können.
Der erste Hinweis auf einen möglichen Quanten-Vorteil bei Suchproblemen war der Deutsch-Algorithmus, der 1985 von David Deutsch vorgeschlagen wurde. In Stunde 2 werden wir diesen Algorithmus behandeln. Er bildet die Grundlage für alle weiteren Algorithmen in diesem Kurs.
Ausblick auf Stunde 2
In der folgenden Stunde wird der Deutsch-Algorithmus behandelt. Der klassische Zugang zu diesem Algorithmus wird hier nicht thematisiert, da es sich um eine künstliche Konstruktion ohne verbreitete praktische Anwendung im Computing handelt. Seine Bedeutung liegt jedoch darin, dass er das erste Beispiel eines Problems war, bei dem ein Quantenalgorithmus schneller war als jeder klassische Algorithmus. Die grundlegenden Elemente des im Deutsch-Algorithmus verwendeten Vorgehens tauchen in allen später behandelten Algorithmen dieses Kurses erneut auf.
Diese Seite teilen