算法知识简要大纲
抽象数据类型(Abstract Data Type, ADT)
抽象数据类型的特征是 实现 与 操作 分离,从而实现 封装 。
抽象数据类型体现了程序设计中 问题分解 和 信息隐藏 的特征。它把问题分解为多个规模较小且容易处理的问题,然后把每个功能模块的实现为一个独立单元,通过一次或多次调用来实现整个问题。
- 可视化网站
算法分析
递归简论
- 基准情况:在某一情况下无须递归就会 return。
- 不断推进:每一次递归都往基准推进。
- 设计法则:所有递归都能调用。
- 合成效益法则:在同一个问题中,勿在不同递归调用中做同一个工作
时间复杂度
(大概是看看就好了吧)
- 定义:如果存在正常数$𝑐$和$𝑛_0$, 使当$𝑁≥𝑛_0$时$𝑇(𝑁)≤𝑐𝑓(𝑁)$, 则记为$𝑇(𝑁)=\mathcal{O}(𝑓(𝑁))$
- 定义:如果存在正常数$𝑐$和$𝑛_0$, 使当$𝑁≥𝑛_0$时$𝑇(𝑁)≥𝑐𝑔(𝑁)$, 则记为$𝑇(𝑁)=Ω(𝑔(𝑁))$
- 定义:$𝑇(𝑁)=Θ(ℎ(𝑁)$当且仅当$𝑇(𝑁)=\mathcal{O}(ℎ(𝑁))$且$𝑇(𝑁)=Ω(ℎ(𝑁))$
- 定义:如果$𝑇(𝑁)=\mathcal{O}(𝑝(𝑁))$且$𝑇(𝑁)≠Ω(𝑝(𝑁))$, $𝑇(𝑁)=\mathcal{O}(𝑝(𝑁))$
名称 | 运行时间 T(N) | 算法举例 |
---|---|---|
常数时间 | $\mathcal{O}(1)$ | 判断一个二进制数的奇偶 |
对数时间 | $\mathcal{O}(logN)$ | 二分搜索 |
(小于 1 次)幂时间 | $\mathcal{O}(N^c)$其中$0<c<1$ | K-d 树的搜索操作 |
线性时间 | $\mathcal{O}(N)$ | 无序数组的搜索 |
线性对数时间 | $\mathcal{O}(N\log{N})$ | 最快的比较排序 |
二次时间 | $\mathcal{O}(N^2)$ | 冒泡排序、插入排序 |
三次时间 | $\mathcal{O}(N^3)$ | 矩阵乘法的基本实现,计算部分相关性 |
指数时间 | $\mathcal{O}(2^N)$ | / |

- 计算时间复杂度
- 一层循环的时间复杂度为$𝑂(𝑁)$
- 多层嵌套循环的时间复杂度是每层循环的时间复杂度之积$𝑂(𝑁^n)$
if…else…
/case…switch…
语句中取各个分支中最长的时间作为时间开销
表、栈、队列
表 ADT(链表) List
习惯上会留出一个 标志节点 , 有时称之为 表头(header) 或 哑节点(dummy node)
- 动画
- Head File
1 | struct Node; |
- 结构体定义
1 | struct Node{ |
- Find 查询
其实就是从头遍历
1 | Position Find( ElementType X, List L){ |
- FindPrevious 查询前一个节点
和 Find 类似, 区别仅在 line2,3
1 | Position FindPrevious( ElementType X, List L){ |
- Delete 删除
1 | void Delete( ElementType X, List L){ |
- Insert 插入
1 | void Insert( ElementType X, List L, Position P){ |
- 其他类型
- 双链表
- 循环链表
- 多重表
- 基数排序(radix sort)
- 低位排序 LSD
- 高位排序 MSD
- 游标(cursor)实现法
需要链表又不能使用指针(如 BASIC 和 FORTRAN 等)
栈 ADT Stack
又名 LIFO(先进后出)表
基本上就Pop(出栈)
和Push(进栈)
操作
实现
- 链表实现
主要时间花费在于分配新空间和删除弹出的结点 - 数组实现
唯一潜在危害: 需要提前声明一个数组大小
- 链表实现
应用
- 后缀表达式
- 中缀到后缀的转换
- 函数调用
- 尾递归(TODO:未懂)
执行的任何递归调用是在这种情况下的最后操作,而且通过封闭递归,递归调用的返回值(若有)立即返回.
必须是 线性递归 (递归内只调用一个新的递归)
- 尾递归(TODO:未懂)
- 平衡符号

队列 Queue
- 基本操作
Enqueue
入队 : 在表的末端(队尾 rear)插入一个元素Dequeue
出队 : 删除 / 返回 表的开头(队头 front)
- 实现
- 链表实现
- 数组实现(循环数组 circular array 实现)
- 应用
- 排队论(queueing theory)
树 Tree
预备知识
- 基本术语
术语 | 定义 |
---|---|
根(root) | / |
边(edge) | 连接节点的有向线 |
- - - | - - - |
子节点(child) | 一个节点含有的子树的根节点称为该节点的子节点 |
父节点(parent) | 若一个节点含有子节点 ,则这个节点称为其子节点 的父节点 |
兄弟节点(sibling) | 具有相同父节点 的节点互称为兄弟节点 |
堂兄弟节点 | 双亲在同一层的节点互为堂兄弟 |
祖先(grandparent) | 从根到该节点所经分支上的所有节点 |
子孙(grandchild) | 以某节点为根的子树中任一节点都称为该节点的子孙 |
- - - | - - - |
树叶(leaf) | 没有子节点 的节点 |
从节点$n_1$到$n_k$的路径(path) | 节点$n_1, n_2, \cdots,n_k$的一个序列 |
路径的长(length) | 路径上边的条数 |
深度(depth) | 从根到$n_i$的位移路径的长 |
高度(height) | 从$n_i$到一片树叶的最长路径的长 |
树的高度或深度 | 树中节点的最大层次 |
- - - | - - - |
非终端节点或分支节点 | 度不为 0 的节点 |
节点的层次 | 从根开始定义起,根为第 1 层,根的子节点 为第 2 层,以此类推 |
节点的度 | 一个节点含有的子树的个数称为该节点的度 |
树的度 | 一棵树中,最大的节点的度称为树的度 |
森林 | 由 m(m>=0)棵互不相交的树的集合称为森林 |
- 种类
- 无序树:树中任意节点的子结点之间没有顺序关系,这种树称为无序树,也称为自由树;
- 有序树:树中任意节点的子结点之间有顺序关系,这种树称为有序树;
- 二叉树:每个节点最多含有两个子树的树称为二叉树;
- 完全二叉树
- 满二叉树
- 霍夫曼树:带权路径最短的二叉树称为哈夫曼树或最优二叉树;
遍历
二叉树
- 种类: 表达式树, 查找树 ADT——二叉查找树
- Head File
1 | struct TreeNode; |
- 结构体定义
1 | struct TreeNode{ |
- Find 查询
1 | Position Find( ElementType X, SearchTree T){ |
- Insert 插入
1 | SearchTree Insert( ElementType X, SearchTree T){ |
- Delete 删除节点
1 | //TODO: |
- PrintTree 打印树
1 | void PrintTree( SearchTree T,int space,int LR,int *N){ |
AVL 树 (Adelson-Velsky 和 Landis)
- 定义: 每个节点的左子树和有字数的高度最大相差 1 的二叉查找树
- 深度: $\mathcal{O}(\log{N})$
- 旋转
- 单旋转
相邻子树高度相差大于等于 2 - 双旋转
相邻子树高度相差为 1
- 单旋转
伸展树 splay tree
- 基本想法: 当一个节点被访问后,它就要经过一系列 AVL 树的旋转被放到根上
- 两种情况
- 之字形(zig-zag): AVL 双旋转
- 一字形(zig-zig): AVL 单旋转两次
B-树 (B-tree)
不是二叉树的查找树
- 称呼
- 4 阶 B-树 == 2-3-4 树
- 3 阶 B-树 == 2-3 树
分析树
这个好像也不考
哈希 Hash
又名散列
哈希函数
- 简单
1
2
3Index HashNormal(ElementType Key, int TableSize){
return Key%TableSize;
} - 优秀
1
2
3
4
5
6
7
8Index HashBeautifully(ElementType Key, int TableSize){
ElementType * ptrKey = &Key;
unsigned int HashVal = 0;
while (*ptrKey !='\0'){
HashVal = (HashVal << 5) + *ptrKey++;
}
return HashVal % TableSize;
}
分离链表法(拉链法)
头文件
1
2
3
4
5
6struct ListNode;
typedef struct ListNode *Position;
struct HashTbl;
typedef struct HashTbl *HashTable;
typedef int ElementType;
typedef int Index;初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24HashTable
InitializeTable(int TableSize){
HashTable H;
int i;
if (TableSize < MinTableSize){
printf("Table size too small");
return NULL;
}
// allocate table
H = malloc(sizeof(struct HashTbl));
if(H == NULL){
printf("out of sapce !!!");
}
// allocate list headers
for (i=0; i<H->TableSize; i++){
H->TheLists[i] = malloc(sizeof(struct ListNode));
if (H->TheLists[i]==NULL){
printf("out of space !!!");
}else{
H->TheLists[i]->Next = NULL;
}
}
return H;
}结构体定义
1
2
3
4
5
6
7
8
9
10
11struct ListNode{
ElementType Element;
Position Next;
};
typedef Position List;
struct HashTbl{
int TableSize;
List *TheLists;
};Find
1
2
3
4
5
6
7
8
9
10
11
12
13Position
Find( ElementType Key, HashTable H, Index HashNum){
Position P;
List L;
// L = H->TheLists[Hash(Key, H->TableSize)];
L = H->TheLists[HashNum];
P = L->Next;
while(P!=NULL && P->Element!=Key){
P=P->Next;
}
return P;
}Insert
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20void
Insert( ElementType Key, HashTable H, Index HashNum){
Position Pos, NewCell;
List L;
Pos = Find(Key,H,HashNum);
if(Pos == NULL){
NewCell = malloc(sizeof(struct ListNode));
if (NewCell == NULL){
printf("out of space!!!");
}else{
// L = H->TheLists[Hash(Key,H->TableSize)];
L = H->TheLists[HashNum];
NewCell->Next = L->Next;
NewCell->Element = Key;
L->Next = NewCell;
}
}else{
printf("already have it!\n");
}
}装填因子(load factor) $\lambda$
尽量让表的大小与预料的元素个数差不多,即 $\lambda\approx 1$ .
开放定址法
头文件
1
2
3
4
5
6
7
8typedef unsigned int Index;
typedef Index Position;
struct HashTbl;
typedef struct HashTbl *HashTable;
typedef int ElementType;
// series of functions
// ......基础声明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
extern int Collision_Square,Collision_Liner,Collision_Double;
enum KindOfEntry{Legitimate,Empty,Deleted};
struct HashEntry{
ElementType Element;
enum KindOfEntry Info;
};
typedef struct HashEntry Cell;
// 哈希表
struct HashTbl{
int TableSize;
Cell *TheCells;
};初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28HashTable
InitializeTable(int TableSize){
HashTable H;
int i;
if(TableSize<MinTableSize){
printf("Table Size too small");
return NULL;
}
// allocate table
H = malloc(sizeof(struct HashTbl));
if (H == NULL){
printf("out of space!!!\n");
}
H->TableSize = NextPrime(TableSize);
// allocate array of cells
H->TheCells = malloc(sizeof(Cell) * H->TableSize);
if (H->TheCells == NULL){
printf("out of space!!!\n");
}
for (i = 0;i<H->TableSize;i++){
H->TheCells[i].Info = Empty;
}
return H;
}
线性探测法
- Find
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17Position
FindByLiner(ElementType Key, HashTable H){
Position CurrentPos;
int CollisionNum;
CollisionNum = 0;
CurrentPos = HashNormal(Key,H->TableSize);
while(H->TheCells[CurrentPos].Info != Empty && // 非空
H->TheCells[CurrentPos].Element != Key){ // 不等于查询值
CurrentPos++;
CollisionNum++;
if(CurrentPos >= H->TableSize){
CurrentPos -= H->TableSize; // 循环查找
}
}// 一旦找到就跳出循环
Collision_Liner += CollisionNum;
return CurrentPos;
} Insert
1
2
3
4
5
6
7
8void InsertByLiner(ElementType Key, HashTable H){
Position Pos;
Pos = FindByLiner(Key,H);
if(H->TheCells[Pos].Info != Legitimate){// 该位置没有数据
H->TheCells[Pos].Info = Legitimate;
H->TheCells[Pos].Element = Key;
}
}一次聚集(primary clustering)
占据的单元形成一些区块预期探测次数
- 插入 / 不成功的查找
- 成功的查找
平方散列法
- 快速公式
CurrentPos += 2 * ++CollisionNum - 1
- Find
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Position
FindBySquare(ElementType Key, HashTable H){
Position CurrentPos;
int CollisionNum;
CollisionNum = 0;
CurrentPos = HashNormal(Key,H->TableSize);
while(H->TheCells[CurrentPos].Info != Empty && // 非空
H->TheCells[CurrentPos].Element != Key){ // 不等于查询值
CurrentPos += 2 * ++CollisionNum - 1; // 平方探测 快速方法
if(CurrentPos >= H->TableSize){
CurrentPos -= H->TableSize; // 循环查找
}
}// 一旦找到就跳出循环
Collision_Square += CollisionNum;
return CurrentPos;
} - Insert
1
2
3
4
5
6
7
8void InsertBySquare(ElementType Key, HashTable H){
Position Pos;
Pos = FindBySquare(Key,H);
if(H->TheCells[Pos].Info != Legitimate){ // 该位置没有数据
H->TheCells[Pos].Info = Legitimate;
H->TheCells[Pos].Element = Key;
}
}
双(倍)散列
Find
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17Position
FindByDouble(ElementType Key, HashTable H){
Position CurrentPos;
int CollisionNum = 0;
int addDouble = HashDoubel(Key,7);
CurrentPos = HashNormal(Key,H->TableSize);
while(H->TheCells[CurrentPos].Info != Empty && // 非空
H->TheCells[CurrentPos].Element != Key){ // 不等于查询值
CurrentPos += addDouble;
CollisionNum++;
if(CurrentPos >= H->TableSize){
CurrentPos -= H->TableSize; // 循环查找
}
}// 一旦找到就跳出循环
Collision_Double += CollisionNum;
return CurrentPos;
}Insert
1
2
3
4
5
6
7
8void InsertByDouble(ElementType Key, HashTable H){
Position Pos;
Pos = FindByDouble(Key,H);
if(H->TheCells[Pos].Info != Legitimate){ // 该位置没有数据
H->TheCells[Pos].Info = Legitimate;
H->TheCells[Pos].Element = Key;
}
}
再散列
当表有超过 70% 的单元是满的, 建立新表
新表大小是原表大小两倍后的第一个素数
- 实现
- 表满一半就
再散列
- 当插入失败是才
再散列
- 途中(middle-of-the-road)策略 : 当表达到某个装填因子时进行
再散列
- 表满一半就
可扩散列
略
优先队列(堆) Heap
二叉堆 binary heap
动画
堆是一刻完全二叉树(complete binary tree)
- 堆序性 heap order
- 最小元在根上
- 任意节点小于其所有后裔
基本堆操作
Insert
- 上滤
加在队尾然后向上排序 - 标记 sentinel
通过添加一条 哑信息(dummy piece of information) , 避免在插入新的最小值时每个循环都执行一次的测试
- 上滤
DeleteMin
- 下滤
删除根, 队尾换到队头, 向下排序
- 下滤
应用
- 选择问题
- 时间模拟
d-堆
略 TODO:
左式堆
略 TODO:
斜堆
略 TODO:
二项队列
略 TODO:
图论 Graph
基本概念
- 图(Graph) 是 顶点(Vertex) 和 边(Edge) 的集合。边是对任意两个顶点的连接。
基本术语
术语 | 定义 |
---|---|
阶(Order) | 图中顶点的个数 |
尺寸(Size) | 图中边的个数 |
子图(Sub-graph) | 一个图的顶点与边的子集。即,当$𝑉(𝐺’)∈𝑉(𝐺)$并且$𝐸(𝐺’ )∈𝐸(𝐺)$时,$𝐺’$是$𝐺$的一个子图 |
生成子图(Sub-graph) | 当$𝐺’$是$𝐺$的子集,并且$𝑉(𝐺’ )=𝑉(𝐺)$时, $𝐺’$是$𝐺$的一个生成子图 |
度(Degree) | 一个顶点 𝑣 的度是与它相连的边的数量,记作 𝑑(𝑣)。$\sum_{\nu \in V}d(\nu)=2 \mid E \mid$ |
出度(In-degree) | 出度为从该顶点出发的边的个数 |
入度(Out-degree) | 入度为终点时该节点的边的个数 |
路径(Path) | 路径经过的边数为 𝑘,即路径长度为$𝑘$ |
简单路径(simple-path) | 路径的所有顶点都不相同或只有起点$𝑣0$和终点$𝑣𝑘$两个顶点相同 |
圈(cycle) | 在有向图中路径起点$𝑣0$和终点$𝑣𝑘$相同。若满足简单路径的条件则是一个简单圈。 |
距离 | 两个顶点之间的最短路径长度就是两点之间的距离 |
图的分类
- 按边
类型 | 定义 |
---|---|
简单图(simple graph) | 既没有平行边,也没有自环 |
多重图(multi-graph) | 有平行边的图 |
伪图(Pseudograph) | 既有平行边的图,也有自环的图 |
完全图 | 每对顶点之间都有边的图是完全图 |
有的作者也允许多重图有自环

- 按路径
类型 | 定义 |
---|---|
连通图 | 一个图中的每个顶点到其他顶点都有路径 |
强连通图 | 有向图满足该条件 |
弱连通图 | 有向图不连通,但是它的基础图连通 |
基础图: 边去掉方向的图
- 按方向
- 按权重
存储显示方式
邻接矩阵 adjacency matrices
邻接矩阵使用$|V|∗|V|$的二维数组来表示图。$g[i][j]$表示的是顶点$i$和顶点$j$的关系。
- 无向图
只需要知道顶点$i$和顶点$j$是否是相连的,因此我们只需要将$g[i][j]$和$g[j][j]$设置为 1 或是 0 表示相连或不相连即可。 - 有向图
只需要知道是否有从顶点$i$到顶点$j$的边,因此如果顶点$i$有一条指向顶点$j$的边,那么$g[i][j]$就设为 1,否则设为 0。有向图与无向图不同,并不需要满足$g[i][j]=g[j][i]$ 有权图
在带权值的图中,g[i][j]表示的是顶点 i 到顶点 j 的边的权值。由于在边不存在的情况下,如果将 g[i][j]设为 0,就无法和权值为 0 的情况区分开来,因此选取适当的较大的常数 INF(只要能和普通的权值区别开来就可以了),然后令 g[i][j]=INF 就好了。当然,在无向图中还是要保持 g[i][j]=g[j][i]。在一条边上有多种不带权值的情况下,定义多个同样的|V|∗|V|数组,或者是使用结构体或类作为数组的元素,就可以和原来一样对图进行处理了。优点
很方便地判断任意两个顶点之间是否有边以及确定顶点的度- 缺点
在这种表示法中扫描所有边至少需要 O(n2)时间,因为必须检查矩阵中的 n2−n 个元素才能确定图中边的条数(邻接矩阵对角线上的 n 个元素都是 0,因此不用检查。又因为无向图的邻接矩阵是对称的,实际只需检查邻接矩阵的一半元素)。
通常把边很少的图成为稀疏图(sparse graphs)。如果用邻接矩阵表示稀疏图就会浪费大量内存空间
邻接表
- 优点: 只需$\mathcal{O}(|V|+|E|)$的内存空间
1 |
|
链接多重表
在无向图的邻接表存储表示中,每一条边$(v_i,v_j)$
都表示为两项:一项在顶点 vi 的邻接表中,而另一项在顶点 $v_j$ 的邻接表中。在多重表中,各链表中的结点可以被几个链表共享,此时图中的每一条边只对应于一个结点,而这个结点出现在该边所关联的两个顶点的每个邻接链表中。
1 | typedef struct edge *edge-pointer |
操作
拓扑排序
最短路径
略……
参考资料: