堆排序
简单选择排序的优化
结合了插入排序和归并排序的优点
原地排序
不稳定
存储结构:数组
堆的顺序表示法:二叉树的层序遍历
二叉堆,完全二叉树
大顶堆:每个节点的值 ≥ 其子节点的值
下标从0开始
父节点:(i-1)/2
左子节点:2i+1
右子节点:2i+2
-
建堆,简化/最简单
根节点的左右子树都是堆
只有根节点不满足堆 -
下沉
将根节点与左右孩子比较
若不满足堆
则交换根与较大的结点
一直到叶子结点
或所有子树均为堆为止 -
倒数第一层一定是堆
倒数第二层不一定满足堆
从下向上,下沉,建堆 -
建堆
从最后一个分支结点(n / 2 - 1)开始建堆
直到根节点为止
是 O (n) 而不是 O (n logn) -
排序
根节点和最后一个元素交换(原地排序)
队尾减1(排序n-1次)
根节点的左右子树都是堆
只有根节点不满足堆
下沉(调整成堆) -
上浮
当前节点与父节点比较
若不满足堆
则交换当前节点与父节点
一直到堆顶
或恢复成堆 -
对比
上浮
新元素插入堆尾
或大根堆中某个元素的值增大
下沉
初始建堆
或堆顶被移除
或大根堆中某个元素的值减小
1→3,3→4
2→4
2→5
每一趟选择最大元素
由简单选择排序的 O(n)降为 O(logn)
上浮和下沉都是 O (logn)
二路归并排序(非递归,自底向上)
分治法
稳定
缓冲区
-
两个有序序列合并成一个有序序列C
逐一比较
较小者放入 C
如果相等,则取靠前的序列(稳定)
取出较小者的下一个元素再次比较
直到其中一个序列的元素全部放入 C
再将另一个序列的元素放在 C 的尾部 -
一趟归并排序
合并相邻序列
三种子序列合并情况
i 为相邻序列中的第一个元素的下标-
i <= n - 2h
至少还有两个长度为 h 的序列 -
i > n - 2h && i < n - h
只有两个子序列
右子序列长度小于 h -
i >= n - h
只有一个子序列
-
-
完整的归并
两个数组相互归并
每进行一趟归并排序后
子序列长度 h 翻倍
直到子序列长度 h >= 总长度n
(临时数组归并到到原数组时
h >= n 可以
但原数组归并到临时数组时不行)
logn 趟排序
每趟排序都要划分成两半
二叉树高度 logn取整 + 1
每趟排序 O(n)
每趟排序都要归并所有元素
二路归并排序(递归,自顶向下)
-
合并
参考非递归的合并 -
划分
int middle = left + (right - left) /2
与数组【middle + 1......right】相比
数组【left......middle】多0到1个元素 -
排序 r,结果 r1
r 划分成左右子列
分别排序左右子列到 r2(递归)
将 r2 的左右子列 合并 到 r1 -
递归终止
当区间长度为 1 时
left == right
数组本身已有序
直接将 r[left] 复制到 r1[left] -
调用
待排数组和结果数组都是r
(结果存回 r)
(最终排序结果直接覆盖原始数组r)
栈深 logn
每趟排序都要划分成两半
每层的归并 O (n)
每层都要遍历所有元素
快速排序(Hoare 分区)
起泡排序的优化(相邻 → 两端向中间)
不稳定
递归树
交换较少
随机数据性能更优
对已排序数组性能较差
-
基准
left 作为基准
分区
将基准放在左右指针上
返回基准元素的位置 -
分区
左指针初始化:left
右指针初始化:right
重复以下操作直至左右指针相等- 右指针向左找小于基准的元素
赋值覆盖左指针指向的元素 - 左指针向右找大于基准的元素
赋值覆盖右指针指向的元素
- 右指针向左找小于基准的元素
-
递归
参考lomuto分区的递归
快速排序(Lomuto 分区)
交换较多
减少有序数组的最坏情况概率
-
基准
middle 作为基准
int middle = left + (left - right) / 2
分区前 交换 right 与 middle
分区后 交换 right 与 A 的下一个位置
返回基准值的索引 -
分区
小于等于区域:A
i 为 A 的最后一个元素的索引
初始为 left - 1(升序排序)
遍历 [ left, right - 1 ]
若当前元素小于等于基准值
扩展 A
交换 A 的最后一个元素 与 当前元素 -
排序 [ left, right ]
分区,获取基准索引 pivot
排序 [ left, pivot - 1 ]
排序 [ pivot + 1, right ] -
递归终止
当区间长度为 1 时
left == right 时直接返回
递归深度 O(log n)
分区 O(n)
希尔排序
直接插入排序的改进
-
增量序列
-
原始序列
n/2, n/4, ..., 1 -
Hibbard 序列
2^k - 1 -
Knuth序列
递归:h = 3h + 1
迭代:(3^k - 1) / 2 -
Sedgewick序列
9×4^k - 9×2^k + 1
4^k - 3×2^k + 1
1, 5, 19, 41, 109, 209, 505…… -
斐波那契序列
-
-
划分子集
数组被分为 gap 个独立的子序列
子序列的元素索引间隔为 gap
对每个子序列执行插入排序 -
插入排序
-
法一
对一个子序列插入排序后
再对下一个子序列插入排序 -
法二
先依次对每一个子序列的第二个元素插入排序
再依次对每一个子序列的第三个元素插入排序
接着是第四个,第五个……
-
直接插入排序
稳定
适合待排序列基本有序
正序时最好,O(n),不用移动
逆序时最差
-
整体思路
无序区 pop_front
有序区 upper_bound
有序区 vector 的 insert 单个元素
重复以上操作直至无序区为空 -
n - 1 趟循环
从 r[1] 开始插入
逐步扩展为 r[n - 1] -
边查找边后移
从后向前(稳定)
顺序查找
折半插入排序/折半查找
有序表
判定树是平衡二叉树
简单选择排序
-
n - 1 趟循环
- 第 i 趟循环选择出第 i 小的
A:已排序区
B:待排序区
- 第 i 趟循环选择出第 i 小的
-
每趟循环的整体思路
B 找最小值的索引 min
swap ( B[min], B.front() )
B.pop_front()
A.push_back() -
找第 i 小的元素
设 B 中第一个元素最小
依次与 B 中的剩余元素比较
查找 B 中最小的元素的位置 -
交换无序区最小元素与无序区第一个元素
当 min != i 时
r [ min ] 与 r [ i ] 交换
相当于从 B 中取出最小的元素
加入 A 中
O(n²)
不稳定
改进的冒泡排序
稳定
-
改进
bound:有序区第一个元素
pos:找本趟排序的 bound
本趟排序前,将上一趟的 pos 赋值给 bound
firstpos
前一趟排序,交换开始位置的前一个位置
本趟排序,交换开始位置 -
改进前后对比
-
改进前:每次都将无序区最大的元素加入有序区
改进后:每次都将至少一个元素加入有序区
包含无序区最大的元素 -
bound 防止后部排好的元素重复比较
firstpos 减少前部的比较次数 -
改进前:循环 n - 1 次
改进后:循环 直至本趟排序结束后 pos == 0
-
-
相邻交换(稳定)
如果 pos == 0,则更新 firstpos
更新 pos
双向冒泡排序
计数排序
O(n + k)
稳定
待排序数组A
2,3操作对应 计数数组B
结果数组C
-
min, max
O (n) -
各个数值的个数
O (n)
创建大小为 max - min + 1 的 B
元素大小 x,则 B 的索引 x - min -
不大于 x 的个数
O (k) -
反向遍历
O (n)
从后向前遍历A(保证稳定性)
A [ i ] 对应 C 的索引 j
C [ j ] 对应 B 的索引 k
B [ k ] = A [ i ]
C [ j ]--
桶排序(ai版本)
O (n + k + nlog(n/k))
-
min, max
O (n) -
桶数
如果元素数量 <= 100,桶数量 = 元素数量
如果元素数量 > 100,桶数量 = 元素数量开平方 -
每个桶的范围
range = (max - min) / bucketCount -
创建桶
vector<vector<T>> 类型 -
分配
O (n)
元素大小 x,则桶索引 (x - min) / range
若为最大值,桶索引要减一 -
桶内排序
O (n log(n/k))
决定了是否稳定 -
遍历,覆盖
O (n + k)
遍历桶,如果桶不空
将元素覆盖在原数组
桶排序(课本版本)
O (n + k)
适合整数,连续
稳定
-
min, max
O (n) -
桶数
最大值 - 最小值 + 1 -
创建桶
vector<list<T>> 类型 -
分配
O (n)
元素大小 x,则桶索引 x - min
插入桶的尾部(保证稳定) -
遍历,拼接
O (k)
从前向后遍历桶,如果桶不空
将其 splice 到 result 的末尾-
从后向前遍历桶,如果桶不空
将其 splice 到 result 的头部 也行 -
result 是 list<T> 类型
-
基数排序(LSD)
O(d*(n + k))
k 一般是 10
稳定
-
找最大值 和 最小值
- 如果事先能确定一定是非负数
则不需要找最小值
- 如果事先能确定一定是非负数
-
最大位数
-
从最低位到最高位
逐位进行 计数排序-
不需要找最值
-
B 的大小为 10
-
第 d 位为 x ,则 B 的索引 x
-
各种排序的比较和选择
简单排序
-
分治:冒泡,直接插入,简单选择
分为已排序区和待排序区 -
原地排序:冒泡,直接插入,简单选择
-
基本有序:直接插入,O(n)时间
-
大型对象:简单选择,O(n)次移动
复杂排序
-
快
快速排序(非基本有序) -
原地
堆排序
希尔排序
归并排序(链表) -
稳定
归并排序 -
最大/最小的 n 个数
堆排序 -
合并两个有序数组
归并排序
非比较排序
-
都是非原地排序
-
都要找最值
-
分布均匀
桶排序(ai 版本) -
小数
桶排序(ai 版本)
基数排序- 只适合小数位数固定
要转换成整数再排序
不如桶排序
- 只适合小数位数固定
-
负数
计数排序
桶排序
基数排序- 正负分离 + 负数子数组反转
-
极差较小
计数排序 -
极差较大
基数排序 -
最大/最小的 n 个数
桶排序
计数排序(稍作修改) -
尾部的 n 个数的排名
计数排序 -
某个元素的个数
计数排序
Floyd
多源
任意两个顶点间的最短路径
dijkstra
无负权
有向图
单源
求一点到各点的最短路径和长度
性质
-
子路径最优性(最优子结构)
最短路径上任意两点之间的路径都最短 -
最优子结构体现为
从源点到某个顶点的最短路径
一定包含路径上其他顶点到源点的最短路径 -
prev数组
以源点为根的最短路径树 -
Q 为已确定最短路径的顶点集合
迭代中,顶点 a 暂未加入 Q
则 dist[a] 满足:-
该路径仅由两部分组成
源点到 Q 中某顶点 u 的最短路径(已确定)
边(u,a) -
在当前集合 Q 下
源点到 a 的最短可能路径
-
-
每次确定的顶点 u 的距离 d[u]
一定是最短路径长度- 若存在更短路径
该路径必然经过某个未确定的顶点 v
但 v 的距离 d[v] 至少为 d[u]
(否则 v 会在 u 之前被选择),
而无负权边无法让路径变短,故矛盾
- 若存在更短路径
-
按照顶点到源点的最短路径长度
从小到大的顺序,
依次确定顶点
算法流程
-
初始
邻接矩阵
顶点数n
源点s:dist[s] = 0
其他顶点v:dist[v] = MAX
(表示不可达)
数组 visited 初始均为 false -
迭代
重复以下操作 n 次
(每次确定 1 个顶点
总共 n 个顶点)-
选顶点
从所有未标记的顶点中
选择 dist[v] 最小的顶点 u
此时 u 的最短路径已确定 -
更新距离
遍历 u 的所有邻接顶点 v
若通过 u 到 v 的距离小于 dist[v]
则更新 dist[v]
-
-
长度
数组 dist: 源点到各顶点的最短路径长度- 长度为0:源点到源点,跳过
- 长度为MAX:不可达
- 长度∈(0, MAX):源点到可达顶点
Kruskal
无向图
最小生成树
- 包含所有顶点
顶点数 - 1 条边
权重之和最小
离散数学知识
-
等价关系
自反,对称,传递
集合S上的二元关系R -
等价类
两个等价类要么相等
要么没有交集
所有等价类的并集为集合S -
商集S/R
等价类的集合
集合的集合
集合S的一个划分 -
规范映射/投影映射
π: S→S/R, π(x)=[x]
数学与编程
-
一个连通分量:一个等价类
-
并查集初始化:
每个元素独立成一个集合:每个元素都是一个等价类
每个元素的根节点是自己:自反性 -
find函数:
根节点:等价类代表元素
返回根节点:规范映射
路径压缩:传递性 -
unite函数:
根是否相同:是否在同一个等价类
合并集合:合并等价类
算法流程
-
初始化
边的数量
顶点数量
三元组表(顶点1,顶点2,权重)
初始化并查集数组 -
排序
edge结构体,重载比较运算符
所有边按权重从小到大排序 -
find函数(递归)
- 查找结点 a 的根节点
- 如果结点 a 不是根节点
- 递归查找其父节点的根节点
- 路径压缩
- 结点 a 的父节点变成根节点
- 后续查询效率接近 O (1)
- 如果结点 a 是根节点
- 递归终止
- 直接返回 a
-
unite函数
- 查找 a 和 b 的根节点 ra 和 rb
- 若根节点相同
- 则两集合合并失败
- 若根节点不同
- 将 ra 的父节点设为 rb
- 反过来也行
- 并查集:通过树结构表示集合
- 则两集合合并成功
- 将 ra 的父节点设为 rb
-
遍历边
若添加的边不会形成环
则依次添加边到生成树中,
直到添加 n-1 条边
或所有边处理完毕
判断是否成环
- 并查集
每个集合由一棵树表示
无向图,若两个节点属于同一集合
则添加连接它们的边会形成环
判断图是否连通
-
是否为n - 1 条边
-
并查集
任选一个节点的根作为基准
判断剩下的结点的根是否与基准相同
若不相同,则图不连通 -
任选一个节点作为起点
BFS 或 DFS 遍历所有可达节点
若访问的节点数等于总节点数
则图连通;否则不连通。
时间复杂度由排序决定
O (ElogE),E 是边的数量
Kruskal与其他算法
- find函数(迭代)
- 找到根节点
int root = x; while (parent[root] != root) { root = parent[root]; }
- 将路径上所有节点的父节点直接设为根
- 保存当前节点的父节点
将当前节点的父节点设为根
处理原父节点
- 保存当前节点的父节点
- 找到根节点
- dijkstra的路径
int pre = i; string path = G.name[pre]; while (Path[pre] != -1) { pre = Path[pre]; path = G.name[pre] + "->" + path; } cout << path << endl;
Kahn,CPM
有向图
数学知识补充
-
拓扑序列:
对于有向边 u → v
u 在排序结果中一定位于 v 之前 -
有向图存在拓扑序列,
←→图是有向无环图
算法流程
- 初始化
- 节点数
- 边数
- 邻接表
- 任务开始的交接点编号小者优先
起点编号相同时
与输入时任务的顺序相反 - 邻接表保持边的插入顺序
逆序遍历即可 - 邻接矩阵不存储边插入顺序
需额外记录边顺序
- 任务开始的交接点编号小者优先
- 入度数组
- 最早发生时间ve
- 最晚发生时间vl
- 输入边(顶点1,顶点2,权重)
- 计算所有顶点的入度
- 输入一条边
顶点2的入度 +1
- 输入一条边
-
拓扑排序
所有入度为 0 的顶点入队
每次从队列中取出一个顶点
加入拓扑序列
邻接顶点的入度减 1
若邻接顶点入度变为 0,入队
若拓扑序列的顶点数等于图的总顶点数,则无环
否则存在环- 未被排序的顶点形成环
-
最早发生时间ve
初始化ve数组全为0
ve[j] = 所有前驱节点中 max(ve[i] + weight(i→j))-
翻译成代码
遍历 i 的所有后继节点
对于某一个后继节点 j
ve[j] = max(ve[j], ve[i] + weight(i→j)) -
思想类似于
稀疏矩阵的乘法
向量的加法和数乘
-
-
最晚发生时间vl
初始化vl数组为工程总时间T
(所有ve的最大值)
vl[i] = 所有后继节点中 min(vl[j] - weight(i→j))- 翻译成代码
遍历 i 的所有后继节点
对于某一个后继节点 j
vl[i] = min(vl[i], vl[j] - weight(i→j))
- 翻译成代码
-
计算方向
ve:拓扑正序
vl:拓扑逆序-
要计算节点i的vl
必须先知道所有后继节点j的vl值
拓扑逆序保证在计算节点i时
其所有后继节点都已计算完成 -
ve数组的计算
拓扑排序
可以放在一起/同时进行
-
-
关键活动
判定:对每条边(i→j):
ve[i] == vl[j] - weight-
活动a:边(i→j)
最早开始时间 e (a) = ve[i]
最迟开始时间 l (a) = vl[j] - weight(i→j) -
关键活动:e(a) = l(a)
活动 a 没有缓冲时间
必须按时完成
否则会延误整个项目
-
kahn:
有向图拓扑序列
有向无环图DAG
- 并查集:无向无环图
dfs
bfs
单向静态链表
-
- 将节点归还至空闲区
nodes[nodeIndex].next = freeList; freeList = nodeIndex;
- 头插法
nodes[newNode].next = head; head = newNode;
- 将节点归还至空闲区
-
- 从空闲区获取节点
int newNode = freeList; freeList = nodes[freeList].next; return newNode;
- 删除
int temp = head; head = nodes[head].next; releaseNode(temp);
- 从空闲区获取节点