diff --git a/200_Sortieralgorithmen.tex b/200_Sortieralgorithmen.tex index c804770fd15a1e6e2eb0f3cc2993e174217f39a6..9cf25b71c3e427cfdbabf64f12e38b36b03b5a33 100644 --- a/200_Sortieralgorithmen.tex +++ b/200_Sortieralgorithmen.tex @@ -4,8 +4,8 @@ In diesem Kapitel werden wir uns mit Sortieralgorithmen beschäftigen. Den Anfan widmen, nur um sie kurz darauf (mit erweiterten Voraussetzungen an die zu sortierenden Objekte) mit \texttt{radixsort} wieder zu brechen. -\input{201_quicksort} -\include{202_mergesort} +\input{201_mergesort} +\include{202_quicksort} \include{203_Untere_Schranke_Sortieralgorithmen} \include{204_Radixsort} \include{205_Eigenschaften_von_Sortieralgorithmen} diff --git a/201_mergesort.tex b/201_mergesort.tex new file mode 100644 index 0000000000000000000000000000000000000000..fd575f3efcd5da6bc2dd4cf5dbd40762e5cfd5ce --- /dev/null +++ b/201_mergesort.tex @@ -0,0 +1,111 @@ +\section{Mergesort} +Diese Sortierverfahren ist ein Paragon der sogenannten \emph{divide et impera}-Strategie. + +Ausgehend von einem linearen Feld $\mathcal{A}$ der Größe $n$ unterteilen wir das Problem rekursiv immer weiter in kleinere Probleme, +welche superlinear schneller gelöst werden können. Der Basisfall, ein Array mit einem einzigen Element, ist dann immer +sortiert. Im letzen Schritt vereinen wir alle sortierten Teilarrays zu einem großen sortierten Array. Siehe Abbildung +\ref{fig:mergesort_diagram} für ein Beispiel. + +\begin{figure}[htb] + \centering + \includegraphics[width=0.8\textwidth]{bilder/mergesort_diagram} + \caption{Ein Beispiellauf des \texttt{mergesort}-Algorithmus auf den Input $\mathcal{A} = (38,27,43,3,9,82,10)$} + \label{fig:mergesort_diagram} +\end{figure} +Im folgenden Pseudocode verwenden wir die Funktion $\texttt{Array}(n)$, welches uns einfach nur ein leeres (also z.B. +mit $0$ gefülltes) Array der Länge $n$ gibt, sowie die Funktion $\texttt{len}(\mathcal{A})$, welche die Länge des arrays $\mathcal{A}$ +zurückgibt. + +\begin{algorithm}[H] + \SetNlSty{texttt}{[}{]} + \caption{\texttt{mergesort}($\mathcal{A}$)} + \KwIn{An array $\mathcal{A}$ of size $n$} + \KwOut{The sorted array $\mathcal{A}$} + \eIf{$n = 1$}{ + \Return $\mathcal{A}$ \; + }{ + $k \leftarrow \floor*{\frac{n}{2}}$ \; + \KwRet $\texttt{merge}(\texttt{mergesort}(\mathcal{A}[0,\dots, k]), \texttt{mergesort}(\mathcal{A}[k+1, \dots, n]))$ + } +\end{algorithm} + +Wobei \texttt{merge} folgendermaßen definiert wird: + +\begin{algorithm}[H] + \SetNlSty{texttt}{[}{]} + \caption{\texttt{merge}($\mathcal{A}_1, \mathcal{A}_2$)} + \KwIn{Two sorted arrays $\mathcal{A}_1$ and $\mathcal{A}_2$} + \KwOut{The sorted union array $\mathcal{A}$ of the input} + $\mathcal{A} \leftarrow \texttt{Array}(\texttt{len}(\mathcal{A}_1) + \texttt{len}(\mathcal{A}_2))$ \; + $i_1, i_2 \leftarrow 0$ \; + \For{$i \leftarrow 0$ \KwTo $\texttt{len}(\mathcal{A})$}{ + \eIf{$\mathcal{A}_1[i_1] \leq \mathcal{A}_2[i_2]$}{ + $\mathcal{A}[i] \leftarrow \mathcal{A}_1[i_1]$ \; + $i_1 \leftarrow i_1 + 1$ \; + }{ + $\mathcal{A}[i] \leftarrow \mathcal{A}_2[i_2]$ \; + $i_2 \leftarrow i_2 + 1$ \; + } + } + \KwRet $\mathcal{A}$ +\end{algorithm} + +\subsubsection{Laufzeit} +Die Laufzeit der Algorithmen ist nicht vom konkreten Input, sondern nur von der Länge des Inputs abhängig. Worst, best +und average case sind also gleich. Für \texttt{merge} ist die Länge des Inputs $n = \texttt{len}(\mathcal{A}_1) + +\texttt{len}(\mathcal{A}_2)$ und es gilt: $T_{\text{m}} = T_{\texttt{merge}} = f \in \mathcal{O}(n)$, da Zeile~3 alles dominiert. + +Damit ist die Laufzeitfunktion für \texttt{mergesort} für $T_\text{ms}(1) \in \mathcal{O}(1)$ und für $n > 1$ +\[ + T_{\text{ms}}(n) = 2 T_{\text{ms}}(\frac{n}{2}) + T_{\text{m}}(n) + d, +\] +wobei $d \in \mathcal{O}(1)$. +Mit dem Hauptsatz der Laufzeitfunktionen haben wir damit Fall 2, die asymptotisch exakte Laufzeit beträgt ($\log_2{2} = 1$) also +\[ + T^{\text{ms}} \in Θ(n \log{n}). +\] + +Um die Methodik der rekursiven Laufzeitberechnungen zu demonstrieren, berechnen wir die eine Seite der Abschätzung +elementar. Um den Beweis übersichtlicher zu halten, nehmen wir an, dass $n=2^k$ für ein $k \in \mathbb{N}$. Für Werte +von $n$, welche keine Zweierpotenz sind, können wir aufgrund der Monotonie von $T_\text{ms}$ dann abschätzen. + +\begin{align*} + T_{\text{ms}} + &= 2T_\text{ms}\left(\frac{n}{2}\right) + T_\text{m}(n) + d \\ + &= 4T_\text{ms}\left(\frac{n}{4}\right) + 2 T_\text{m}\left(\frac{n}{2}\right) + 2d + T_\text{m}(n) + d \\ + &= 8T_\text{ms}\left(\frac{n}{8}\right) + 4 T_\text{m}\left(\frac{n}{4}\right) + 4d + 2 T_\text{m}\left(\frac{n}{2}\right) + 2d + T_\text{m}(n) + d \\ + &\vdots \\ + &= \underbrace{n T_\text{ms}(1)}_{\in \mathcal{O}(n)} + + \sum_{l=0}^{\log_2 \left(\frac{n}{2}\right)} 2^l T_\text{m}\left(\frac{n}{2^l} \right) + + \underbrace{\sum_{l=0}^{\log_2 \left(\frac{n}{2}\right)} 2^l d}_{\in \mathcal{O}(n)} +\end{align*} + +Wir müssen also nurnoch den asymptotischen Aufwand für die rekursiven Aufrufe von $T_\text{m}$ finden. +Wir wissen, da $T_\text{m} \in \mathcal{O}(n)$, dass es ein $n_0 \in \mathbb{N}$ und $c \in \mathbb{R}$ gibt, sodass +$T_\text{m}(n) \leq c n$ für \emph{alle} $n > n_0$. Insbesondere also für die nächstgrößere Zweierpotenz, sodass wir +o.B.d.A. annehmen können: $n_0 = 2^k_0$ für ein $k_0 \in \mathbb{N}$. Für alle $n \leq n_0$ können wir den Aufwand von +$T_\text{m}(n)$ zudem durch die Konstante $m = \max(\{T_\text{m}(i) | i \in \{0, \dots, n_0\}\})$ abschätzen. + +Das erlaubt es uns, obige Summe aufzuspalten: +\[ + \sum_{l=0}^{k-1} 2^l T_\text{m}\left( \frac{n}{2^l} \right) = + \sum_{l=0}^{k-k_0} 2^l T_\text{m}\left( \frac{n}{2^l} \right) + + \underbrace{\sum_{l=k-k_0+1}^{k-1} 2^l T_\text{m}\left( \frac{n}{2^l} \right)}_{\leq 2^k m \in \mathcal{O}(n)}. +\] +Es bleibt noch der erste Teil. Aber hier greift endlich die Abschätzung $T_\text{m}(n)~\leq~cn$, und damit +\[ + \sum_{l=0}^{k-k_0} 2^l T_\text{m}\left( \frac{n}{2^l} \right) + = c \sum_{l=0}^{k-k_0} 2^l \frac{n}{2^l} + = c \sum_{l=0}^{k-k_0} n + \in \mathcal{O}(n \log_2 n). +\] +Dieser Term dominiert also alle anderen Terme, und damit $T_\text{ms} \in \mathcal{O}(n \log n)$. + + +\subsubsection{Speicherverbrauch} +In der hier vorgestellten Variante muss bei jedem Aufruf von \texttt{merge} ein neues Array angelegt werden. Damit braucht +$\texttt{mergesort}$ asymptotisch $\mathcal{O}(n)$ zusätzlichen Speicher. + +Es gibt allerdings auch weiterentwicklungen, die mit $\mathcal{O}(1)$ Speicher auskommen (z.B. TimSort oder blocksort). + + diff --git a/202_quicksort.tex b/202_quicksort.tex new file mode 100644 index 0000000000000000000000000000000000000000..f961c8bc22c1bdfffb3c6846bad210a1fd451ac6 --- /dev/null +++ b/202_quicksort.tex @@ -0,0 +1,290 @@ +\section{Quicksort} +Quicksort ist einer der meist verwendeten Sortieralgorithmen. Die Idee ist wieder das divide-et-impera-Prinzip, +der Kern des Algorithmus ist die Zerlegung des Arrays, die sogenannte Partition. + +\subsection{Partition} +Für eine Partition eines Arrays $\mathcal{A}$ wählen wir zuerst ein Pivotelement $p$. Nun sortieren wir das Array so, +dass links von $p$ nur Elemente stehen, die kleiner als $p$ sind, rechts nur Elemente, die größer sind. +Vorerst nehmen wir als Pivotelement einfach das letzte Arrayelement. + +\noindent Die Funktion $\texttt{swap}(a,b)$ im folgenden Pseudocode tauscht hierbei die Werte von $a$ und~$b$. + +\begin{algorithm}[H] + \SetNlSty{texttt}{[}{]} + \caption{\texttt{partition}($\mathcal{A}$)} + \KwIn{An unsorted array $\mathcal{A}$ of lenght $n$} + \KwOut{The partition of $\mathcal{A}$ wrt. the pivot $p$ and its index $k$} + $p \leftarrow \mathcal{A}[n-1]$ \; + $i \leftarrow -1$ \; + \For{$j \leftarrow 0$ \KwTo $n-2$}{ + \If{$\mathcal{A}[j] \leq p$}{ + $\texttt{swap}(\mathcal{A}[i+1], \mathcal{A}[j])$ \; + $i \leftarrow i + 1$ \; + } + } + $\texttt{swap}(\mathcal{A}[i+1], \mathcal{A}[n-1])$ \; + $i \leftarrow i + 1$ \; + \KwRet $(\mathcal{A}, i)$ +\end{algorithm} + +\subsubsection{Korrektheitsüberlegungen:} +Was passiert hier? Das Pivotelement $p$ ist das letzte Element von $\mathcal{A}$, siehe Zeile [1]. +Wie oben erwähnt, partionieren wir das Array in +Elemente, welche kleiner oder gleich $p$ sind, sowie die Elemente, welche größer als $p$ sind. +Das Pivotelement $p$ dient erstmal nur zum Vergleichen ([4]), bleibt aber +ansonsten außen vor, bis es in Zeile [7] an seine endgültige Position getauscht wird. + +Die endgültige Position von $p$ wird von dem Index $i$ bestimmt. Dabei erfüllt $i$ zu jedem Zeitpunkt die Bedingung, +dass alles, was sich links von $i$ befindet ($i$ eingeschlossen), stets kleiner oder gleich $p$ ist (Zeile [4] und [5])! Unter +den Elementen, die bereits mit $p$ verglichen wurden (siehe Laufindex $j$) nimmt $i$ dabei den maximalen Wert ein (Nach +Ausführung von Zeile [6], bzw Zeile [8]). Zu beachten ist außerdem, dass durch das rechtzeitige Addieren von $i+1$ nie ein +Arrayzugriff an undefinierter Stelle geschieht([5]), selbst wenn $i$ mit $-1$ initialisiert wurde([2]). + +Als letztes muss nur noch die Rolle des Laufindex $j$ geklärt werden. Definiert in Zeile [3] startet $j$ beim ersten Element +und geht bis zum vorletzten (also exklusiv $p$). Da pro Schleifendurchgang $i$ um maximal eins inkrementiert werden +kann, ist $j$ also stets größergleich $i$. Dabei zeigt $j$ an, welche Elemente des Arrays bereits mit $p$ verglichen +wurden. +Findet $j$ mit Zeile $[4]$ ein Element, welches kleiner ist als $p$, wird dieses in den von $i$ markierten Bereich +vertauscht([5]). Dabei wird eine Sortierung innerhalb einer Partition zwar vielleicht zerstört, aber das ist für die +Korrektheit des Algorithmus nicht relevant. + +Man kann also feststellen: Das Array ist stets in vier (möglicherweise leere) Teilbereiche unterteilt. Der +Speicherort von $p$ (Anfangs $n-1$), die noch nicht verglichenen Elemente $j < \text{ index } \leq n-2$, die kleiner als +$p$ eingestuften Elemente $0 \leq \text{ index } \leq i$ und die als größer als $p$ eingestuften Elemente $i < \text{ index } +\leq j$. + +Da ein Schleifendurchlauf die Schleifeninvarianten (also der Programmzustand exakt vor dem Inkrementieren von $j$) +\begin{itemize} + \item Elemente mit Index $x$, wobei $x \leq i$, sind kleiner oder gleich $p$, + \item Elemente mit Index $x$, wobei $i < x$ und $x \leq j$, sind größer $p$, + \item Elemente mit Index $x$, wobei $j < x$, sind noch nicht überprüft, +\end{itemize} +erhält, aber die Anzahl der nicht überprüften Elemente pro Schleifendurchlauf um eins schrumpft, terminiert der +Algorithmus. Mit Zeile [7] wird am Ende noch $p$ an seinen richtigen Platz verschoben, damit eine korrekte Partition +zurückgegeben werden kann. + +%TODO: Bild von partition analog zu Cormen, Figure 7.1 + +\subsubsection{Laufzeit:} +Die Schleife in Zeile [3] bis [6] hat an sich eine Laufzeit in $\mathcal{O}(1)$, wird aber $\mathcal{O}(n)$ mal +aufgerufen. Dadurch ergibt sich, dass die Laufzeit von $T_{\text{p}} = T_\texttt{partition}(\mathcal{A})$ in $\Theta(n)$ +liegt, wobei $n = \texttt{len}(\mathcal{A})$. Es gibt keinen asmyptotischen Unterschied zwischen best/worst-case. + + +\subsection{Quicksort} +Auf der Basis von $\texttt{partition}$ kann der Sortieralgorithmus $\texttt{quicksort}$ konstruiert werden. + +\begin{algorithm}[H] + \SetNlSty{texttt}{[}{]} + \caption{\texttt{quicksort}($\mathcal{A}$)} + \KwIn{An unsorted array $\mathcal{A}$ of length $n$} + \KwOut{The same array $\mathcal{A}$, but sorted} + \eIf{$n > 1$}{ + $(\mathcal{A}, k) \leftarrow \texttt{partition}(\mathcal{A})$ \; + \KwRet $\texttt{concat}(\texttt{quicksort}(\mathcal{A}[0, \dots, k-1]), [A[k]], \texttt{quicksort}(\mathcal{A}[k+1, + \dots, n-1])$ \; + }{ + \KwRet $\mathcal{A}$ + } +\end{algorithm} + +Die Funktion $\texttt{concat}$ ist hierbei eine $\mathcal{O}(1)$-Operation, da sie vom Compiler wegoptimiert werden +kann. Aber auch als $\mathcal{O}(n)$-Operation wäre sie asymptotisch irrelevant, da $\texttt{partition}$ bereits +$\mathcal{O}(n)$ Zeit braucht. Sollten wir Arrays auf Grenzen wie $[0,-1]$ aufrufen, so ist damit das leere Array +gemeint. + +\subsubsection{Korrektheitsüberlegungen:} +Die Korrektheit von $\texttt{quicksort}$ ist schneller einsehbar als die von $\texttt{partition}$. Ist die +Paritionseigenschaft bezüglich des Pivotelements in Position $k$ erfüllt, so haben wir links und rechts davon echt +kleinere Unterarrays, welche durch Rekursion sortiert werden (der Basisfall von einem Element ist trivial sortiert). Der +Algorithmus $\texttt{quicksort}$ terminiert als spätestens nach $n$ rekursiven Aufrufen und arbeitet dabei korrekt. + +\subsubsection{Laufzeit:} +Die allgemeine Rekusionsgleichung für $T_{\text{qs}} = T_{\texttt{quicksort}}$ lautet +\[ + T_{\text{qs}}(n) = T_{\text{qs}}(m) + T_{\text{qs}}(n-m) + T_p(n) + f(n), \text{ wobei } f \in \mathcal{O}(1). +\] +Dabei verschwindet $f$ völlig unter $T_p \in \mathcal{O}(n)$. +Anders als bei $\texttt{partition}$ gibt es hier aber Unterschiede im best/worst case. + +\paragraph{Best case} +Im besten Fall treffen wir mit dem Pivotelement $p$ genau den Median von $\mathcal{A}$. Dann haben wir durch $m = +\frac{n}{2}$ pro Rekursionsschritt eine balancierte Aufteilung des Rekursionsbaums. Dann haben wir nach dem Hauptsatz +der Laufzeitfunktionen also $a=2, b=2$, $f \in \mathcal{O}(n)$ und damit eine best-case-Laufzeit von +$\mathcal{O}(n \log(n))$. Wir rechnen aber noch einmal per Hand nach: Einerseits um zu sehen wie das geschieht, +andererseits, da der worst-case nicht mit dem Satz berechnet werden kann. +Sei der Einfachheit halber $n = 2^{k'}$ für ein $k' \in \mathbb{N}$. +\begin{align*} + T_{\texttt{qs}}^{\text{best}}(n) +&= T_{\text{qs}}^{\text{best}} \left(\frac{n}{2} \right) + T_{\text{qs}}^{\text{best}} \left(\frac{n}{2} \right) + T_{\text{p}}(n) \\ + &= 2T_{\text{qs}}^{\text{best}} \left(\frac{n}{2} \right) + T_{\text{p}}(n) \\ + &= 4T_{\text{qs}}^{\text{best}}\left(\frac{n}{4} \right) + 2T_{\text{p}} \left(\frac{n}{2} \right) + T_{\text{p}}(n) \\ + &= 8T_{\text{qs}}^{\text{best}}\left(\frac{n}{8} \right) + 4T_{\text{p}} \left(\frac{n}{4} \right) + 2T_{\text{p}}\left(\frac{n}{2} \right) + T_{\text{p}}(n) \\ + &= \ldots \\ + &= nT_{\text{qs}}^{\text{best}}(1) + \sum_{k=0}^{\log_2(n)} 2^k T_{\text{p}}\left(\frac{n}{2^k}\right) \\ +\end{align*} +Wir sehen nun in einer Nebenrechnung, dass $n \mapsto 2^k T_{\text{p}}(\frac{n}{2^k}) \in \Theta(n)$ für alle +$k \in \mathbb{N}$. +Sei also $k$ fixiert. Dann gilt $c_1 \floor*{\frac{n}{2^k}} \leq T_{\text{p}}(\floor*{\frac{n}{2^k}}) \leq c_2 +\ceil*{\frac{n}{2^k}}$ für alle $n \geq n_0 2^k$, wobei $n_0$ durch $T_{\text{p}} \in \Theta(n)$ gegeben ist. Wir +multiplizieren die Ungleichung mit $2^k$, erhalten damit $2^k c_1 \floor*{\frac{n}{2^k}} \leq 2^k +T_{\text{p}}(\floor*{\frac{n}{2^k}}) \leq 2^k c_2 \ceil*{\frac{n}{2^k}}$, was wieder für alle $n \geq n_0 2^k$ gültig ist. +Da $2^k \floor*{\frac{n}{2^k}} \leq n$ und $2^k \ceil*{\frac{n}{2^k}} \geq n$ gilt, können wir die Ungleichung zu +$c_1 n \leq 2^k T_{\text{p}}(\floor*{\frac{n}{2^k}}) \leq c_2 n$ vereinfachen. Damit gilt mit $c_1, c_2$ und $n_0' = 2^k +n_0$: +\[ + n \mapsto 2^k T_{\text{p}}\left(\floor*{\frac{n}{2^k}}\right) \in \Theta(n). +\] + +Damit haben wir +\begin{align*} + T_{\text{qs}}^{\text{best}}(n) + &= nT_{\text{qs}}^{\text{best}}(1) + \sum_{k=0}^{\log_2(n)} 2^k T_{\text{p}}\left(\frac{n}{2^k}\right) \\ + &\in \mathcal{O}(n) + \log_2(n) \mathcal{O}(n) \\ + &= \mathcal{O}(n \log(n)). \\ +\end{align*} + +\paragraph{Worst case} +Im schlechtesten Fall teilen wir das Array sehr ungünstig: Das Pivotelement ist immer das Maximum oder das Minimum, +unser Array wird also aufgeteilt in ein $n-1$ großes Array, $m=1$ in jedem Rekursionsschritt. +Damit haben wir keinen echten Bruchteil pro Rekursionsschritt und das Master-Theorem lässt sich nicht anwenden. Also +rechnen wir per Hand: +\begin{align*} + T_{\text{qs}}(n) & = T_{\text{qs}}(1) + T_{\text{qs}}(n-1) + T_\text{p}(n) \\ + &= 2T_{\text{qs}}(1) + T_{\text{qs}}(n-2) + T_\text{p}(n-1) + T_\text{p}(n) \\ + &= 3T_{\text{qs}}(1) + T_{\text{qs}}(n-3) + T_\text{p}(n-2) + T_\text{p}(n-1) + T_\text{p}(n) \\ + &\qquad \qquad \vdots \\ + &= \underbrace{n T_{\text{qs}}(1)}_{\text{$\in Θ(n)$}} + +\underbrace{\sum_{i=1}^n T_\text{p}(i)}_{\text{$\in Θ(n^2)?$}} \\ +\end{align*} +Dabei gibt uns die Eulersche Summenformel $\sum_{i=1}^n i = \frac{n^2 - n}{2} \in \mathcal{O}(n^2)$ einen Hinweis, in welcher +Aufwandsklasse der Summenterm liegen könnte. Wir fomalisieren jetzt also die Idee, dass wir $T_{\text{p}}(n)$ durch $c_2 n$ von +oben und $c_1 n$ von unten abschätzen können, sobald die $n$ groß genug werden. + +Betrachten wir also $\sum_{i=1}^k T_\text{p}(i)$. Mit $T_\text{p} \in Θ(n)$ wissen wir, dass $n_0,c_1,c_2$ existieren, sodass +$c_1 n' \leq T_\text{p}(n') \leq c_2 n'$ für alle $n' > n_0$. Dieses $n_0$ ist aber fix, d.h. für ein groß genuges $k$ (und +$k$ soll später gegen unendlich gehen) betrachten wir also $\sum_{i=n_0}^k T_p(i)$. Hier gilt: +\[ + \underbrace{c_1 \sum_{i=n_0}^k i}_{\text{$\in Ω(k^2)$}} + \leq \sum_{i=n_0}^k T_p(i) \leq + \underbrace{c_2 \sum_{i=n_0}^k i}_{\text{$\in \mathcal{O}(k^2)$}} +\] +wie uns die Eulersche Summenformel verrät. +Mit einer beidseitigen Abschätzung haben wir hier also obige Vermutung bewiesen. + + + +Wir würden nun gerne noch eine average-case Analyse durchführen. Bei einer Gleichverteilung des Inputs ergibt sich +nämlich auch eine average-case Aufwand von $T_{\text{qs}}^{\text{avg}} \in \mathcal{O}(n \log n)$. Allerdings benötigt eine +average-case Analyse Annahmen über die Verteilung des Inputs. + +Wir analysieren daher eine stark verwandte Variante, den randomisierten Quicksort. + +\subsection{Randomisierter Quicksort} +Grundidee des randomisierten Quicksort Algorithmus ist es, nicht mehr ein fixes Pivotelement in der Partition zu wählen +(wie bei uns das erste Element), sondern ein zufälliges. Dadurch kann man eine gute +\emph{durchschnittliche} Performance erreichen. + +Das Wort \emph{durchschnittlich} bekommt hier aber eine andere andere Bedeutung +als in der average-case-Analyse! In der average-case-Analyse betrachtet man die durchschnittliche Laufzeit über alle +möglichen Inputs (gewichtet mit der Wahrscheinlichkeit des entsprechenden Inputs), hier hingegen betrachtet man die +durchschnittliche Laufzeit \emph{über die verschiedenen zufälligen Läufe des nichtdeterministischen Algorithmus} +(gewichtet nach Wahrscheinlichkeit des entsprechenden Durchlaufs). + +Definieren wir zuerst die randomisierte Partition: + +\begin{algorithm}[H] + \SetNlSty{texttt}{[}{]} + \caption{\texttt{randomized\_partition}($\mathcal{A}$)} + \KwIn{An unsorted array $\mathcal{A}$ of length $n$} + \KwOut{The partition of $\mathcal{A}$ wrt. the randomized pivot $p$ and its post-partitioning-index $k$} + $i \leftarrow \texttt{random\_uniform}(n-1)$ \; + $\texttt{swap}(\mathcal{A}[0], \mathcal{A}[i])$ \; + $p \leftarrow \mathcal{A}[n-1]$ \; + $i \leftarrow -1$ \; + \For{$j \leftarrow 0$ \KwTo $n-2$}{ + \If{$\mathcal{A}[j] \leq p$}{ + $\texttt{swap}(\mathcal{A}[i+1], \mathcal{A}[j])$ \; + $i \leftarrow i + 1$ \; + } + } + $\texttt{swap}(\mathcal{A}[i+1], \mathcal{A}[n-1])$ \; + $i \leftarrow i + 1$ \; + \KwRet $(\mathcal{A}, i)$ +\end{algorithm} + +Neu ist also nur das Vertauschen des ersten Elements von $\mathcal{A}$ mit dem durch $\texttt{random\_uniform}$ +zufällig (gleichverteilt) erwählten anderen Elements des Arrays, der Rest ist wie bei $\texttt{partition}$. +Dabei hat $\texttt{random\_partition}$ die gleichbleibende Laufzeit $T_{\text{rp}} \in Θ(n)$ mit $n = +\texttt{len}(\mathcal{A})$. + +Der Algorithmus $\texttt{randomized\_quicksort}$ hat dabei genau den gleichen Pseudocode wie $\texttt{quicksort}$, nur +dass $\texttt{randomized\_partition}$ statt $\texttt{partition}$ in Zeile $[2]$ gerufen wird. Die Laufzeit von +$\texttt{randomized\_quicksort}$ mit Input der Länge $n$ bezeichnen wir mit $T_{\text{rqs}}(n)$. Wir werden nun zeigen, +dass $T_{\text{rqs}} \in \mathcal{O}(n \log n)$ liegt. + + +\subsubsection{Herleitung der average case Laufzeit von \texttt{randomized\_quicksort}:} Für die erwartete Laufzeit gilt +\begin{displaymath} + T_{\text{rqs}}(n) = \sum_{k=1}^{n-1} P(k) \left( T_{\text{rqs}}(k) + T_{\text{rqs}}(n-k) + T_{\text{rp}}(n) \right), +\end{displaymath} +wobei $P(k)$ die Wahrscheinlichkeit ist, dass $\texttt{randomized\_partition}(\mathcal{A})$ den Index $k$ liefert. +Wir nehmen mit $k \in \{1,\dots, n-1\}$ eine Gleichverteilung $P(k) = \frac{1}{n-1}$ an. +% TODO: Mehr Details +\begin{align*} + T_{\text{rqs}} & = \sum_{k=1}^{n-1} \left( \frac{1}{n-1} \left( T_{\text{rqs}}(k) + T_{\text{rqs}}(n-k) + T_{\text{rp}}(n) \right) \right) \\ + & = \frac{1}{n-1} \sum_{k=1}^{n-1} \left( T_{\text{rqs}}(k) + T_{\text{rqs}}(n-k) \right) + \frac{n-1}{n-1} T_{\text{rp}}(n) \\ + & = \frac{2}{n-1} \sum_{k=1}^{n-1} T_{\text{rqs}}(k) + T_{\text{rp}}(n). +\end{align*} +Wir zeigen nun vermöge einer vollständiger Induktion, dass mit $c = \max\{T_{\text{rqs}}(1) + T_{\text{rp}}(2), 8 c_{\text{rp}}\}$ gilt: +\[ + T_{\text{rqs}}(n) \leq c \cdot n \log_2 n \text{ für alle } n > 2. +\] + +Hierbei ist $c_{\text{rp}}$ die Konstante mit welcher $T_\text{rp}(n) \leq c_{\text{rp}} n$ gilt. Streng genommen +gilt das erst ab irgendeinem $n_0$, aber den Aspekt vernachlässigen wir hier, um die Beweisstruktur etwas +übersichtlicher zu gestalten. Es ist bei \texttt{randomized\_partition} aber auch leicht ersichtlich, dass $n_0 = 2$ +gewählt werden kann. + +Induktionsanfang $n=2$: +\begin{align*} + T_{\text{rqs}}(n) + &\leq 2 T_{\text{rqs}}(1) + T_{\text{rp}}(n) \\ + &\leq c n \log_2 2 +\end{align*} +Für $c = \max(T_{\text{rqs}}(1) + T_{\text{rp}}(2), 8 c_{\text{rp}}) \geq T_{\text{rqs}}(1) + T_{\text{rp}}(2)$ ist die +Abschätzung definitiv erfüllt. + +Induktionsschritt $n-1 \mapsto n$: Zu zeigen ist, dass mit mit der Aussage wahr für alle $n \in \{2, \dots, n-1\}$ gilt: +$T_{\text{rqs}}(n) \leq c n \log_2 n$. Wir rechnen: +\begin{align*} + T_{\text{rqs}}(n) &= \frac{2}{n-1} \sum_{k=1}^{n-1} T_{\text{rqs}}(k) + T_{\text{rp}}(n) \\ + &\overset{{\scriptscriptstyle \text{IV}}}{\leq} \frac{2}{n-1} \sum_{k=1}^{n-1} c \cdot k \log_2 k + T_{\text{rp}}(n) \\ + &= \frac{2c}{n-1} \sum_{k=1}^{n-1} k \log_2 k + T_{\text{rp}}(n) \\ + &\overset{\scriptscriptstyle (*)}{\leq} \frac{2c}{n-1} \left( (\log_2 n) \left( \frac{n(n-1)}{2} \right) - \frac{\frac{n}{2}(\frac{n}{2}-1)}{2} \right) + T_{\text{rp}}(n) \\ + &= c \cdot n \log_2 n - c \left( \frac{n}{4} - \frac{1}{2} \right) + \underbrace{T_{\text{rp}}(n)}_{\leq c_{\text{rp}} \cdot n} \\ + &\leq c \cdot n \log_2 n - \left( \frac{2c \frac{n}{2} (\frac{n}{2} - 1)}{(n-1)2} \right) + c_{\text{rp}} \cdot n \\ + &\leq c \cdot n \log_2 n - c \cdot \left( \frac{n}{4} \frac{(n-2)}{(n-1)} \right) + c_{\text{rp}} \cdot n \\ + &\leq c \cdot n \log_2 n. +\end{align*} +Damit der letzte Schritt geht, muss +\begin{align*} + c &\leq \frac{c_{\text{rp}}}{\frac{n}{4} \frac{n-2}{n-1}} \\ + &= 4 c_{\text{rp}} \underbrace{\frac{(n-1)}{(n-2)}}_{\text{$\leq 2$ für $n > 2$}} \\ + &\leq 8c_{\text{rp}} +\end{align*} +sein. Durch unsere Wahl von $c = \max\{T_{\text{rqs}}(1) + T_{\text{rp}}(2), 8 c_{\text{rp}}\}$ ist das der Fall. + +Jetzt gilt es nurnoch, die in Schritt $(*)$ getroffene Abschätzung der Summe $\sum_{k=1}^{n-1} k \log_2 k$ zu beweisen: +\begin{align*} + \sum_{k=1}^{n-1} k \log_2 k & = \sum_{k=1}^{\ceil*{\frac{n}{2}}-1} k \underbrace{\log_2 k}_{\leq \log_2 \frac{n}{2}} + + \sum_{k=\ceil*{\frac{n}{2}}}^{n-1} k \underbrace{\log_2 k}_{\leq \log_2 n} \\ + &\leq \sum_{k=1}^{\ceil*{\frac{n}{2}}-1} k (\log_2 n - 1) + \sum_{k=\ceil*{\frac{n}{2}}}^{n-1} k \log_2 n \\ + &= \log_2 n \sum_{k=1}^{\ceil*{\frac{n}{2}}-1} k - \sum_{k=1}^{\ceil*{\frac{n}{2}}-1} k + \log_2 n \sum_{k = \ceil*{\frac{n}{2}}}^{n-1} k \\ + &= \log_2 n \underbrace{\sum_{k=1}^{n-1} k}_{= \frac{n(n-1)}{2}} - \underbrace{\sum_{k=1}^{\ceil*{\frac{n}{2}}-1} k}_{\geq \frac{\frac{n}{2}(\frac{n}{2}-1)}{2}} \\ + &\leq (\log_2 n) \left( \frac{n(n-1)}{2} \right) - \frac{\frac{n}{2}(\frac{n}{2}-1)}{2} +\end{align*} + + +Unsere vollständige Induktion ist damit bewiesen, und wir haben gezeigt, dass die durchschnittliche Laufzeit von $\texttt{randomized\_quicksort}$ in +$\mathcal{O}(n \log n)$ liegt.