Skip to content

Commit

Permalink
Added imperative skewheap implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
liuxinyu95 committed Apr 17, 2023
1 parent 32289f4 commit e9d760e
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 25 deletions.
16 changes: 8 additions & 8 deletions datastruct/heap/binary-heap/bheap-en.tex
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ \section{Definition}
\section{Binary heap by array}
\label{ibheap} \index{binary heap by array} \index{complete binary tree}

The first implementation is to represent the a complete binary tree with an array. The complete binary tree is `almost' full. The full binary tree of depth $k$ contains $2^k - 1$ nodes. We can number every node top-down, from left to right as 1, 2, ..., $2^k -1$. The node number $i$ in the complete binary tree is located at the same position in the full binary tree. The leaves only appear in the bottom layer, or the second last layer. Figure \cref{fig:tree-array-map} shows a complete binary tree and the array. As the complete binary tree, the $i$-th cell in array corresponds to a node, its parent node maps to the $\lfloor i/2 \rfloor$-th cell; the left sub-tree maps to the $2i$-th cell, and the right sub-tree maps to the $2i + 1$-th cell. If any sub-tree maps to an index out of the array bound, then the sub-tree does not exist (i.e. leaf node). We can define the map as below (index starts from 1):
The first implementation is to represent the a complete binary tree with an array. The complete binary tree is `almost' full. The full binary tree of depth $k$ contains $2^k - 1$ nodes. We can number every node top-down, from left to right as 1, 2, ..., $2^k -1$. The node number $i$ in the complete binary tree is located at the same position in the full binary tree. The leaves only appear in the bottom layer, or the second last layer. \Cref{fig:tree-array-map} shows a complete binary tree and the array. As the complete binary tree, the $i$-th cell in array corresponds to a node, its parent node maps to the $\lfloor i/2 \rfloor$-th cell; the left sub-tree maps to the $2i$-th cell, and the right sub-tree maps to the $2i + 1$-th cell. If any sub-tree maps to an index out of the array bound, then the sub-tree does not exist (i.e. leaf node). We can define the map as below (index starts from 1):

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -93,7 +93,7 @@ \subsection{Heapify}
\EndFunction
\end{algorithmic}

For index $i$ in array $A$, any sub-tree node should not be less than $A[i]$. Otherwise, we exchange $A[i]$ with the smallest one, and recursively check the sub-trees. As the process time is proportion to the height of the tree, \textproc{Heapify} is bound to $O(\lg n)$, where $n$ is the length of the array. Figure \cref{fig:heapify} gives the steps when apply \textproc{Heapify} from $2$ to array [1, 13, 7, 3, 10, 12, 14, 15, 9, 16]. The result is [1, 3, 7, 9, 10, 12, 14, 15, 13, 16].
For index $i$ in array $A$, any sub-tree node should not be less than $A[i]$. Otherwise, we exchange $A[i]$ with the smallest one, and recursively check the sub-trees. As the process time is proportion to the height of the tree, \textproc{Heapify} is bound to $O(\lg n)$, where $n$ is the length of the array. \Cref{fig:heapify} gives the steps when apply \textproc{Heapify} from $2$ to array [1, 13, 7, 3, 10, 12, 14, 15, 9, 16]. The result is [1, 3, 7, 9, 10, 12, 14, 15, 13, 16].

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -130,7 +130,7 @@ \subsection{Build}
\label{eq:build-heap-2}
\ee

Subtract (\cref{eq:build-heap-1}) from (\cref{eq:build-heap-2}):
Subtract \cref{eq:build-heap-1} from \cref{eq:build-heap-2}:

\bea*{rcll}
2S - S & = & n [\dfrac{1}{2} + (2 \dfrac{1}{4} - \dfrac{1}{4}) + (3 \dfrac{1}{8} - 2 \dfrac{1}{8}) + ...] & \text{shift by one and subtract} \\
Expand All @@ -139,7 +139,7 @@ \subsection{Build}
\eea*


Figure \cref{fig:build-heap-3} shows the steps to build a min-heap from array $[4, 1, 3, 2, 16, 9, 10, 14, 8, 7]$. The black node is where \textproc{Heapify} is applied. The grey nodes are swapped to maintain the heap property.
\Cref{fig:build-heap-3} shows the steps to build a min-heap from array $[4, 1, 3, 2, 16, 9, 10, 14, 8, 7]$. The black node is where \textproc{Heapify} is applied. The grey nodes are swapped to maintain the heap property.

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -292,7 +292,7 @@ \subsection{Heap sort}
\section{Leftist heap and skew heap}
\label{ebheap}

When implement the heap with a explicit binary tree, after pop the rot, there remain two sub-trees. Both are heaps as shown in figure \cref{fig:lvr}. How can we merge them to a new heap? To maintain the heap property, the new root must be the minimum for the remaining. We can give the first edge cases easily:
When implement the heap with a explicit binary tree, after pop the rot, there remain two sub-trees. Both are heaps as shown in \cref{fig:lvr}. How can we merge them to a new heap? To maintain the heap property, the new root must be the minimum for the remaining. We can give the first edge cases easily:

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -443,7 +443,7 @@ \subsubsection{Heap sort}
\subsection{Skew heap}
\label{skew-heap} \index{Skew heap}

Leftist heap may lead to unbalanced tree in some cases as shown in figure \cref{fig:unbalanced-leftist-tree}. Skew heap is a self-adjusting heap. It simplifies the leftist heap and improves balance\cite{wiki-skew-heap} \cite{self-adjusting-heaps}. When build the leftist heap, we swap the left and right sub-trees when the rank on left is smaller than the right. However, this method can't handle the case when either sub-tree has a NIL node. The rank is always 1 no matter how big the sub-tree is. Skew heap simplified the merge, it always swap the left and right sub-trees.
Leftist heap may lead to unbalanced tree in some cases as shown in \cref{fig:unbalanced-leftist-tree}. Skew heap is a self-adjusting heap. It simplifies the leftist heap and improves balance\cite{wiki-skew-heap} \cite{self-adjusting-heaps}. When build the leftist heap, we swap the left and right sub-trees when the rank on left is smaller than the right. However, this method can't handle the case when either sub-tree has a NIL node. The rank is always 1 no matter how big the sub-tree is. Skew heap simplified the merge, it always swap the left and right sub-trees.

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -475,7 +475,7 @@ \subsubsection{Merge}
\end{array}
\ee

Similar with leftist tree, the other operations, including insert, top, and pop are implemented with $merge$. Skew heap outputs a balanced tree even for ordered list as shown in figure \cref{fig:skew-tree}.
Similar with leftist tree, the other operations, including insert, top, and pop are implemented with $merge$. Skew heap outputs a balanced tree even for ordered list as shown in \cref{fig:skew-tree}.

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -560,7 +560,7 @@ \subsection{Splay}
\label{fig:splay-result}
\end{figure}

Figure \cref{fig:splay-result} gives the splay tree built from $[1, 2, ..., 10]$. It generates a well balanced tree. Okasaki found a simple rule for splaying \cite{okasaki-book}. Whenever we follow two left branches, or two right branches continuously, we rotate the two nodes. When access node of $x$, if move to left or right twice, then partition $T$ as $L$ and $R$, where $L$ contains all the elements less than $x$, while $R$ contains the remaining. Then we create a new tree with $x$ as the root, and $L$, $R$ as the left and right sub-trees. The partition process is recursively applied to sub-trees.
\Cref{fig:splay-result} gives the splay tree built from $[1, 2, ..., 10]$. It generates a well balanced tree. Okasaki found a simple rule for splaying \cite{okasaki-book}. Whenever we follow two left branches, or two right branches continuously, we rotate the two nodes. When access node of $x$, if move to left or right twice, then partition $T$ as $L$ and $R$, where $L$ contains all the elements less than $x$, while $R$ contains the remaining. Then we create a new tree with $x$ as the root, and $L$, $R$ as the left and right sub-trees. The partition process is recursively applied to sub-trees.

\be
\resizebox{\linewidth}{!}{\ensuremath{
Expand Down
24 changes: 12 additions & 12 deletions datastruct/heap/binary-heap/bheap-zh-cn.tex
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ \section{定义}
\section{由数组实现的隐式二叉堆}
\label{ibheap} \index{隐式二叉堆} \index{完全二叉树}

第一种实现称为“隐式二叉树”。它用数组来表示一棵完全二叉树。所谓完全二叉树,是一种“几乎”满的二叉树。深度为$k$的满二叉树含有$2^k-1$个节点。如果将每个节点从上到下,从左向右编号为1,2, ..., $2^k - 1$,则完全二叉树中编号为$i$的节点和满二叉树中编号为$i$的节点在树中的位置相同。完全二叉树的叶子节点仅出现在最下面一行和倒数第二行。\cref{fig:tree-array-map}给出了一棵完全二叉树和相应的数组表示形式。由于二叉树是完全的,对数组中第$i$个元素代表的节点,它的父节点定位到第$\lfloor i/2 \rfloor$个元素;左子树对应第$2i$个元素,而右子树对应第$2i+1$个元素。如果子节点的索引超出了数组的长度,说明它不含有相应的子树(例如叶子节点)。树和数组之间的映射可以定义如下(令数组的索引从1开始):
第一种实现称为“隐式二叉树”。它用数组来表示一棵完全二叉树。所谓完全二叉树,是一种“几乎”满的二叉树。深度为$k$的满二叉树含有$2^k-1$个节点。如果将每个节点从上到下,从左向右编号为1,2, ..., $2^k - 1$,则完全二叉树中编号为$i$的节点和满二叉树中编号为$i$的节点在树中的位置相同。完全二叉树的叶子节点仅出现在最下面一行和倒数第二行。\cref{fig:tree-array-map}给出了一棵完全二叉树和相应的数组表示形式。由于二叉树是完全的,对数组中第$i$个元素代表的节点,它的父节点定位到第$\lfloor i/2 \rfloor$个元素;左子树对应第$2i$个元素,而右子树对应第$2i+1$个元素。如果子节点的索引超出了数组的长度,说明它不含有相应的子树(例如叶子节点)。树和数组之间的映射可以定义如下(令数组的索引从1开始):

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -94,7 +94,7 @@ \subsection{堆调整}
\EndFunction
\end{algorithmic}

对于数组$A$和索引$i$,堆性质要求$A[i]$的子节点都不应比它小。否则,我选出最小的元素保存在$A[i]$,并将较大的元素交换至子树,然后自顶向下检查并调整堆,使得所有子树都满足堆性质。\textproc{Heapify}的时间复杂度为$O(\lg n)$,其中$n$是元素个数。这是因为算法中的循环次数和完全二叉树的高度成正比。\cref{fig:heapify}描述了\textproc{Heapify}从索引2开始,按照小顶堆调整数组[1, 13, 7, 3, 10, 12, 14, 15, 9, 16]的步骤。数组最终变换为[1, 3, 7,9, 10, 12, 14, 15, 13, 16]。
对于数组$A$和索引$i$,堆性质要求$A[i]$的子节点都不应比它小。否则,我选出最小的元素保存在$A[i]$,并将较大的元素交换至子树,然后自顶向下检查并调整堆,使得所有子树都满足堆性质。\textproc{Heapify}的时间复杂度为$O(\lg n)$,其中$n$是元素个数。这是因为算法中的循环次数和完全二叉树的高度成正比。\cref{fig:heapify}描述了\textproc{Heapify}从索引2开始,按照小顶堆调整数组[1, 13, 7, 3, 10, 12, 14, 15, 9, 16]的步骤。数组最终变换为[1, 3, 7,9, 10, 12, 14, 15, 13, 16]。

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -131,15 +131,15 @@ \subsection{构造堆}
\label{eq:build-heap-2}
\ee

用式(\cref{eq:build-heap-2})减去式(\cref{eq:build-heap-1},我们有:
\cref{eq:build-heap-2}减去\cref{eq:build-heap-1},我们有:

\bea*{rcll}
2S - S & = & n [\dfrac{1}{2} + (2 \dfrac{1}{4} - \dfrac{1}{4}) + (3 \dfrac{1}{8} - 2 \dfrac{1}{8}) + ...] & \text{错开第一项两两相减} \\
S & = & n [\dfrac{1}{2} + \dfrac{1}{4} + \dfrac{1}{8} + ...] & \text{等比级数和}\\
& = & n
\eea*

\cref{fig:build-heap-3}描述了从数组$[4, 1, 3, 2, 16, 9, 10, 14, 8, 7]$构造小顶堆的过程。黑色表示执行\textproc{Heapify}的目标节点;灰色表示进行交换的节点。
\cref{fig:build-heap-3}描述了从数组$[4, 1, 3, 2, 16, 9, 10, 14, 8, 7]$构造小顶堆的过程。黑色表示执行\textproc{Heapify}的目标节点;灰色表示进行交换的节点。

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -197,7 +197,7 @@ \subsubsection{Top-k}
\subsubsection{提升优先级}
\index{二叉堆!提升优先级}

堆的一个应用是实现带有优先级的任务调度,称为“优先级队列”。将若干带有优先级的任务放入堆中,每次从堆顶取出优先级最高的任务执行。为了尽早执行堆中的某个任务,可以提升它的优先级。对于小顶堆,这意味着减小某个元素的值,如图\cref{fig:decrease-key-2}所示。
堆的一个应用是实现带有优先级的任务调度,称为“优先级队列”。将若干带有优先级的任务放入堆中,每次从堆顶取出优先级最高的任务执行。为了尽早执行堆中的某个任务,可以提升它的优先级。对于小顶堆,这意味着减小某个元素的值,\cref{fig:decrease-key-2}所示。

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -293,7 +293,7 @@ \subsection{堆排序}
\section{左偏堆和斜堆}
\label{ebheap}

考虑不用数组而用显式的二叉树实现堆。当弹出堆顶元素后,剩余部分是左右两棵子树,它们都是堆,如图\cref{fig:lvr}所示。我们如何将它们再次合并成一个堆呢?为保持堆性质,新的根节点必须是剩余元素中最小的。我们可以先给出两个特殊情况下的结果:
考虑不用数组而用显式的二叉树实现堆。当弹出堆顶元素后,剩余部分是左右两棵子树,它们都是堆,\cref{fig:lvr}所示。我们如何将它们再次合并成一个堆呢?为保持堆性质,新的根节点必须是剩余元素中最小的。我们可以先给出两个特殊情况下的结果:

\begin{figure}[htbp]
\centering
Expand All @@ -315,7 +315,7 @@ \section{左偏堆和斜堆}
\subsection{左偏堆}
\index{左偏堆} \index{左偏堆!秩} \index{左偏堆!S-值}

使用左偏树实现的堆称为左偏堆。左偏树最早由C. A. Crane于1972年引入\cite{wiki-leftist-tree}。树中每个节点都定义了一个秩,也称作$S$值。节点的秩是到最近的NIL节点的距离。而NIL节点的秩等于0。如图\cref{fig:rank}所示,距离根节点4最近的叶子节点为8,所以根节点的秩为2。节点6和8都是叶子,它们的秩为1。虽然节点5的左子树不为空,但是它的右子树为空,因此秩等于1。使用秩,我们可以定义合并策略如下,记左右子树的秩分别为$r_l$$r_r$
使用左偏树实现的堆称为左偏堆。左偏树最早由C. A. Crane于1972年引入\cite{wiki-leftist-tree}。树中每个节点都定义了一个秩,也称作$S$值。节点的秩是到最近的NIL节点的距离。而NIL节点的秩等于0。\cref{fig:rank}所示,距离根节点4最近的叶子节点为8,所以根节点的秩为2。节点6和8都是叶子,它们的秩为1。虽然节点5的左子树不为空,但是它的右子树为空,因此秩等于1。使用秩,我们可以定义合并策略如下,记左右子树的秩分别为$r_l$$r_r$

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -412,7 +412,7 @@ \subsubsection{插入}
insert\ k\ H = merge\ (1, \nil, k, \nil)\ H
\ee

或写成柯里化的形式$insert\ k\ = merge\ (1, \nil, k, \nil)$。这样插入的时间复杂度也和合并相同,为$O(\lg n)$。我们可以将一个列表中的元素依次插入,从列表构造左偏堆。\cref{fig:leftist-tree}给出了一个构造左偏堆的例子。
或写成柯里化的形式$insert\ k\ = merge\ (1, \nil, k, \nil)$。这样插入的时间复杂度也和合并相同,为$O(\lg n)$。我们可以将一个列表中的元素依次插入,从列表构造左偏堆。\cref{fig:leftist-tree}给出了一个构造左偏堆的例子。

\be
build\ L = fold_r\ insert\ \nil\ L
Expand Down Expand Up @@ -449,7 +449,7 @@ \subsubsection{堆排序}
\subsection{斜堆}
\label{skew-heap} \index{斜堆}

左偏堆在某些情况下会产生不平衡的结构,如图\cref{fig:unbalanced-leftist-tree}所示。斜堆(skew heap)是一种自调整堆,它简化了左偏堆的实现并提高了平衡性\cite{wiki-skew-heap}、\cite{self-adjusting-heaps}。构造左偏堆时,若左侧的秩小于右侧,则交换左右子树。但是这一策略不能很好处理某一分支含有一个NIL子节点的情况:不管树有多大,它的秩总为1。斜堆简化了合并策略:每次都交换左右子树。
左偏堆在某些情况下会产生不平衡的结构,\cref{fig:unbalanced-leftist-tree}所示。斜堆(skew heap)是一种自调整堆,它简化了左偏堆的实现并提高了平衡性\cite{wiki-skew-heap}、\cite{self-adjusting-heaps}。构造左偏堆时,若左侧的秩小于右侧,则交换左右子树。但是这一策略不能很好处理某一分支含有一个NIL子节点的情况:不管树有多大,它的秩总为1。斜堆简化了合并策略:每次都交换左右子树。

\begin{figure}[htbp]
\centering
Expand Down Expand Up @@ -481,7 +481,7 @@ \subsubsection{合并}
\end{array}
\ee

其他的操作,包括插入,获取和弹出顶部都和左偏树一样通过合并来实现。即使用斜堆处理已序序列,结果仍然是一棵较平衡的二叉树,如图\cref{fig:skew-tree}所示。
其他的操作,包括插入,获取和弹出顶部都和左偏树一样通过合并来实现。即使用斜堆处理已序序列,结果仍然是一棵较平衡的二叉树,\cref{fig:skew-tree}所示。

\begin{figure}[htbp]
\centering
Expand All @@ -498,7 +498,7 @@ \section{伸展堆}
\subsection{伸展操作}
\index{伸展堆!splay}

有两种方法可以实现伸展操作。第一种利用模式匹配,但需要处理较多的情况;第二种具备统一的形式,但是实现较为复杂。记正在访问的节点元素为$x$,它的父节点元素为$p$,如果存在祖父节点,其元素记为$g$。伸展操作分为三个步骤,每个步骤有两个对称的情况,我们以每步中的一种情况举例说明,如图\cref{fig:splay}所示。
有两种方法可以实现伸展操作。第一种利用模式匹配,但需要处理较多的情况;第二种具备统一的形式,但是实现较为复杂。记正在访问的节点元素为$x$,它的父节点元素为$p$,如果存在祖父节点,其元素记为$g$。伸展操作分为三个步骤,每个步骤有两个对称的情况,我们以每步中的一种情况举例说明,\cref{fig:splay}所示。

\begin{enumerate}
\item zig-zig步骤,$x$$p$都是左子树或者$x$$p$都是右子树。我们通过两次旋转,将$x$变成根节点。
Expand Down Expand Up @@ -566,7 +566,7 @@ \subsection{伸展操作}
\label{fig:splay-result}
\end{figure}

\cref{fig:splay-result}给出了逐一插入$[1, 2, ..., 10]$的结果。伸展树产生了较平衡的结果。冈崎发现了一条简单的伸展操作规则\cite{okasaki-book}:每次连续向左或者向右访问两次的时候,就旋转节点。当访问节点$x$的时候,如果连续向左侧或者右侧前进两次,我们将树$T$分割成两部分:$L$$R$,其中$L$中的所有元素小于$x$$R$中的元素都大于$x$。然后以$x$为根,$L$$R$为左右子树构建一棵新树。分割过程递归地对子树进行伸展操作。
\cref{fig:splay-result}给出了逐一插入$[1, 2, ..., 10]$的结果。伸展树产生了较平衡的结果。冈崎发现了一条简单的伸展操作规则\cite{okasaki-book}:每次连续向左或者向右访问两次的时候,就旋转节点。当访问节点$x$的时候,如果连续向左侧或者右侧前进两次,我们将树$T$分割成两部分:$L$$R$,其中$L$中的所有元素小于$x$$R$中的元素都大于$x$。然后以$x$为根,$L$$R$为左右子树构建一棵新树。分割过程递归地对子树进行伸展操作。

\be
\resizebox{\linewidth}{!}{\ensuremath{
Expand Down
Loading

0 comments on commit e9d760e

Please sign in to comment.