Skip to main content

2 posts tagged with "algorithms"

View All Tags

· 6 min read
info

Oft wird der Nachweis, dass das Halteproblem nicht entscheidbar ist, in der Fachliteratur (Schöning [📖Sch08, p. 119 f.], Asteroth und Baier [📖BA02, p. 106 f.], Sipser [📖Sip12, p. 216 f.]) mithilfe einer Turingmaschine und einem Widerspruchsbeweis gezeigt, in etwa:

Angenommen,

K={wΣw=T,T stoppt bei Eingabe w}K' = \{w \in \Sigma^* | w = \langle T \rangle, \text{$T$ stoppt bei Eingabe $w$}\}

ist entscheidbar.

Sei TT' ist die Turingmaschine, die KK' entscheidet.
Sei w=Tw = \langle T \rangle die Codierung3 einer Turingmaschine, die wie folgt arbeitet:

  • TT simuliert das Verhalten von TT' bei der Eingabe ww:
    • TT stoppt, wenn TT' die Eingabe ww verwirft (wKw \notin K')
    • TT stoppt nicht, wenn TT' die Eingabe ww akzeptiert (wKw \in K')

Damit folgt: TT stoppt bei Eingabe ww \Leftrightarrow TT' verwirft ww \Leftrightarrow wKw \notin K' \Leftrightarrow TT stoppt nicht

Widerspruch!

Bei Vossen und Witt wird der Beweis anhand unentscheidbarer Mengen gezeigt - hierzu wird in dem Abschnitt über Berechenbarkeit die Abzählbarkeit von Turingmaschinen und analog dazu die Standardnummerierung von P\mathcal{P}1 als theoretischer Unterbau festgelegt, um darauf aufbauend den Nachweis der Semi-Entscheidbarkeit und Unentscheidbarkeit zu führen. Im Folgenden findet sich die Beweisführung nach Vossen und Witt [📖VW16, p. 360 ff.] mit ergänzenden Kommentaren.

Die Standardnummerierung ist festgelegt als:

(N0,P,φ)(\mathbb{N}_0, \mathcal{P}, \varphi)
  • N0\mathbb{N}_0: Die Menge aller Turingmaschinen (ein iN0i \in \mathbb{N}_0 repräsentiert die Gödelnummer einer Turingmaschine), wobei T\mathcal{T} die Menge aller normierten Turingmaschinen ist. Die Abbildung h:N0Th: \mathbb{N}_0 \rightarrow \mathcal{T} ist definiert durch h(i)={T,falls p(τ(T))=iTω,sonsth(i) = \begin{cases} T, & \text{falls } p(\tau(T)) = i\\ T_{\omega}, & \text{sonst} \end{cases} bildet also auf eine natürliche Zahl eine Turingmaschine ab.
  • P\mathcal{P}: Die Menge aller berechenbaren Funktionen.
  • φ:N0P\varphi: \mathbb{N}_0 \rightarrow \mathcal{P} ist eine Abzählung aller berechenbaren Funktionen. φ\varphi ist festgelegt durch φ(i)=ffh(i)=f\varphi(i) = f \Leftrightarrow f_{h(i)} = f Die berechenbare Funktion φ(i)\varphi(i) wird also durch die ii-te Turingmaschine berechnet.

Die Standardnummerierung ist im Folgenden die Grundlage für die Formulierung von Entscheidbarkeitsproblemen bei Mengen.

Das spezielle Halteproblem

Zunächst wird das spezielle Halteproblem (auch Selbstanwendbarkeitsproblem) betrachtet.
Vossen und Witt definieren es über folgende Menge:

K={iiDef(φi)}K = \{i \mid i \in \text{Def}(\varphi_i)\}

KK enthält alle Nummern von Turingmaschinen, die, angewendet auf sich selbst, anhalten:

  • ii ist die Codierung einer Turingmaschine.
  • φi\varphi_i ist die berechenbare Funktion, die von der Turingmaschine p(τ(T))=ip(\tau(T)) = i berechnet wird.
  • Def(φi)\text{Def}(\varphi_i) ist der Definitionsbereich der Funktion φi\varphi_i, also alle erlaubten Eingaben.
  • iDef(φi)i \in \text{Def}(\varphi_i) bedeutet: Die Codierung ii der Turingmaschine p(τ(T))=ip(\tau(T)) = i, die φi\varphi_i berechnet, ist eine erlaubte Eingabe.

Vossen und Witt definieren KK als die "Menge aller Turingmaschinen, die, [...] anhalten."
Das bedeutet, dass von einer entscheidbaren Menge ausgegangen wird:
Es wird angenommen, dass sowohl KK als auch K\overline{K} semi-entscheidbar sind und die charakteristische Funktion χK\chi_K von KK berechenbar ist.

Mit Satz 10.7 formulieren Vossen und Witt den Kern des Selbstanwendbarkeitsproblems, nämlich, dass KK zwar semi-entscheidbar, aber nicht entscheidbar ist:

  • KK ist rekursiv aufzählbar
  • KK ist nicht entscheidbar

Damit wird behauptet:

  • KK ist semi-entscheidbar, da rekursiv-aufzählbar.
    Wenn KK semi-entscheidbar ist, ist auch die semi-charakteristische Funktion von KK, χK\chi'_K, berechenbar.
    Wenn KK semi-entscheidbar und damit rekursiv aufzählbar ist, dann gilt äquivalent dazu: KK ist der Definitionsbereich einer berechenbaren Funktion g:N0N0g: \mathbb{N}_{0} \rightarrow \mathbb{N}_0 (s. Folgerung 10.2 [📖VW16, p. 356]). Es reicht also aus, eine solche Funktion anzugeben, um die Semi-Entscheidbarkeit und damit auch die rekursive Aufzählbarkeit nachzuweisen.
  • Wenn KK entscheidbar ist, dann ist auch die charakteristische Funktion χK\chi_K berechenbar: χK\chi_K gibt 11 aus, wenn die Turingmaschine TT angewendet auf i=p(τ(T))i = p(\tau(T)) anhält (also wenn φi(i)\varphi_i(i) berechnet wird); und ansonsten ist χK(i)=0\chi_K(i) = 0, wenn TT nicht anhält2.

Beweisführung

Der Beweis wird wie folgt geführt:

a) Vossen und Witt definieren die Funktion f:N0N0f: \mathbb{N}_0 \rightarrow \mathbb{N}_0 durch

f(i)=uφ(i,i)f(i) = u_{\varphi}(i, i)

Bei uφu_{\varphi} handelt es sich um die universelle Funktion von (N0,P,φ)(\mathbb{N}_0, \mathcal{P}, \varphi):

uφ:N0×N0N0definiert durchuφ(i,x)=φi(x)u_{\varphi} : \mathbb{N}_0 \times \mathbb{N}_0 \rightarrow \mathbb{N}_0 \quad \text{definiert durch} \quad u_{\varphi} (i, x) = \varphi_i(x)

uφ(i,j)u_{\varphi}(i, j) berechnet die ii-te berechenbare Funktion für die Eingabe jj.

Mit ff als berechenbare Funktion kann gezeigt werden:

iKiDef(φi)(Definition K)(i,i)Def(uφ)(Definition uφ)iDef(f)(Definition f)\begin{align} i \in K &\Leftrightarrow i \in \text{Def}(\varphi_i) \quad \text{(Definition $K$)}\\ &\Leftrightarrow (i, i) \in \text{Def}(u_{\varphi}) \quad \text{(Definition $u_{\varphi}$)}\\ &\Leftrightarrow i \in \text{Def}(f) \quad \text{(Definition $f$)} \end{align}

womit K=Def(f)K = \text{Def}(f) ist und damit KK rekursiv-aufzählbar und semi-entscheidbar ist.

b) Angenommen, KK ist entscheidbar. Dann ist χK\chi_K berechenbar.
Vossen und Witt definieren eine berechenbare (Hilfs-)Funktion

g:N0N0g: \mathbb{N}_0 \rightarrow \mathbb{N}_0

durch

g(x)={uφ(x,x)+1,falls χK(x)=10,falls χK(x)=0g(x) = \begin{cases} u_{\varphi}(x, x) + 1, & \text{falls }\chi_K(x) = 1\\ 0, & \text{falls }\chi_K(x) = 0 \end{cases}

gg ist total berechenbar: Wenn χK(x)=1\chi_K(x) = 1, dann ist xDef(φx)x \in \text{Def}(\varphi_x), und damit ist uφ(x,x)+1=φx(x)+1u_\varphi(x, x) + 1 = \varphi_x(x) + 1.

Da gg total berechenbar ist (gRPg \in \mathcal{R} \subset \mathcal{P}), muss es ein pN0p \in \mathbb{N}_0 geben mit g=φpg = \varphi_p. Es folgt

g(x)=φp(x)xN0g(x) = \varphi_p(x) \quad \forall x \in \mathbb{N}_0

Für g(p)g(p) werden nun zwei Fälle untersucht:

  • Fall 1: g(p)=0g(p) = 0

    g(p)=0χK(p)=0(Entscheidbarkeit K)pKpDef(φp)(g=φp)pDef(g)g(p)=(Widerspruch zu g total berechenbar)\begin{align} g(p) = 0 &\Leftrightarrow \chi_K(p) = 0 \quad \text{(Entscheidbarkeit $K$)}\\ & \Leftrightarrow p \notin K \\ & \Leftrightarrow p \notin \text{Def}(\varphi_p) \quad \text{($g = \varphi_p$)}\\ & \Leftrightarrow p \notin \text{Def}(g) \\ & \Leftrightarrow g(p) = \bot \quad \text{(Widerspruch zu $g$ total berechenbar)} \end{align}

    womit insgesamt die widersprüchliche Aussage folgt:

    g(p)=0g(p)=g(p) = 0 \Leftrightarrow g(p) = \bot
  • Fall 2: g(p)=uφ(p,p)+1g(p) = u_{\varphi}(p,p) + 1

    g(p)=uφ(p,p)+1(Definition g)=φp(p)+1(uφ(p,p)=φp(p))=g(p)+1(φp=g)\begin{align} g(p) &= u_{\varphi}(p, p) + 1 \quad \text{(Definition $g$)}\\ &= \varphi_p(p) + 1 \quad \text{($u_{\varphi}(p, p) = \varphi_p(p)$)}\\ &= g(p) + 1 \quad \text{($\varphi_p = g$)} \end{align}

    womit insgesamt die widersprüchliche Aussage folgt:

    g(p)=g(p)+1g(p) = g(p) + 1

Insgesamt wird durch den Beweis gezeigt: Die Anwendung von gg auf pp - also von gg auf sich selbst (wegen g=φp=fh(p),φp(p)=uφ(p,p)g = \varphi_p = f_{h(p)}, \varphi_p(p) = u_{\varphi}(p, p)) - führt in jedem Fall zu einem Widerspruch.
Damit ist gezeigt, dass χK\chi_K nicht berechenbar und folglich KK nicht entscheidbar ist.

info

Mit dem Wissen um die Unentscheidbarkeit und der Semi-Entscheidbarkeit von KK ist es nun möglich, eine Funktion ff anzugeben, mit der sich KK auf HH reduzieren lässt, so dass KfHK \leq_f H. Da KK unentscheidbar ist, ist auch HH unentscheidbar. Es muss gezeigt werden, dass auch HH semi-entscheidbar ist, da dies nicht allein aus der Reduktion von KK auf HH folgt (vgl. Satz 10.5 [📖VW16, p. 358] sowie Folgerung 10.3 [📖VW16, p. 359]). Der Beweis erfolgt analog zum Nachweis der rekursiven Aufzählbarkeit von KK mit Satz 10.8 [📖VW16, p. 363].


  1. Anstatt Gödelnummer wird im Folgenden auch einfach der Begriff Codierung verwendet, wobei streng genommen τ(T)\tau(T) die Codierung einer normierten Turingmaschine ist, bevor ihr eine Gödelnummer zugewiesen wird
  2. P\mathcal{P} bezeichnet die Menge der partiell-rekursiven Funktionen. Es gilt PRRP\mathcal{PR} \subset \mathcal{R} \subset \mathcal{P}, mit PR\mathcal{PR} als die Menge der primitiv-rekursiven Funktionen, die eine echte Teilmenge der total berechenbaren Funktionen R\mathcal{R} sind
  3. der Widerspruch liest sich hier bereits heraus.

· 14 min read

Donald Shell stellt 1959 in [📖She59] einen Sortieralgorithmus vor, der später nach ihm benannt wird: Shellsort.
Die in dem Algorithmus verwendete Sortiermethode ist auch bekannt als Sortieren mit abnehmenden Inkrementen [📖OW17b, p. 88], das Verfahren ist eine Variation von Insertion Sort:

[Shellsort] uses insertion sort on periodic subarrays of the input to produce a faster sorting algorithm. [📖CL22, p. 48]

In der vorliegenden Implementierung (siehe Listing 1) werden t=log2(n)t = log_2(n) Inkremente1 hth_t2 der Form n2i\lfloor {\frac{n}{2^i}} \rfloor verwendet, um lg(n)lg(n) hh-sortierte Folgen zu erzeugen. Im letzten Schritt sortiert der Algorithmus dann in h1h_1 die Schlüssel mit Abstand=11.

Die Effizienz des Sortierverfahrens ist stark abhängig von hh: So zeigt Knuth, dass O(n32)O(n^{\frac{3}{2}}) gilt, wenn für hh gilt: hs=2s+11h_s = 2^{s+1} - 1 mit 0s<t=lg(n)0 \leq s < t = \lfloor{lg(n)} \rfloor (vgl. [📖Knu97c, p. 91]3. In unserem Fall können wir von O(n2)O(n^2) ausgehen.

Anzahl der Aufrufe für Feldvergleiche

Gesucht ist die Anzahl der zwischen zwei Feldelementen gemachte Vergleiche für große randomisierte Felder. Wir vermuten hierfür einen Wert zwischen O(n lg n)O(n\ lg\ n) und O(n2)O(n^2),

Um die Aufrufe der Feldvergleiche zu zählen, kann wie in Listing 1 eine Zählvariable c3c3 eingesetzt wird, die jeden Aufruf über ein increment protokolliert.

Listing 1: Zur Protokollierung der Aufrufe können in dem Code Zählvariablen eingesetzt werden (c1, ... ,c4).
while (distanz > 0) {
c1++;
for (i = distanz; i < feld.length; i++) {
j = i - distanz;
c2++;
while (j >= 0) {
c3++;
if (feld[j] > feld[j + distanz]) {
c4++;
swap(feld, j, j+distanz);
j = j - distanz;
} else {
j =-1;
}
}
}
distanz = distanz / 2;
}

Zur Bestimmung von c3c3 können randomisierte Felder erzeugt werden, wie es der Test-Code in Listing 2 demonstriert. Dann wird überprüft, in welchen Bereichen sich c3c3 bewegt. Da jeder auf Vergleichsoperationen basierende Sortieralgorithmus zu der Laufzeitklasse Ω(n log n)\Omega(n\ log\ n) gehört, kann im Mittel c3n log nc3 \geq n\ log\ n angenommen werden:

Jedes allgemeine Sortierverfahren benötigt zum Sortieren von N verschiedenen Schlüsseln sowohl im schlechtesten Fall als auch im Mittel wenigstens Ω(N log N)\Omega(N\ log\ N) Schlüsselvergleiche.

[📖OW17b, p. 154]

Allerdings wird n1.3n^{1.3} ab n=982n=982 schneller wachsen als n log nn\ log\ n (siehe Abbildung 1), was in der Auswertung berücksichtigt werden muss. Sonst könnte man fälschlicherweise folgern, das in dem geg. Fall die Laufzeit im Durchschnitt n log nn\ log\ n beträgt.

Listing 2: Code zum Erzeugen randomisierter Felder zum Testen von Shellsort.
int epochs = 2000;
while(epochs-- >= 0) {
int[] tests = new int[]{10, 100, 1000, 100_000, 1_000_000};
java.util.Random r = new java.util.Random();
for (int i = 0; i < tests.length; i++) {
int l = tests[i];
int[] arr = new int[l];

for (int j = l; j > 0; j--) {
arr[l - j] = r.nextInt(l + 1);
}

ShellSort.sort(Arrays.copyOfRange(arr, 0, arr.length));
}
}
info

Mit einem Wert von 100100 für die Epochen und einer Feldgröße von 2.000.0002.000.000 liefert der Test in Listing 2 folgende Ausgabe:

nlgn<n1.3<x<n2:74nlgn < n^{1.3} < x < n^2: 74
nlgn<x<n1.3:26nlgn < x < n^{1.3}: 26

Figure 1 Ab n=982 wächst n(4/3) schneller al n log n.

Wegen Ω(n log n)\Omega(n\ log\ n) interessieren wir uns für die Intervalle:

  • n log nc3n1.3n\ log\ n \leq c3 \leq n^{1.3}

  • n log nn1.3c3n2n\ log\ n \leq n^{1.3} \leq c3 \leq n^2

Die Ausführung des Tests zeigt, dass die Aufrufanzahl von c3c3 bei großen nn unterhalb n1.3n^{1.3} oder zwischen n1.3n^{1.3} und n2n^2 liegt:

It appears that the time required to sort nn elements is proportional to n1.226n^{1.226}.4 [📖She59, p. 31]

Laufzeitanalyse

Für die nachfolgenden Betrachtungen sei eine Eingabegröße n=2pn = 2^p gegeben. Die Inkremente hh entsprechen der über die Implementierung vorgegebenen Folge hh mit

ht=n2,ht1=n4,,h1=1h_t = \frac{n}{2}, h_{t-1} = \frac{n}{4}, \dots, h_1 = 1

Für die Analyse ist die Shellsort-Implementierung in Listing 3 gegeben.

Listing 3: Implementierung des Shellsort-Algorithmus.
public static int[] sort(int[] arr) {

int n = arr.length;
int delta = n/2;
int min;
int j;

while (delta > 0) { // c1

for (int i = delta; i < arr.length; i++) {

// c2

min = arr[i];
j = i;

while (j - delta >= 0 && min < arr[j - delta]) {

// c3;

arr[j] = arr[j - delta];
j -= delta;
}
arr[j] = min;
}

delta = delta / 2;

}
return arr;
}

Im Folgenden betrachten wir die Anzahl für die im Code durch Kommentare markierten Stellen c1c1 in Zeile 8, c2c2 in Zeile 12 und c3c3 in Zeile 19.

Für eine erste worst-case-Analyse ist ein Feld der Länge 1616 gegeben, in absteigender Reihenfolge sortiert (s. Abbildung 2):

Figure 2 Die für die Laufzeitabschätzung verwendete Eingabefolge 16..1.

Ganz offensichtlich gilt für c3, dass es lg(n)lg(n)-mal aufgerufen wird5. c2 befindet sich im Block der durch die in Zeile 11 definierten Zählschleife.

Der Startwert für i ist in jedem Durchgang des Blocks c1c1 der aktuelle Wert von delta 6, und läuft jeweils bis n1n - 1.

In einem kompletten Durchlauf der Schleife entspricht die Anzahl der Aufrufe von c2c2 also

(n1)delta+1=ndelta(n - 1) - delta + 1 = n - delta

info

Hinweis: Für das Beispiel betrachten wir der Einfachheit halber Feldlängen der Form 2p2^p. Für den allgemeinen Fall gilt für die Anzahl der Aufrufe von c2c2:

i=1lg(n)nn2i=nlg(n)i=1lg(n)n2i\sum_{i=1}^{\lfloor lg(n) \rfloor} n - \lfloor \frac{n}{2^i} \rfloor = n * lg(n) - \sum_{i=1}^{\lfloor lg(n) \rfloor} \lfloor \frac{n}{2^i} \rfloor

Für die Gesamtzahl der Aufrufe von c2c2 ergibt sich somit unter Berücksichtigung von c1c1

i=1lg(n)nn2i1\sum_{i=1}^{lg(n)} n - \frac{n}{2^{i-1}}

was nach Auflösen

nlg(n)n+1n * lg(n) - n + 1

entspricht, und für unser Beispiel

16lg(16)16+1=4916 * lg(16) - 16 + 1 = 49

ist.

info

Die Aufrufzahlen für c2c2 für verschiedene nn. Schlüsselvergleiche finden erst später statt. Mit n>2156n > 2156 wächst O(n43)O(n^\frac{4}{3}) schneller als c2c2 und mit n>=982n >= 982 schneller als O(n log n)O(n\ log\ n).

nc2O(n1.1)O(n^{1.1})O(n1.3)O(n^{1.3})O(n log n)O(n\ log\ n)O(n2)O(n^2)
8179142464
1649213664256
3212945901601024
64321972223844096
12876920754889616.384
25617934451351204865.536
1024921720488192102401.048.576
215621.5654.645,2921.564,6923.875,844.648.336
215721.5764.647,6621.577,7023.888,364.652.649
215821.5864.650,0321.590,7023.900,884.656.964
..................
10.000120.00525.118158.489132.8771.0E8
100.0001.500.006316.2273.162.2771.660.9641.0E10
200.0003.200.006677.8497.786.4403.521.9284.0E10
500.0008.500.0071.857.2352.56..E79.465.7842.5E11
1.000.00018.000.0073.981.0716.30..E71.99..E71.0E12

In der while-Schleife in Zeile 17 findet die eigentliche Arbeit des Algorithmus statt: Es wird überprüft, ob deltadelta-entfernte Elemente in aufsteigender Reihenfolge sortiert angeordnet sind.

Ist das nicht der Fall, werden die Elemente an den Stellen jj und jdeltaj - delta ausgetauscht, bis die hh-Folge sortiert ist.
Für den ersten Durchgang des Algorithmus an dieser Stelle mit h4=8h_4 = 8 ergibt sich somit die in Abbildung 3 dargestellte Reihenfolge der Schlüssel:

Figure 3 Nach dem ersten Durchgang sind die Schlüssel in den Abständen h4 = 8 sortiert, es ergeben sich zwei sortierte Teilfolgen der Länge 8.

Die weiteren Durchgänge des Algorithmus sortieren das Feld entsprechend der Größe hh: Es sind danach jeweils Schlüssel mit den Abständen 44 (Abbildung 4 und 22 (Abbildung 5) sortiert; im letzten Schritt dann vollständig (Abbildung 6):

Figure 4 Die Sortierung für h3 = 4, es sind 4 Folgen deren Schlüssel jeweils den Abstand 4 zueinander haben.
Figure 5 Im vorletzten Sortierschritt sind 8 Folgen der Länge 2 sortiert.
Figure 6 Der letzte Durchgang des Algorithmus vergleicht Schlüssel mit Distanzfolgen der Länge 1, also direkt benachbarte Schlüssel.

Für die Berechnung der Anzahl der Aufrufe von c3c3 stellt man fest, das in diesem Fall in jedem Schritt immer 2 Elemente, die eine Distanz von hsh_s aufweisen, falsch sortiert sind.

Der Algorithmus tauscht daraufhin in jedem Durchgang alle Schlüssel untereinander aus, die er über min < arr[j-delta] miteinander vergleicht, was folglich die maximale Anzahl von Schlüsselvertauschungen in dieser vergleichsbasierten Implementierung für ein Feld der Größe nn ergibt, nämlich n2\frac{n}{2}. Insgesamt finden dadurch für c3c3

lg(n)n2lg(n) * \frac{n}{2}

Aufrufe statt.

Mit der Anzahl der berechneten Aufrufe c1,c2,c3c1, c2, c3 ergibt sich somit für die Laufzeit T(n)T(n) für diesen Fall

lg(n)+nlg(n)n+1+lg(n)n2lg(n) + n * lg(n) - n + 1 + lg(n) * \frac{n}{2}

und zusammengefasst

f(n)=32nlg(n)+lg(n)n+1f(n) = \frac{3}{2} * n * lg(n) + lg(n) - n + 1

was nach Einsetzen zu

lg(16)+16lg(16)16+1+lg(16)162=85lg(16) + 16 * lg(16) - 16 + 1 + lg(16) * \frac{16}{2} = 85

Aufrufen für unser Beispiel führt.

Nachweis der Komplexitätsklasse

Um OO zu ermitteln, werden nun alle Konstanten der Funktion

f(n)=32nlg(n)+lg(n)n+1f(n) = \frac{3}{2} * n * lg(n) + lg(n) - n + 1

eliminiert, und der "dominante" Summand in Abhängigkeit von nn betrachtet, der in diesem Fall lg(n)nlg(n) * n ist.

Wir vermuten ein NlogNN-log-N-Wachstum7 (vgl. [📖OW17a, p. 5]), und wollen nun zeigen, dass T(n)T(n) in O(n log n)O(n\ log\ n) liegt.
Hierfür müssen wir ein geeignetes cc und ein n0n_0 finden, so dass gilt:

fO(n log n):n0N,cR,c>0:nn0:f(n)cnlg(n)f \in O(n\ log\ n): \leftrightarrow \exists n_0 \in \mathbb{N}, c \in \mathbb{R}, c > 0: \forall n \geq n_0: f(n) \leq c * n*lg(n)

(vgl. [📖GD18a, p. 11]).

Zu zeigen ist

32nlg(n)+lg(n)n+1cnlg(n)\frac{3}{2} * n * lg(n) + lg(n) - n + 1 \leq c * n * lg(n)

Wir wählen für n0=1n_0 = 1 und c=32c = \frac{3}{2}, denn es gilt sicher

nn0:32nlg(n)32nlg(n)\forall n \geq n_0: \frac{3}{2} * n * lg(n) \leq \frac{3}{2} * n * lg(n).

Ausserdem gilt stets nN:lg(n)<n\forall n \in \mathbb{N}: lg(n) < n, woraus lg(n)n<0lg(n) - n < 0 folgt, und damit auch lg(n)n+10lg(n) - n + 1 \leq 0.

Insgesamt gilt also

n0=1,c=32:nn0:32nlg(n)+lg(n)n+1cnlg(n)n_0 = 1, c = \frac{3}{2}: \forall n \geq n_0: \frac{3}{2} * n * lg(n) + lg(n) - n + 1 \leq c * n * lg(n)

womit f=O(nlog(n))f = O(n * log(n)) gezeigt ist. ◻

Worst-Case-Analyse

Unter der Annahme, dass ein in umgekehrter Reihenfolge sortiertes Feld zu einer Laufzeit von O(n2)O(n^2) bei dem Shellsort-Algorithmus führt, konnten wir mit dem in dem vorherigen Abschnitt gewählten Parametern nur eine Laufzeit von O(n log n)O(n\ log\ n) nachweisen.
Tatsächlich stellt der Anwendungsfall nicht den worst-case für den Algorithmus dar, da ja gerade diese Form von Sortierreihenfolge dem Algorithmus die Vorsortierung der hh-Folgen ermöglicht:

The idea underlying Shellsort is that moving elements of A long distances at each swap in the early stages, then shorter distances later, may reduce this O(n2)O(n^2) bound. [📖Pra72, p. 3]

Es gilt also, die Vorsortierung auszuhebeln.
Für Insertion-Sort ist die Laufzeit im worst-case O(n2)O(n^2) (vgl. [📖OW17b, p. 87]). Da Shellsort mindestens im letzten Schritt diese Sortiermethode auf Distanzfolgen der Länge 11 anwendet, muss der Algorithmus eine Folge als Eingabe erhalten, die durch die ersten lg(n)1lg(n) - 1-Durchgänge (mit hs=2s1,1s<lg(n)h_s = 2^{s - 1}, 1 \leq s < lg(n)) keine Änderungen erfährt, um dann im allerletzten Schritt auf einen Schlag sortiert wird, was maximal n2\frac{n}{2} Inversionen8 bedeutet, und damit ebenso viele Vertauschungen (zuzüglich der benötigten Verschiebe-Operationen).
Hierzu kann wie im vorherigen Beispiel für n=16n=16 ein Feld mit folgender Schlüsselanordnung verwendet werden: Felder mit geradem Index enthalten kleinere Schlüssel als Felder mit ungeradem Index. Hier gilt für alle Elemente aus dem Feld AA:

i,jN[0,n1],2i,2j:A[i]<A[j]A[i]<A[i+1]A[j]<A[j+1]\forall i, j \in \mathbb{N}_{[0, n - 1]}, 2 \mid i, 2 \nmid j: A[i] < A[j] \land A[i] < A[i + 1] \land A[j] < A[j +1]

Abbildung 7 veranschaulicht die Anordnung.

Figure 7 Eine worst-case Schlüsselfolge für Shellsort. Felder mit geradem Index enthalten kleinere Schlüssel als Felder mit ungeradem Index.

Markiert sind die direkt benachbarten Felder, die eine Inversion aufweisen, die im letzten Schritt des Algorithmus eine bzw. mehrere Verschiebungen bedingen.
In den vorherigen Schritten - also bei den Durchgängen mit Distanzfolgen hs>1h_s > 1 - findet der Algorithmus jeweils Schlüssel in korrekter Sortierreihenfolge vor.

Mit h4=8h_4 = 8 werden die Felder A[0..7]A[0..7] mit den Feldern A[8..15]A[8..15] verglichen - hier gilt in jedem Fall, dass die Schlüssel A[i]<A[j](i<j,ji=8)A[i] < A[j] (\forall i < j, j - i = 8) sind.

Auch im darauffolgenden Durchgang (h3=4,ji=4h_3 = 4, j - i = 4) finden keine Verschiebungen statt, da keine Inversion gefunden wird. Erst im letzten Schritt, in dem direkt benachbarte Elemente miteinander verglichen werden, werden die Inversionen ermittelt und die Verschiebung der Elemente findet statt (s. Abbildung 8.

Figure 8 Für die worst-case Schlüsselfolge werden im letzten Schritt 7 Inversionen festgestellt. Jede dieser Fehlstellung bedingt eine Verschiebung um die angegebene Anzahl von Positionen.

In diesem Fall wird c3c3 insgesamt 3232 mal aufgerufen.

Da jeweils zwei Schlüssel bereits korrekt sortiert sind (A[0]A[0] und A[n1]A[n-1]) existieren noch n21\frac{n}{2} - 1 Inversionen. Jede Inversion erzwingt eine Verschiebung des größten Elements um i2\frac{i}{2} Positionen nach links.

Für die Berechnung der Laufzeitkomplexität ergibt sich somit der Term

i=1n21i=n2(n21)2=n22n8\sum_{i=1}^{\frac{n}{2} - 1} i = \frac{\frac{n}{2}(\frac{n}{2} - 1)}{2} = \frac{n^2 - 2n}{8}

und unter Berücksichtigung der Terme für c1c1, c2c2, c3c3 die Funktion

f(n)=lg(n)+nlg(n)n+1+n22n8f(n) = lg(n) + n * lg(n) - n + 1 + \frac{n^2 - 2n}{8}

was zu einer Laufzeitabschätzung von O(n2)O(n^2) führt.


  1. mit n=n = Länge des zu sortierenden Feldes. Im folgenden lglg für log2log_2.
  2. vgl. [📖Knu97c, p. 84]
  3. Knuth bezieht sich hier auf die Arbeit von Papernov und Stasevich ([📖PS65]). Weitere Verweise auf Laufzeiten in Abhängigkeit von nn und hh fasst übersichtlich der Wikipedia-Artikel zusammen: https://en.wikipedia.org/wiki/Shellsort (abgerufen 23.01.2024).
  4. Shell führt in seinem Paper keinen Beweis auf. Er stützt seine Behauptung auf Messungen, die er selber in Tests durchgeführt hat: "[...] an analytical determination of the expected speed has eluded the writer. However, experimental use has established its speed characteristics." (ebenda)
  5. hier wie im folgenden ohne Betrachtung der Schleifenbedingung, die an dieser Stelle insgesamt lg(n)+1lg(n) + 1-mal aufgerufen wird
  6. 8,4,2,1{8, 4, 2, 1} für das gegebene Beispiel
  7. mit O(log n)O(log\ n) bzw O(n log n)O(n\ log\ n) nehmen wir für die OO-Notation wieder die in der Fachliteratur gebräuchlichere Schreibweise auf. Sowohl Güting und Dieker als auch Ottmann und Widmayer weisen in [📖GD18a, p. 15] bzw. [📖OW17a, p. 5] darauf hin, dass die Angabe der Basis keine Rolle spielt, da sich Logarithmen mit verschiedenen Basen ohnehin nur durch einen konstanten Faktor unterscheiden (es gilt loga(x)=logb(x)logb(a)log_a(x) = \frac{log_b(x)}{log_b(a)}).
  8. in diesem Zusammenhang bedeutet Inversion: Fehlstellung (vgl. [📖OW17b, p. 87])