diff --git a/104_Rekursionen.tex b/104_Rekursionen.tex index 7caeb1fc4c056ebe0a9197aaccf68b70f1795050..ebef06c18b1aeda7dcf9d1061e2dc96e463e00ba 100644 --- a/104_Rekursionen.tex +++ b/104_Rekursionen.tex @@ -16,8 +16,7 @@ Laufzeitfunktionen finden und ihre Komplexitätsklasse bestimmen. Allerdings kann es auch vorkommen, dass eine (naive) rekursive Lösung deutlich langsamer läuft und mehr Speicher benötigt. -\subsection{Beispiele} -\subsubsection{Die Fakultätsfunktion $n!$} +\subsection{Die Fakultätsfunktion $n!$} Wir berechnen $\cdot!: \mathbb{N} \rightarrow \mathbb{N}$. \begin{minipage}[t]{0.4\textwidth} @@ -87,7 +86,7 @@ sodass bei ihrem Einsatz Speicherverbrauch/Geschwindigkeit eigentlich kein aussc Momentan (Stand 2020) führen beispielsweise Haskell und Scala solche Umwandlungen durch, während beispielsweise C, Java, Python mit call stacks arbeiten. -\subsubsection{Die Fibonacci-Zahlen $\text{fib}(n)$} +\subsection{Die Fibonacci-Zahlen $\text{fib}(n)$} Wir berechnen die Fibonacci-Zahlen durch die Funktion $\text{fib}: \mathbb{N} \rightarrow \mathbb{N}$, welche wie folgt definiert ist: $\text{fib}(n) := \text{fib}(n-1) + \text{fib}(n-2)$. @@ -132,7 +131,7 @@ zudem $\mathcal{O}(2^n)$ (Breitensuche) oder auch ``nur'' $\mathcal{O}(n)$ (Tief Auch funktionale Sprachen haben hier keine wirklich Hebel, die Laufzeit zu verbessern. Diese naive Implementierung ist somit leider für größere $n$ unbrauchbar. Selbst eine moderne CPU berechnet $\texttt{fibrec}(1000)$ nichtmehr in unserer Lebenszeit. -\subsubsection{Die Türme von Hanoi} +\subsubsection{Die Türme von Hanoi*} Ein weiteres klassisches Problem sind die Türme von Hanoi. Wir haben folgende Spielregeln: \begin{itemize} \item $3$ Stäbe $A, B$ und $C$. @@ -142,9 +141,9 @@ Ein weiteres klassisches Problem sind die Türme von Hanoi. Wir haben folgende S \item Es darf nie eine größere auf einer kleineren Scheibe liegen. \end{itemize} -TODO: Kommt das überhaupt dran? +TODO: Wird noch ausgeführt. Oder eine Übungsaufgabe. -\subsubsection{Binärsuche} +\subsection{Binärsuche} Ein sehr praxisnahes Beispiel für Rekursion ist die Binärsuche auf sortierten Arrays. Wir suchen in einem sortierten Array, welches Objekte enthält, die nach dem Schlüssel $k$ sortiert sind, das Objekt mit dem Schlüssel $k_0$. Die Funktion $\texttt{key}(x)$ verrät uns hierbei den Schlüssel eines Objekts $x$. Wir nehmen dabei an, dass das @@ -156,10 +155,10 @@ gesuchte Objekt existiert. \KwOut{Das Objekt mit dem Schlüssel $k_0$} \caption{\texttt{binsearchrec}} $m \leftarrow \floor{\frac{\texttt{len}(\mathcal{A})}{2}}$ \; - \uIf{$\texttt{key}(\mathcal{A}[m]) > k_0$}{ + \uIf{$\emph{\texttt{key}}(\mathcal{A}[m]) > k_0$}{ return $\texttt{binsearchrec}(\mathcal{A}[0,\dots,m])$ \; } - \ElseIf{$\texttt{key}(\mathcal{A}[m]) < k_0$}{ + \ElseIf{$\emph{\texttt{key}}(\mathcal{A}[m]) < k_0$}{ return $\texttt{binsearchrec}(\mathcal{A}[m+1, \dots, \texttt{len}(\mathcal{A})])$ \; } \Else{ @@ -171,10 +170,10 @@ Machen wir zuerst eine Aufwandsanalyse per Hand, bevor wir im nächsten Kapitel Die Laufzeit von \texttt{binsearchrec} hat Unterschiede im best-case oder worst-case Szenario. Best-case wäre, wenn wir schon beim ersten Aufruf zufällig unser gesuchtes Element finden (Aufwand $f \in \mathcal{O}(1)$). -Im pessimistischen Fall teilen wir unser Array so lange, bis nur noch ein Element übrig bleibt (nach Annahme ist es dann -das gesuchte). Würden wir auch die Suche nach nicht-enthaltenen Schlüsseln erlauben, wären wir auch automatisch immer im -worst-case. Betrachten wir hier als $T(n) = T_{\text{worst}}^{\texttt{binsearchrec}}$. Ein Durchlauf von -$\texttt{binsearchrec}$ hat dabei Aufwand $f \in \mathcal{O}(1)$. +Im pessimistischen Fall teilen wir unser Array so lange, bis nur noch ein Element, das Gesuchte, übrig bleibt. +Würden wir auch die Suche nach nicht-enthaltenen Schlüsseln erlauben, wären wir auch immer im +worst-case. Betrachten wir hier also $T(n) = T_{\text{worst}}^{\texttt{binsearchrec}}$. Ein Durchlauf von +$\texttt{binsearchrec}$ hat dabei Aufwand $f \in \mathcal{O}(1)$ sowie den Aufwand rekursiver Aufrufe. \begin{align*} T(n) &= f(n) + T(\floor{\frac{n}{2}}) \\ @@ -184,7 +183,7 @@ $\texttt{binsearchrec}$ hat dabei Aufwand $f \in \mathcal{O}(1)$. &\leq \log_2(n) f(n) \in \mathcal{O}(\ln(n)) \end{align*} -Der asymptotische Aufwand liegt in $T^{\texttt{binsearchrec}} \in \mathcal{O}(\log{n})$, siehe Hauptsatz der +Der asymptotische Aufwand liegt damit in $T_{\text{worst}}^{\texttt{binsearchrec}} \in \mathcal{O}(\log{n})$, siehe Hauptsatz der Laufzeitfunktionen. Damit ist überhaupt erst die Motivation für Sortieralgorithmen gegeben: Suchen in sortierten Arrays geht verdammt schnell. @@ -235,5 +234,9 @@ Greift keiner der drei Fälle, so muss die Laufzeit anders bestimmt werden. Insb $\texttt{fibrec}$ lassen sich nicht mit dem Hauptsatz der Laufzeitfunktionen behandeln, da ihre Teilprobleme kein Bruchteil $\frac{n}{b}$ des Hauptproblems sind. -Wir untersuchen nun als Beispiel die asymptotische Laufzeit von +Wir untersuchen nun als Beispiel die asymptotische Laufzeit von $\texttt{binsearchrec}$ noch einmal. Wir haben pro +Rekursionsschritt ein ($a=1)$ halb so großes Unterproblem ($b=2$), damit also $\log_b a = 0$. Die konstante Funktion +$f$ liegt für ein $ε \in \mathbb{R}^+$ gerade nichtmehr in $\mathcal{O}(n^{log_b(a) - ε})$, aber definitv in +$\mathcal{O}(n^{\log_b a}) = \mathcal{O}(n^0)$, also haben wir den zweiten Fall und unser Aufwand liegt in $Θ(n^{\log_b +a} \log n) = Θ(\log n)$. diff --git a/201_quicksort.tex b/201_quicksort.tex index 73637a36de297c09415ee9c697fd95258454607d..91bf201fa6ddc90e44170fe4f11c07fb89d9a172 100644 --- a/201_quicksort.tex +++ b/201_quicksort.tex @@ -1,10 +1,6 @@ \section{Quicksort} -Quicksort ist einer der meist verwendeten Sortieralgorithmen. Die Kernidee ist wieder das divide-et-impera-Prinzip. -Der Kern des Algorithmus ist die Zerlegung des Arrays, die sogenannte Partition. - -%\begin{center} -%\includegraphics[bb=0cm 0bp 1059bp 324bp,scale=0.2]{bilder/quick} -%\par\end{center} +Quicksort ist einer der meist verwendeten Sortieralgorithmen. Die Idee ist wieder das divide-et-impera-Prinzipr, +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, @@ -76,7 +72,7 @@ Sei der Einfachheit halber $n = 2^{k'}$ für ein $k' \in \mathbb{N}$. &= \ldots \\ &= nT_{\text{qs}}^{\text{best}}(1) + \sum_{k=0}^{\log_2(n)} 2^k T_{\text{p}}^{\text{best}}\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 +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 diff --git a/Datenstrukturen_und_Algorithmen.tex b/Datenstrukturen_und_Algorithmen.tex index 15d45514dd7c8b42c64fc16e053485f09d50c074..4735ceb848591ded6640dfac8c94d4d6eeae0dc9 100644 --- a/Datenstrukturen_und_Algorithmen.tex +++ b/Datenstrukturen_und_Algorithmen.tex @@ -2,6 +2,8 @@ \usepackage[ngerman]{babel} %encoding \usepackage[utf8]{inputenc} +\usepackage{alphabeta} + %math symbols \usepackage{amsmath} @@ -56,6 +58,6 @@ \tableofcontents \input{100_Grundlagen} -%\input{200_Sortieralgorithmen} +\input{200_Sortieralgorithmen} %\input{300_Datenstrukturen} \end{document}