Kurs:Algorithmen und Datenstrukturen/Kapitel 2/BubbleSort: Unterschied zwischen den Versionen

Aus testwiki
Zur Navigation springen Zur Suche springen
imported>Hannoman
K Average-Case Analyse: Formel-Alignment
 
(kein Unterschied)

Aktuelle Version vom 17. September 2015, 17:57 Uhr

BubbleSort

Herleitung

Ausgehend von der Definition eines sortierten Arrays

aiai+1,i=1n1

(wir verwenden hier das "" fuer eine allgemeine Ordnungsrelation), koennen wir einen ersten, naiven Algorithmus zum sortieren eines Arrays konstruieren:

Wir durchlaufen das Array und vergleichen dabei jedes Element ai (beginnend bei i = 0) mit dem darauffolgenden Element ai+1. Sind die zwei Elemente nicht sortiert, so werden sie vertauscht. Wir wiederholen diesen Vorgang bis das Array sortiert ist.


Implementierung in Oberon

In Oberon würde eine PROCEDURE, welche folgenden Algorithmus für Daten des Typs INTEGER implementiert, wie folgt aussehen:

 1. PROCEDURE BubbleSort* ( VAR a : ARRAY OF INTEGER ; n : INTEGER );
 2.     VAR
 3.         i : INTEGER;
 4.         temp, swaps : INTEGER;
 5.     BEGIN

            (* Arraylänge überprüfen *)
 6.         IF n > LEN(a) THEN
 7.             Out.String("Error: n groesser als Arraylaenge!"); Out.Ln;
 8.             RETURN;
 9.         END;

            (* Wiederhole bis alles sortiert ist... *)
10.         REPEAT
11.             swaps := 0;

                (* Für jedes Element... *)
12.             FOR i := 0 TO n-2 DO

                    (* Vergleiche und Vertausche mit Nachbar *)
13.                 IF ~(a[i] <= a[i+1]) THEN
14.                     temp := a[i]; a[i] := a[i+1]; a[i+1] := temp;
15.                     INC(swaps);
16.                 END

17.             END

18.         UNTIL swaps = 0;
19.     END BubbleSort;

Am Anfang unserer Prozedur (Zeile 6) überprüfen wir zuerst, ob n innerhalb der Grenzen des Arrays liegt. Die Schleife von Zeile 10 bis 18 ist die Hauptschleife unseren Algorithmus. Wir beachten, dass die Schleife in Zeile 12 von 0 bis n-2 geht, da die Array-Nummerierung in Oberon bei 0 anfängt.

Die Variable swaps gibt an, wieviele Vertauschungen im Durchgang vorgenommen wurden. Wurde nichts vertauscht, so ist unser Array gemäß Definition sortiert und wir können aufhören (Zeile 14).

Funktioniert sowas überhaupt?

Wir können unsere Sortierprozedur in ein Modul namens Sorting packen und mit ein paar Zahlen testen:

MODULE Sorting;

    IMPORT
        In, Out;
        
    CONST
        MaxN = 100;
        
    PROCEDURE BubbleSort* ( VAR a : ARRAY OF INTEGER ; n : INTEGER );
        ...
        END BubbleSort;
        
    PROCEDURE Go*;
        VAR
            a : ARRAY MaxN OF INTEGER;
            i, n : INTEGER;
        BEGIN
            In.Open; In.Int(n);
            FOR i := 0 TO n-1 DO In.Int(a[i]); END;
            BubbleSort(a,n);
            FOR i := 0 TO n-1 DO Out.Int(a[i],10); END;
            Out.Ln;
        END Go;

BEGIN
    Out.Open;
END Sorting.

Sorting.Go 5 3 4 2 5 1 ~

Und sie funktioniert tatsächlich. Die übergebenen Zahlen werden in aufsteigender Reihenfolge sortiert und ausgegeben. Funktioniert das aber auch auf jeden Fall?

Was sicher ist, ist dass im ersten Durchlauf das (nach unserer Relation) größte Element zur letzten Stelle des Arrays verschoben wird. Dies kann man damit zeigen, dass, falls ai das groesste Element ist, aiai+1 immer versagen wird und das Element an der Stelle ai+1 vertauscht wird. Das gleiche spielt sich dann ab, bis das größte Element am Ende des Arrays bei an sitzt. Aus dieser Position fällt es auch nie wieder heraus, denn, ist an das größte Element, so wird an1an fuer jede Besetzung von an1 gelten.

Sitzt das größte Element nach dem ersten Durchlauf am Ende des Arrays, so kann das zweitgrößte Element nach dem gleichen Prinzip ungehindert zur zweitletzten Stelle rutschen, und so weiter bis alle Elemente sortiert sind.

Wegen dieses Hinaufrutschens der Elemente, ähnlich des Aufsteigens von Blasen in einer Flüssigkeit, heißt dieser Algorithmus in der Literatur auch BubbleSort.

Was kostet das?

Worst-Case Analyse

Im ersten Durchlauf führt BubbleSort n1 Vergleiche und, im schlimmsten Fall n1 Vertauschungen aus.

Im k-ten Durchlauf werden nk Vergleiche benötigt.

Average-Case Analyse

Die average-case Analyse ist wesentlich komplizierter. Wir betrachten hierzu das Schicksal eines einzelnen Elements ak. Um an der richtigen Stelle im Array zu landen, wird ak von allen Elementen, welche größer sind als es selber und links von ihm liegen, "überholt" werden. Zudem muss es selber alle Elemente, welche kleiner sind als es selber und rechts von ihm liegen, selber überholen.

Für ein Element an der Stelle k, welches eigentlich an die Stelle i gehören würde, ist die durchschnittliche Anzahl Elemente, welche größer wären, aber links von ihm liegen

l(i,k)=nin1(k1)

Anders ausgedrückt: wählen wir (k1) Elemente aus, die mit einer Wahrscheinlichkeit nin1 kleiner sind als das i-te Element. Analog dazu sitzen im Durchschnitt rechts von unserem Element

r(i,k)=i1n1(nk)

Elemente, die kleiner wären als unser Element. Die zu erwartende Anzahl Vertauschungen für ein Element, welches an die i-te Stelle gehört und mit Wahrscheinlichkeit 1n an der Stelle k sitzt, ist somit

V(i)=1nk=1n[l(i,k)+r(i,k)]=1nk=1n[nin1(k1)+i1n1(nk)]==n12

Die durchschnittliche Anzahl Vertauschungen ist somit unabhängig von der eigentlichen Stelle des Elements. Bei n Elementen haben wir somit im Durchschnitt

12n(n1)2=n(n1)4

Vertauschungen. Der zusätzliche Faktor 1/2 kommt daher, dass jede Vertauschung beiden vertauschten Elementen zugute kommt. Die Anzahl Vertauschungen im average-case ist somit auch in der Größenordnung 𝒪(n2).

Um die Anzahl der durchschnittlich zu erwartenden Durchläufe zu berechnen, müssen wir beachten, dass pro Durchlauf eines der größeren Elemente links weggeschoben wird. Die kleineren Elemente rechts können in einem einzigen Durchlauf übersprungen werden. Dies geschieht aber selten in einem einzigen Zug: es können rechts größere Elemente dazwischenliegen, welche unser Element ausbremsen. Es bestimmt also dasjenige Element, welches die meisten Durchläufe benötigt, wieviele Durchläufe im Schnitt notwendig sind. Dieses Element ist dasjenige, für das ki maximal ist -- also das Element, das am weitesten rechts von der ihm zugehörigen Stelle steht.

Wir betrachten die Wahrscheinlichkeit Pr(d𝗆𝖺𝗑), dass in einer Permutation von n Elementen kein Element weiter als d𝗆𝖺𝗑 rechts von seiner angestammten Position steht.

Für d𝗆𝖺𝗑=n ist die Lösung trivialerweise 1, denn es kann beim besten Willen kein Element n Schritte von seinem angestandenen Platz stehen, weil das Array gerade mal n Elemente hat:

Pr(n)=1

Die Lösung für d𝗆𝖺𝗑=n1 ist ähnlich trivial: einen Abstand von n1 ist nur dann möglich, wenn das kleinste Element -- nennen wir es e1 an der letzten Stelle im Array an steht. Wir haben also nur (n1) von n Möglichkeiten, e1 irgendwo unterzubringen:

Pr(n1)=n1n

Für d𝗆𝖺𝗑=n2 ist die Lage ähnlich: wir können e1 weder an der Stelle an noch an1 platzieren und e2 nicht an der Stelle an hinlegen. Wir haben also für e1 (n2) von n und für e2 nur (n2) von (n1) (wir haben ja schon e1 dazwischen platziert) Möglichkeiten:

Pr(n2)=(n2n)(n2n1)

Folgt man dieser Logik weiter, erhalten wir die Formel

Pr(nk)=i=0k1(nkni)=(nk)k(nk)!n!

Die Wahrscheinlichkeit nun, dass ein unsortiertes Array der Länge n einen maximalen rechten Abstand von genau d𝗆𝖺𝗑=nk enthält ist

Pr(nk+1)Pr(nk)=(nk+1)k1(nk+1)!n!(nk)k(nk)!n!=1n!((nk+1)k1(nk+1)!(nk)k(nk)!)=(nk)!n!((nk+1)k(nk)k)
  • Durchschnittstiefe = k=0n(Pr(nk+1)Pr(nk))(nk)
  • gibts dafür eine elegante, geschlossene Form? Sieht aus wie eine Binomialverteilung...