Entdecken Sie eBooks
Kategorien
Entdecken Sie Hörbücher
Kategorien
Entdecken Sie Zeitschriften
Kategorien
Entdecken Sie Dokumente
Kategorien
Sommersemester 2020
Session 2:
Suchen und Sortieren in Arrays
Erste Komplexitätsbetrachtungen
Bearbeitungszeitraum: 2020-04-29 — 2020-05-15
Jan Peleska
peleska@uni-bremen.de
Issue 1.3
2020-04-29
Kapitel 1
Vorwort
In diesem Dokument erhalten Sie Material für die zweite Session der Ver-
anstaltung Praktische Informatik 2, PI2a im Sommersemester 2020. Im
Fokus dieser Session stehen die Themen Suchen und Sortieren, sowie eine
Einführung in die Berechnung der Komplexität von Algorithmen.
• In Abschnitt 2 untersuchen wir Code aus dem JDK und diskutie- Übersicht
ren, warum Arrays, die ja eigentlich einen im Vergleich mit den ober-
coolen Java-Sammlungen etwas “altmodisch gewordenen” Datentyp re-
präsentieren, immer noch so wichtig sind.
1
lohnt, und wann man einfach naiv die Liste oder Menge traversieren
sollte, bis das Element gefunden oder das Ende der Datenstruktur er-
reicht wurde.
2
Inhaltsverzeichnis
1 Vorwort 1
3 Binäre Suche 9
3.1 Der einfache Fall: Suchen nach Zahlen in Arrays . . . . . . . . 9
3.1.1 Berechnung des mittleren Index . . . . . . . . . . . . . 11
3.1.2 Nachstellen des Fehlers . . . . . . . . . . . . . . . . . . 13
3.1.3 Rückgabewerte . . . . . . . . . . . . . . . . . . . . . . 15
3.1.4 Rekursive Varianten der binären Suche . . . . . . . . . 16
3.1.5 Binäre Suche in Arrays von beliebigem primitiven Da-
tentyp . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.2 Der allgemeine Fall: Suchen nach vergleichbaren Objekten in
Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.2.1 Binäre Suche auf Objekten, die Comparable implemen-
tieren . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
3.2.2 Binäre Suche auf Objekten mit Komparator . . . . . . 18
4 Erste Komplexitätsbestimmungen 20
4.1 Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
4.2 Laufzeitkomplexität der binären Suche . . . . . . . . . . . . . 21
4.3 Asymptotische Abschätzung der Worst-Case Laufzeitkomple-
xität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.3.1 Weiterführende Literatur . . . . . . . . . . . . . . . . . 34
3
5.1.2 Komplexitätsaspekte von Quicksort . . . . . . . . . . . 44
5.1.3 Optimierungen von Quicksort im OpenJDK . . . . . . 47
5.2 Stabile Sortierverfahren . . . . . . . . . . . . . . . . . . . . . . 48
5.3 Mergesort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6 Laufzeit-Gesamtbetrachtung:
Transformation in Arrays,
Sortieren, Suchen 49
4
Abbildungsverzeichnis
5
Listings
6
Kapitel 2
Wenn Sie im OpenJDK den Source Code von List.java studieren, sehen
Sie folgende Implementierung der Default-Methode sort: List.sort
1 @ S u p p r e s s W a r n i n g s ({ " unchecked " , " rawtypes " }) im
2 default void sort ( Comparator <? super E > c ) { OpenJDK
3 Object [] a = this . toArray () ;
4 Arrays . sort (a , ( Comparator ) c ) ;
5 ListIterator <E > i = this . listIterator () ;
6 for ( Object e : a ) {
7 i . next () ;
8 i . set (( E ) e ) ;
9 }
10 }
Die Liste (egal wie sie implementiert ist) wird zunächst in ein Array vom Arrays.sort
Typ Object[] kopiert (Zeile 3), und dann wird eine Sortiermethode auf
Arrays verwendet (Zeile 4), um die eigentliche Sortierung durchzuführen.
Dann wird der Inhalt des sortierten Arrays in einer Schleife (Ziele 6 — 9)
wieder in die Liste kopiert.
Diese Implementierungstechnik deutet darauf hin, dass Sortiervorgänge
auf Arrays wesentlich effizienter zu realisieren sind als auf komplexeren Da-
tenstrukturen. Das liegt daran, dass die Elemente eines Arrays garantiert
hintereinander im Speicher liegen, während einzelne Listenelemente1 und die
Referenzen darauf häufig im Speicher “verstreut” sind. Infolgedessen kann
man beispielsweise mehrere Array-Elemente hintereinander im CPU-Cache
finden, so dass Zugriffe gar nicht bis in den Hauptspeicher erfolgen müssen,
1
Implementierung von Listen lernen wir in Session 3 kennen.
7
sondern direkt auf dem Cache erledigt werden können. Bei den verstreut im
Speicher liegenden Referenzen, wie sie in Listen vorkommen, ist die Wahr-
scheinlichkeit für solche Cache Hits dagegen viel geringer.
Arrays dagegen schneiden in der Effizienz deutlich schlechter als Listen
ab, wenn sehr viele Einfügeoperationen auf der linearen Datenstruktur durch-
zuführen sind: Sie erinnern sich, bei der MyArrayList-Implementierung aus
dem letzten Semester mussten immer Teile oder das gesamte Array kopiert
werden, wenn ein Element an einer Stelle einzufügen war oder wenn die
Größe nicht mehr ausreichte. Ebenso mussten beim Löschen einzelner Ele-
mente Teile des Arrays kopiert werden (wenn nicht gerade das letzte Element
zu löschen war).
Infolgedessen gelten folgende Entwurfsregeln:
• Für lineare Datenstrukturen, deren Größe sich nach dem Anlegen selten Entwurfsregeln
ändert, sind Arrays gut verwendbar.
8
Kapitel 3
Binäre Suche
9
lautet hierzu wie folgt:
10
mid exakt gleich weit von low und high entfernt. Wenn die genannte
Differenz ungerade ist, ist mid um 1 näher bei low als bei high.
Dass die Formel in Zeile 12 tatsächlich unter allen Umständen diesen
mittleren Index berechnet, benötigt noch ein bisschen liebevolle Ana-
lyse, diese wir in Abschnitt 3.1.1 durchgeführt. Falls Ihnen Zeile 12
unheimlich vorkommt, sollten Sie diesen Abschnitt erstmal lesen, be-
vor Sie hier weiter machen.
(a) Ist midVal kleiner als der gesuchte Wert key, ist er entweder gar Fall
nicht in a oder im Array-Bereich a[mid+1], ..., a[high] ent- midVal <
halten. Dies folgt aus der Vorbedingung, dass a aufsteigend sor- key
tiert ist. Also setzt man low auf mid + 1 (Zeile 16) und beginnt
mit dem nächsten Schleifendurchlauf.
Weil bei Schleifeneintritt nur low ≤ high gegolten hat3 , ist mid +
1 unter Umständen um 1 größer als high. Dies bedeutet aber
einfach, dass das gesuchte Element nicht in a enthalten ist, so
dass man die Schleife abbrechen kann. Dies passiert auch, denn
die Schleifenbedingung low ≤ high wertet in diesem Fall zu false
aus.
(b) Ist midVal größer als der gesuchte Wert key, können wir die Su- Fall
che analog im Bereich a[low], ..., a[mid-1] fortsetzen, solan- midVal >
ge low ≤ mid − 1 gilt. Daher setzen wir high = mid −1 in Zeile 18 key
und beginnen mit dem nächsten Schleifendurchlauf. Analog zu
Fall (a) ist der gesuchte Wert nicht im Array, wenn low > mid − 1,
und die Suche in der Schleife kann abgebrochen werden.
(c) Als dritte Möglichkeit kann jetzt nur noch midVal == key gelten; Fall
mid ist also der gesuchte Index, und die Methode kann mit der midVal == key
Rückgabe von mid terminieren (Zeile 20).
11
wir hierzu das Array und die Grenzen high und low, zwischen denen der
“mittlere Index” zu suchen ist:
a[0] a[1] ... a[low] ... a[mid] ... a[high] ... a[a.length-1]
Um mid zu berechnen, müssen wir um Index low noch die Hälfte der
Distanz zwischen low und high dazu addieren. Die Division in der folgenden
Formel ist die ganzzahlige Division:
high − low
mid = low + (3.1)
2
Jetzt erinnern wir uns daran (lesen Sie hier auch im Java 13 Standard
nach), dass Array mittels int-Werten indiziert werden und das Längenfeld
a.length als int codiert wird. Daher ist die maximale Array Länge4
Bei der Ausführung des Algorithmus ist der maximale Wert für untere und
obere Schranke, also für den Fall, dass das Array maximale Länge hat und
Unter- und Obergrenze auf den höchsten Index gesetzt sind,
Aus diesen Fakten können wir folgern, dass mid, wenn es nach Formel 3.1
berechnet wird, immer kleiner oder gleich 231 − 2 ist, denn der Wert liegt
immer zwischen low und high. Die Berechnung ist auch in int-Arithmetik
immer korrekt, denn der Term
high − low
2
subtrahiert eine kleinere Zahl von einer größeren, die kleiner als 231 − 2 ist,
und die Hälfte davon liegt natürlich erst recht im Bereich 0, . . . , 231 − 2.
Während wir jedoch mit ganzzahliger Division in Z einfach Umrechnung
4 in Z gilt
De facto kann man nur Arrays mit maximaler Länge Integer.MAX VALUE − 2 dekla-
rieren. nicht in
int
12
high − low high + low
mid = low + =
2 2
umrechnen dürfen, ist dies für große Werte von low und high in int-
Arithmetik nicht korrekt, denn der int-Ausdruck (high + low)/2 wird ne-
gativ, wenn die Summe (high + low) in Z größer gleich 231 wird: Dann ist
nämlich das Vorzeichen-Bit Nummer 31 gesetzt, und in int-Arithmetik wird
(high + low) als negative Zahl interpretiert. Man erhält also einen Wert für
mid, der kleiner gleich 0 ist, nicht mehr zwischen low und high liegt, und im
Negativ-Fall eine ArrayIndexOutOfBoundsException auslöst.
Jetzt bedenken wir, dass low und high beide kleiner oder gleich 231 − 2
sind. Ihre Summe in Z ist daher kleiner gleich 2 · (231 − 2) = 232 − 4 < 232 .
Das bedeutet, dass es bei der Summe niemals zu einem Overflow kommt:
Das Ergebnis ist immer mit 32 Bit darstellbar. Wenn wir Bit 31 nicht als
Vorzeichen-Bit, sondern als den Wert 231 interpretieren könnten, wäre das
Summenergebnis also korrekt. Nun erinnern wir uns daran, dass der Shift-
Operator >>> den Bit-Ausdruck um eins nach rechts schiebt, und zwar ein-
schließlich des Vorzeichen-Bits 31. Von links wird immer eine Null nachgezo-
gen. Infolgedessen stellt der Wert
13
ausgeführt, um die volle Statement Coverage zu erreichen. Mit beiden fehler-
freien Varianten, mid zu berechnen, läuft der Test fehlerfrei. Mit der fehlerhaf-
ten Variante erhält man eine ArrayIndexOutOfBoundsException, denn an
einer Stelle der binären Suche wird mittels Zuweisung mid = (high + low)/2;
der negative Wert −536870914 erzeugt, den man wohl zu Recht als “out of
bounds” bezeichnen kann.
Während der binären Suche werden die Werte von
low high mid (low+high)/2 MAX_VALUE ausgegeben. Man sieht, dass
die Exception bei einem großen Array sehr schnell kommt (2. Iteration),
hätten die Programmierprofis bloß mal ausprobieren müssen . . . .
Wenn eine korrekte Index-Berechnung verwendet wird, sieht man sehr maximal
schön, dass die Suche auch für den Worst Case (gesuchter Wert im letzten 31 Itera-
Element, oder gesuchter Wert größer als alle im Array enthaltenen, oder ge- tionen
suchter Wert ist kleiner als alle im Array enthaltenen) schon nach 31 Iteratio-
nen terminiert. Dies wird in den nachfolgenden Komplexitätsbetrachtungen
näher begründet.
Um diese Experimente durchzuführen, muss der Java VM mitgeteilt wer- Heap
den, dass sie ausreichend Heap-Speicher verwenden darf, um ein so großes Größe für
Array zu allokieren: Man benötigt JVM
setzen
(Länge des Arrays) · (Größe eines int-Wertes)
= Integer.MAX VALUE · 4
= (231 − 1) · 22
= 233 − 4
Bytes, das sind etwa 8 GB. Zusätzlich wird natürlich noch sonstiger Speicher
benötigt. Man konfiguriert hierzu bei IntelliJ-IDEA im Menü Run → Edit
Configurations im Reiter Configuration unter VM Options
-ea -Xmx10g
Parameter -ea bedeutet nur, dass Java-Assertions zur Laufzeit ausgewertet
werden. Parameter -Xmx10g setzt den maximalen Heap auf 10GB.
Hinweis: Bei Rechnern, die nur 8GB Hauptspeicher haben, muss bei Ar- Falls Sie
rays dieser Größe ein sog. Paging5 durchgeführt werden, was häufig in sehr nur 8GB
langen Ausführungszeiten resultiert. In diesem Fall sollten Sie das Beispiel- Haupt-
projekt zur Nachstellung des Fehlers so abändern, dass ein byte-Array an- speicher
stelle eines int-Arrays verwendet wird. haben . . .
5
Das bedeutet, das Hauptspeicherseiten auf die Platte ausgelagert werden müssen.
14
3.1.3 Rückgabewerte
In dem Fall, dass der gesuchte Wert im Array gefunden wird, ist der Rückgabewert
Rückgabewert von binarySearch0 natürlich ein Index idx, so dass a[idx] – Element
den gesuchten Wert enthält. Denken Sie daran, dass das Array mehrere Stel- gefunden
len enthalten kann, in welchen der gesuchte Wert gespeichert ist. Es ist nicht
allgemein vorhersagbar, welcher dieser Indices dann zurückgegeben wird.
In dem Fall, dass der gesuchte Wert nicht im Array a vorhanden ist, wird Rückgabewert
laut Spezifikation der Wert – Element
-(Insertion Point) - 1 nicht
gefunden
zurückgegeben. Der Insertion Point (Einfügepunkt) ist der Index, an welchem
der gesuchte Wert in den sortierten Array einzufügen wäre:
• Ist der gesuchte Wert größer als alle Werte im Array, so ist Insertion
Points
Insertion Point = a.length
denn das Array müsste ja zum Einfügen des neuen Wertes um 1
verlängert werden, und der neue Wert würde an der letzten Stelle ste-
hen.
• Ist der gesuchte Wert kleiner als alle Werte in a, so ist
Insertion Point = 0
denn der Wert müsste am Index 0 in das Array eingefügt werden, und
alle anderen Werte würden einen Platz nach rechts rücken.
• Gilt für den gesuchten Wert x die Bedingung a[i − 1] < x < a[i] mit
i ∈ {1, . . . , a.length − 1}, so ist
Insertion Point = i
Wie man leicht sieht, ist im Fall, dass der gesuchte Wert nicht gefunden wird,
ist der Rückgabewert
-(Insertion Point) - 1
garantiert kleiner als 0: Der kleinste Insertion Point ist 0, dies ergibt
Rückgabewert -1. Alle anderen Insertion Points sind positiv, ihre Nega-
tion also kleiner gleich (−1). Infolgedessen kann man wie gewohnt auf
“Rückgabewert kleiner Null?” abfragen, um festzustellen, dass der Wert nicht
gefunden wurde.
15
3.1.4 Rekursive Varianten der binären Suche
In [3] ist noch eine rekursive Variante der binären Suche dargestellt. Wir Rekursion
überspringen diese Möglichkeit, da Rekursion durch Erzeugung neuer Stack-
frames etwas Overhead im Vergleich zu einer simplen While-Schleife erzeugt,
und wir uns daher Rekursion für Anwendungen aufheben, wo eine Schlei-
fenlösung wirklich extrem schwierig zu programmieren wäre.6 Wir werden
dann auch besprechen, wie man die Stacksize entsprechend setzt, um den
berüchtigten java.lang.StackOverflowError zu vermeiden.
16
3.2.1 Binäre Suche auf Objekten, die Comparable im-
plementieren
Für die erste Variante stellt die Klasse Arrays eine statische Methode
zur Verfügung, die einfach Objekte vom Typ Object im Array speichert.
Die Verantwortung, dass diese Objekte tatsächlich wechselseitig vergleich-
bar sind und Instanzen von Klassen, welche Comparable implementieren,
liegt beim Nutzer: Sind diese Voraussetzungen nicht erfüllt, wird eine
ClassCastException bei dem Versuch geworfen, einen Objekt-Cast auf ein
vergleichbares Objekt durchzuführen. Der Algorithmus aus dem OpenJDK,
Datei Arrays.java, ist in Listing 3.2 dargestellt.
17
nämlich eine Instanz, die Comparable implementiert. Da dies aber nun
unsere Voraussetzung ist, können wir diese Compiler-Warnung getrost
mit @SuppressWarnings("rawtypes") unterdrücken. Analog gibt es dann
in Zeile 15 noch eine “unchecked”-Warnung, denn zur Übersetzungszeit
kann nicht geprüft werden, ob das Objekt wirklich die Vergleichsme-
thode compareTo implementiert. Diese Warnung beseitigen wir mittels
@SuppressWarnings("unchecked").
Der Methodenaufruf midVal.compareTo(key) liefert einen negativen
Wert zurück, wenn midVal in der natürlichen Ordnung kleiner als key ist.
Bei positivem Rückgabewert ist midVal größer als key, und bei Rückgabe 0
gleichen sich die Objekte. Mit dieser Interpretation von compareTo sind die
Fallunterscheidungen in Ziele 17 — 22 völlig analog zu den Zeilen 15 — 20
in Listing 3.1.
Der Rückgabewert wird wieder nach der in Abschnitt 3.1.3 beschriebenen
Formel berechnet.
18
Listing 3.3: Binäre Suche auf Object-Arrays mit Comparator im OpenJDK.
1 public static <T > int binarySearch ( T [] a , T key , Comparator <? super T > c ) {
2 return binarySearch0 (a , 0 , a . length , key , c ) ;
3 }
4
5 private static <T > int binarySearch0 ( T [] a , int fromIndex , int toIndex ,
6 T key , Comparator <? super T > c ) {
7 if ( c == null ) {
8 return binarySearch0 (a , fromIndex , toIndex , key ) ;
9 }
10 int low = fromIndex ;
11 int high = toIndex - 1;
12
13 while ( low <= high ) {
14 int mid = ( low + high ) >>> 1;
15 T midVal = a [ mid ];
16 int cmp = c . compare ( midVal , key ) ;
17
18 if ( cmp < 0)
19 low = mid + 1;
20 else if ( cmp > 0)
21 high = mid - 1;
22 else
23 return mid ; // key found
24 }
25 return -( low + 1) ; // key not found .
26 }
19
Kapitel 4
Erste
Komplexitätsbestimmungen
4.1 Begriffe
Komplexitätsbestimmungen bei Algorithmen dienen den Zielen,
20
Hinweis: Komplementär zur Worst Case Running Time gibt es noch den
Begriff Worst Case Execution Time (WCET). Letzterer bezieht sich
jedoch auf absolute maximale Laufzeit einer konkreten Implementierung ei-
nes Algorithmus (oder allgemeiner eines Programms) auf einer konkreten
Hardware. WCET-Analyse ist für sicherheits-relevante Steuerungssysteme im
Luftfahrtbereich vorgeschrieben: Beispielsweise muss für den Cabin Pressure
Controller in der Passagierkabine eines Flugzeugs angegeben werden, wie lan-
ge es maximal dauert, bei Erkennung von Kabinenunterdruck den Befehl zum
Auswerfen der Sauerstoffmasken zu geben. Die WCET-Analyse ist heute ein
wichtiges Forschungsgebiet und wird mit Hilfe von Modellprüfungsverfahren
auf dem Maschinencode des Programms und einem mathematischen Modell
der Computer-Hardware durchgeführt. In unserer Vorlesung werden wird uns
mit diesem Thema nicht weiter beschäftigen.
• 2 Variableninitialisierungen in Zeile 8 — 9
21
• k+1 Bedingungsauswertungen der Schleifenbedingung in Zeile 11, wenn
die while-Schleife k mal durchlaufen wird
• Pro Schleifendurchlauf
• 1 Rückgabeanweisung in Zeile 22
Das bedeutet, dass die Differenz high−low nach höchstens (1+log2 n) Schlei-
fendurchläufen Null ist. Danach wird die Schleife nur noch einmal durchlau-
fen, denn entweder wird darin low inkrementiert oder high dekrementiert
(wir nehmen ja an, dass key nicht im Array ist), so dass die Auswertung von
low ≤ high am nächsten Schleifenbeginn in Zeile 11 false ergibt.
22
Zusammengefasst bedeutet dies, dass die Schleife nach maximal k = (2 +
log2 n) Durchläufen terminiert. Mit diesem Wert für k lautet Formel (4.1)
Amax (n) = 5 + 6 · k
= 5 + 6 · (2 + log2 n)
= 17 + 6 · log2 n
• 1 return-Anweisung in Zeile 5
23
300 3n+3
250
200
150
100
17 + 6 log2 (n)
50
n
0 20 40 60 80 100
Abbildung 4.1: Vergleich der Worst-Case Laufzeit von linearer und binärer
Suche.
Andererseits müssen wir auf diesen Vergleich noch einmal zurück kom-
men, sobald wir über die Running Time von effektiven Sortierverfahren ge-
sprochen haben. Die binäre Suche erfordert schließlich noch, das Array zu
sortieren, was bei der linearen Suche nicht erforderlich ist.
Zumindest kann man aber jetzt schon sagen, dass binäre Suche effekti-
ver als lineare Suche ist, wenn man das Array selten ändert (also selten neu
sortieren muss), aber sehr häufig darauf sucht, und das Array groß ist. Bei
kleinen Arrays ist es offensichtlich sinnvoller, einfach die lineare Suche zu ver-
wenden, auch wenn man mit dem zugehörigen Code niemanden beeindrucken
kann.
24
achten sind. Wir untersuchen also lieber Abschätzungen der Worst-Case Run-
ning Time für große Problemgrößen n. Die nachfolgende formale Definition
ist durch folgende Grundüberlegungen motiviert:
• Diese Überlegung dehnen wir sogar auf den Fall aus, wo sich die Anzahl
der benötigten Instruktionen um einen konstanten Faktor unterschei-
den: Sobald ein um diesen Faktor schnellerer Rechner verfügbar ist,
kann ich das Problem genauso gut lösen wie mit dem schnelleren Algo-
rithmus.
Ist die Obergrenze der Worst-Case Laufzeit durch eine Funktion f(n) ∈
O(g(n)) beschrieben, so wird die Sprechweise “Die asymptotische Worst-Case
Laufzeit des Algorithmus ist O(g(n))” verwendet. Die Schreibweise
f(n) = O(g(n))
mit “=” anstelle von “∈” wird gelesen als “f(n) ist asymptotisch gleich
g(n)”.
25
natürliche Zahl ist. Da g für alle n größer als Null ist, können wir die positive
reellwertige Funktion
g(n) + b
c(n) =
g(n)
definieren, mit der
∀n ∈ N : f(n) = c(n) · g(n)
gilt, denn
g(n) + b
c(n) · g(n) = · g(n) = g(n) + b = f(n).
g(n)
Diese Funktion c(n) ist nach oben durch eine Konstante beschränkt:
g(n) + b b
∀n ∈ N : c(n) = =1+ ≤1+b
g(n) g(n)
Es gilt also
Damit haben wir bewiesen, dass f(n) = O(g(n)), denn (1 + b) ist eine
Konstante; es gilt also der folgende Satz:
Eine triviale Konsequenz aus Theorem 1 ist, dass Algorithmen mit kon- Algorithmen
stanter (d.h. von der Problemgröße unabhängiger) Worst-Case Laufzeit alle mit
dieselbe asymptotische Worst-Case Laufzeit besitzen. Deshalb sagt man von konstanter
solchen Algorithmen einfach, “ihre asymptotische Worst-Case Laufzeit ist Worst-
O(1)”. Man verwendet niemals Bezeichnungen wie O(9) oder O(131), da Case
diese Funktionsmengen alle identisch zu O(1) sind. Als ein wichtiges Beispiel Laufzeit
26
für O(1)-Algorithmen nennen wir hier Algorithmen für prioritätsbasiertes
Scheduling2 und Priority Queues mit beschränkten Prioritäten3 .
Direkt aus der obigen Definition folgt das nächste Theorem, welches zeigt, Konstante
dass sie auch die zweite obige Überlegung (“Unterscheidung durch einen kon- Faktoren
stanten Faktor”) widerspiegelt. ändern
nicht die
Theorem 2 Unterscheidet sich die Worst-Case Laufzeit zweier Algorithmen asympto-
nur durch einen konstanten Faktor, dann haben beide dieselbe asymptotische tische
Worst-Case Laufzeit. In O-Notation ausgedrückt: Komple-
xität
∀g : N −→ R, c ∈ R+ : O(c · g(n)) = O(g(n))
27
1.5 × 1027 1.5 n 3
1.0 × 1027 n3 + n2
5.0 × 1026
n
2 × 108 4 × 108 6 × 108 8 × 108 1 × 109
Abbildung 4.2: Vergleich des Wachstums von Polynomen g(n) = 1.5 · n3 und
g 0 (n) = n3 + n2 .
6
n3 + n2
5 1.5 n 3
n
0.2 0.4 0.6 0.8 1.0 1.2 1.4
28
Wir schließen aus dieser Beobachtung, dass die Polynom-Summanden
a` n` bei asymptotischen Betrachtungen keine Rolle spielen, sobald ` klei-
ner als der Grad des Polynoms ist. Das folgt auch wirklich aus dem nächsten
Lemma, welches allgemein für Polynome P : R −→ R; x 7→ an xn +an−1 xn−1 +
· · · + a1 x + a0 gilt.
Lemma 1 Sei
folgt direkt
|P(x)| ≤ (|an | + 1) · xn für alle x > R(1),
was zu beweisen war.
Aus diesem Lemma folgt sofort das nächste Theorem über asymptotische
Worst-Case Abschätzungen der Laufzeit:
29
Theorem 3 Wird die Worst-Case Laufzeit zweier Algorithmen durch zwei
verschiedene Polynome desselben Grades beschrieben, dann haben beide die-
selbe asymptotische Worst-Case Laufzeit. In O-Notation ausgedrückt:
Bei der binären Suche haben wir oben die Worst-Case Laufzeit 17 + 6 · Logarithmische
log2 n berechnet. Aus den bisherigen asymptotischen Betrachtungen kann asympto-
man bereits sagen, dass sich dies als O(log2 n) bezeichnen ließe. Es geht tische
jedoch noch einfacher: Für große n unterscheiden sich Logarithmen zu un- Worst-
terschiedlicher Basis gar nicht so sehr, siehe Abb. 4.4. Case
Beispielhaft können wieder Faktoren c gefunden werden, so dass für Laufzeit
genügend große n beispielsweise c · log10 n ≥ log2 n gilt (siehe Beispiel in
Abb. 4.5). Dies lässt sich auch allgemein beweisen; es folgt direkt aus diesem
Lemma, das aus der Analysis bekannt ist:
Der Logarithmus zur Basis a lässt sich also durch den Logarithmus zur
Basis b, multipliziert mit der Konstanten loga b ausdrücken.
30
30 log2 (n)
25
log(n)
20
15
10
log10 (n)
Abbildung 4.4: Vergleich des Wachstums der Logarithmen zur Basis 2, e, 10.
Hier bezeichnet log(n) den Euler’schen Logarithmus (Basis e).
2 log(n)
40
4 log10 (n)
30 log2 (n)
20
10
x
2 × 108 4 × 108 6 × 108 8 × 108 1 × 109
Abbildung 4.5: Vergleich des Wachstums der Logarithmen zur Basis 2, e, 10,
multipliziert mit unterschiedlichen Faktoren.
31
Diese Erkenntnis führt zum nächsten Theorem über asymptotische Kom-
plexität:
32
• O(n!) – faktorielle Laufzeit, typisches Beispiel: naive Lösung des Tra-
velling Salesman Problems4
n
• O(22 ) – doppelt exponentielle Laufzeit, typisches Beispiel: Entschei-
dung des Wahrheitswertes einer Aussage in Presburger Arithmetik5
n
22
n!
1500
4n
2n
1000 n3
n2
n log2 (n)
500
n
log2 (n)
n
2 4 6 8 10 1
33
n
10300 22
n!
4n
10200 2n
10100
n3
n2
5 10 50 100 500 1000
34
Kapitel 5
• besonders schnell geht (O(log n)), wenn das Array sortiert ist.
• Quicksort
• Mergesort
35
5.1 Quicksort
5.1.1 Der Algorithmus
Quicksort ist wohl das bekannteste Sortierverfahren, vielleicht auch deshalb,
weil es so beeindruckend elegant ist, auf jeden Fall aber, weil es auch heute
noch als das “in der Regel” schnellste Verfahren bei minimalem Speicher-
verbrauch gilt (siehe die Komplexitätsdiskussion weiter unten). Es wurde
von Sir Tony Hoare erfunden, einem der berühmtesten Informatiker unserer
Zeit.1 Ein entscheidender Vorteil von Quicksort ist, dass die Sortierung auf
dem Eingabe-Array abläuft: Es sind keine internen Kopien des Arrays oder
eines Teils davon erforderlich, und es ist keine Kopie in ein Rückgabe-Array
notwendig. Das Quicksort-Verfahren wird in vielen Varianten beschrieben,
wir stellen hier die von Aho et al. [1, 8.3] vor, die in Listing 5.1 abgebildet
ist.
Wir erläutern im folgenden die Funktionsweise des Algorithmus und be- Videos
gründen seine Korrektheit. Das erfordert intensives Mitdenken, aber am
Schluss sollte sich die Überzeugung einstellen, dass dies ein korrekter und
sehr eleganter Algorithmus ist. Falls Sie eine besonders einfache Einführung
in Quicksort suchen, schauen Sie doch wieder bei den Integer.MAX VALUE
vielen Videos auf YouTube unter dem Stichwort “Quicksort Java” nach, zum
Beispiel ist https://www.youtube.com/watch?v=mN5ib1XasSA gar nicht so
schlecht, weicht aber leicht von unserer Version in Listing 5.1 ab.
1
Ich kenne ihn persönlich, er ist nicht nur ein Genie, sondern auch ungeheuer nett.
36
Listing 5.1: Quicksort auf int-Arrays – Implementierung nach Aho et al. [1].
1 public static void quicksort ( int [] a ) {
2 if ( a == null || a . length ==0) {
3 return ;
4 }
5 quicksort (a , 0 , a . length -1) ;
6 }
7
8 private static void quicksort ( int [] a , int lb , int ub ) {
9 int pIdx = getPivotIdx (a , lb , ub ) ;
10 if ( pIdx == -1 ) return ;
11 int p = a [ pIdx ];
12 int lo = lb ;
13 int hi = ub ;
14
15 while ( lo < hi ) {
16 swap (a , lo , hi ) ;
17 while ( a [ lo ] < p ) lo ++;
18 while ( a [ hi ] >= p ) hi - -;
19 }
20 quicksort (a , lb , lo -1) ;
21 quicksort (a , lo , ub ) ;
22 }
Das Interface für den Quicksort-Algorithmus ist in Listing 5.1, Zeile 1 Interface
abgebildet. Ähnlich wie bei der binären Suche betrachten wir die einfachste für int[]-
Version von zu sortierenden Datentypen: ein Array von int-Werten. Analog Sortierung
37
zur binären Suche lässt sich dies recht einfach auf Arrays von Objekten mit
natürlicher Ordnung und auf Arrays, deren Elemente mit einem Komparator
versehen sind, erweitern. Das sollen Sie bitte selbst in Aufgaben 8.2.2 und
8.2.3 programmieren (unbedingt machen, kommt in der Klausur vor . . . ).
Im Interface wird nur geprüft, ob überhaupt etwas zu tun ist: wenn das
Array null ist oder die Länge 0 hat, können wir schon Feierabend machen
(Zeile 2 — 4). Wenn’s wirklich ‘was zu tun gibt, wird die Arbeit an Metho-
de void quicksort(int[] a, int lb, int ub) delegiert, deren Parame-
ter folgende Bedeutung haben:
38
Property-QS-2 Wenn die rekursiven Aufrufe von quicksort in Zei-
le 20 und 21 erfolgen, sind alle Elemente aus dem Array-Abschnitt
a[lb]..a[lo-1] kleiner als alle Elemente aus dem Array-Abschnitt
a[lo]..a[ub]. Etwas formaler:
∀x ∈ {a[lb], . . . , a[lo − 1]}, y ∈ {a[lo], . . . , a[ub]} : x < y
Jetzt gehen wir den Methodenkörper (Zeile 9 — 22) stückweise durch: Schrittweise
Erläuterung
1. In Zeile 9 wird eine Hilfsmethode getPivotIdx aufgerufen, deren Co- von
de in Listing 5.2 dargestellt ist. Diese Methode erfüllt folgende Nach- quicksort(a,lb,ub)
bedingungen, wie man aus der Implementierung in Listing 5.2 sofort
entnehmen kann.
Post-PIVOT-1: Wenn alle Elemente a[lb]..a[ub] identisch sind,
dann ist der Rückgabewert (−1).
Post-PIVOT-2: Wenn nicht alle Elemente a[lb]..a[ub] iden-
tisch sind, dann wird ein Index pIdx im Bereich lb..ub
zurückgegeben, so dass a[pIdx] nicht das kleinste Element un-
ter allen a[lb]..a[ub] ist. Etwas formaler:
∃` ∈ lb..ub : a[`] < a[pIdx]
39
• lo läuft von lb an “nach rechts”, also in aufsteigender Richtung.
• hi läuft von ub an “nach links”, also in absteigender Richtung.
(a) Zu Beginn wird in Zeile 16 der Inhalt von a[lo] und a[hi] ver-
tauscht – das wird unten weiter diskutiert.
(b) In der inneren while-Schleife in Zeile 17 wird lo so lange erhöht,
bis a[lo] größer oder gleich dem Pivot-Element ist. Diese Schleife
terminiert immer:
• Beim ersten Durchlauf der äußeren while-Schleife, weil initial
lo == lb gilt und pIdx ≥ lb garantiert ist.
• Bei den folgenden Durchläufen, weil lo < hi aufgrund der
Schleifenbedingung Zeile 15 erfüllt ist und weil nach dem Auf-
ruf von swap in Zeile 16 immer a[lo] < p ∧ p ≤ a[hi] gilt –
das wird gleich weiter vertieft.
Was können wir über die Nachbedingung bei Terminierung der
Schleife sagen? Auf jeden Fall ist a[lo] ≥ p – dies folgt direkt aus
der Terminierungsbedingung. Des Weiteren lässt sich aus der Ter-
minierungsbedingung sofort folgern, dass entweder lo == lb und
a[lb] ≥ p gilt, oder aber lo ist wirklich in der Schleife inkremen-
tiert worden, sodass die vorigen Elemente a[lo-i] noch kleiner
als p sind. lo kann niemals größer als ub werden, denn es wird das
Pivotelement im Indexbereich oberhalb des Anfangswertes von lo
gefunden. Zusammengefasst ergibt dies folgende Nachbedingung:
6. Als nächstes analysieren wir die while-Schleife in Zeile 18. Hier wird
hi so lange dekrementiert, bis a[hi] < p erfüllt ist. Auch diese Schleife
terminiert immer:
40
• Bei den folgenden Durchläufen, weil die Schleifenbedingung in Zei-
le 15 lo < hi garantiert und swap in Zeile 16 die Nachbedingung
a[lo] < p realisiert.
7. Da die Schleife in Zeile 17 nur lo verändert und die in Zeile 18 nur hi,
sind am Ende der äußeren Schleife (Zeile 19) beide Nachbedingungen
erfüllt.
8. Im Fall lo < hi wird ein weiterer Zyklus der äußeren Schleife durch-
geführt. Da die Nachbedingungen der inneren Schleifen
Das ist genau die Vorbedingung, welche wir – wie oben diskutiert –
für die Terminierung der beiden inneren while-Schleifen benötigen.
Weiterhin ist wegen dieser Vorbedingung garantiert, dass in den in-
neren Schleifen lo um mindestens 1 inkrementiert und hi um min-
destens 1 dekrementiert wird. Damit ist auch die Terminierung der
äußeren Schleife garantiert.
lo ≥ hi
41
• Die “linke” Partition umfasst den Indexbereich lb..(lo-1) und
hat die Eigenschaft ∀i ∈ lb..(lo − 1) : a[i] < p Da es mindestens
ein Element im Array-Abschnitt a[lb]..a[ub] gibt, das echt klei-
ner als das Pivotelement ist, muss die Partition a[lb]..a[lo-1]
mindestens ein Element enthalten.
• Die “rechte” Partition umfasst den Indexbereich lo..ub und hat
die Eigenschaft ∀i ∈ lo..ub : a[i] ≥ p Auch diese Partition muss
mindestens ein Element enthalten, nämlich das Pivotelement.
42
(wenn es solche gibt), deren Elemente größer als die von P sind.
Damit ist die aufsteigende Sortierung des Arrays bewiesen.3
3
Trompeten und Geigen spielen etwas sieghaft-symphonisches, eine Wissenschaftlerin
reitet mit wehendem Staubmantel in den Sonnenuntergang, die Leser*innen wischen sich
Freudentränen aus den Augenwinkeln.
43
5.1.2 Komplexitätsaspekte von Quicksort
Worst-Case Running Time
Ein Kritikpunkt an Quicksort ist, dass seine Worst-Case O(n2 ) beträgt,
während die von Mergesort nur O(n log n) ist4 . Die Berechnung der Worst-
Case Laufzeit geht wie folgt:
1. Mit ein bisschen Überlegung kommt man darauf, dass der Worst-Case
auftritt, wenn immer so ungünstig partitioniert wird, dass die eine Par-
tition nur die Länge 1 hat. Hat das Array die Länge n, benötigt man
etwa (n − 1) Schritte in der while-Schleife, um die große Partition
zu bestimmen und einen Schritt, um die kleine Partition zu bestim-
men. Die Weiterverarbeitung der kleinen Partition terminiert sofort,
die große Partition hat die Länge n − 1.
2. Setzten wir dieses Schema fort, bekommt man folgende Anzahl von
Verarbeitungsschritten:
X
n−1
!
(n − 1) · n − i + (n − 2)
i=1
44
X
n−1
!
A = (n − 1) · n − i + (n − 2)
i=1
1
= (n − 1) · n − · (n − 1) · n + (n − 2)
2
1
= · (n − 1) · n + n − 2
2
1 2 1
= n + n−2
2 2
Hieraus folgt:
Die asymptotische Worst-Case Running Time von Quicksort be-
trägt O(n2 ).
In Frage 8.1.9 sollen Sie sich bitte überlegen, in welcher (sehr einfachen!)
Situation dieser Worst-Case auftritt.
45
n
= 1
2k
⇔
n = 2k
⇔
k = log2 n
In Abb. 5.1 ist noch einmal veranschaulicht, wie viel stärker die Laufzeit
im schlimmsten Fall mit n wächst, und wie sie im besten Fall relativ sanft
mit n steigt.
10 000 n2
8000
6000
4000
2000
n log2 (n)
n
0 20 40 60 80 100
46
Mittlere Laufzeit
Studieren Sie bitte den Abschnitt [2, 7.2], dort wird sehr anschaulich
erläutert, dass die Performance von Quicksort im Mittel nahe an der Best-
Case Running Time liegt, und nicht etwa “in der Mitte zwischen Worst-Case
und Best-Case”. Infolgedessen kann man sich “meistens” auf die exzellente
Performance von Quicksort verlassen.
• Der Fall mit nur einem Pivotelement wird weiterhin verwendet, wenn
viele gleiche Elemente im Array sind. Das Pivotelement wird aber etwas
47
aufwändiger identifiziert als bei der Variante von Aho et al., indem man
ein Element wählt, welches weder das kleinste noch das größte ist.
5.3 Mergesort
Mergesort ist die Alternative zu Quicksort mit folgenden Vor- und Nachteilen.
Vorteil Der Hauptvorteil besteht darin, dass die Laufzeit auch im Worst-
Case O(n log n) ist, so dass man sich nicht vor seltenen, aber dann sehr
unangenehmen, Ausreißern in der Laufzeit fürchten muss.
Nachteil Der Nachteil gegenüber Quicksort besteht darin, dass die Sortie-
rung nicht “in place” auf dem Eingangsarray gemacht wird, sondern
ein internes Hilfsarray benötigt wird, um Zwischenstadien der Sortie-
rung abzuspeichern. Das sortierte Endergebnis muss dann aus dem Zwi-
schenspeicher wieder in das Eingabearray kopiert werden.
Lesen Sie in [3, 2.4], wie das Mergesort-Verfahren funktioniert. Falls Sie
Interesse daran haben, wie man die Verwendung des Zwischenspeichers ver-
meiden kann, lesen Sie die Diskussion in [4, 2.2.1] – das geht jedoch über den
Vorlesungs- und Prüfungsstoff hinaus.
48
Kapitel 6
Laufzeit-Gesamtbetrachtung:
Transformation in Arrays,
Sortieren, Suchen
Wenn wir jetzt die erforderlichen Worst-Case Laufzeiten für das `-malige
Suchen in einer Menge oder Liste mit n Elementen aufsummieren, kommen
wir auf folgende Anzahl von Instruktionen:
Wenn wir dagegen die naive Suche auf einer Liste oder Menge betreiben,
können wir dies mit n Anweisungen realisieren1 , so dass ` Suchvorgänge ` · n
Anweisungen erfordern. In Abb. 6.1 ist die Worst-Case Laufzeit für beide
Fälle in Abhängigkeit von der Anzahl ` der Suchvorgänge aufgetragen; es
wurde dabei n = 109 angenommen.
1
werden wir uns in Session 3 im Zusammenhang mit Listen klarmachen
49
1 × 1011 109 ell
8 × 1010
6 × 1010
4 × 1010
109 + 109 + ell log2 109
2 × 1010
ell
20 40 60 80 100
Abbildung 6.1: Vergleich der `-maligen linearen Suche mit der Worst-Case
Laufzeit der `-maligen Suche auf sortiertem Array.
An diesem Beispiel sehen wir, dass sich die binäre Suche auf sortiertem
Array lohnt, wenn wir mehr als etwa 30 Suchvorgänge haben, bevor das Array
aufgrund von Änderungen neu erstellt und sortiert werden muss. Wenn wir
100 mal auf demselben sortierten Array suchen können, bevor ein neues zu
erstellen ist, benötigen wir sogar nur etwa ein Drittel der Zeit, die insgesamt
mit linearer Suche vertrödelt wurde.
Jetzt brennen Sie natürlich darauf zu berechnen, wie das ` von n abhängt.
Das rechnen wir schnell aus; die Frage lautet
50
n · ` ≥ n + n · log n + ` · log n
⇔
1
` ≥ 1 + log n + ` · log n
n
⇔
1
`−`· log n ≥ 1 + log n
n
⇔
1
` · (1 − log n) ≥ 1 + log n
n
⇔
1 + log n
` ≥
(1 − n1 log n)
51
32 1 + log2 (n)
log2 (n)
1-
n
31
30
29
28
27
n
5.0 × 108 1.0 × 109 1.5 × 109 2.0 × 109
52
Kapitel 7
Das Divide-and-Conquer
Paradigma
Die in dieser Session studierten Algorithmen folgen alle dem Teile und Abstraktes
Herrsche (engl. Divide-and-Conquer) Paradigma: Die Lösung des Pro- Verfahren
blems wird auf folgende Weise erreicht:
Teile (Divide) die Eingabemenge in Teilmengen, die zu kleineren Sub-
Problemen führen.
Beherrsche (Conquer) die Sub-Probleme, indem nach Bedarf iterativ
oder rekursiv eine weitere Aufteilung in Unterprobleme vorgenommen
wird.
Identifikation von Basisfällen (Base Cases): “Passt” ein Unterpro-
blem auf einen Basisfall, muss keine weitere Teilung der Eingabe-
menge mehr vorgenommen werden, weil für diesen Fall ein einfaches
Lösungsverfahren existiert. Es kann auch mehrere Basisfälle geben, wie
die Beispiele unten zeigen werden.
Kombiniere (Combine) die Teillösungen zu einer Gesamtlösung
Diese abstrakte Verfahrensbeschreibung wurde bei der binären Suche (sie- Umsetzung
he Listing 3.1) folgendermaßen umgesetzt: bei der
binären
1. Die Eingabemenge ist ein aufsteigend sortiertes Array
Suche
2. Die Teilung der Eingabemenge erfolgt immer in der Mitte, wodurch
sich die Problemgröße halbiert.
53
3. Bei der Beherrschung des Problems nutzt man aus, dass die Lösung
nur in einer Hälfte des geteilten Arrays sein kann. Dies bedeutet, dass
man die andere Hälfte nicht mehr untersuchen muss. Hierdurch ist die
rekursive Bearbeitung des verbleibenden Unterproblems nicht unbe-
dingt erforderlich – die iterative Lösung ist nicht weniger elegant.
2. Die Teilung der Eingabemenge erfolgt für das Problem und alle Sub-
Probleme durch die Vorgabe, dass im “linken” Teil nur Elemente ein-
geordnet werden, die kleiner als das vorher ermittelte Pivot-Element
sind, und im “rechten” Teil nur Elemente, die größer oder gleich dem
Pivot-Element sind.
4. Der (einzige) Basisfall bei Quicksort besteht darin, dass die Eingabe-
menge des (Unter-)problems lauter identische Elemente enthält. Dies
wird daran erkannt, dass kein Pivot-Element gefunden wird. Beachten
Sie, dass die mit Basisfällen assoziierten Unterprobleme verschieden
groß sein können.
54
5. Das sortierte Array setzt sich aus den sortierten Unter-Arrays zusam-
men. Für diese Kombination ist nichts mehr zu tun, denn es wird
auf dem Original-Array sortiert, der auch das Ausgabe-Array ist. Da
jede Unterproblemlösung kleinere Elemente enthält als die rechts be-
nachbarte Problemlösung, sind die sortierten Teillösungen auch in der
richtigen Reihenfolge im Array angeordnet.
55
Kapitel 8
8.1 Fragen
8.1.1 Falsche Implementierungen im Internet
Suchen Sie mindestens 3 Stellen im Internet und geben die URLs an, wo
ein Java-Algorithmus für die binäre Suche angegeben wird, der den oben
beschriebenen berüchtigten Fehler noch enthält1 . Erschrecken Sie darüber so
sehr, dass Sie bis ans Ende Ihres Lebens keinen einzigen Algorithmus aus dem
Netz mehr übernehmen, ohne seine Korrektheit im Detail zu analysieren.
Sie könnten den Personen, welche die falschen Implementierungen
veröffentlicht haben, noch freundlich (!) schreiben, welche Korrektur anzu-
bringen ist – das würde die Welt zu einem besseren Ort machen.
Was ist der Rückgabewert des folgenden Aufrufs der Methode aus Li-
sting 3.3 ?
1 Arrays . binarySearch (a , 13 , ( o1 , o2 ) -> ( o1 %2 - o2 %2) ) ;
Führen Sie den Aufruf mit Hilfe von Listing 3.3 als “Schreibtischtest” aus.
Das heißt, Sie inspizieren das Listing und schreiben sich für jeden Schritt die
1
Ich habe innerhalb von 120 Sekunden 3 falsche Implementierungen gefunden. Finden
Sie mehr in dieser Zeit?
56
Variablenbelegung auf, bis der Rückgabewert bestimmt ist und die Funktion
terminiert.
57
1. Berechnen Sie die exakte Worst-Case Laufzeit in Abhängigkeit von der
Array-Größe n = a.length und der Konstanten stepSize. Hierbei
dürfen Sie annehmen, dass n ein Vielfaches von stepSize ist.
1. O(n2 ) ⊆ O(n3 )
3. O(2n ) ⊆ O(4n )
n
4. O(2n ) ⊆ O(22 )
58
8.1.8 Varianten von Quicksort
1. Lesen Sie die Beschreibung von Quicksort in [4, Abschnitt 2.3] und
erklären Sie, wie sich diese algorithmische Variante von unserer auf [1]
basierenden Version unterscheidet.
8.2 Aufgaben
8.2.1 Quicksort Test
Erstellen Sie ein IntelliJ-Projekt für den Algorithmus aus Listing 5.1 und
erstellen Sie JUnit Tests, die 100% Statement Coverage erzielen.
Schreiben Sie wieder Requirements für die Methoden quicksort(int[]
a, int lb, int ub), swap und getPivotIdx auf und verweisen von den
Testfällen auf die Anforderungen.
Weisen Sie die Teststärke nach, indem Sie wieder Mutationen in die Me-
thoden “injizieren”3 und prüfen, dass ihre Testfälle diese Fehler aufdecken.
59
8.2.2 Quicksort auf Elementen mit natürlicher
Ordnung
Programmieren Sie die oben eingeführte auf [1] beruhende Variante des
Quicksort-Algorithmus auf Object-Arrays, deren Elemente das Interface
Comparable implementieren. Das Interface soll genauso sein, wie bei der Java
Klasse Arrays spezifiziert:
1 public static void sort ( Object [] a )
60
Kapitel 9
Anstelle weiterer
Literatur-Hinweise
Lange Jahre, aber vor Ihrer Zeit, war der Cartoonist Gary Larson1 ein Favorit
von Informatikern und Informatikerinnen. Sie sollten seinen Namen zumin-
dest kennen (siehe Abb. 9.1), auch wenn seine Cartoons heute nicht mehr
zum Prüfungsstoff gehören.
1
https://de.wikipedia.org/wiki/Gary_Larson
61
Abbildung 9.1: Gary Larson – Lieblings-Cartoonist vieler inzwischen nicht
mehr ganz junger Informatiker*innen.
62
Literaturverzeichnis
[1] Alfred V. Aho, John E. Hopcroft, and Jeffrey D. Ullman. The Design and
Analysis of Computer Algorithms. Addison-Weley Publishing Company,
2009.
63