Skip to content

Commit

Permalink
update: 250102
Browse files Browse the repository at this point in the history
  • Loading branch information
V1CeVersaa committed Jan 2, 2025
1 parent 83a5526 commit aeecdd3
Show file tree
Hide file tree
Showing 23 changed files with 22,370 additions and 31 deletions.
78 changes: 70 additions & 8 deletions docs/Computer Science/Algorithm/Lec 1.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,25 +12,50 @@ $$

### 1.2 核算法

我们希望计算平均成本,完美的平均成本就是完美的截长补短,但是显然完美的截长补短很难做到,而摊还分析要求我们的结果是一个上界,截长补短的结果要比原来长,这样就有下面:记第 i 次操作的真实成本为 $c_i$,截长补短的摊还成本为 $\hat{c_i} = c_i + \Delta_i$,其中 $\Delta_i$ 就是截的长(负值),或者补的短(正值),并且我们还要保证摊还成本比平均成本大,这样我们分析出来的摊还成本才是平均成本的上界,即我们要求

$$
\sum_{i=1}^{n} \hat{c_i} \geq \sum_{i=1}^{n} c_i \Leftrightarrow \sum_{i=1}^{n} \Delta_i \geq 0
$$

对于经典的栈操作而言:

- 原来的 Push 操作的成本是 1,Pop 操作的成本也是 1,MultiPop 操作的成本是 $\min(k, \operatorname*{size}(S))$;
- 摊还后的 Push 操作的成本是 2,Pop 操作的成本是 0,MultiPop 操作的成本是 0。

由于 $\operatorname*{size}(S) \geq 0$,所以我们可以证明 $\sum_{i=1}^{n} \Delta_i \geq 0$。

### 1.3 势能法

<!-- ???- Info "Example" -->
经典栈操作的势函数为当前栈内元素的数目。

## 2 AVL Tree

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

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

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

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

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

### 3.2 Implementation
AVL 树有四种典型的旋转操作:LL、RR、LR 和 RL,其命名是根据插入元素(或者**引起失衡的节点**)的位置和最早失衡的节点的位置关系来的。LL 就意味着插入元素在最早失衡的节点的左子树的左子树上,别的同理。RR 就需要向左旋转一圈、LL 向右旋转一圈、LR 先向左旋转再向右旋转、RL 先向右旋转再向左旋转。

<img class="center-picture" src="../assets/avl-1.png" alt="drawing" width="550" />

!!! Note "注意"
1. 在插入和删除的时候有可能多个节点的平衡性质都被破坏;
2. 尽管插入时有可能多个节点的平衡性质被破坏,但是一次旋转就可以让所有平衡受到破坏的节点恢复平衡;
3. 但是删除的时候不再有一次宣传修复所有节点的性质,就需要我们递归的向上检查。
4. AVL 树的搜索、插入和删除的时间复杂度都是 $O(\log N)$。

记 $n_k$ 为高度为 $k$ 的 AVL 树的最少节点数,有下面递推公式 $n_k = n_{k-1} + n_{k-2} + 1$,其中 $n_0 = 1, n_1 = 2$,空树的树高为 $-1$。这也可以看出 AVL 树的高度是 $O(\log N)$ 的。

### Implementation

=== "LL Rotation"

Expand Down Expand Up @@ -141,9 +166,24 @@ AVL 数是一种平衡的二叉搜索树,我们首先约定下面一些东西
}
```


## 3 Splay Tree

Splay tree 希望从一棵空树开始连续的M个操作是 $O(M \log N)$ 的,实现的基本想法是将刚刚访问(查找、插入、删除)的节点调整到根部,其中操作定义如下:

- 搜索:使用一般二叉树的搜索方法找到结点,然后通过 Splay 操作将搜索的结点移动到根结点的位置;
- 插入:使用一般二叉树的搜索方法找到要插入的位置进行插入,然后把刚刚插入的结点通过 Splay 操作将其移动到根结点的位置;
- 删除:使用一般二叉树的搜索方法找到要删除的结点,然后通过 Splay 操作将要删除的结点移动到根结点的位置,然后删除根结点(现在根结点就是要删除的点),然后和一般二叉树的删除一样进行合理的 merge 即可。

Splay 操作定义如下:

- Zig:如果访问的节点是根节点的er儿子,直接旋转交换父子关系就可以;
- Zig-Zig:如果是 LL 型或者 RR 型的,那么先旋转父亲 $P$ 和爷爷 $G$,然后再旋转 $P$ 和 $X$;
- Zig-Zag:如果是 LR 型或者 RL 型的,正常处理即可。

<img class="center-picture" src="../assets/avl-2.png" alt="drawing" width="600" />

Splay 树的每个操作的**摊还代价**是 $O(\log N)$ 的。选用的势函数为 $\Phi(T) = \sum\limits_{i \in T} \log S(i) = \sum\limits_{i \in T} R(i)$,其中 $i$ 是 $T$ 的后代,$S(i)$ 是以 $i$ 为根节点的子树的节点数,将其取对数就是 $i$ 的秩/Rank $R(i)$。

## 4 Red-Black Tree

一棵红黑树是满足下面性质的二叉搜索树:
Expand All @@ -153,11 +193,28 @@ AVL 数是一种平衡的二叉搜索树,我们首先约定下面一些东西
3. 如果一个结点是红色的,那么它的两个子结点都是黑色的;
4. 对每个结点,从该结点到其所有后代叶结点的简单路径上,均包含相同数目的黑色结点。

这里的叶子结点 `NIL` 被重新定义了,定义成空的没有子结点的黑色结点。同时,我们称 `NIL` 为外部结点,其余有键值的结点为内部结点。合法红黑树不存在只有一个非底部结点(有两个 `NIL` 作为子结点)作为子结点的红色结点。
这里的叶子结点 `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})$。这就完成了证明。

- 插入内部节点个数为 $n$ 的红黑树的时间复杂度是 $O(\log n)$ 的;
- 删除内部节点个数为 $n$ 的红黑树的时间复杂度是 $O(\log n)$ 的。

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

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

1. 如果其父亲和叔叔节点都是红色的,而祖父节点是黑色的,那么将父亲和叔叔节点染成黑色,祖父节点染成红色,然后将祖父节点当作新插入的节点进行调整;
2. 插入节点的父亲是红色的,叔叔节点是黑色的,且插入节点是父亲节点的右子树;
3. 插入节点的父亲是红色的,叔叔节点是黑色的,且插入节点是父亲节点的左子树。

第二种情况可以一次性转化为第三种情况,也就是对新插入的节点向左旋转一次就可以;第三种情况需要对新插入节点的父节点向右旋转一次,再将新插入节点的原父节点和祖父节点的颜色交换。这其实就和 AVL 树处理方法一样:我们将红色视为 Trouble。

<img class="center-picture" src="../assets/RedBlack-1.png" alt="drawing" width="600" />

**红黑树插入的最多旋转次数为 2**



Expand All @@ -168,6 +225,9 @@ B+ 树是一种多叉搜索树,每个结点通常有多个子结点。一棵 B
- 根结点要么是叶子结点,要么有 $2$ 到 $M$ 个孩子(至多有 $M$ 个子树,2021 期中);
- 所有非叶子结点(除了根结点)有 $\lceil M/2 \rceil$ 到 $M$ 个孩子;
- 所有叶子结点都在同一层。
- 假定所有非根的叶子节点也有 $\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)$。

Expand All @@ -189,3 +249,5 @@ Btree Insert ( ElementType X, Btree T ) {
}
}
```
删除不考,就不写了。
28 changes: 22 additions & 6 deletions docs/Computer Science/Algorithm/Lec 2.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
# Advanced Heaps

简而言之,除了下面几个特例,所有堆操作的时间复杂度都是 $O(\log n)$ 的:

1. 普通的二叉堆的合并是 $O(n)$ 的;
2. 二项堆的插入是 $O(1)$ 的;
3. 斜堆不考虑单点删除 Delete 和 DecreaseKey 操作。

## Leftist Heaps

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

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

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

左顷堆有一个很浅显的性质:若一个左顷堆的右路径上有 $r$ 个结点,这里右路径指的是从根结点到最右下方的孩子的路径,那么这个堆至少会有 $2^r - 1$ 个结点。
左顷堆有一个很浅显的性质:若一个左顷堆的**右路径**上有 $r$ 个结点,这里右路径指的是从根结点到最右下方的孩子的路径,那么这个堆至少会有 $2^r - 1$ 个结点。左倾堆右路径节点个数至多是 $\lfloor \log (n + 1) \rfloor$ 的

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

```c
LeftistHeap merge_recursive(LeftistHeap h1, LeftistHeap h2) {
Expand Down Expand Up @@ -43,6 +49,11 @@ LeftistHeap merge_recursive(LeftistHeap h1, LeftistHeap h2) {
}
```
迭代方式更加简洁:
<img class="center-picture" src="../assets/heap-2.png" alt="drawing" width="600" />
单点插入可以看作是将一个结点作为一个左顷堆,然后与原堆合并的过程。DeleteMin 则是将根结点删除后,将其左右子树合并的过程。没啥可说的。
在递归的过程中,我们发现递归的深度不会超过两个堆的右路径的长度之和,因为每次递归都会使得两个堆的其中一个向着右路径上下一个右孩子推进,并且直到其中一个推到了叶子结点就不再加深递归。此递归向下的过程是 $O(\log n)$ 的。同时每一层的操作也是常数的,因为只需要完成接上指针、判断根结点、交换子树与更新 $\operatorname*{NPL}$ 的操作,所以整体的复杂度是 $O(\log n)$ 的。
Expand All @@ -51,7 +62,7 @@ LeftistHeap merge_recursive(LeftistHeap h1, LeftistHeap h2) {
## Skew Heaps
### 数据结构介绍
### 1. 数据结构介绍
我们回忆 Splay 树和 AVL 树,它们都是通过旋转操作来维持平衡性质的,但是唯一区别就是 AVL 树需要自底向上维持平衡性质,但是 Splay 树就不需要,其无条件的旋转操作提醒我们,或许可以通过无条件地交换左右子树来完成合并操作。具体操作如下:
Expand All @@ -60,7 +71,12 @@ LeftistHeap merge_recursive(LeftistHeap h1, LeftistHeap h2) {
斜堆一般不考虑单点删除和 DecreaseKey 这两个操作。
### 摊还分析
<img class="center-picture" src="../assets/heap-3.png" alt="drawing" width="600" />
!!! Note
It is an open problem to determine **precisely** the **expected right path length** of both leftist and skew heaps.
### 2. 摊还分析
**Definition**:一个结点 $p$ 被称为**重的/Heavy**,如果果它的右子树结点个数至少是 $p$ 的所有后代的一半(后代包括该结点 $p$ 自身)。反之称为轻结点/Light。
Expand Down
Loading

0 comments on commit aeecdd3

Please # to comment.