Skip to content

Commit

Permalink
Merge pull request #48 from Jacob953/development/algorithm
Browse files Browse the repository at this point in the history
docs: --20220111 add Analysis of Complexion
  • Loading branch information
hanyuancheung authored Jan 11, 2022
2 parents c4e6975 + b6dd2c7 commit a1769ab
Show file tree
Hide file tree
Showing 5 changed files with 290 additions and 37 deletions.
56 changes: 19 additions & 37 deletions docs/Algorithm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,77 +6,59 @@
在进行算法学习前,我想要先明确一下算法与数据结构之间的关系:

- 数据结构是为算法服务的
- 算法是需要建立在特定的数据结构之上的
- 数据结构是为算法服务的
- 算法是需要建立在特定的数据结构之上的

所以,如果你还没有学过数据结构,我希望你能够先参考一下:
[GO 数据结构](https://github.com/zhyChesterCheung/GoGetit/blob/main/docs/Data-Structure/README.md)
[GO 数据结构](https://github.com/Superego-CodeEngineer/GoGetit/blob/main/docs/Data-Structure/README.md)

同时,本算法模块将全程使用 Go 语言实现,如果你对 Go 的掌握程度还不够自信,也可以参考一下:
[Go 基础](https://github.com/zhyChesterCheung/GoGetit/tree/main/docs/Language)
[Go 基础](https://github.com/Superego-CodeEngineer/GoGetit/blob/main/docs/Language/README.md)

接下来,我会假设你已经有了一定的数据结构基础和 Go 编程基础
接下来,我会假设你已经有了一定的数据结构基础和 Go 编程基础

不过,在详细介绍算法之前,我认为还有一样十分重要的内容需要明确:

- 对于算法方面的研究,复杂的数学推理和证明是非常重要的,但是,本算法模块可能并不太适用于这类需求
- 对于算法方面的研究,复杂的数学推理和证明是非常重要的,但是,本算法模块可能并不太适用于这类需求

- 本算法模块不会过度关注数学性的推导,而是由浅入深,逐步分析不同算法的具体的环境,给予最直观的感受
- 本算法模块不会过度关注数学性的推导,而是由浅入深,逐步分析不同算法的具体的环境,给予最直观的感受

因此,请放心,接下来的内容不需要过于强悍的数学基础
因此,请放心,接下来的内容不需要过于强悍的数学基础

## Go - 基础篇

0. Complexion of Time & Space - 时空复杂度
0. [Analysis of Complexion - 复杂度分析](basic/00-complexion-analysis.md)

Go - 复杂度分析的意义

1. Divide & Conquer - 分治
Go - 大 O 复杂度表示法

Go - 时间复杂度分析

2. Recursion - 递归
Go - 空间复杂度分析

Go - 常见的复杂度量级

3. Hash - 哈希
1. [Recursion - 递归](basic/01-recursion.md)

2. [Divide & Conquer - 分治](basic/02-divide-conquer.md)

4. Backtracking - 回溯
3. Hash - 哈希

4. Backtracking - 回溯

5. Greedy - 贪心


6. Dynamic Programming - 动态规划


7. Sort - 排序


8. Search - 搜索


9. String Matching - 字符串匹配


## Go - 进阶篇

排序:拓扑排序

最短路径

位图

概率统计

并行

B+树

搜索:A*

索引

拜占庭

## Go - 刷题指导

## Go - 高频题总结
## Go - 高频题总结
135 changes: 135 additions & 0 deletions docs/Algorithm/basic/00-complexion-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# 0. Analysis of Complexion - 复杂度分析

在学习完数据结构之后,相信你能感受到特定环境下,特定数据结构带来的高效性

算法和数据结构也是一样,它们本身就是为了解决代码的运行速度和存储空间消耗的问题

这个问题所分析的对象,就是我们常说的时空复杂度,即时间复杂度和空间复杂度

## Go - 复杂度分析的意义

通常情况下,想要知道一段程序的运行时间,在 Golang 中可以配合以下代码进行计算:

```go
import "time"

// get start
start := time.Now()

// <code-segment>

// time cost from start
cost := time.Since(start)
```

通过类似的统计、监控,可以得到代码执行的时间和内存占用大小,但是,这种基于测算的事后统计法局限性极大,它非常依赖测试环境、数据规模等其他环境因素等影响

譬如,对同一段代码进行计算,可以得到如下结果:

<img algin="center" src="../../../image/Algorithm/basic/00-complexion-analysis/01-result-of-test.go.png" alt="01-result-of-test.go">

为此,我们需要一种更加粗粒度的方法来对复杂度进行分析,这就是常被提到的大 O 复杂度表示法

> 注:真实世界的复杂度远不止时空复杂度这么简单,但为了方便理解,本章以时间复杂度的分析为主
## Go - 大 O 复杂度表示法

因为只需要做粗略计算,我们假设:

- 代码的执行时间用 T(n) 来表示
- 代码的执行次数用 f(n) 来表示
- 每行语句的执行效率是 unit-time,通常将其假设为单位 1

以如下代码段为例:

```go
func cal(n int) {
//Loop 1
i := 1
for ; i <= n; i++ {
//Loop 2
for j:= 1; j <= n ; j++{
fmt.Println("i: ", i,"j: ", j)
}
}
}
```

在 Loop 2 中,每行语句需要执行 n 遍,即 f(n) = 2 _ n ,在 Loop 1 中,Loop 2 需要执行 n 遍,那么这段代码的执行时间 T(n) = (( 2 _ n<sup>2</sup> ) + 1 ) \* 1( 前 1 是 i 初始化,后 1 是 unit-time )。

很容易看出,T(n) 与 f(n) 是成正比的,于是,可以总结:**T(n) = O(f(n))**

因此,类似 T(n) = O(2 \* n<sup>2</sup>) 这样的表达式被称为大 O 时间复杂度表示法。

## Go - 时间复杂度分析

大 O 时间复杂度只能粗略地描述代码执行时间随数据规模增长的变化趋势,故也叫作渐进时间复杂度(asymptotic time complexity),简称时间复杂度。

想要精益求精,优化自己的代码,降低其时间复杂度,首先就要学会如何分析时间复杂度:

1. 基本法则:只关注 f(n) 最多的代码段。

T(n) = O(f(1) + f(1000000) + f(n)) = O(f(n))

1. 加法法则:O(n) 是量级最大的 f(n) 的代码段之和。

T(n) = T(f(2 \* n)) + T(f(n<sup>2</sup>)) = O(n<sup>2</sup>)

T(n) = T(g(2 \* n)) + T(f(n<sup>2</sup>)) = O(max(O(g(n)), O(f(n)))

1. 乘法法则:O(n) 是嵌套代码段的 f(n) 之积。

T(n) = T(g(n)) _ T(f(2 _ n)) = O(2 \* n<sup>2</sup>)

1. 保留法则:保留不同的数据规模

加法法则失效:T(n) = T(f(n)) + T(f(m)) = O(m + n)

乘法法则有效:T(n) = T(f(n)) _ T(f(m)) = O(m _ n)

## Go - 空间复杂度分析

大 O 表示法同样也可以用来表示空间复杂度,当其粗略地描述代码的存储空间与数据规模之间的增长关系时,就被称为渐进空间复杂度(asymptotic space complexity),简称空间复杂度。

空间复杂度的分析通过类比时间复杂度的分析即可,只需要记住,现在的关注点在于算法的存储空间,即运行代码所需要使用的内存。

如以下片段:

```go
func cal() {
// ...
//<code-segment>
n := [5]int{1, 2, 3, 4, 5}
// ...
//<code-segment>
}
```

无论其他代码段如何,只要不涉及存储空间,那么 f(n) = 5。

## Go - 常见的复杂度量级

代码段的写法千变万化,但常见的复杂度量级并不多,通常可以分为非多项式量级和多项式量级。

非多项式量级:O(2<sup>n</sup>) 和 O(n!)

非多项式量级的算法问题通常被叫做 NP 问题,即 Non-Deterministic Polynomial,这类问题几乎只有做此类算法研究的同学才会涉及,因此,本章将重心放在多项式量级:

- 常量级:O(1)

只要不含循环、递归语句:T(n) = T(2) + T(100000) = O(1)

- 线性、对数级:O(n), O(logn), O(nlogn)

大 O 表示法只需要做粗略计算,故忽略线性级的系数,对数级的底数:

- T(n) = T(2 _ n) + T(5 _ n) = O(n)
- T(n) = T(log<sub>3</sub>n) = O(logn)

而 O(nlogn) 可以看作线性级和对数级的嵌套,即遵循了乘法法则。

- 次方级:O(n<sup>k</sup>)

多个相同数据代码段的嵌套,如循环、递归。

对于空间复杂度而言,只需要掌握 O(1), O(n), O(n<sup>2</sup>) 即可。
136 changes: 136 additions & 0 deletions docs/Algorithm/basic/01-recursion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# 1. Recursion - 递归

简单地介绍完了复杂度分析,接下来就要进入算法实战了。

递归,可以说是算法学习之路上最难理解的知识点之一,很多复杂的算法实现都需要运用递归的编程技巧。

## Go - 什么是递归?

即使是科班生也很难第一时间理解透递归的思路,但其实只要将其拆开来看,递归并不难。

递归实际上只涉及两个操作,“递”和“归”。从实际生活中去理解递归:

场景一:监考老师分发试卷,由于多印了很多试卷,可以靠手去掂量,对试卷粗略分组。

要求:将多余的试卷回收给教务处。

具体步骤:

1. 将试卷发给第一排的同学,让其只能向后传;

2. 每个人只能拿一张试卷,如果没到场,留在其桌上即可;

3. 直到最后一排的同学拿到试卷后,多余的试卷将由最后一排向第一排传;

4. 传到第一排,等待监考老师来收即可。

> 这里为了帮助理解算法,我们没有让最后一排的同学直接叫老师,而是一排一排向前传
分析上述具体步骤,理解如下:

步骤 1 即递归算法的入口,试卷将一排一排的传递下去,这就是“递”;

步骤 2 即递归算法的多个子问题,下一排不管是否有人,都要留一张试卷在桌上;

步骤 3 即递归算法的终止条件,试卷传到最后一排后,则要被传回第一排,这就是“归”;

步骤 4 即递归算法的出口,在代码实现过程中,这里便回到了调用递归算法的主函数中。

以上思路的代码实现如下:

```go
// LAST :最后一排
// TOTAL :该组的分发试卷数
func rest(pos int) int {
if pos == LAST {
return TOTAL - 1
}
return rest(pos+1) - 1
}
```

## Go - 递归的三个条件

1. 可分解成多个子问题

实现递归算法的第一件事就是将问题拆分为多个子问题,子问题即数据规模更小的问题,比如,将“试卷还剩多少”,分解为多个“下一排试卷还剩多少”的问题。

2. 子问题除数据规模不同,算法思路完全一样

场景一中,在传递给下一排时,可以使用同一种算法思路解决子问题,如“无论是否到场,留一张试卷在桌上”。

同样,我们可以根据不同场景更改算法思路,如场景二:不需要给没来的同学留试卷,直接传给再下一排。这样的话,算法也会跟着变化:

```go
// LAST: 最后一排
// TOTAL: 该组的分发试卷数
// stu[]: 保存考生的到场状态,0 为未到场
func rest(pos int) int {
if pos == LAST {
return TOTAL - 1
}
if stu[pos + 1] == 0 {
if pos + 1 == LAST {
return TOTAL - 1
}
return rest(pos+2) - 1
}
return rest(pos+1) - 1
}
```

3. 存在终止条件

推导出递归公式后,还需要找到递归的终止条件。递归之于循环一样,如果没有终止条件,算法将会无限递归下去,因此,在实现递归算法时,一定要找到合适的终止条件。

## Go - 递归的注意事项

1. 警惕内存溢出

如果你已经开始实现递归算法了,相信你肯定碰到过这样的问题 `fatal error: stack overflow` ,这便是因为一直进行函数调用,导致栈、堆内存溢出产生的错误。

最大允许的递归深度跟当前线程剩余的栈空间有关,不误进行事前计算,因此,这种错误很难控制和预防。

2. 警惕重复计算

先来看看每每提到递归,就必谈的斐波那契数的实现吧:

> Fibonacci:由 01 开始,之后的斐波那契数就是由之前的两数相加而得出,如:1, 1, 2, 3, 5, 8, 13, 21, 34, ...

```go
func fib(n int) int {
if n == 1 {
return 1
}
if n == 0 {
return 0
}
return fib(n-1) + fib(n-2)
}
```

不难发现,fib(fib(n-1)-2) 与 fib(fib(n-2)-1) 所计算的值是相同的,以 fib(6) 为例,如 Figure 1. Fibonacci 所示,fib(2) 会被计算 5 次:

<img algin="center" src="../../../image/Algorithm/basic/01-recursion/01-fibonacci.jpg" alt="Figure 1. fibonacci">

如果求 fib(20),难以想象同样一个值会被重复计算多少次,因此,我们可以利用一些数据结构(如数组、散列表等)巧妙地避免这个问题:

```go
// tmp[]: 存储中间值
func fib(n int) (res int) {
if n == 0 {
return 0
}
if n == 1 {
return 1
}
if tmp[n] != 0 {
return tmp[n]
}
res = fib(n-1) + fib(n-2)
tmp[n] = res
return res
}
```

当然,为了避免重复计算,还有更加高级的方法,我提出以上方法,便是在这里抛砖引玉了。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit a1769ab

Please sign in to comment.