Skip to content

Commit

Permalink
C++ Primer Ch2
Browse files Browse the repository at this point in the history
  • Loading branch information
bowling233 committed Oct 30, 2023
1 parent 75eeae8 commit d86c203
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 10 deletions.
146 changes: 142 additions & 4 deletions docs/books/Algorithm.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,61 @@ $$
$$
<!-- prettier-ignore-end -->

## 算法分析及例子
## 算法分析基础

### 算法正确性的证明

<!-- prettier-ignore-start -->
!!! note "循环不变式"

《算法导论》中提出了“循环不变式”这一概念,类似于数学中的归纳法,用于证明迭代和递归算法的**正确性**。
<!-- prettier-ignore-end -->

### 算法复杂度的分析

在算法分析中,我们往往只关注**最坏情况运行时间**。“平均情况”往往较难定义,对于特定问题,“平均输入”的构成也不一定明显。我们会学习到“随机化算法”,使得算法可以使用概率论分析,产生期望运行时间。

<!-- prettier-ignore-start -->
!!! note "记号"

- $O$:大 $O$ 记号,表示上界。
- $\Omega$:大 $\Omega$ 记号,表示下界。
- $\Theta$:大 $\Theta$ 记号,表示紧界。
- $o$:小 $o$ 记号,表示严格上界。
- $\omega$:小 $\omega$ 记号,表示严格下界。

!!! note "主方法:求解分治算法"

《算法导论》中介绍了三种求解递归式(分治策略)的方法:代入法(猜测)、递归树法(简单的递推式)和主方法。主方法最为通用。

主方法求解递归式 $T(n)=a(T(n/b))+f(n)$。其中,$a\geq 1$ 表示子问题的个数,$b>1$ 表示子问题的规模,$f(n)>0$ 表示除去子问题的代价。

1. 若对某个常数 $\epsilon>0$,有 $f(n)=O(n^{\log_b a-\epsilon})$,则 $T(n)=\Theta(n^{\log_b a})$。
2. 若 $f(n)=\Theta(n^{\log_b a})$,则 $T(n)=\Theta(n^{\log_b a}\lg n)$。
3. 若对某个常数 $\epsilon>0$,有 $f(n)=\Omega(n^{\log_b a+\epsilon})$,且对某个常数 $c<1$ 和所有足够大的 $n$,有 $af(n/b)\leq cf(n)$,则 $T(n)=\Theta(f(n))$。

上面的分析定义很严谨,但是看起来很难理解。其实只需要求解 $\frac{f(n)}{n^{\log_b a}}$ 的极限即可。如果该极限是 $\lg n$ 等小于 $n^\epsilon$ 的低阶量,则无法应用主方法。

<!-- prettier-ignore-end -->


### 复杂度分析的例子

这里罗列了一些无法归类到某一类算法的小问题。

<!-- prettier-ignore-start -->
!!! note "3-sum"

问题:从 $N$ 个数中找出 3 个和为 $0$ 的整数元组的数量。

解法:

- 首先对数组进行归并排序($O(N\lg N)$)。
- 对每对元素 $a_i$ 和 $a_j$($N(N-1)/2$ 次),使用二分查找($O(\lg N)$)在数组中查找 $-(a_i+a_j)$。

该解法的复杂度为 $N^2\lg N$。3-sum 问题的最优解可能是平方级别的,目前还没有发现。
<!-- prettier-ignore-end -->


## 数据结构

Expand Down Expand Up @@ -240,6 +294,14 @@ $$

### 高级数据结构

#### (二叉)堆

(二叉)堆是一个近似完全的二叉树。除了最底层外,该树是完全充满的,且从左向右填充。

堆采用数组实现。堆包含两个属性:该数组的大小 `length` 和堆中元素的数量 `heap_size`

堆的父子节点

## 算法

### 排序 Sort
Expand All @@ -249,9 +311,34 @@ $$
这一类排序算法都通过**比较元素间的大小**确定它们的次序。任何比较排序算法的最坏情况都需要 $\Omega(N\lg N)$ 的时间,我们来证明一下:

<!-- prettier-ignore-start -->
!!! note "title"
!!! note "归并排序"

典型的归并排序实现由两个子程序组成:

- `MERGE(A, p, q, r)`:将两个有序数组 `A[p..q]` 和 `A[q+1..r]` 合并为一个有序数组 `A[p..r]`。
- `MERGE-SORT(A, p, r)`:将数组 `A[p..r]` 排序。

text
`MERGE-SORT` 负责递归地将数组分成两半,直到只剩下一个元素,然后调用 `MERGE` 合并两个有序数组。

```title="MERGE-SORT(A, p, r)"
if p<r
q = (p+r)/2
MERGE-SORT(A, p, q)
MERGE-SORT(A, q+1, r)
MERGE(A, p, q, r)
```

`MERGE` 有多种实现方式:

- 《算法导论》将两个子数组拷贝出去,然后在两个数组上进行比较,将较小的元素放入原数组中。两个拷贝后的新子数组尾部设置了哨兵,避免在比较时检查数组是否为空。

!!! note "复杂度"

归并排序的递推式为:$T(n)=2T(n/2)+cn$,画出递归树很容易知道总代价为 $cn\lg n+cn=O(n\lg n)$。

!!! note "优化"

- 小数组插入排序:当子数组长度较小时,可以使用插入排序代替归并排序,这样可以减少递归的深度。
<!-- prettier-ignore-end -->

#### 线性时间排序
Expand Down Expand Up @@ -288,4 +375,55 @@ $$
!!! note "中序遍历 Inorder Traversal"

- 先处理左儿子,再处理节点,最后处理右儿子。
<!-- prettier-ignore-end -->
<!-- prettier-ignore-end -->

### 动态规划

<!-- prettier-ignore-start -->
!!! note "最大子数组问题"

最大子数组问题的最优解法是动态规划。

!!! note "分治解法"

最大子数组要么在左半边,要么在右半边,要么跨越中点。前两种情况可以递归求解,第三种情况可以在 $O(n)$ 时间内求解,即分别寻找形如 $A[i..mid]$ 和 $A[mid+1..j]$ 的最大子数组,然后合并。

从上面的分析可知:最大子数组问题的分治解法的时间复杂度为 $T(n)=2T(n/2)+O(n)$,即 $O(n\lg n)$。
<!-- prettier-ignore-end -->

## 案例

### union-find 动态连通性

输入:一列整数对,表示整数对之间的连通性。

目标:去除能被前面的整数对连通的整数对。

#### 定义问题

- `union(p, q)`:将 `p``q` 连通。
- `find(p)`:返回 `p` 所在的连通分量的标识符。
- `connected(p, q)`:判断 `p``q` 是否在同一个连通分量中。
- `count()`:返回连通分量的数量。

#### 实现

##### quick-find

简单想法:使用数组 `id[]` 存储每个整数对所在的连通分量的标识符。这样,`connected(p, q)` 只需要判断 `id[p] == id[q]` 即可。

缺点:`union(p, q)` 的实现需要遍历数组 `id[]`,将所有与 `id[p]` 相同的元素都改为 `id[q]`。这样,`union(p, q)` 的时间复杂度为 $O(N)$。

评估:如果最后只得到了一个连通分量,那么 `union(p, q)` 至少调用 $N-1$ 次,即 $O(N^2)$。

##### quick-union

想法:优化 `union(p, q)` 的实现。`id[]` 数组中存储相同分量中的另一个触点,这样就能持续跳转到达根触点。此时 `union(p, q)` 的实现只需要将 `p` 的根触点指向 `q` 的根触点即可。

评估:该算法有最坏情况,即所有触点之间的连通构成了一条链,极大地影响了 `find(p)` 的性能。此时,`find(p)` 的时间复杂度为 $O(N)$,处理整个数组所需的时间复杂度为 $O(N^2)$。

##### 加权 quick-union

想法:优化 `quick-union` 的实现。在 `union(p, q)` 时,总是将小树的根触点连接到大树的根触点上。这样,`find(p)` 的性能就能得到保证。


24 changes: 20 additions & 4 deletions docs/books/Algorithm/ds/dsaac4.2.2_expression_tree.c
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,16 @@ Node* popStack(Stack *s)

void printTree(Node *n)
{

if(n->left)
{
printf("[Node %c] left leaf: %c\n", n->key, n->left->key);
printTree(n->left);
}
if(n->right)
{
printf("[Node %c] right leaf: %c\n", n->key, n->right->key);
printTree(n->right);
}
}

int main(void)
Expand All @@ -78,8 +87,9 @@ int main(void)
for(int i = 0; i < size; i++)
{
char temp;
scanf(" %c", &temp);
if(scanf(" %c", &temp) == EOF) break;
Node *new = createNode(temp);
printf("input %c\n", temp);
switch(temp)
{
case '+': case '-': case '*': case '/':
Expand All @@ -90,11 +100,17 @@ int main(void)
default:
if(!isalpha(temp))
{
printf("switch error: %c\n", temp);
exit(1);
}
pushStack(s, createNode(temp));
}
}


if(s->top != 1)
{
printf("s->top %d\n", s->top);
exit(1);
}
printTree(s->stack[s->top]);
return 0;
}
1 change: 1 addition & 0 deletions docs/books/Algorithm/ds/expression.in
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
100
a b + c d e + * *
Loading

0 comments on commit d86c203

Please sign in to comment.