Laufzeitanalyse: Unterschied zwischen den Versionen

Aus FLBK-Wiki
Zur Navigation springen Zur Suche springen
Keine Bearbeitungszusammenfassung
Markierung: Manuelle Zurücksetzung
Keine Bearbeitungszusammenfassung
Zeile 1: Zeile 1:
== Einführung ==
== Einführung ==
[[Datei:Laufzeitanalyse asymptotisch.png|mini|Asymptotische Annäherung einer Funktion]]
[[Datei:Laufzeitanalyse asymptotisch.png|mini|Asymptotische Annäherung einer Funktion]]
Die Laufzeitanalyse untersucht die Zeitkomplexität eines [[Algorithmus]]. Da die reale Ausführungszeit stark von der Hardwareausstattung des Zielsystems abhängt, wird für eine allgemeine, hardwareunabhängige Betrachtung die Anzahl der elementaren [[Anweisung|Befehle]] (Operationen) analysiert, die ein Algorithmus zur Lösung eines Problems benötigt. Diese Anzahl ist abhängig von der Größe der Eingabemenge, deren Umfang auch als '''Problemgröße''' (oft <math>n</math>) bezeichnet wird.
Die Laufzeitanalyse untersucht die sogenannte Zeitkomplexität eines [[Algorithmus]]. Man stellt sich dabei im Kern die Frage: '''„Wie verhält sich die Rechenzeit, wenn die Menge der zu verarbeitenden Daten extrem groß wird?“'''


Für kleine Eingabemengen ist die Analyse häufig von geringerer Bedeutung, da auch ineffiziente [[Algorithmus|Algorithmen]] hier oft ausreichend schnelle Ergebnisse liefern. Entscheidend ist die Betrachtung für sehr große Problemgrößen, insbesondere unter ungünstigen Startbedingungen. Hier spricht man von der asymptotischen [[Programmausführung|Laufzeit]]. In Anlehnung an eine mathematische Asymptote beschreibt sie das Zeitverhalten des Algorithmus für eine potenziell unendlich wachsende Eingabemenge (z. B. eine theoretisch unendlich lange Zahlenfolge beim [[Sortieren]]).
Da die echte Ausführungszeit (in Sekunden) stark vom jeweiligen Computer abhängt, misst man stattdessen die Anzahl der elementaren [[Anweisung|Befehle]] (z. B. Vergleiche oder Tauschoperationen). Diese Anzahl hängt von der Größe der Eingabemenge ab, die in der Informatik meist als '''Problemgröße <math>n</math>''' bezeichnet wird.
 
Für kleine Datenmengen (z. B. eine Liste mit 10 Elementen sortieren) ist fast jeder Algorithmus schnell genug. Spannend wird es erst bei sehr großen Datenmengen (z. B. Millionen von Kundendatensätzen). Hier betrachtet man die sogenannte asymptotische [[Programmausführung|Laufzeit]]. Sie beschreibt das Verhalten des Algorithmus, wenn die Datenmenge <math>n</math> theoretisch ins Unendliche wächst.


Lässt sich die Laufzeit eines Algorithmus mathematisch durch eine Funktion beschreiben, wie beispielsweise:
Lässt sich die Laufzeit eines Algorithmus mathematisch durch eine Funktion beschreiben, wie beispielsweise:
Zeile 10: Zeile 12:
So nähert sich die rot dargestellte Funktion <math>\color{red}{f(x)}</math> für große <math>x</math> der grün dargestellten Asymptote <math>\color{green}{g(x) = x}</math> an.
So nähert sich die rot dargestellte Funktion <math>\color{red}{f(x)}</math> für große <math>x</math> der grün dargestellten Asymptote <math>\color{green}{g(x) = x}</math> an.


Die Laufzeit wird in Abhängigkeit von der Länge <math>n</math> der Eingabe angegeben und für immer größer werdende <math>n</math> asymptotisch unter Verwendung der Landau-Notation (Groß-O-Notation) abgeschätzt.
Die Laufzeit wird mit der sogenannten Landau-Notation (Groß-O-Notation, z. B. <math>\mathcal{O}(n^2)</math>) abgeschätzt. Dabei werden konstante Vorfaktoren (wie <math>\frac{1}{2}</math>) oder kleinere Summanden ignoriert, da bei riesigen Datenmengen nur noch der dominierende Teil der Funktion das Wachstum bestimmt.
 
=== Was die Laufzeitanalyse NICHT ist ===
Sie misst '''nicht''' den tatsächlichen Zeitaufwand in Millisekunden auf einem echten Computer. Sie ist ein theoretisches Modell, um Algorithmen hardwareunabhängig miteinander vergleichen zu können.
 
== Szenarien der Laufzeitabschätzung ==
Da Algorithmen je nach Vorsortierung der Daten unterschiedlich schnell arbeiten, unterscheidet man drei klassische Fälle:
 
* Das '''Worst-Case-Szenario''' (Schlechtester Fall): Die Daten liegen so ungünstig vor, dass der Algorithmus maximal lange braucht. Dies liefert eine absolute Sicherheitsgarantie (obere Schranke).
* Das '''Best-Case-Szenario''' (Bester Fall): Die Daten liegen optimal vor (z. B. bereits sortiert). Der Algorithmus ist minimal schnell fertig.
* Das '''Average-Case-Szenario''' (Durchschnittlicher Fall): Beschreibt die zu erwartende Laufzeit bei völlig zufällig gemischten Daten. Dies ist oft das realistischste Szenario in der Praxis.
 
== Mathematische Berechnung des Average-Case ==
Oft wird fälschlicherweise angenommen, der Average-Case sei einfach „die Hälfte des Worst-Case“. Das ist mathematisch ungenau. Tatsächlich berechnet man den Average-Case mit Hilfe der Stochastik: Er entspricht dem '''Erwartungswert''' der Laufzeit.
 
Man berechnet ihn, indem man alle möglichen Eingaben durchspielt und gewichtet. Die Formel für den Erwartungswert <math>E(T)</math> lautet:
 
<math>E(T) = \sum_{i=1}^{|E|} p_i \cdot t_i</math>
 
* <math>p_i</math> ist die Wahrscheinlichkeit für einen bestimmten Fall <math>i</math>.
* <math>t_i</math> ist die Anzahl der Rechenschritte, die der Algorithmus in diesem Fall braucht.
 
Für die Schule gehen wir meistens von einer '''Gleichverteilung''' aus: Jeder Fall tritt mit derselben Wahrscheinlichkeit auf.


In der Praxis ist die Laufzeitanalyse essenziell, um entscheiden zu können, ob ein Programm bei stark anwachsenden Datenmengen noch performant und wirtschaftlich betrieben werden kann.
=== Beispiel 1: Average-Case der Linearen Suche ===
Wir suchen eine bestimmte Zahl in einem Array der Länge <math>n</math>. Wir nehmen an, die Zahl ist im Array und jede Position ist gleich wahrscheinlich.


=== Abgrenzung ===
* '''Schritt 1: Wahrscheinlichkeit bestimmen.''' Die Wahrscheinlichkeit, dass unsere Zahl an Position 1, 2 oder <math>n</math> steht, ist jeweils <math>p = \frac{1}{n}</math>.
Es ist wichtig zu betonen, was die asymptotische Laufzeitanalyse '''nicht''' leistet:
* '''Schritt 2: Schritte zählen.''' Steht die Zahl an Position 1, brauchen wir 1 Vergleich. An Position 2 brauchen wir 2 Vergleiche. An Position <math>n</math> brauchen wir <math>n</math> Vergleiche.
Sie misst nicht den tatsächlichen Zeitaufwand (in Sekunden) eines implementierten Algorithmus auf einem spezifischen Computer für eine konkrete, endliche Eingabemenge.
* '''Schritt 3: Erwartungswert berechnen.''' Wir setzen dies in die Formel ein:


== Szenarien der Laufzeitabschätzung ==
<math>E(T) = \frac{1}{n}\cdot 1 + \frac{1}{n}\cdot 2 + \dots + \frac{1}{n}\cdot n = \sum_{i=1}^{n} \left( \frac{1}{n} \cdot i \right)</math>
Man unterscheidet bei der Laufzeitabschätzung klassischerweise drei Varianten:
 
Wir klammern <math>\frac{1}{n}</math> aus:
 
<math>E(T) = \frac{1}{n} \cdot \sum_{i=1}^{n} i</math>
 
Die Summe der Zahlen von 1 bis <math>n</math> lässt sich mit der '''Gaußschen Summenformel''' (kleiner Gauß) vereinfachen zu <math>\frac{n(n+1)}{2}</math>:
 
<math>E(T) = \frac{1}{n} \cdot \frac{n(n+1)}{2} = \frac{n+1}{2} = \frac{1}{2}n + \frac{1}{2}</math>
 
'''Fazit:''' Im Durchschnitt braucht die lineare Suche <math>\frac{n+1}{2}</math> Vergleiche. Da in der <math>\mathcal{O}</math>-Notation Konstanten wie <math>\frac{1}{2}</math> wegfallen, gehört die lineare Suche im Durchschnitt zur Klasse <math>\mathcal{O}(n)</math>.
 
=== Beispiel 2: Vorbereitung auf Quicksort (Maximumsuche) ===
Bei komplexeren Algorithmen wie [[Quicksort]] reicht einfaches Zählen nicht mehr aus. Hier arbeiten wir mit einem mathematischen Trick: '''Indikatorvariablen''' und der '''Harmonischen Reihe'''. Um das zu verstehen, analysieren wir einen simplen Code zur Maximumsuche:
 
<syntaxhighlight lang="java">
public int findMax(int[] A) {
    int max = A[0];
    for (int i = 1; i < A.length; i++) {
        if (A[i] > max) {
            max = A[i]; // Update-Operation
        }
    }
    return max;
}
</syntaxhighlight>
Die Vergleiche in der <code>if</code>-Bedingung werden immer <math>n-1</math> Mal ausgeführt. Aber wie oft wird die Variable <code>max</code> im Durchschnitt überschrieben (Update-Operation)?
 
Wir stellen uns vor, das Array enthält völlig zufällig gemischte Zahlen.
* '''Intuitiver Ansatz:'''
Das 1. Element ist automatisch das bisherige Maximum (Wahrscheinlichkeit: 1).
Damit das 2. Element ein neues Maximum ist, muss es größer als das erste sein. Die Chance dafür ist exakt 50% (<math>\frac{1}{2}</math>).
Damit das 3. Element ein neues Maximum ist, muss es das Größte der ersten drei Elemente sein. Die Chance dafür ist <math>\frac{1}{3}</math>.
 
* '''Mathematischer Ansatz (Indikatorvariablen):'''
Wir definieren einen „Schalter“ (die Indikatorvariable) <math>X_i</math>. Er ist <math>1</math>, wenn ein neues Maximum an Stelle <math>i</math> gefunden wird, sonst <math>0</math>. Die Wahrscheinlichkeit dafür ist <math>P(X_i = 1) = \frac{1}{i}</math>.
 
Um die durchschnittliche Gesamtzahl der Updates zu finden, addieren wir einfach diese Wahrscheinlichkeiten auf:
 
<math>E[X] = \sum_{i=1}^{n} \frac{1}{i} = 1 + \frac{1}{2} + \frac{1}{3} + \frac{1}{4} + \dots + \frac{1}{n}</math>
 
Diese spezielle Summe nennt man in der Mathematik die '''Harmonische Reihe''' (<math>H_n</math>). Sie wächst extrem langsam. Für große Zahlen <math>n</math> entspricht sie in etwa dem '''natürlichen Logarithmus''' (<math>\ln(n)</math>).
 
'''Erkenntnis für Quicksort:''' Obwohl das Array <math>n</math> Elemente hat, wird die Update-Operation im Durchschnitt nur logarithmisch oft (<math>\mathcal{O}(\log n)</math>) ausgeführt. Genau dieses stochastische Prinzip (das Aufsummieren von Einzelwahrscheinlichkeiten, die zu einem Logarithmus führen) ist der Schlüssel, um später selbst zu beweisen, dass Quicksort im Average-Case bei <math>\mathcal{O}(n \log n)</math> liegt.


* Das '''Worst-Case-Szenario''' (schlechtester Fall): Beschreibt diejenige Datenanordnung, die zu einer maximalen Durchlaufzeit des Algorithmus führt. Dies entspricht der oberen Schranke des Problems.
* Das '''Average-Case-Szenario''' (durchschnittlicher Fall): Beschreibt eine zufällige Problemgröße und Datenanordnung, die zu einer mittleren (erwarteten) Durchlaufzeit führt.
* Das '''Best-Case-Szenario''' (bester Fall): Beschreibt diejenige Datenanordnung, die zu einer minimalen Durchlaufzeit führt (z. B. eine bereits vollständig sortierte Liste). Dies entspricht der unteren Schranke.


== Beispiel: Laufzeitanalyse von Insertion Sort ==
== Beispiel 3: Laufzeitanalyse von Insertion Sort ==
Im Folgenden wird die Laufzeitanalyse beispielhaft anhand des [[Insertion-sort|Insertion Sorts]] (Sortieren durch Einfügen) durchgeführt. Wir betrachten eine mögliche Implementierung in der Programmiersprache [[Java]]. Der Algorithmus arbeitet mit einem [[Array]] als Datenstruktur und verwendet als [[Kontrollstruktur|Kontrollstrukturen]] [[Verzweigung|Verzweigungen]] und [[Schleife|Schleifen]].
Nun wenden wir unser Wissen auf das Sortierverfahren [[Insertion-sort|Insertion Sort]] (Sortieren durch Einfügen) an.  


<syntaxhighlight lang="java">
<syntaxhighlight lang="java">
Zeile 47: Zeile 111:
</syntaxhighlight>
</syntaxhighlight>


Wir analysieren nun die drei Szenarien für eine Liste mit <math>n</math> Elementen und betrachten zunächst den Zeitverbrauch für die durchzuführenden Vergleichsoperationen. Den konstanten Zeitverbrauch für einen einzelnen Vergleich bezeichnen wir allgemein mit <math>c</math>.
Wir analysieren eine Liste mit <math>n</math> Elementen. Den Zeitverbrauch für einen einzelnen Vergleich nennen wir <math>c</math>.


=== 1. Best-Case-Szenario (Bester Fall) ===
=== 1. Best-Case-Szenario (Bester Fall) ===
Ein Best-Case-Szenario liegt vor, wenn die Liste bereits aufsteigend sortiert ist, beispielsweise:
Die Liste ist bereits perfekt aufsteigend sortiert (z. B. <code>[0, 2, 3, 5, 7]</code>).
<code>[0, 2, 3, 5, 7, 11]</code>
* '''Verhalten:''' Da jedes Element schon größer als sein Vorgänger ist, ist die <code>if</code>-Bedingung immer falsch. Die innere <code>do-while</code>-Schleife wird ignoriert.
 
* '''Aufwand:''' Wir machen exakt einen Vergleich pro Element in der äußeren Schleife. Das ergibt <math>c \cdot (n - 1)</math> Vergleiche.
* '''Verhalten:''' Die äußere <code>for</code>-Schleife durchläuft das Array <math>n - 1</math> Mal. Da das aktuell betrachtete Element stets größer oder gleich seinem Vorgänger ist, ist die Bedingung der <code>if</code>-Anweisung (<code>zSortfeld[i] < zSortfeld[i - 1]</code>) '''immer falsch'''. Die innere <code>do-while</code>-Schleife wird somit nie betreten.
* '''Komplexität:''' Der Aufwand wächst wie eine Gerade (linear). Die Komplexität ist <math>\mathcal{O}(n)</math>.
* '''Aufwand:''' Es findet pro Durchlauf der äußeren Schleife exakt ein Vergleich statt. Der Gesamtaufwand lautet <math>c \cdot (n - 1)</math>.
* '''Komplexität:''' Für sehr große <math>n</math> wächst der Aufwand linear zur Eingabemenge. Der Insertion Sort hat im Best-Case somit eine Zeitkomplexität von <math>\mathcal{O}(n)</math>.


=== 2. Worst-Case-Szenario (Schlechtester Fall) ===
=== 2. Worst-Case-Szenario (Schlechtester Fall) ===
Ein Worst-Case-Szenario liegt vor, wenn die Liste in genau umgekehrter (absteigender) Reihenfolge vorliegt, beispielsweise:
Die Liste ist exakt falsch herum sortiert (z. B. <code>[7, 5, 3, 2, 0]</code>).
<code>[11, 7, 5, 3, 2, 0]</code>
* '''Verhalten:''' Die <code>if</code>-Bedingung ist immer wahr. Jedes Element muss durch die innere Schleife komplett bis an den Anfang der Liste getauscht werden.
 
* '''Aufwand:''' Beim 1. Durchlauf machen wir 1 Vergleich. Beim 2. Durchlauf 2 Vergleiche. Beim letzten Durchlauf <math>n-1</math> Vergleiche. Wir müssen also wieder addieren: <math>1 + 2 + 3 + \dots + (n-1)</math>.
* '''Verhalten:''' Die <code>if</code>-Bedingung ist hier '''immer wahr'''. Das aktuell betrachtete Element muss in der inneren <code>do-while</code>-Schleife jedes Mal bis ganz an den Anfang des Arrays verschoben werden.
* '''Aufwand:'''  
** Beim ersten Durchlauf vergleichen wir nur die 7 mit der 11 (1 Vergleich). Aufwand: <math>c \cdot 1</math>.
** Beim zweiten Durchlauf müssen wir das nächste Element mit den zwei bereits sortierten vergleichen. Aufwand: <math>c \cdot 2</math>.
** Dies setzt sich fort bis zum letzten Durchlauf, der <math>n - 1</math> Vergleiche erfordert. Aufwand: <math>c \cdot (n - 1)</math>.


Der Gesamtaufwand für die Vergleiche entspricht einer [[Arithmetische-reihe|arithmetischen Reihe]] (Gaußsche Summenformel):
Wie bei der linearen Suche hilft uns hier die Gaußsche Summenformel:
<math>c \cdot (1 + 2 + 3 + \dots + (n - 1)) = c \cdot \frac{(n - 1) \cdot n}{2} = \frac{c}{2}n^2 - \frac{c}{2}n</math>
<math>c \cdot \frac{(n - 1) \cdot n}{2} = \frac{c}{2}n^2 - \frac{c}{2}n</math>


[[Datei:Asymptote an quadratische Funktion2.png|mini|Quadratisches Wachstum]]
[[Datei:Asymptote an quadratische Funktion2.png|mini|Quadratisches Wachstum]]
Für die asymptotische Betrachtung extrem großer Werte von <math>n</math> verwerfen wir den langsamer wachsenden (niederwertigen) Term <math>\frac{c}{2}n</math> sowie die Konstante <math>\frac{c}{2}</math>.  
* '''Komplexität:''' Für extrem große <math>n</math> dominiert das <math>n^2</math> alles andere. Wir ignorieren den Faktor <math>\frac{c}{2}</math> und den kleineren Teil <math>n</math>. Das Wachstum ist quadratisch, also <math>\mathcal{O}(n^2)</math>.
* '''Komplexität:''' Der Aufwand wächst quadratisch. Der Insertion Sort hat im Worst-Case somit eine Zeitkomplexität von <math>\mathcal{O}(n^2)</math>.


=== 3. Average-Case-Szenario (Durchschnittlicher Fall) ===
=== 3. Average-Case-Szenario (Durchschnittlicher Fall) ===
Das Average-Case-Szenario geht von einer völlig zufälligen Verteilung der Werte im Array aus.
Die Werte im Array sind völlig zufällig verteilt.
 
* '''Verhalten:''' Im Durchschnitt müssen wir ein Element nicht bis ganz an den Anfang schieben (wie im Worst-Case), sondern nur bis zur '''Hälfte''' des bisher sortierten Bereichs.
* '''Verhalten:''' Im Durchschnitt ist das betrachtete Element kleiner als die Hälfte der bereits sortierten Elemente. Die innere Schleife muss das Element also nicht bis ganz an den Anfang, sondern im Mittel nur bis in die Mitte des bisher sortierten Teilbereichs verschieben.
* '''Aufwand:''' Die Anzahl der Vergleiche halbiert sich grob im Vergleich zum Worst-Case. Der Aufwand liegt bei ca. <math>\frac{1}{2} \cdot \frac{c}{2}n^2</math>.
* '''Aufwand:''' Die Anzahl der Vergleiche und Verschiebungen halbiert sich im Vergleich zum Worst-Case in etwa. Der rechnerische Aufwand läge grob bei <math>\frac{1}{2} \cdot \frac{c}{2}n^2</math>.
* '''Komplexität:''' Wie wir gelernt haben, ignoriert die Groß-O-Notation konstante Brüche (wie <math>\frac{1}{4}</math>). Es bleibt bei der höchsten Potenz: Das Wachstum ist und bleibt quadratisch. Die Komplexität lautet also weiterhin <math>\mathcal{O}(n^2)</math>.
* '''Komplexität:''' Da in der asymptotischen Landau-Notation konstante Faktoren (wie die Halbierung) ignoriert werden, da primär die höchste Potenz das Wachstum dominiert, bleibt es auch hier bei einem quadratischen Wachstum. Die Zeitkomplexität im Average-Case lautet ebenfalls <math>\mathcal{O}(n^2)</math>.
 
=== Einfluss weiterer Operationen ===
Wie beeinflussen Lese- und Schreiboperationen (das tatsächliche Verschieben der Werte im Array) die Zeitkomplexität? Für das Verschieben käme lediglich ein konstanter Aufwand je Vergleich hinzu. Nennen wir diesen Aufwand <math>v</math>. Die angepasste Worst-Case-Funktion lautet dann beispielsweise:
 
<math>\frac{v+c}{2}n^2 - \frac{v+c}{2}n</math>
 
Auch hier gilt: Für sehr große <math>n</math> werden der niederwertige Term und die konstanten Faktoren ignoriert. Es bleibt bei der Erkenntnis, dass Lese- und Schreibzugriffe die fundamentale Komplexitätsklasse nicht verändern. Das Laufzeitverhalten bleibt asymptotisch in der Klasse <math>\mathcal{O}(n^2)</math>.


[[Kategorie:Programmierung]]
[[Kategorie:Programmierung]]
[[Kategorie:AHR_I_Informatik_LK]]
[[Kategorie:AHR_I_Informatik_LK]]

Version vom 20. April 2026, 09:34 Uhr

Einführung

Asymptotische Annäherung einer Funktion

Die Laufzeitanalyse untersucht die sogenannte Zeitkomplexität eines Algorithmus. Man stellt sich dabei im Kern die Frage: „Wie verhält sich die Rechenzeit, wenn die Menge der zu verarbeitenden Daten extrem groß wird?“

Da die echte Ausführungszeit (in Sekunden) stark vom jeweiligen Computer abhängt, misst man stattdessen die Anzahl der elementaren Befehle (z. B. Vergleiche oder Tauschoperationen). Diese Anzahl hängt von der Größe der Eingabemenge ab, die in der Informatik meist als Problemgröße [math]\displaystyle{ n }[/math] bezeichnet wird.

Für kleine Datenmengen (z. B. eine Liste mit 10 Elementen sortieren) ist fast jeder Algorithmus schnell genug. Spannend wird es erst bei sehr großen Datenmengen (z. B. Millionen von Kundendatensätzen). Hier betrachtet man die sogenannte asymptotische Laufzeit. Sie beschreibt das Verhalten des Algorithmus, wenn die Datenmenge [math]\displaystyle{ n }[/math] theoretisch ins Unendliche wächst.

Lässt sich die Laufzeit eines Algorithmus mathematisch durch eine Funktion beschreiben, wie beispielsweise: [math]\displaystyle{ f(x) = \frac{1}{x} + x }[/math]

So nähert sich die rot dargestellte Funktion [math]\displaystyle{ \color{red}{f(x)} }[/math] für große [math]\displaystyle{ x }[/math] der grün dargestellten Asymptote [math]\displaystyle{ \color{green}{g(x) = x} }[/math] an.

Die Laufzeit wird mit der sogenannten Landau-Notation (Groß-O-Notation, z. B. [math]\displaystyle{ \mathcal{O}(n^2) }[/math]) abgeschätzt. Dabei werden konstante Vorfaktoren (wie [math]\displaystyle{ \frac{1}{2} }[/math]) oder kleinere Summanden ignoriert, da bei riesigen Datenmengen nur noch der dominierende Teil der Funktion das Wachstum bestimmt.

Was die Laufzeitanalyse NICHT ist

Sie misst nicht den tatsächlichen Zeitaufwand in Millisekunden auf einem echten Computer. Sie ist ein theoretisches Modell, um Algorithmen hardwareunabhängig miteinander vergleichen zu können.

Szenarien der Laufzeitabschätzung

Da Algorithmen je nach Vorsortierung der Daten unterschiedlich schnell arbeiten, unterscheidet man drei klassische Fälle:

  • Das Worst-Case-Szenario (Schlechtester Fall): Die Daten liegen so ungünstig vor, dass der Algorithmus maximal lange braucht. Dies liefert eine absolute Sicherheitsgarantie (obere Schranke).
  • Das Best-Case-Szenario (Bester Fall): Die Daten liegen optimal vor (z. B. bereits sortiert). Der Algorithmus ist minimal schnell fertig.
  • Das Average-Case-Szenario (Durchschnittlicher Fall): Beschreibt die zu erwartende Laufzeit bei völlig zufällig gemischten Daten. Dies ist oft das realistischste Szenario in der Praxis.

Mathematische Berechnung des Average-Case

Oft wird fälschlicherweise angenommen, der Average-Case sei einfach „die Hälfte des Worst-Case“. Das ist mathematisch ungenau. Tatsächlich berechnet man den Average-Case mit Hilfe der Stochastik: Er entspricht dem Erwartungswert der Laufzeit.

Man berechnet ihn, indem man alle möglichen Eingaben durchspielt und gewichtet. Die Formel für den Erwartungswert [math]\displaystyle{ E(T) }[/math] lautet:

[math]\displaystyle{ E(T) = \sum_{i=1}^{|E|} p_i \cdot t_i }[/math]

  • [math]\displaystyle{ p_i }[/math] ist die Wahrscheinlichkeit für einen bestimmten Fall [math]\displaystyle{ i }[/math].
  • [math]\displaystyle{ t_i }[/math] ist die Anzahl der Rechenschritte, die der Algorithmus in diesem Fall braucht.

Für die Schule gehen wir meistens von einer Gleichverteilung aus: Jeder Fall tritt mit derselben Wahrscheinlichkeit auf.

Beispiel 1: Average-Case der Linearen Suche

Wir suchen eine bestimmte Zahl in einem Array der Länge [math]\displaystyle{ n }[/math]. Wir nehmen an, die Zahl ist im Array und jede Position ist gleich wahrscheinlich.

  • Schritt 1: Wahrscheinlichkeit bestimmen. Die Wahrscheinlichkeit, dass unsere Zahl an Position 1, 2 oder [math]\displaystyle{ n }[/math] steht, ist jeweils [math]\displaystyle{ p = \frac{1}{n} }[/math].
  • Schritt 2: Schritte zählen. Steht die Zahl an Position 1, brauchen wir 1 Vergleich. An Position 2 brauchen wir 2 Vergleiche. An Position [math]\displaystyle{ n }[/math] brauchen wir [math]\displaystyle{ n }[/math] Vergleiche.
  • Schritt 3: Erwartungswert berechnen. Wir setzen dies in die Formel ein:

[math]\displaystyle{ E(T) = \frac{1}{n}\cdot 1 + \frac{1}{n}\cdot 2 + \dots + \frac{1}{n}\cdot n = \sum_{i=1}^{n} \left( \frac{1}{n} \cdot i \right) }[/math]

Wir klammern [math]\displaystyle{ \frac{1}{n} }[/math] aus:

[math]\displaystyle{ E(T) = \frac{1}{n} \cdot \sum_{i=1}^{n} i }[/math]

Die Summe der Zahlen von 1 bis [math]\displaystyle{ n }[/math] lässt sich mit der Gaußschen Summenformel (kleiner Gauß) vereinfachen zu [math]\displaystyle{ \frac{n(n+1)}{2} }[/math]:

[math]\displaystyle{ E(T) = \frac{1}{n} \cdot \frac{n(n+1)}{2} = \frac{n+1}{2} = \frac{1}{2}n + \frac{1}{2} }[/math]

Fazit: Im Durchschnitt braucht die lineare Suche [math]\displaystyle{ \frac{n+1}{2} }[/math] Vergleiche. Da in der [math]\displaystyle{ \mathcal{O} }[/math]-Notation Konstanten wie [math]\displaystyle{ \frac{1}{2} }[/math] wegfallen, gehört die lineare Suche im Durchschnitt zur Klasse [math]\displaystyle{ \mathcal{O}(n) }[/math].

Beispiel 2: Vorbereitung auf Quicksort (Maximumsuche)

Bei komplexeren Algorithmen wie Quicksort reicht einfaches Zählen nicht mehr aus. Hier arbeiten wir mit einem mathematischen Trick: Indikatorvariablen und der Harmonischen Reihe. Um das zu verstehen, analysieren wir einen simplen Code zur Maximumsuche:

public int findMax(int[] A) {
    int max = A[0];
    for (int i = 1; i < A.length; i++) {
        if (A[i] > max) {
            max = A[i]; // Update-Operation
        }
    }
    return max;
}

Die Vergleiche in der if-Bedingung werden immer [math]\displaystyle{ n-1 }[/math] Mal ausgeführt. Aber wie oft wird die Variable max im Durchschnitt überschrieben (Update-Operation)?

Wir stellen uns vor, das Array enthält völlig zufällig gemischte Zahlen.

  • Intuitiver Ansatz:

Das 1. Element ist automatisch das bisherige Maximum (Wahrscheinlichkeit: 1). Damit das 2. Element ein neues Maximum ist, muss es größer als das erste sein. Die Chance dafür ist exakt 50% ([math]\displaystyle{ \frac{1}{2} }[/math]). Damit das 3. Element ein neues Maximum ist, muss es das Größte der ersten drei Elemente sein. Die Chance dafür ist [math]\displaystyle{ \frac{1}{3} }[/math].

  • Mathematischer Ansatz (Indikatorvariablen):

Wir definieren einen „Schalter“ (die Indikatorvariable) [math]\displaystyle{ X_i }[/math]. Er ist [math]\displaystyle{ 1 }[/math], wenn ein neues Maximum an Stelle [math]\displaystyle{ i }[/math] gefunden wird, sonst [math]\displaystyle{ 0 }[/math]. Die Wahrscheinlichkeit dafür ist [math]\displaystyle{ P(X_i = 1) = \frac{1}{i} }[/math].

Um die durchschnittliche Gesamtzahl der Updates zu finden, addieren wir einfach diese Wahrscheinlichkeiten auf:

[math]\displaystyle{ E[X] = \sum_{i=1}^{n} \frac{1}{i} = 1 + \frac{1}{2} + \frac{1}{3} + \frac{1}{4} + \dots + \frac{1}{n} }[/math]

Diese spezielle Summe nennt man in der Mathematik die Harmonische Reihe ([math]\displaystyle{ H_n }[/math]). Sie wächst extrem langsam. Für große Zahlen [math]\displaystyle{ n }[/math] entspricht sie in etwa dem natürlichen Logarithmus ([math]\displaystyle{ \ln(n) }[/math]).

Erkenntnis für Quicksort: Obwohl das Array [math]\displaystyle{ n }[/math] Elemente hat, wird die Update-Operation im Durchschnitt nur logarithmisch oft ([math]\displaystyle{ \mathcal{O}(\log n) }[/math]) ausgeführt. Genau dieses stochastische Prinzip (das Aufsummieren von Einzelwahrscheinlichkeiten, die zu einem Logarithmus führen) ist der Schlüssel, um später selbst zu beweisen, dass Quicksort im Average-Case bei [math]\displaystyle{ \mathcal{O}(n \log n) }[/math] liegt.


Beispiel 3: Laufzeitanalyse von Insertion Sort

Nun wenden wir unser Wissen auf das Sortierverfahren Insertion Sort (Sortieren durch Einfügen) an.

public void sortierenEinfuegen_MC() {
    int v, i, j;
    for (i = 1; i < zSortfeld.length; i++) {
        c++; // Zähler für Vergleiche
        if (zSortfeld[i] < zSortfeld[i - 1]) {
            v = zSortfeld[i];
            j = i;
            do {
                zSortfeld[j] = zSortfeld[j - 1];
                j--;
            } while ((j > 0) && (zSortfeld[j - 1] > v));
            zSortfeld[j] = v; 
        }
        printAnalyse(i);
    }
}

Wir analysieren eine Liste mit [math]\displaystyle{ n }[/math] Elementen. Den Zeitverbrauch für einen einzelnen Vergleich nennen wir [math]\displaystyle{ c }[/math].

1. Best-Case-Szenario (Bester Fall)

Die Liste ist bereits perfekt aufsteigend sortiert (z. B. [0, 2, 3, 5, 7]).

  • Verhalten: Da jedes Element schon größer als sein Vorgänger ist, ist die if-Bedingung immer falsch. Die innere do-while-Schleife wird ignoriert.
  • Aufwand: Wir machen exakt einen Vergleich pro Element in der äußeren Schleife. Das ergibt [math]\displaystyle{ c \cdot (n - 1) }[/math] Vergleiche.
  • Komplexität: Der Aufwand wächst wie eine Gerade (linear). Die Komplexität ist [math]\displaystyle{ \mathcal{O}(n) }[/math].

2. Worst-Case-Szenario (Schlechtester Fall)

Die Liste ist exakt falsch herum sortiert (z. B. [7, 5, 3, 2, 0]).

  • Verhalten: Die if-Bedingung ist immer wahr. Jedes Element muss durch die innere Schleife komplett bis an den Anfang der Liste getauscht werden.
  • Aufwand: Beim 1. Durchlauf machen wir 1 Vergleich. Beim 2. Durchlauf 2 Vergleiche. Beim letzten Durchlauf [math]\displaystyle{ n-1 }[/math] Vergleiche. Wir müssen also wieder addieren: [math]\displaystyle{ 1 + 2 + 3 + \dots + (n-1) }[/math].

Wie bei der linearen Suche hilft uns hier die Gaußsche Summenformel: [math]\displaystyle{ c \cdot \frac{(n - 1) \cdot n}{2} = \frac{c}{2}n^2 - \frac{c}{2}n }[/math]

Quadratisches Wachstum
  • Komplexität: Für extrem große [math]\displaystyle{ n }[/math] dominiert das [math]\displaystyle{ n^2 }[/math] alles andere. Wir ignorieren den Faktor [math]\displaystyle{ \frac{c}{2} }[/math] und den kleineren Teil [math]\displaystyle{ n }[/math]. Das Wachstum ist quadratisch, also [math]\displaystyle{ \mathcal{O}(n^2) }[/math].

3. Average-Case-Szenario (Durchschnittlicher Fall)

Die Werte im Array sind völlig zufällig verteilt.

  • Verhalten: Im Durchschnitt müssen wir ein Element nicht bis ganz an den Anfang schieben (wie im Worst-Case), sondern nur bis zur Hälfte des bisher sortierten Bereichs.
  • Aufwand: Die Anzahl der Vergleiche halbiert sich grob im Vergleich zum Worst-Case. Der Aufwand liegt bei ca. [math]\displaystyle{ \frac{1}{2} \cdot \frac{c}{2}n^2 }[/math].
  • Komplexität: Wie wir gelernt haben, ignoriert die Groß-O-Notation konstante Brüche (wie [math]\displaystyle{ \frac{1}{4} }[/math]). Es bleibt bei der höchsten Potenz: Das Wachstum ist und bleibt quadratisch. Die Komplexität lautet also weiterhin [math]\displaystyle{ \mathcal{O}(n^2) }[/math].