Laufzeitanalyse: Unterschied zwischen den Versionen

Aus FLBK-Wiki
Zur Navigation springen Zur Suche springen
Keine Bearbeitungszusammenfassung
Keine Bearbeitungszusammenfassung
Zeile 1: Zeile 1:
== Einführung ==
== Einführung ==
[[Datei:Laufzeitanalyse asymptotisch.png|mini]]
[[Datei:Laufzeitanalyse asymptotisch.png|mini]]
Die Laufzeitanalyse betrachtet die Zeitkomplexität eines [[Algorithmus]]. Da die Zeit zur Ausführung eines Algorithmuses von der Hardwareausstattung des Zielsystems abhängt, wird für eine allgemeine Betrachtung die Anzahl der [[Anweisung|Befehle]] analysiert, die ein Algorithmus zur Lösung eines gegebenen Problems benötigt. Die Lösung eines Problems mit Hilfe eines gegebenen Algorithmus ist abhängig von der Eingabe.  Die Eingabe wird auch als Problemgröße bezeichnet.
Die Laufzeitanalyse betrachtet 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 [[Anweisung|Befehle]] (Operationen) analysiert, die ein Algorithmus zur Lösung eines Problems benötigt. Diese Anzahl ist abhängig von der Eingabe, deren Umfang auch als '''Problemgröße''' (oft <math>n</math>) bezeichnet wird.


Die Analyse für kleine Eingabemengen ist häufig nicht so bedeutsam, weil für diesen Fall auch schwach konzipierte [[Algorithmus|Algorithmen]] befriedigende Ergebnisse liefern. Wichtiger sind die Betrachtungen für sehr große Problemgrößen und einer ungünstigen Anordnung der Werte (worst-case). Dabei wird von der asymptotischen [[Programmausführung|Laufzeit]] gesprochen und meint damit, in Anlehnung an eine Asymptote, das Zeitverhalten des Algorithmus für eine potenziell unendlich große Eingabemenge. Soll z.B. eine Folge von Zahlen [[Sortieren|sortiert]] werden, dann betrachtet man eine theoretisch unendliche lange Folge von Zahlen.
Für kleine Eingabemengen ist die Analyse häufig von geringerer Bedeutung, da auch ineffiziente [[Algorithmus|Algorithmen]] hier oft befriedigend schnelle Ergebnisse liefern. Entscheidend ist die Betrachtung für sehr große Problemgrößen, insbesondere unter ungünstigen Startbedingungen (Worst-Case). 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]]).


Lässt sich ein Algorithmus mathematisch durch die Funktion beschreiben, so gilt: <math>f(x)=1/x+x</math>
Lässt sich die Laufzeit eines Algorithmus mathematisch durch eine Funktion beschreiben, wie beispielsweise:
<math>f(x) = \frac{1}{x} + x</math>


Die rot dargestellte Funktion<math> \color{Red}{f(x) = 1/x + x} </math>nähert sich für große x der grün dargestellte Asymptote <math> \color{green}{g(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 n der Eingabe angegeben und für immer größer werdende n asymptotisch unter Verwendung der Landau-Notation(Groß-O-Notation) abgeschätzt.  
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.


In der Praxis ist die Laufzeitanalyse interessant, weil hierdurch entschieden werden kann, ob ein Programm bei anwachsender Eingabemenge wirtschaftlich betrieben werden kann.
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.
Abgrenzung
Abgrenzend kann gesagt werden, dass die Laufzeitanalyse folgendes nicht betrachtet:


Den Zeitaufwand eines implementierten Algorithmus auf einem bestimmten Computer für eine konkrete endliche Eingabemenge.  
=== Abgrenzung ===
Es ist wichtig zu betonen, was die asymptotische Laufzeitanalyse '''nicht''' leistet:
Sie misst nicht den tatsächlichen Zeitaufwand (in Sekunden) eines implementierten Algorithmus auf einem spezifischen Computer für eine konkrete, endliche Eingabemenge.


== Szenarien ==
== Szenarien ==
Man unterscheidet die folgenden Varianten zur Laufzeitabschätzung:
Man unterscheidet bei der Laufzeitabschätzung klassischerweise drei Varianten:


* Das '''worst-case-szenario''' (engl. schlechtester Fall) beschreibt die Problemgröße, die zu einer maximal langen Durchlaufzeit eines Algorithmus führt und entspricht der oberen Schranke zur Lösung eines Problems.
* Das '''Worst-Case-Szenario''' (schlechtester Fall): Beschreibt die Startanordnung der Daten, die zu einer maximalen Durchlaufzeit des Algorithmus führt. Dies entspricht der oberen Schranke des Problems.
* Das '''average-case-Szenario''' (engl. durchschnittlicher Fall) beschreibt die Problemgröße die zu einer mittleren Durchlaufzeit eines Algorithmus führt.
* Das '''Average-Case-Szenario''' (durchschnittlicher Fall): Beschreibt die Problemgröße und Datenanordnung, die zu einer mittleren (erwarteten) Durchlaufzeit führt.
* Das '''best-case-Szenario''' (engl. bester Fall)   beschreibt die Problemgröße die zu einer minimalen Durchlaufzeit eines Algorithmus führt. Dies entspricht der untere Schranke zur Lösung eines Problems.
* Das '''Best-Case-Szenario''' (bester Fall): Beschreibt die Datenanordnung, die zu einer minimalen Durchlaufzeit führt (z. B. eine bereits sortierte Liste). Dies entspricht der unteren Schranke.
 
== Beispiel: Laufzeitanalyse von Insertion Sort ==
Im Folgenden wird die Laufzeitanalyse beispielhaft anhand des [[Insertion-sort|Insertion Sorts]] 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]].


== Beispiel ==
Im Folgenden wird die Laufzeitanalyse anhand des [[Insertion-sort|Insertionsorts]] praktisch durchgeführt. Wir betrachten den Insertionsort anhand einer möglichen Implementierung in der Programmiersprache [[Java]]. Der [[Algorithmus]] arbeitet mit einem [[Array]] als Datenstruktur und verwendet als [[Kontrollstruktur|Kontrollstrukturen]] [[Verzweigung|Verzweigungen]] und [[Schleife|Schleifen]].
<syntaxhighlight lang="java">
<syntaxhighlight lang="java">
public void sortierenEinfuegen_MC() {
public void sortierenEinfuegen_MC() {
Zeile 45: Zeile 47:
</syntaxhighlight>
</syntaxhighlight>


Eine sortierte Liste in umgekehrter Reihenfolge ist das Worst Case Szenario für den InsertionSort:
Ein Worst-Case-Szenario für den Insertion Sort ist eine Liste, die in genau umgekehrter (absteigender) Reihenfolge vorliegt, beispielsweise:
<code>[11, 7, 5, 3, 2, 0]</code>


[11,7,5,3,2,0]
Wir betrachten zunächst nur den Zeitverbrauch für die durchzuführenden Vergleichsoperationen. Wir bezeichnen diesen konstanten Zeitverbrauch für einen einzelnen Vergleich allgemein mit <math>c</math>. Die Anzahl der zu sortierenden Elemente sei <math>n</math>.


Wir betrachten zunächst nur den Zeitverbrauch für die durchzuführenden Vergleichsoperationen. Diesen Zeitverbrauchen kennen wir nicht und geben ihn allgemein durch die Konstante c an. Die Anzahl der zu sortierenden Elemente entspricht der Variablen n.  
* Beim ersten Durchlauf der äußeren [[Schleife]] vergleichen wir nur die 7 mit der 11. Aufwand: <math>c \cdot 1</math>. Die 7 wird einsortiert.
* Beim zweiten Durchlauf müssen wir das nächste Element mit den beiden bereits sortierten Elementen 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>.


Beim ersten äußeren [[Schleife]]ndurchlauf gilt für den Zeitaufwand c*1. Denn wir müssen nur die Zahl 7 mit der Zahl 11 vergleichen. 7 wird einsortiert und es bildet sich der bereits sortierte Teil (siehe [[Insertion-sort|InsertionSort]]) der Datenstruktur.  Beim zweiten Durchlauf der äußeren [[Schleife]] benötigen wir zwei Vergleiche, c * 2. Beim dritte Mal, c * 3 bis zum letzten Mal, wenn <math> c * n-1 </math>. Es gilt also:
Der Gesamtaufwand für die Vergleiche lautet also:
 
<math>c \cdot (1 + 2 + 3 + \dots + (n - 1))</math>
<math>c⋅*(1+2+3++(n−1))</math>


[[Datei:Asymptote an quadratische Funktion2.png|mini]]
[[Datei:Asymptote an quadratische Funktion2.png|mini]]
Hierbei handelt es sich um eine [[Arithmetische-reihe|arithmetische Reihe]], mit der Ausnahme, dass sie bis zu n-1 anstatt n ansteigt. Unter Verwendung unserer Formel für arithmetische Reihen, erhalten wir:
Hierbei handelt es sich um eine [[Arithmetische-reihe|arithmetische Reihe]] (angelehnt an den Gaußschen Summensatz), die bis <math>n - 1</math> läuft. Unter Verwendung der entsprechenden Summenformel erhalten wir:
 
<math>c⋅*(n−1+1)*((n−1)/2)=c*n​2​​/2 − c*n/2</math>


Für sehr große n können  wir den niederwertigen Term n/2 und die konstanten Faktoren c und  1/2 verwerfen, so dass gilt:
<math>c \cdot \frac{(n - 1) \cdot n}{2} = \frac{c}{2}n^2 - \frac{c}{2}n</math>


Für sehr große n verhält sich <math> C(n)</math> asymptotisch wie n2. Oder anders ausgedrückt <math> C(n)</math> liegt in der Klasse <math>O(n2)</math>.
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>. Daraus folgt:


Und wie beeinflussen die anderen Operationen wie Lesen und Schreiben, die zum Vertauschen der Werte benötigt werden die Zeitkomplexität? Für das Vertauschen der Werte würde nur ein konstanter Aufwand je Vergleich hinzukommen. Wir nennen ihn v und passen unsere Funktion entsprechend an:
Für sehr große <math>n</math> verhält sich der Aufwand <math>C(n)</math> asymptotisch wie <math>n^2</math>. Anders ausgedrückt: <math>C(n)</math> liegt in der Komplexitätsklasse <math>\mathcal{O}(n^2)</math>.


<math>(v+c) * n​2​​/2 − (v+c)* n/2</math>
'''Einfluss weiterer Operationen (Lesen/Schreiben):'''
Wie beeinflussen Lese- und Schreiboperationen (das Verschieben der Werte) 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 Funktion lautet dann:


Und wieder gilt, für sehr große n können wir den niederwertigen Term <math>(v+c)* n/2 </math>und die konstanten Faktoren v,c und 1/2 verwerfen, so dass immer noch gilt:
<math>\frac{v+c}{2}n^2 - \frac{v+c}{2}n</math>


Für sehr große n verhält sich <math>C(n) </math>asymptotisch wie n2. Oder anders ausgedrückt <math>C(n) </math>liegt in der Klasse <math>O(n2)</math>.
Auch hier gilt: Für sehr große <math>n</math> können der niederwertige Term <math>\frac{v+c}{2}n</math> und der konstante Faktor <math>\frac{v+c}{2}</math> ignoriert werden. Es bleibt bei der Erkenntnis:
Das Laufzeitverhalten <math>C(n)</math> liegt asymptotisch weiterhin 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 14. April 2026, 09:13 Uhr

Einführung

Die Laufzeitanalyse betrachtet 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 Befehle (Operationen) analysiert, die ein Algorithmus zur Lösung eines Problems benötigt. Diese Anzahl ist abhängig von der Eingabe, deren Umfang auch als Problemgröße (oft [math]\displaystyle{ n }[/math]) bezeichnet wird.

Für kleine Eingabemengen ist die Analyse häufig von geringerer Bedeutung, da auch ineffiziente Algorithmen hier oft befriedigend schnelle Ergebnisse liefern. Entscheidend ist die Betrachtung für sehr große Problemgrößen, insbesondere unter ungünstigen Startbedingungen (Worst-Case). Hier spricht man von der asymptotischen 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).

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 in Abhängigkeit von der Länge [math]\displaystyle{ n }[/math] der Eingabe angegeben und für immer größer werdende [math]\displaystyle{ n }[/math] asymptotisch unter Verwendung der Landau-Notation (Groß-O-Notation) abgeschätzt.

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.

Abgrenzung

Es ist wichtig zu betonen, was die asymptotische Laufzeitanalyse nicht leistet: Sie misst nicht den tatsächlichen Zeitaufwand (in Sekunden) eines implementierten Algorithmus auf einem spezifischen Computer für eine konkrete, endliche Eingabemenge.

Szenarien

Man unterscheidet bei der Laufzeitabschätzung klassischerweise drei Varianten:

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

Beispiel: Laufzeitanalyse von Insertion Sort

Im Folgenden wird die Laufzeitanalyse beispielhaft anhand des Insertion Sorts durchgeführt. Wir betrachten eine mögliche Implementierung in der Programmiersprache Java. Der Algorithmus arbeitet mit einem Array als Datenstruktur und verwendet als Kontrollstrukturen Verzweigungen und Schleifen.

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);
    }
}

Ein Worst-Case-Szenario für den Insertion Sort ist eine Liste, die in genau umgekehrter (absteigender) Reihenfolge vorliegt, beispielsweise: [11, 7, 5, 3, 2, 0]

Wir betrachten zunächst nur den Zeitverbrauch für die durchzuführenden Vergleichsoperationen. Wir bezeichnen diesen konstanten Zeitverbrauch für einen einzelnen Vergleich allgemein mit [math]\displaystyle{ c }[/math]. Die Anzahl der zu sortierenden Elemente sei [math]\displaystyle{ n }[/math].

  • Beim ersten Durchlauf der äußeren Schleife vergleichen wir nur die 7 mit der 11. Aufwand: [math]\displaystyle{ c \cdot 1 }[/math]. Die 7 wird einsortiert.
  • Beim zweiten Durchlauf müssen wir das nächste Element mit den beiden bereits sortierten Elementen vergleichen. Aufwand: [math]\displaystyle{ c \cdot 2 }[/math].
  • Dies setzt sich fort bis zum letzten Durchlauf, der [math]\displaystyle{ n - 1 }[/math] Vergleiche erfordert. Aufwand: [math]\displaystyle{ c \cdot (n - 1) }[/math].

Der Gesamtaufwand für die Vergleiche lautet also: [math]\displaystyle{ c \cdot (1 + 2 + 3 + \dots + (n - 1)) }[/math]

Hierbei handelt es sich um eine arithmetische Reihe (angelehnt an den Gaußschen Summensatz), die bis [math]\displaystyle{ n - 1 }[/math] läuft. Unter Verwendung der entsprechenden Summenformel erhalten wir:

[math]\displaystyle{ c \cdot \frac{(n - 1) \cdot n}{2} = \frac{c}{2}n^2 - \frac{c}{2}n }[/math]

Für die asymptotische Betrachtung extrem großer Werte von [math]\displaystyle{ n }[/math] verwerfen wir den langsamer wachsenden (niederwertigen) Term [math]\displaystyle{ \frac{c}{2}n }[/math] sowie die Konstante [math]\displaystyle{ \frac{c}{2} }[/math]. Daraus folgt:

Für sehr große [math]\displaystyle{ n }[/math] verhält sich der Aufwand [math]\displaystyle{ C(n) }[/math] asymptotisch wie [math]\displaystyle{ n^2 }[/math]. Anders ausgedrückt: [math]\displaystyle{ C(n) }[/math] liegt in der Komplexitätsklasse [math]\displaystyle{ \mathcal{O}(n^2) }[/math].

Einfluss weiterer Operationen (Lesen/Schreiben): Wie beeinflussen Lese- und Schreiboperationen (das Verschieben der Werte) die Zeitkomplexität? Für das Verschieben käme lediglich ein konstanter Aufwand je Vergleich hinzu. Nennen wir diesen Aufwand [math]\displaystyle{ v }[/math]. Die angepasste Funktion lautet dann:

[math]\displaystyle{ \frac{v+c}{2}n^2 - \frac{v+c}{2}n }[/math]

Auch hier gilt: Für sehr große [math]\displaystyle{ n }[/math] können der niederwertige Term [math]\displaystyle{ \frac{v+c}{2}n }[/math] und der konstante Faktor [math]\displaystyle{ \frac{v+c}{2} }[/math] ignoriert werden. Es bleibt bei der Erkenntnis: Das Laufzeitverhalten [math]\displaystyle{ C(n) }[/math] liegt asymptotisch weiterhin in der Klasse [math]\displaystyle{ \mathcal{O}(n^2) }[/math].