Skip to content

Commit

Permalink
update: 241206
Browse files Browse the repository at this point in the history
  • Loading branch information
V1CeVersaa committed Dec 6, 2024
1 parent c7078ea commit c33985e
Show file tree
Hide file tree
Showing 60 changed files with 1,954 additions and 1,057 deletions.
61 changes: 51 additions & 10 deletions docs/Computer Science/Algorithm/Lec 1.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
# Balanced Tree and Amortized Analysis
# Trees and Amortized Analysis

## 1 Amortized Analysis

$$
\mathrm{worst\ case\ bound} \geq \mathrm{amortized\ bound} \geq \mathrm{average\ case\ bound}
$$

<!-- ?worst-case bound≥amortized bound≥average-case bound -->

### 1.1 聚合分析

### 1.2 核算法
Expand All @@ -14,15 +20,15 @@

AVL 数是一种平衡的二叉搜索树,我们首先约定下面一些东西:

- 空树的高度为 $-1$
- 对于每个节点 `node`,其平衡因子定义为 $\textrm{BF}(\textrm{node})\coloneqq h_L - h_R$;
- 空树的高度为 -1
- 对于每个结点 `node`,其平衡因子定义为 $\textrm{BF}(\textrm{node}) = h_L - h_R$;

所以 AVL 树就是满足以下性质的二叉搜索树:

- 一个空的二叉树是一棵 AVL 树;
- 如果 $T$ 是一棵 AVL 树,那么其左右子树也是 AVL 树,并且 $\lvert\textrm{height}(T_L) - \textrm{height}(T_R)\rvert \leq 1$
- 如果 $T$ 是一棵 AVL 树,那么其左右子树也是 AVL 树,并且 $\lvert\textrm{height}(T_L) - \textrm{height}(T_R)\rvert \leq 1$,也就是其平衡因子的绝对值不超过 1。

二叉树的删除、插入都和一般的二叉搜索树没有差别:插入需要首先进行一次失败的查找来确定插入的位置,删除需要和其后继节点交换之后删除。但是两个操作都可能引起树的不平衡,换句话说就会引起某个节点的平衡因子的绝对值大于 1。这样就需要进行一些旋转操作来保持树的平衡。
二叉树的删除、插入都和一般的二叉搜索树没有差别:插入需要首先进行一次失败的查找来确定插入的位置,删除需要和其后继结点交换之后删除。但是两个操作都可能引起树的不平衡,换句话说就会引起某个结点的平衡因子的绝对值大于 1。这样就需要进行一些旋转操作来保持树的平衡。

### 3.2 Implementation

Expand Down Expand Up @@ -140,11 +146,46 @@ AVL 数是一种平衡的二叉搜索树,我们首先约定下面一些东西

## 4 Red-Black Tree

一棵红黑树是满足下面性质的二叉搜索树:

1. 每个结点或者是红色的,或者是黑色的;
2. 根结点和每个叶结点(`NIL`)是黑色的;
3. 如果一个结点是红色的,那么它的两个子结点都是黑色的;
4. 对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。

这里的叶子结点 `NIL` 被重新定义了,定义成空的没有子结点的黑色结点。同时,我们称 `NIL` 为外部结点,其余有键值的结点为内部结点。合法红黑树不存在只有一个非底部结点(有两个 `NIL` 作为子结点)作为子结点的红色结点。

**性质**:一个有 $N$ 个内部节点(不包括叶子结点)的红黑树,其高度最大为 $2\log_2(N+1)$。首先显然有 $N \geq 2 \mathit{bh} - 1$,也就是 $\mathit{bh} \leq \log_2(N+1)$;然后显然有 $2^\mathit{bh} \geq \mathit{h}(\mathit{Tree})$。这就完成了证明。

**插入**:红黑树插入的核心思路是:先按照二叉搜索树的方式插入,将需要插入的结点插入到红黑树的根部,由于红色结点并不改变红黑树的黑高,所以先将插入的结点染成红色,然后再进行调整,令其满足红黑树的性质。值得注意的是,尽管红黑树有从顶向下的调整方式,但是我们这里讨论的方式都是从底



## 5 B+ Tree

B+ 树是一种多叉排序树,每个节点通常有多个子节点。一棵 B+ 树一般包含三种节点:根节点、内部节点和叶子节点,根节点可能是一个叶子节点,也可能是一个包含两个或者两个以上孩子节点的节点。一个 $M$ 阶的 B+ 树一般满足下面性质:
B+ 树是一种多叉搜索树,每个结点通常有多个子结点。一棵 B+ 树一般包含三种结点:根结点、内部结点和叶子结点,根结点可能是一个叶子结点,也可能是一个包含两个或者两个以上孩子结点的结点。一个 $M$ 阶的 B+ 树一般满足下面性质:

- 根结点要么是叶子结点,要么有 $2$ 到 $M$ 个孩子(至多有 $M$ 个子树,2021 期中);
- 所有非叶子结点(除了根结点)有 $\lceil M/2 \rceil$ 到 $M$ 个孩子;
- 所有叶子结点都在同一层。

对于常见的 $M$,比如 $M = 4$,我们一般称这样的树为一棵 $2$-$3$-$4$ 树。特别的,对于 B+ 树,将它的叶子结点拼接起来,实际上就是一个有序数列。由于 B+ 树在空间最浪费的情况下是一棵 $\lceil M/2 \rceil$ 叉树,所以 B+ 树的深度是 $O(\lceil \log \lceil M/2 \rceil N \rceil)$。

一般来说,B+ 树的每个结点存储的内容分为两部分,一部分是键,另一部分是键分割出的指向子树的指针,比如一棵典型的 $2$-$3$-$4$ 树的结点结构如下:

- 根节点要么是叶子节点,要么有 $2$ 到 $M$ 个孩子;
- 所有非叶子节点(除了根节点)有 $\lceil M/2 \rceil$ 到 $M$ 个孩子;
- 所有叶子节点都在同一层;
- 假设每个非根叶子节点也有 $\lceil M/2 \rceil$ 到 $M$ 个孩子;
B+ 树的插入和查找都非常自然,查找只需要对着非叶子结点的键进行比较,查找正确的子树的位置,直到找到叶子结点,遍历叶子结点的每一个键就可以。决定查找的时间复杂度有两个重要因素:一个是树的高度,另一个是每一层搜索需要的时间。树的高度非常好计算,最差的情况也是每个结点都存 $\lceil M/2 \rceil$ 个结点,因此最大高度是 $O(\log_{\lceil M/2 \rceil} N)$ 的。然后每一层因为键值是排好序的,因此用二分查找找到要去哪个孩子结点,复杂度为 $O(\log_2 M)$,综合可得搜索的时间复杂度为 $O(\log N)$。

插入的方法也很简单,只需要注意一件事:如果这个插入导致了 B+ 树的性质不再成立,即导致其家长结点的子结点数量为 $M+1$ 时,我们需要将这个结点平均分裂成两个,此时显然有两个子树的结点数量都不小于 $\lceil M+1 \rceil$。但这还不够,分裂导致家长结点的家长结点的子结点变多,所以我们还得向上递归。

```c
Btree Insert ( ElementType X, Btree T ) {
Search from root to leaf for X and find the proper leaf node;
Insert X;
while ( this node has M+1 keys ) {
split it into 2 nodes with ceil((M+1)/2) and floor((M+1)/2) keys.
if (this node is the root)
create a new root with two children;
check its parent;
}
}
```
159 changes: 155 additions & 4 deletions docs/Computer Science/Algorithm/Lec 2.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,165 @@
# Heaps

## Mergeable Heaps

## Leftist Heaps

定义每个结点的**零路径长/Null Path Length/**$\operatorname*{NPL}$ 为该结点到没有两个孩子的子结点的长度,并且定义 $\operatorname*{NPL}(\mathrm{null}) = -1$。注意到有下面事情:

$$
\operatorname*{NPL}(p) = \min\left\{\operatorname*{NPL}(\text{left}(p)), \operatorname*{NPL}(\text{right}(p)\right\} + 1
$$

若一个堆满足左倾堆性质,那么对于任意结点 $p$,有其左孩子的 $\operatorname*{NPL}$ 不小于右孩子的 $\operatorname*{NPL}$。那么称这样的堆为一个左顷堆/Leftist Heap。

左顷堆有一个很浅显的性质:若一个左顷堆的右路径上有 $r$ 个结点,这里右路径指的是从根结点到最右下方的孩子的路径,那么这个堆至少会有 $2^r - 1$ 个结点。

左顷堆的核心是合并操作,递归式是通过下面操作完成的:先比较当前两个待合并子树的根结点的键值,选择较小(较大)的那个作为根结点,其左子树依然为左子树,右子树更新为「右子树和另一个待合并子树的合并结果」。在递归地更新完后,需要不断检查左子树和右子树是否满足 $\operatorname*{NPL}_{\text{left child}} \geq \operatorname*{NPL}_{\text{right child}}$​ 的性质,如果不满足,我们则需要交换左右子树来维持性质。

```c
LeftistHeap merge_recursive(LeftistHeap h1, LeftistHeap h2) {
if (h1 == NULL) return h2;
if (h2 == NULL) return h1;
if (h1->key > h2->key) {
// Choose the smaller one as the root
LeftistHeap temp = h1;
h1 = h2;
h2 = temp;
}
h1->right = merge_recursive(h1->right, h2);
// Swap left and right child if needed
if (h1->left == NULL) {
h1->left = h1->right;
h1->right = NULL;
} else {
if (h1->left->npl < h1->right->npl) {
// Swap if NPL(left) < NPL(right)
LeftistHeap temp = h1->left;
h1->left = h1->right;
h1->right = temp;
}
// Update NPL
h1->npl = h1->right->npl + 1;
}
return h1;
}
```
单点插入可以看作是将一个结点作为一个左顷堆,然后与原堆合并的过程。DeleteMin 则是将根结点删除后,将其左右子树合并的过程。没啥可说的。
在递归的过程中,我们发现递归的深度不会超过两个堆的右路径的长度之和,因为每次递归都会使得两个堆的其中一个向着右路径上下一个右孩子推进,并且直到其中一个推到了叶子结点就不再加深递归。此递归向下的过程是 $O(\log n)$ 的。同时每一层的操作也是常数的,因为只需要完成接上指针、判断根结点、交换子树与更新 $\operatorname*{NPL}$ 的操作,所以整体的复杂度是 $O(\log n)$ 的。
单点删除也很简单,只需要将那个结点进行删除,然后其左右子树合并为新的子树(这里记为 $H_{\text{sub}}$,然后将 $H_{\text{sub}}$ 与原堆合并,最后从底向上递归的维护 $\operatorname*{NPL}$ 即可。注意到如果某个堆结点的左右子树都是左顷堆,那么这个结点也是左顷堆,所以我们合并成的 $H_{\text{sub}}$ 既然一定是左顷堆,那么就可以保证整个堆的性质不会被破坏。
## Skew Heaps
### 数据结构介绍
我们回忆 Splay 树和 AVL 树,它们都是通过旋转操作来维持平衡性质的,但是唯一区别就是 AVL 树需要自底向上维持平衡性质,但是 Splay 树就不需要,其无条件的旋转操作提醒我们,或许可以通过无条件地交换左右子树来完成合并操作。具体操作如下:
1. Base Case:若堆 $H$ 与空堆 `null` 连接的情况时,斜堆需要处理 $H$ 的右路径,我们要求 $H$ 右路径上除了最大结点之外都必须交换其左右孩子(但是最大结点显然是右路径的最底端,不可能同时有左右结点,所以其不需要交换,除此之外对右路径上的每个结点,都需要对其左右结点进行交换)。
2. 非 Base Case:若 $H_1$ 的根结点小于 $H_2$,选择好根结点之后,其左子树甩到右边,成为右子树,然后递归地合并右子树和剩下的堆到新的根结点的左子树上。
斜堆一般不考虑单点删除和 DecreaseKey 这两个操作。
### 摊还分析
**Definition**:一个结点 $p$ 被称为**重的/Heavy**,如果果它的右子树结点个数至少是 $p$ 的所有后代的一半(后代包括该结点 $p$ 自身)。反之称为轻结点/Light。
**引理**:对于右路径上有 $l$ 个**轻**结点的斜堆,整个斜堆至少有 $2^l - 1$ 个结点,这意味着**一个** $n$ **个结点的斜堆右路径上的轻结点个数为** $O(\log n)$。
**定理** 若我们有两个斜堆 $H_1$ 和 $H_2$,它们分别有 $n_1$ 和 $n_2$ 个结点,则合并 $H_1$ 和 $H_2$ 的摊还时间复杂度为 $O(\log n)$,其中 $n = n_1 + n_2$。
!!! Note "证明"
有两个非常重要的观察:
- **只有在** $H_1$ 和 $H_2$ **右路径上的结点才可能改变轻重状态**,这是很显然的,因为其它结点合并前后子树是完全被复制的,所以不可能改变轻重状态;
- $H_1$ 和 $H_2$ **右路径上的重结点在合并后一定会变成轻结点**,这是因为右路径上结点一定会交换左右子树,并且后续所有结点也都会继续插入在左子树上(这也表明轻结点不一定变为重结点)。
## Binomial Heaps
### 数据结构介绍和时间复杂度分析
**二项树**通过递归定义:一个只有一个结点的树为一个二项树,记为 $B_0$,其高度为 $0$,阶数也为 $0$;而 $k$ 阶 $B_k$ 为由两个 $B_{k-1}$ 的树根连接而成的树,连接方式是将一个树的树根链接到另外一个树的树根上。二项树 $B_k$ 的根结点有一棵 $B_{k-1}$ 子树,并且根结点的度数为 $k$,高度为 $k$。
$k$ 阶二项树有着一些简单的性质:
- 二项树不是二叉树,是 $N$ 叉树,$N$ 恰好是二项树的阶数;
- $k$ 阶二项树都是同构的,其树根都有 $k$ 个孩子,$2^k$ 个后代结点;
- $k$ 阶二项树的树根的孩子恰好为 $B_0$、$B_1$、$\cdots$、$B_{k-1}$;
- $k$ 阶二项树的深度为 $l$ 的结点恰好有 $\binom{k}{l}$ 个(根结点深度为 $0$)。
**二项堆**是一堆二项树组成的森林,其中每一个二项树都满足堆性质,且每一个二项树都具有不同的高度。
最简单的操作是 FindMin(对应最小堆),只需要遍历二项堆的根结点就可以了,这时候时间复杂度为 $O(\log n)$,也可以通过维护一个指向最小结点的指针来实现 $O(1)$ 的时间复杂度。
其次就是合并 Merge:首先,每个二项堆都唯一对应着一个二进制数,两个二项堆之间的合并可以看作两个二进制数的相加。我们从最低位向最高位进行操作,如果某一位进行的操作相当于 `1 + 1`,那么就会产生进位,这时候我们就需要将进位传递到下一位,这就恰好是两个相同阶数的二项树合并成一个阶数加一的二项树的过程。从二进制加法的角度来看,合并操作的时间复杂度显然是 $O(\log n)$ 的。
插入是合并的特例,相当于将一个结点作为一个二项树,然后与原堆合并,最差情况是不断产生进位,时间复杂度也是 $O(\log n)$。如果一个二项堆中最小的不存在的二项树为 $B_k$,那么插入的时间为 $T_{p} = \mathit(Const)\cdot (k + 1)$。而向一个空的二项堆插入 $n$ 个结点的时间复杂度为 $O(n)$,均摊时间复杂度因此就是常数时间。利用聚合法分析如下:
!!! Note "聚合法均摊分析"
由于插入的操作和二进制数加一有着完全的对应关系,由于 $n$ 有着 $\lfloor\log n\rfloor + 1$ 个二进制比特位,并且每次加一都会有反转比特,每次反转比特对应的堆操作和树操作都是常数时间的。而最低位每次加一都会反转比特,次低位每两次加一就会反转比特,之后同理,所以 $n$ 次操作的整体时间复杂度为
$$
n + \frac{n}{2} + \frac{n}{4} + \cdots + \frac{n}{2^{\lfloor\log n\rfloor + 1}} \to 2n
$$
极限在 $n\to \infty$ 的时候取到,这样就可以获得单步操作的常数摊还时间。
DeleteMin 的时间复杂度也 $O(\log n)$,大概分四步,首先找到最小的根结点与对应的树 $B_k$,然后将 $B_k$ 从二项堆中删除,接着将 $B_k$ 的子树们和原来的二项堆合并。每一步都是 $O(\log n)$ 或 $O(1)$ 的时间复杂度,所以整体的时间复杂度是 $O(\log n)$ 的。
### 二项堆的代码实现
首先,因为每个结点的孩子数量可能不只有两个,因此我们使用 LeftChild 和 NextSibling 的组合实现。
```cpp
BinQueue Merge( BinQueue H1, BinQueue H2 ){
BinTree T1, T2, Carry = NULL;
int i, j;
if ( H1->CurrentSize + H2-> CurrentSize > Capacity ) ErrorMessage();
H1->CurrentSize += H2-> CurrentSize;
for ( i=0, j=1; j<= H1->CurrentSize; i++, j*=2 ) {
T1 = H1->TheTrees[i]; T2 = H2->TheTrees[i]; /* Current Status */
switch( 4*!!Carry + 2*!!T2 + !!T1 ) {
case 0: /* 000 */ break;
case 1: /* 001 */ break;
case 2: /* 010 */ H1->TheTrees[i] = T2; H2->TheTrees[i] = NULL; break;
case 4: /* 100 */ H1->TheTrees[i] = Carry; Carry = NULL; break;
case 3: /* 011 */ Carry = CombineTrees( T1, T2 );
H1->TheTrees[i] = H2->TheTrees[i] = NULL; break;
case 5: /* 101 */ Carry = CombineTrees( T1, Carry );
H1->TheTrees[i] = NULL; break;
case 6: /* 110 */ Carry = CombineTrees( T2, Carry );
H2->TheTrees[i] = NULL; break;
case 7: /* 111 */ H1->TheTrees[i] = Carry;
Carry = CombineTrees( T1, T2 );
H2->TheTrees[i] = NULL; break;
}
}
return H1;
}
```

简单解释如下:

1. `Case 000`:没有树合并,直接 `break`
2. `Case 001`:只有 $H_1$ 有树,不用合并,直接 `break`
3. `Case 010`:只有 $H_2$ 有树,将 $H_2$ 的树移动到 $H_1$ 上;
4. `Case 100`:有进位,将进位树移动到 $H_1$ 上,进位清空;
5. `Case 011`:产生进位,将 $H_1$ 和 $H_2$ 的树合并为 `Carry`,$H_1$ 和 $H_2$ 的树清空;
6. `Case 101`:有进位且产生进位,将 $H_1$ 和进位树合并为 `Carry`,$H_1$ 的树清空;
7. `Case 110`:有进位且产生进位,将 $H_2$ 和进位树合并为 `Carry`,$H_2$ 的树清空;
8. `Case 111`:有进位且产生进位,将进位树移动到 $H_1$ 上,$H_1$ 和 $H_2$ 的树合并为进位树。

## Fibonacci Heap


### Amortized Analysis for Skew Heaps
## Amortized Performance

**Definition**:一个节点 $p$ 被称为**重的/Heavy**,如果
| Operation | Binary Heap | Leftist Heap | Skew Heap | Binomial Heap | Fibonacci Heap |
| ----------- | ------------- | ------------- | ------------- | ------------- | -------------- |
| Insert | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(1)\) | \(O(1)\) |
| Merge | \(O(n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(1)\) |
| DeleteMin | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) | \(O(\log n)\) |
| Delete | \(O(\log n)\) | \(O(\log n)\) | | \(O(\log n)\) | \(O(\log n)\) |
| DecreaseKey | \(O(\log n)\) | \(O(\log n)\) | | \(O(\log n)\) | \(O(1)\) |
Loading

0 comments on commit c33985e

Please # to comment.