Skip to content
Snippets Groups Projects
Commit f1b934a2 authored by Florian Unger's avatar Florian Unger
Browse files

first draft of RBT chapter

parent 253b5e00
No related branches found
No related tags found
No related merge requests found
......@@ -4,4 +4,4 @@
\include{302_Hashtabellen}
\include{303_Binaerbaeume}
\include{304_Halden}
%\include{305_2-4-Baeume}
\include{305_RBT}
\section{Red-Black-Trees (RBT)}
\subsection{Balancierte Bäume und Definition}
Wir haben in vorherigen Beispielen und Übungsaufgaben gesehen, dass Binärbäume leicht degenerieren können und ihre Höhe
dann nicht mehr $h \simeq \log_2(n)$ entspricht. Da alle Baumoperationen in $\mathcal{O}(h)$ liegen, ist das ein fatales
Problem. Der Lösungsansatz liegt auf der Hand: Wir müssen die Bäume balanciert halten.
Dazu gibt es mehrere Ansätze, der älteste ist der AVL-Baum (1962). In dieser Verfeinerung eine Binärbaums wird sofort ein relativ ressourcensparender
Balanciermechanismus, das Rotieren (todo: PIC) gestartet, sobald die Höhe des Linken Unterbaums sich von der Höhe des rechten Unterbaums um
$\leq 2$ unterscheidet. Dieses Modell ist sehr strikt und einfach zu verstehen, aber durch die Striktheit wird der
Balanciermechanismus öfter aktiviert, als eigentlich nötig wäre.
Eine andere Möglichkeit sind $(2-4)$-Bäume. Diese sind keine Binärbäume, sondern jedes nicht-Blatt hat zwei, drei oder
vier Kinder.
Durch das Speichern der Nutzdaten in Blättern ist damit die Höhe $\log_4 n ≤ h ≤ log_2 n$ garantiert.
Wird beim Einfügen die Obergrenze von vier Kindern verletzt, wird der entsprechende Knoten in zwei Knoten
aufgeteilt. Damit kann diese Verletzung nach oben weitergegeben werden, aber nach spätestens $h$ Schritten ist diese
Bedingung wieder erfüllt.
Analog kann beim Löschen eines Blattes die Untergrenze verletzt werden. Zur Korrektur werden nun Nachbarknoten
zusammengelegt, potentiell wieder rekursiv bis zur Wurzel.
Da nicht-Binärbäume aufwendig in der Formalisierung und Implementierung sind, schauen wir uns ein sehr verwandtes Konzept an:
Den Red-Black-Tree.
\begin{definition}[RBT]
Red-Black-Trees (RBT) über einen Datentyp $D$ sind Binäre Suchbäume, bei denen jeder Knoten zusätzlich eine Farbe aus
$\{R,B\}$ abspeichert.
Folgende zwei Invarianzen müssen zudem immer gewahrt werden:
\begin{itemize}
\item[lokal] Ein Roter Knoten darf keinen roten Kindknoten haben.
\item[global] Jeder Pfad von der Wurzel zu einem Blatt durchschreitet die gleiche Anzahl schwarzer Knoten.
\end{itemize}
\end{definition}
Per Konvention nehmen wir an, dass der Wurzelknoten immer Schwarz ist.
Wenn der kürzeste vorstellbare Pfad von der Wurzel zu einem Blatt in einem RBT-Tree durch $l$ ausschließlich durch schwarze Knoten geht, ist der längste
Pfad dann durch $2l$ Knoten, immer einen Schwarzen und einen roten. Dadurch ist gewährleistet, dass das höchste Blatt
maximal halb so hoch hängt wie eins auf der untersten Ebene.
Dadurch folgt:
\begin{lemma}
In einem nichtlehren Red-Black-Tree der Größe $n$ liegt die Höhe $h$ in $Θ(\log n)$.
\label{lemma:RBT_height}
\end{lemma}
Die Details des Beweises sind eine Übungsaufgabe.
\subsection{Einfügen in Red-Black-Trees}
Da RBT insbesondere Binäre Suchbäume sind, sind die Operationen zum Suchen von Elementen, dem Finden des Maximums,
Minimums, Nachfolgers etc identisch.
Auch die grundsätzliche Idee des Einfügens bleibt gleich: Wie bei BST gehen wir von der Wurzel abwärts und suchen dabei
den passenden Platz für das neue Element. Eingefügte Elemente sind also erst einmal Blätter.
Würde man das neue Blatt schwarz färben, wäre sofort die globale Invarianz verletzt. Daher färben wir das neue Blatt
rot. Allerdings kann es dabei zu einer Verletzung der lokalen Invarianz kommen. Skizze \ref{fig:rbt_balance}
veranschaulicht dabei, welche Fälle auftreten können und wie der rebalancierte Baum aussieht:
\begin{figure}[h!]
\centering
\input{bilder/rbt_balance.tex}
\caption{Die potentiellen Verletzung der lokalen Invarianz nach dem Einfügen und wie sie ausbalanciert werden.}
\label{fig:rbt_balance}
\end{figure}
\paragraph{Suchen in (2-4)-Bäumen}
Mit Hilfe der Hilfsinformationen in den inneren Knoten kann, von der Wurzel
absteigend, der entsprechende Wert gesucht werden. Pro Knoten
benötigt der Suchprozess eine konstante Zeit $\mathcal{O}(1)$. Die gesamte
Laufzeit hängt also direkt von der Höhe des Baumes ab und ist damit $Θ(\log n)$.
Wir starten dabei folgenden Algorithmus mit $\texttt{search\_24}(\texttt{root}(\mathcal{B}_{2,4}), v)$.
\begin{algorithm}[h]
\SetNlSty{texttt}{[}{]}
\caption{\texttt{search\_24}($k, v$)}
\KwIn{A node $k$ and and a data value $v$.}
\KwOut{The node $k'$ of the subtree of $k$ which has $\texttt{data}(k') = v$. an error, if there is no such $k'$.}
\eIf{$α(k) = 0$}{
\eIf{$\texttt{data}(k) = v$}{
\KwRet $k$ \;
}{
\texttt{error: value not found} \;
}
}{
\uIf{$\texttt{child}_3(k) \neq \texttt{void\_node}$ and $m_2 < v$}{
\KwRet $\texttt{search\_24}(\texttt{child}_3(k), v)$ \;
}
\uElseIf{$\texttt{child}_2(k) \neq \texttt{void\_node}$ and $m_1 < v$}{
\KwRet $\texttt{search\_24}(\texttt{child}_2(k), v)$ \;
}
\uElseIf{$m_0 < v$}{
\KwRet $\texttt{search\_24}(\texttt{child}_1(k), v)$ \;
}
\Else{
\KwRet $\texttt{search\_24}(\texttt{child}_0(k), v)$ \;
}
}
\end{algorithm}
\paragraph{Einfügen in (2-4)-Bäumen}
Wollen wir in den Baum $\mathcal{B}_{2,4}$ ein Element einfügen, so müssen wir zuerst mittels
$\texttt{search\_insert\_node}$ den richtigen inneren Knoten $k$ in der vorletzten Ebene suchen.
Das geschieht analog zu $\texttt{search\_24}$, nur, dass wir statt dem Blatt bzw Fehler den entsprechenden
Elternknoten inneren Knoten.
Dann fügen wir vermöge $\texttt{insert\_at\_node}(k,l)$ den neuen Knoten $l$ an der passenden Stelle bei $k$ ein (sodass
die Reihenfolge nicht verletzt wird).
Nun müssen wir zwei Fälle unterschieden:
\begin{enumerate}
\item $\alpha(k)\leq 4$ nach dem Einfügen: Damit ist $\mathcal{B}_{2,4}$ weiterhin ein (2-4)-Baum.
\item$\alpha(k)=5$ nach dem Einfügen: Es liegt nun \emph{kein} (2,4)-Baum vor. Um den Fehler zu beheben, führen wir
$\texttt{split}$ aus. Dieser Algorithmus spaltet die zwei rechtesten Knoten von $k$ ab in einen neuen Knoten
$k'$, welcher rechts von $k$ am gleichen Elternknoten eingehängt wird.
Dabei kann es vorkommen, dass auch der Elterknoten nun $5$ Kinder hat, weswegen wir unter Umständen
$\texttt{split}$ wiederholt aufrufen müssen, bis wir an der Wurzel sind. Muss auch die Wurzel aufgespalten werden,
wird eine neue Wurzel definiert, die dann die zwei Kinder $k$ und $k'$ hat.
\end{enumerate}
\begin{center}
\includegraphics[scale=0.2]{bilder/Spalten}
\par\end{center}
In Pseudocode:
\begin{algorithm}[h]
\SetNlSty{texttt}{[}{]}
\caption{\texttt{insert\_24}($\mathcal{B}_{2,4}, k, v$)}
\KwIn{A (2-4)-Tree $\mathcal{B}_{2,4}$, the intended target node $k$ and a new value $v$ to be inserted.}
\KwOut{Side effects on the memory: $\mathcal{B}_{2,4}$ contains now $v$ in its correct place. Several splits of inner
nodes may happen to ensure (2-4)-integrity.}
$l = \text{new node with } \texttt{parent}(l) = k \text{ and } \texttt{data}(l) = v$ \;
$\texttt{insert\_at\_node}(k,l)$ \;
\uIf{$α(k) = 5$}{
$\texttt{split}(\mathcal{B}_{2,4}, k)$ \;
}
\end{algorithm}
\begin{algorithm}[h]
\SetNlSty{texttt}{[}{]}
\caption{\texttt{split}($\mathcal{B}_{2,4}, k$)}
\KwIn{An almost-(2-4)-Tree $\mathcal{B}_{2,4}$ which is violated in node $k$ (too many child nodes).}
\KwOut{Side effects on the memory: $\mathcal{B}_{2,4}$ is modified by splitting the violating nodes recursivly. After
that, $\mathcal{B}_{2,4}$ is a flawless (2-4)-tree again.}
\uIf{$k = \texttt{root}(\mathcal{B}_{2,4})$}{
$r = \text{new node with } \texttt{child}_0(r) = k$ \;
$\texttt{parent}(k) \leftarrow r$ \;
$\texttt{root}(\mathcal{B}_{2,4}) \leftarrow r$ \;
}
$k' = \text{new node with } \texttt{parent}(k') = \texttt{parent}(k)$ \;
$\texttt{child}_0(k') \leftarrow \texttt{child}_3(k)$ \;
$\texttt{child}_3(k) \leftarrow \texttt{void\_node}$ \;
$\texttt{child}_1(k') \leftarrow \texttt{child}_4(k)$ \;
$\texttt{child}_4(k) \leftarrow \texttt{void\_node}$ \;
\texttt{insert\_at\_node}(\texttt{parent}(k), k') \;
\uIf{$α(\texttt{parent}(k)) = 5$}{
$\texttt{split}(\mathcal{B}_{2,4}, \texttt{parent}(k))$ \;
}
\end{algorithm}
Der Aufwand für \texttt{insert\_24} beträgt im besten Fall $\mathcal{O}(1)$ und im schlimmsten Fall
$\mathcal{O}(\log n)$, falls wir rekursiv bis zur Wurzel spalten müssen.
Man kann aber mittels aggregierter Analyse schnell zeigen, dass der amortisierte Aufwand fürs Einfügen nur
$\mathcal{O}(1)$ beträgt.
\subsubsection{Entfernen in (2-4)-Bäumen}
Auch hier wird zuerst der entsprechende Knoten $k$ gesucht. Nach dem Entfernen
muss man wieder zwei Fälle unterscheiden:
\begin{enumerate}
\item Ist $\alpha(k)\geq 2$ nach dem Entfernen, so haben wir wieder einen (2-4)-Baum.
\item Ist $\alpha(k)=1$ nach dem Entfernen: Es liegt nun \emph{kein} (2,4)-Baum mehr vor. Unser Knoten $k$ muss sich
nun Kinder von anderen Knoten besorgen. Es wird zuerst ein Knoten $k'$ ausgewählt, der ein direkter Bruder von
$k$ ist: Ist $k = \texttt{child}_i(\texttt{parent}(k))$, so ist $k' = \texttt{child}_{j}(\texttt{parent}(k))$ mit $j
\in \{i-1,i+1\}$, sofern vorhanden. Ein Bruderknoten muss immer vorhanden sein, da $\texttt{parent}(k)$ immernoch
der (2-4)-Bedinungen genügen muss und mindestens 2 Kinder hat.
Hat man $k'$ ausgewählt und hat $k'$ genug Kinder ($α(k') \geq 3$), so stiehlt man nun das nächstgrößere / nächstkleinere
Blatt von $\texttt{child}_0(k)$ von $k'$ und hängt es bei $k$ ein. Damit hat $k$ wieder $2$ Kinder und der
(2-4)-Baum ist repariert.
Hat $k'$ allerdings selber nur $α(k')=2$ Kinder, so würde ein Stehlen die $(2-4)-Baum-Bedingung$ bei $k'$
verletzten. Deswegen verschmilzt man $k$ und $k'$ zu einem Knoten. Allerdings kann das, analog zu \texttt{split},
bewirken, dass $\texttt{parent}(k)$ nun auch zu wenig Knoten hat. Deswegen muss auch die verschmelz-Operation potentiell
$h \in Θ(\log n)$ mal aufgerufen werden.
\end{enumerate}
\begin{center}
\includegraphics[scale=0.2]{bilder/Stehlen}
\par\end{center}
\begin{center}
\includegraphics[scale=0.2]{bilder/Verschmelzen}
\par\end{center}
Aufgrund der vielen zu behandelnden Fallunterscheidungen verzichten wir hier auf Pseudocode.
\subsection{Sortieren mit (2-4)-Bäumen}
Eine primitive Methode des Sortierens mit (2-4)-Bäumen ist schnell gefunden. Wir nehmen unser zu sortierendes Array und
fügen es von Anfang bis Ende in einen anfangs leeren (2-4)-Baum ein. Der Algorithmus ist worst-case-optimal, hat aber
ansonsten keine weiteren erstrebenswerten Eigenschaften.
In unserem Algorithmus \texttt{24treesort} gehen wir ein bisschen geschickter vor und erreichen dadurch Adaptivität. Sei
also ein zu sortierendes Array $\mathcal{A} = [a_0, \dots, a_{n-1}]$ gegeben, welches wir nun von hinten nach vorne,
also $a_{n-1}$ bis $a_0]$, in einen anfangs leeren (2-4)-Baum einfügen.
Die Kernidee ist nun, die Suche nach dem passenden Einfügeort nicht von der Wurzel, sondern von dem bisher
linkesten inneren Knoten $k$ zu beginnen! Ist $\mathcal{A}$ nun bereits sortiert, folgen in unserer umgekehrten
Einfügereihenfolge also immer kleinere Elemente und unser $k$ ist direkt das richtige $k$ für \texttt{insert\_24}.
Damit müssen wir nicht aufwendig den Einfügeort $k$ für \texttt{insert\_24} suchen, sondern können direkt einfügen, was
einem amortisierter Aufwand von $\mathcal{O}(1)$ entspricht. Bei $n$ Einfügeoperationen haben wir im best-case also $\mathcal{O}(n)$ Aufwand.
Haben wir nun aber ein Element $a_i$ mit Fehlstellung $f_i$, so müssen wir den Baum aufsteigen, bis wir zu einem
passenden Knoten $k'$ kommen ($k'$ hat $m_0 > a_i$) und von dort wieder absteigen.
Wie viele Knoten müssen wir aber hoch? Da alle Elemente $a_{n-1}$ bis $a_{i+1}$ bereits sortiert sind, müssen wir nur
genau $f_i$ viele Elemente überspringen, also maximal $\log_2 f_i$ Knoten aufsteigen und wieder absteigen. Dies entspricht
also dem Zusatzaufwand $\mathcal{O}(\log f_i)$ für jede Fehlstellung.
Damit ist der Zeitaufwand für alle $n$ Elemente
\[
\mathcal{O}(n + \sum_{i=1}^n \log f_i) = \mathcal{O}(n + n \log \left( \frac{F}{n} \right)),
\]
wobei wir für die Umformung $\sum_{i=1}^n \log f_i = \log Π_{i=1}^n f_i \leq \log \left( \frac{F}{n} \right)^n = n \log
\left( \frac{F}{n} \right)$ benutzen.
Damit sind die Bedingungen für Adaptivität auch im strengeren Sinne erfüllt.
TODO: Pseudocode für \texttt{24treesort}, falls es sich lohnt.
\tikzsetnextfilename{rbt_balance}
\begin{tikzpicture}[
treenode/.style = {align=center, inner sep=0pt, text centered, font=\sffamily},
node_black/.style = {treenode, circle, white, font=\sffamily\bfseries, draw=black, %TODO: Make font thick.
fill=black, text width=1.5em},% arbre rouge noir, noeud noir
node_red/.style = {treenode, circle, red, draw=red,
text width=1.5em, very thick},% arbre rouge noir, noeud rouge
subtree/.style = {treenode, circle, text width=1.5em},
level/.style={sibling distance = 5cm/#1,
level distance = 1.5cm},
scale=0.4
]
\begin{scope} % transformation target
\node [node_red] (ty) {$y$}
child{ node [node_black] (tx) {$x$}
child{ node [subtree] (ta) {$a$}}
child{ node [subtree] (tb) {$b$}}}
child{ node [node_black] (tz) {$z$}
child{ node [subtree] (tc) {$c$}}
child{ node [subtree] (td) {$d$}}}
;
\end{scope}
\begin{scope}[shift={(10,0)}] % case 1: right-right
\node [node_black] (c1x) {$x$}
child{ node [subtree] (c1a) {$a$}}
child{ node [node_red] (c1y) {$y$}
child{ node [subtree] (c1b) {$b$}}
child{ node [node_red] (c1z) {$z$}
child{ node [subtree] (c1c) {$c$}}
child{ node [subtree] (c1d) {$d$}}}}
;
\end{scope}
\begin{scope}[shift={(0,8.5)}] % case 2: left-right
\node [node_black] (c2z) {$z$}
child{ node [node_red] (c2x) {$x$}
child{node [subtree] (c2a) {$a$}}
child{ node [node_red] (c2y) {$y$}
child {node [subtree] (c2c) {$c$}}
child {node [subtree] (c2b) {$b$}}}}
child{ node [subtree] (c2d) {$d$}}
;
\end{scope}
\begin{scope}[shift={(-10,0)}] % case 3: left-left
\node [node_black] (c3z) {$z$}
child{ node [node_red] (c3y) {$y$}
child{ node [node_red] (c3x) {$x$}
child {node [subtree] (c3a) {$a$}}
child {node [subtree] (c3b) {$b$}}}
child {node [subtree] (c3c) {$c$}}}
child{ node [subtree] (c3d) {$d$}}
;
\end{scope}
\begin{scope}[shift={(0,-7.5)}] % case 4: right-left
\node [node_black] (c4x) {$x$}
child{ node [subtree] (c4a) {$a$}}
child{ node [node_red] (c4z) {$z$}
child{ node [node_red] (c4y) {$y$}
child{ node [subtree] (c4b) {$b$}}
child{ node [subtree] (c4c) {$c$}}}
child{ node [subtree] (c4d) {$d$}}}
;
\end{scope}
\end{tikzpicture}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment