算法设计
1: 注意数组是否是有序的
1 链表转置
使用头插法插入到新链表中
2 数组转置
设置头尾两个指针,交换头尾,一共循环n/2次
3 数组连续部分整体移动
考虑用转置法:
1: 该整体转置一次
2: 剩下部分转置一次
3: 整个数组转置一次,注意整体必须在最后转置
4 链表找中点
设置两个指针,一个指针走一步时另一个指针走两步。
Node* p1 = head;
for(Node* p2 = head; p2->next; p2 = p2->next) {
p1 = p1->next;
if(p2->next) p2 = p2->next;
}
5 树的高度和深度
如果是需要树的层次,则将level作为参数传入
void DFS(Node* tree, int level) {
if(!tree) return;
DFS(tree->left, level+1);
DFS(tree->right, level+1);
}
如果是计算树的高度,则将level作为返回值
int DFS(Node* tree) {
if(!tree) return 0;
int left = DFS(tree->left);
int right = DFS(tree->right);
return 1 + MAX(left, right);
}
6 非递归层序遍历
用队列遍历,当每层最后一个访问时,先将该点孩子入队(如果有),再将队列中最后一个元素标记
7 计算节点到根的路径
回溯思想,设置一个全局变量path,一个全局指针cur表示path中的位置
int path[100];
int cur = -1;
int DFS(Node* tree, int dist/*目的地*/) {
if(!tree) { return 0; }
path[++cur] = tree->data; //入path
if(dist == tree->data) { return 1; } //找到了直接返回
int find = DFS(tree->left, dist);
if(find) { return 1; } //找到后不出path
find = DFS(tree->right, dist);
if(find) { return 1; }
path[cur--]; //出path;
return 0;
}
8 奇数移动到偶数前方
两个指针从两侧开始,遇到奇数在右侧偶数在左侧就交换位置
9 求第k小的元素
- 对区间进行划分
- 如果划分结果=k, 则返回,否则在左区间寻找k,或者在右区间寻找k-(pivot-left)
10 三色划分
- 设置三个指针,两个在左侧,一个在右侧
- 如果中间指针遇到了两种颜色,则和左侧或者右侧指针的元素交换
- 指针移动(注意和右侧交换时候中间指针不能移动,防止换过来的还是该颜色)
11 划分为元素个数相等,但是和相差最大的2个集合
- 对区间进行划分
- 如果划分结果=中间值,则成功,否则在左区间或者右区间继续划分
12 计算数组元素的排名
- 复杂度O(n^2),两个循环,比较任意两个元素
- 大的元素count++
1 链表
1.1 有关指针的问题
- 画图解决即可
- 注意带头节点的循环链表为空时,前指针与后指针指向头节点(p->front->front始终不为空)
1.2 循环链表问题
- 如果在头插入或者删除头,一定需要表尾的指针(即使有头节点也一样)
2 栈
2.1 出栈入栈问题
- 根据当前指针指向的元素是否为空,出栈入栈,空满判断都不相同!
- 对于一个入栈序列,出栈的顺序有 C 2 n n n + 1 \frac{C^{n}_{2n}}{n+1} n+1C2nn
- 问出栈顺序的题:
- 先看 最后一个入栈是不是第一个出栈,此时出栈顺序唯一
- 在用手动模拟
2.2 共享栈
- 栈底在两端
- 满时 中间不需要元素分隔
2.3 栈计算表达式
- 遇到操作数入栈,遇到操作符弹出两个操作数,注意此时有顺序
- 看清操作顺序,是a op b还是 b op a,尤其是减法
3 队列
3.1 出队入队顺序问题
- 入队顺序=出队顺序
- 应该和栈一起考,出栈后再入队,再出队,还是出栈的顺序
3.2 循环队列问题
- 入队:(rear+1)%Max (一定是后入)
- 出队:(front+1)%Max
下面与front和rear指向有关,front指向非空单元,rear指向空单元
- 长度:(rear-front+Max)%Max
- 空: rear=front
- 满: (rear+1)%Max=front
3.3 输入输出受限问题
- 如果是输出受限,那么队列中的顺序就是输出顺序,相当于两端不断加入元素
- 如果是输入受限很复杂,用穷举法进行模拟
3.4 中缀表达式转为后缀
方法1:
- 遇到操作数直接输出,遇到操作符入栈
- 入栈时该操作符优先级必须压住栈顶(优先级最高),否则将出栈,直到该操作符压住栈顶
- 相同符号,栈中优先级高(栈中先出现),括号在栈外优先级最高,栈中优先级最低
方法2:
- 求出后缀表达式,然后用字符和中缀表达式对齐
- 扫描后缀表达式,消去后缀中的字符。
- 是否能够消去中缀栈中的字符
- 不能的话是否可以消去中缀表达式中字符
- 中缀无法被消掉的必须入栈
演示:a/b+(c*d-e*f)
,用树求后缀:ab/cd*ef*-+
后缀 | a | b | / | c | d | * | e | f | *-+ | |||
---|---|---|---|---|---|---|---|---|---|---|---|---|
中缀 | a | / | b | +( | c | * | d | - | e | * | f | ) |
栈 | [/] | [/] | [+(] | [+(] | [*+(] | [*+(] | [-+(] | [-+(] | [*-+(] | |||
第几条规则 | 2 | 3 | 2 | 1,3 | 2 | 3 | 2 | 3 | 2 | 3 | 2 | 1,2 |
4 数组
4.1 矩阵压缩
- 如果是表达式,直接带
a[1][1]
- 如果是较小的数(不超过10):直接画图
- 如果是较大的数,则:
- 看清是行优先还是列优先
- 不管下标从0还是1开始,计算该元素前面有几个元素
- 如果是n个元素,从0开始为
b[n+0]=b[n]
, 从1开始为b[n+1]
5 串
5.1 求next数组
- 先画基本的表格,前两个固定是0, 1
- 第三个开始比较前一列上下两个数字代表的字符是否相等:
- 相等:下面的值+1
- 不相等:替换下面的数字,用该列下面的数字对应的列替换,重复2
- 如果用0替换,则无法继续替换,直接+1
- 如果下标从0开始,则上下数字均减去1
演示:abaa
1 | 2 | 3 | 4 |
---|---|---|---|
a | b | a | a |
0 | 1 | 1 | 2 |
第1条 | 第1条 | 2,1不等, 用第1列下面的0替换, 然后+1 | 1,3相等,直接+1 |
5.2 模式比较算法
- 需要有一个指针i指向主串,j指向模式串
- 如果失配,则主串i不动,j=next[j]
- 如果匹配或者j=0(下标从1开始,否则是-1), 则i++, j++
5.3 求nextval数组
从第3列开始,如果该列Next代表的符号=该列符号,将Next对应的列的Next值拷贝到该列
1 | 2 | 3 | 4 |
---|---|---|---|
a | b | a | a |
0 | 1 | 0 | 2 |
0,1相等,改为0 | 4,2不等,不用改 |
6 树
6.1 概念:
- 节点的度:树的孩子个数
- 树的度:最大的节点的度
- 分支节点,非终结节点:度>0
- 叶子节点,终端节点,度=0
- 高度:叶子=0,非叶子为最大子树高度+1
- 节点路径长度:根到节点的边个数
- 树的路径长度:根到每个节点的路径之和
计算:
- 度之和=n-1
- 边个数=n-1
6.2 2叉树
-
二叉树和度为2的树区别:二叉树可以为空,但是2度树至少有3个节点
-
计算:
- 根从1开始编号,则左孩子为2i, 右孩子为2i+1(根据最后一个节点判断是左孩子还是右孩子)
- n0=n2+1,因为有n0+n1+n2-1=n1+2n2=> n0=n2+1
- 只规定了叶子个数和总个数:使用特殊值法,下面是7个节点,4个叶子的树
-
完全二叉树计算:
- 第一个非叶子节点:[n/2]
- 计算叶子的个数:n-[n/2]
- n1=1或者0
6.3 树的遍历
-
如果 前序与后序完全相反,则说明树是一根线,没有左子树或者右子树
-
前中后序,叶子的相对位置不变
-
已知中序+前后序画树:
- 根据前序得到树的根
- 在中序中画出根的位置,分出左右子树
- 找到左右子树在前序中的位置,重新找根
-
已知前后序画树:
- 如果前序中有xxABxx,后序中有xxxBAxx,则说明A是B的父节点
- 如果前序中有xxABCDxx,后序中有xxxBCDAxxx,则说明BCD都是A的子树
-
通过先序确定的二叉树个数: C 2 n n n + 1 \frac{C^{n}_{2n}}{n+1} n+1C2nn,相当于先序入栈,出栈顺序为中序
-
前序+中序+后续可以用递归实现,区别在于访问时间,
-
栈实现遍历:
- 前序:入栈访问,
- 中序:出栈访问
- 后序:节点必须设置一个flag,当栈顶是该节点时,flag=1,可以出栈并访问,flag=0,将右子树入栈,设置flag=1
6.4 线索二叉树
-
问线索二叉树的线索次序:
- 根据前序或者后序写出遍历结果,
- 问哪个节点,左子树为null,则指向遍历序列中前一个,右子树为null,则指向遍历序列中后一个
-
线索二叉树的线索个数:叶子2个,非叶子1个=n1+2n0=n1+n0+n2+1=n+1
-
空链域问题:只有2个节点的链域可能是空:
- 第一个遍历的节点是否有左子树,
- 最后一个遍历的节点是否有右子树
-
线索树不能解决的遍历问题:前序的前继,后序的后继
6.5 森林
- 森林遍历:从左到右遍历每一颗树
- 森林的树个数=森林点数-边数
6.6 树的二叉化
- 无左右孩子问题:
- 非叶节点 【二叉化】产生 一个无右孩子的节点(+最右节点也无右孩子)
- 叶节点 【二叉化】 产生一个无左孩子的节点
- 树的遍历问题
- 树的中/后根序 【对应的二叉树】中序
- 树的前根序 【对应的二叉树】前序
6.7 WPL
- WPL指的是带权路径值
- 计算方式,根到该节点的路径长度*该点的节点权值
6.8 并查集
- 用孩子指向父节点的树
- Union时候,首先检查根是否相同,然后必须将根进行合并(而不是该节点直接指向另一个节点,该节点可能已经指向一个节点)
- 使用:最小生成树(克鲁斯卡尔算法)、寻找联通分量、判断是否是连通图
6.9 哈夫曼树
- 只有度为0和k的树
- 叶子节点为n时,总结点为2n-1(需要自己算)
- 哈夫曼树虚节点计算 n0是需要归并的段个数,k是k路归并:
- 列出度数和节点数的关系 n 0 + n k − 1 = k ⋅ n k n_0+n_k-1=k\cdot n_k n0+nk−1=k⋅nk
- 表示出nk: n k = ( n 0 − 1 ) / ( k − 1 ) . . . . u n_k=(n_0-1)/(k-1)....u nk=(n0−1)/(k−1)....u,余的u个表示多出来的段
- 增加k个节点,1个节点用叶子的值填充,u个用多出来的段填充,k-u-1个就是虚节点
7 图
7.1 概念
- 完全图:任意两个点之间存在一条边,无向完全图边个数:C(n,2), 有向完全图边的个数2C(n,2)
- 子图:顶点和边的集合属于原来的图(边集合和顶点集合属于原来的图并不一定是子图,可能边和顶点不搭配)
- 生成子图:要求顶点集和原来的图一样,边集属于原来的图
- 极大联通子图(联通分量):加入任意一个点将不连通。联通图只有一个联通分量(本身),非联通图有多个联通分量
- 极小联通子图:删去任意一条边将不连通。联通图极小联通子图就是生成树
- 点联通:点A到点B之间存在路径
- 联通图(无向图概念):任意两点之间存在一条路径,边的个数至少n-1(生成树),最大为C(n,2)
- 强连通图(有向图概念):任意两点之间存在一条路径,A->B, B->A,强联通图必有环,边的个数最少n,最大为2C(n,2)
- 强联通分量:任意两点之间存在一条路径,必定存在环。计算方法:拓扑排序法,统计遇到的环的个数
- DAG图:有向无环图
- AOV网: 一种DAG图,表示活动网络
7.2 计算
- 有向图:出度之和=入度之和=e,无向图:度之和=2e
- 一个点的出度不一定等于入度,但是图的出度=入度
7.3 至少有几条边,至少有几个点问题
- 联通图考虑:
- 边的个数最大: 完全图 C(n,2)
- 边的个数最小:生成树 n-1
- 确保联通:构造去掉一个点的完全图:C(n-1, 2)+1
- 非联通图考虑:
- 最大边数/最少点数:构造去掉一个点的完全图,但是不连上 C(n-1, 2)
7.4 拓扑排序
- 用邻接矩阵存储,对角线以下为0的上三角阵一定有拓扑排序
如果可以拓扑排序,则不一定为上三角阵,但是可以通过修改编号变为上三角阵
7.5 邻接表和邻接矩阵
- 存储比较
有向图 无向图 邻接表 每一条边存放一次 每一条边存放两次 邻接矩阵 边的个数为矩阵中1的个数,
主对角线为0,a[i][i]=01的个数一定是偶数,
边=1的个数/2
7.6 图操作的时间复杂度
操作 | 邻接表 | 邻接矩阵 |
---|---|---|
DFS/BFS | O(E+V) | O(V^2) |
拓扑排序 | O(E+V) | O(V^2) |
7.7 图的遍历
- DFS:和树的先序相同,使用栈或者递归
int visit[V] = {0};
void DFS(int node) {
visit[node] = 1;
for(int i = 0; i < V; i++) {
if(A[i][j] && !visit[j]) { DFS(j); }
}
}
- BFS:和树的层序相同,使用队列,不能使用递归
7.8 求联通分量个数
- DFS:最外层调用DFS的次数
- 并查集:统计根节点的个数(父节点为-1的点)
7.9 回路检测
- DFS:如果检测到访问了已经到达的点,说明存在回路
- 拓扑排序:如果存在没有被排序的点,但是不存在入度为0的点,说明存在环路
7.10 判断是否是树
- 回路检测
- 边的个数是n-1+DFS遍历了所有的点
7.11 最小生成树
- 是否唯一:
如果有权值相同的边,则可能不唯一。
如果不存在权值相同的边,则一定唯一。唯一不一定权值全部不同。 - PK算法:
Prim普里姆算法 Kruskal克鲁斯卡尔算法 描述 找到距离点集最近的点加入树 找到最小的边加入树,不能构成回路 复杂度 V^2 ElogE 实现 并查集+堆寻找最小边
7.12 最短路径
Dijkstra迪杰斯特拉算法 | Floyd弗洛伊德算法 | BFS | |
---|---|---|---|
描述 |
|
| 一个外部数组记录距离,该节点的距离等于父节点距离+1 |
复杂度 | V^2 | V^3 | V^2 |
问题 | 负权图不行,因为无法修改已经访问的点 | 允许有负权,不能有负权的环。可用于无向图 | 权值必须相同 |
7.13 拓扑排序
- 删去入度为0的点,可以用于环路检测
- 删除出度为0的点,可以变为逆拓扑序
- 拓扑排序不唯一
- 复杂度:使用邻接矩阵V^2, 使用邻接表E+V
7.14 关键路径
- AOV网中最长的路径
- 关键路径不唯一
- 减小关键路径不一定减小工期,增大关键路径一定增大工期
- 事件(点):
- 最早开始:多个汇入选择最长的(前一个点+权值)
- 最晚开始:多个发出选择最小的(前一个点-权值)
- 活动(边):
- 最早开始:起点的最早开始事件
- 最晚开始:终点最晚开始-该活动的长度
- 拓扑排序求解最早开始时间,逆拓扑排序求解最晚开始时间。
- 关键活动:最早开始=最晚开始的活动
- 关键节点:最早开始=最晚开始的节点
- 求关键路径时,首先计算节点的关键路径,同时标记到底从哪条边汇入的。
7.15 DAG表达式
- 叶子节点必须重新利用
- 先画出完整的图,然后再消去相同的。
8 查找
8.1 概念
- 平均查找长度:查找到该点的比较次数乘以该点的概率,分为查找成功时的和查找失败时的。
8.2 顺序查找
- 查找成功的次数(无序表和有序表相同):第一个元素1,第二个元素2,…一共是1+…+n
- 查找失败的次数:无序表每个元素均是n, 有序表查找失败次数更小。
8.3 折半查找
- 判定树是平衡二叉树而不是完全二叉树,因为可能有偶数个元素
- 树节点是查找不成功的结果
- 问折半查找的平均查找长度需要画树
- 查找最多次数和最少次数最多相差1,如果问查找成功的次数,则画图做。
- 首先画出一个总结点个数最接近的满二叉树,然后在最左侧或者最右侧添加节点
(注意只能往一个方向添加,做到左子树始终大于右子树或者右子树始终大于左子树) - 如果给查找树按照中序遍历可以得到节点的值
- 首先画出一个总结点个数最接近的满二叉树,然后在最左侧或者最右侧添加节点
- 折半查找树形状判定:
- 左子树节点个数>右子树节点个数,或者相反。不能同时出现。
- 或者用中序标号,用两个点计算父节点,是否都是上取整或者下取整
8.4 分块查找
- 块长取 n \sqrt{n} n
8.5 BST二叉查找树
- 插入:插入到第一个查找失败的位置
- 删除:
- 叶子:直接删除
- 一个孩子:孩子替换
- 2个孩子:用右子树中序第一个节点进行替换
- 操作复杂度:平均log2n, 最坏n(退化为一个链)
- 删除后插入:删除叶子:不变,删非叶子:变
- 有序插入时退化为一条链,复杂度为n
- 合法的查找顺序:对于abABCD, 如果a>b,则b在a的左子树,ABCD也必须小于a。如果a<b, b在a的右子树,则ABCD也必须大于a
8.6 平衡二叉树
- 插入和删除本质是先插入/删除,再修改问题节点,因此从问题节点从最长路径进行修改。
- 修改方法:RR/LL: 中间节点为根,LR: RL:最下方节点为根,其他节点按照二叉树的规则挂上去。
- 平衡二叉树最少节点: n h = n h − 1 + n h − 2 + 1 , n 0 = 0 , n 1 = 1 , n 2 = 2 n_h=n_{h-1}+n_{h-2}+1, n_0=0, n_1=1, n_2=2 nh=nh−1+nh−2+1,n0=0,n1=1,n2=2
- 平衡因子:NULL节点:-1, 叶节点:0,非叶节点:MAX{left, right}+1,平衡因子为左子树高度-右子树高度
- 删除后插入:不管是叶子还是非叶,都可能变,也可能不变。要看是否进行了调整。
8.7 红黑树
-
黑节点个数>红节点个数
- 根到叶子最长路径<= 2*最短路径
- 2*黑节点>= 总节点
- 红黑树的最大高度:按照全部为黑节点的满二叉树计算,然后高度*2
-
红黑树适合于较多的插入删除操作,AVL树适合于较多查找操作
-
插入:插入总是调整红红冲突
- 叔叔为红色,则两个节点同时变黑,爷爷变红,循环调整
- 叔叔为黑色,则和AVL调整相同,父+两个红色节点(RR/LL,RL/LR),调整后子树的根为黑色,两个孩子为红色
删除:
- 两个孩子:用右子树中序第一进行替换,转而删除该节点
- 一个孩子:孩子替代,强制变为红色。
- 0个孩子:红节点直接删除,黑节点,兄必须是红色
8.8 B树
- m阶即树的度
- 如果给出树型问度:
- 看最大的子树的个数
- 看节点中关键词最多个数+1
- 节点中的关键词有序
- 度:关键词个数为度-1
- 根:度为0, 2, 3, 4, …
- 非根:度为 ⌈ m ⌉ \lceil m\rceil ⌈m⌉到m
- 根非常特殊,如果提到非叶节点,需要考虑是不是根
- 非叶节点中也存放关键词,叶节点都在同一层
- h层节点:
- 最多节点数: m h − 1 m^h-1 mh−1,每一个节点都为m-1个
- 最少节点数: 2 ⌈ m 2 ⌉ h − 1 − 1 2\lceil \frac m2\rceil^{h-1}-1 2⌈2m⌉h−1−1,根节点1个,然后分为两个子树,每个节点都是m/2个
- n个关键词:n+1个叶节点(n+1)种失败的可能
- 节点的个数:
- 最多 1 + ( n − 1 ) ⌈ m 2 ⌉ = 关键词个数 1+(n-1)\lceil\frac m2\rceil=关键词个数 1+(n−1)⌈2m⌉=关键词个数,根为1个key, 其他都是m/2个
- 最少: ( m − 1 ) n = 关键词个数 (m-1)n=关键词个数 (m−1)n=关键词个数
- 节点的个数必须凑好,否则可能不能成树,因此尽可能画图
- 阶数如果是3或者4,则每个节点关键词最少可以只有1个(关键词数=节点数),变为一个满二叉树
- 插入:插入后如果关键词变为m个,则分裂。中间一个关键词插入到父节点。查看父节点是否需要分裂。
- 删除:
- 非叶节点:用左孩子最大值或者右孩子最小值替换,递归向下删除,直到叶子
- 叶子节点:如果删除后关键词小于
⌈
m
/
2
⌉
\lceil m/2\rceil
⌈m/2⌉个,则:
- 左右兄弟借:左右兄弟够借,则一个key去父节点,父节点的key下降到该节点
- 左右兄弟不够借:父节点key下降,和左右孩子节点合并。递归删除父节点中的key
- B树内存相关:
- 计算内存大小:B树的每个节点都需要记录文件,因此任意一个节点:m-1个索引值+m-1个索引指向的地址+m个指向孩子的指针,
- 计算节点个数:最小:总记录数/(m-1),最大:总记录数/([m/2]-1)
8.9 B+ 树
- 阶数=度=key的最大个数
- 叶子节点之间用指针相连,可以进行顺序查找
- key指向一个节点,该key是节点中最大的key,因此节点中的key和子树个数相等
8.10 散列查找
-
概念:
- 填装因子:元素个数/表长,对于拉链法这个计算方式可能会>1(不懂)
- 开放地址:地址对于同义词和非同义词均开放
- 聚集:指同义词在相邻的位置上聚集,降低查找效率(多在线性探测时),原因:处理冲突的方式不当
-
删除:
- 开放地址:删除时必须使用伪删除
- 拉链法:可以物理删除
-
发生冲突的可能性和表长无关,只和填装因子有关。
-
查找长度:
- 查找失败的长度:看Hash函数值有几个,计算到查找到该函数值的时候需要比较几次到空元素(所有的探测法都是如此),和空元素比较也算1次
-
伪随机序列法:di必须是确定的,预先算出的,而不是每调用一次Hash函数计算一次random()
-
是否是Hash函数:
- 要求对于同一个key返回同一个值
- 要求返回值不超过0-n-1
-
如果Hash(key)=key%p,需要自己选取Hash函数,则p选择最大的素数
9 排序
9.1 概念
- 稳定性:排序关键词相等时,排序之和元素的相对顺序不变
- 内部排序:排序时元素都在内存中
- 外部排序:排序时元素在内存和外存中移动
9.2 插入排序
- 每趟针对每个元素,都向前比较最少1次,最多n-1次,并移动,共n-1趟
- 最好最差情况:
- 如果元素有序,需要比较n次,复杂度O(n)
- 如果元素无序,需要比较/移动:0+1+2+…n-1次,复杂度O(n^2)
- 如果最小的元素在最后一个位置上,则最后一趟之前,没有一个元素在正确位置上
- 分辨:前i个是有序的,虽然不是最小的。
9.3 希尔排序
- 每趟对a[i], a[i+d], a[i+2d],…a[i+kd]进行插入排序,即第1个元素开始,往后数d个(第1个元素不算),再往后数d个,形成子表进行排序,每趟修改d的个数
- 不稳定,因为相同元素会进入不同的子表
- 复杂度:无法确定,但是有序时会退化成O(n)
- 只能在顺序表实现
- 分辨:寻找交换了位置的多对元素,如果间隔相等说明是。如果求d,则可能是间隔的因数,对题目中的选项逐个验证。
9.4 冒泡排序
- 每趟从最后一个元素向前交换,直到进入有序表,共n-1趟
- 最好最差情况:
- 如果有序,则只需要比较n-1次,进行一趟
- 如果无序,需要比较/交换 n-1+n-2+…+1次
- 分辨:第i趟最前面有i个最小值,或者最后面有i个最大值(从前面开始冒泡)
9.5 快排
- 每趟:
- 把a[left]拷贝到pivot,
- right不断减小,直到找到小于pivot的值,拷贝到a[left],
- left不断增加,直到找到大于pivot的值,拷贝到a[right]
- 重复直到left>=right, 然后把pivot拷贝到a[left]
- 空间复杂度:使用到栈,需要O(logn)~O(n)
- 时间复杂度:
- 最好情况:无序,pivot两侧元素个数基本相等:O(nlogn)
- 最坏情况:有序或者逆序,退化为O(n^2)
- 每趟都有一个元素到达正确位置,分辨时先排好序,然后比较
- 可以使用队列替换栈,区别在于处理子区间的顺序
while(!empty()) { left,right = pop(); int pivot = partition(left, right); push(left, pivot-1); push(pivot+1, right); }
9.6 选择排序
- 每趟都从i+1到n-1中挑选最小的和i+1交换,共n-1趟
- 复杂度总是O(n^2),比较总是O(n^2),交换为1-n,交换次数最少的算法
- 分辨:第i趟,前面i个是最小的
9.10 堆排序
- 堆:
- 完全二叉树
- 父节点大于两个子节点a[i]>a[2i], a[i]>a[2i+1]
- 建堆:
- 从n/2到1的顺序检测节点
- 节点和子节点比较,选择较大的换上去
- 换下来的父节点需要递归的向下更换
- 插入:
- 插入到最后并且向上调整
- 删除:
- 堆顶和堆中最后一个元素交换,然后向下调整
- 复杂度:建堆O(n),插入/删除O(log2n),排序O(nlog2n)
9.11 归并排序
- 两个元素之间相互归并,然后是4个元素,8个元素…
- 复杂度:
- 归并:归并最坏复杂度: m+n-1,情况是一个表前m-1个元素都小于另一个表第一个元素,而最后一个元素大于另一个表所有元素
- 趟数 ⌈ log k n ⌉ \lceil\log_kn\rceil ⌈logkn⌉
- 辅助空间:O(n)
- 复杂度和子排列无关
9.12 基数排序
- 从低位开始排(LSD)或者高位开始(MSD),分配+收集
- 辅助空间O(r),r是数字的范围比如0-9
- 复杂度:d趟(d是key的位数),每趟分配: n,每趟收集: r,共d(n+r)
- 注意排序时如果位数不足需要补足,比如2补为 002
9.13 内部排序性质总结
- 稳定性:归并,插入,基数,冒泡(跪插鸡毛)
- 辅助空间:冒泡(n~logn),归并O(n)
- 复杂度和初始状态有关:插入,冒泡,快排,希尔排序
- 趟数和初始值有关:冒泡
- 一趟后可能没有一个元素在正确位置上:归并,插入
- 一趟后总有元素在正确位置上:选择,冒泡,快排,堆排序
9.14 外部归并
- 外部排序分为两部分,首先生成最大归并段,然后将归并段进行归并,假设可用内存为M
- 最大归并段长度为M,由此可以计算出归并段的个数
- 内存中可以放入m块,则m-1个为输入,1个为输出,即归并路数为m-1
- 读写次数计算:需要m趟,则读+写:(m*磁盘块数+磁盘块数)2,第一部分是外部排序,第二部分是内部排序,最后是读写2
- 归并趟数计算: ⌈ log k r ⌈ \lceil \log_kr\lceil ⌈logkr⌈,k是归并路数,r是磁盘总块数
- 输入输出块计算:
- k路归并,需要1个输出块和k个输入块,
- 如果是并行,则至少2个输出+2k输入,
- 如果是已知总数,则1个为输出,m-1个为输入,也就是归并路数
- 计算归并段的长度:归并段长度就是最大内存值。
9.15 败者树
- 用于找出k个元素中最小的元素。
- 对于r个磁盘块,k路归并,则趟数为 ⌈ log k r ⌉ \lceil\log_kr\rceil ⌈logkr⌉,
- 每趟需要寻找n-1个最小元素(n是总块数),
- 寻找一个最小元素,普通比较法需要k-1次比较,而败者树需要 log 2 k \log_2k log2k次比较。
- 叶节点为缓存块,非叶节点为失败者的块号(大的值),在根上方写入冠军的块号(最小值)
- 败者树比较次数和路数k无关
9.16 置换选择算法
- 生成较长的有序文件用于归并
- 生成方法:
- 工作区空了就用输入文件填充
- 工作区中找到比上个输入大的最小元素
- 如果找不到则结束输入,重新输出到一个新的文件
例如:17,21,05, 44, 10, 3,工作区长度为3
[17,21,5], 输出5, [17,21,44], 输出17, [10,21,44], 此时必须输出大于17的最小值21
9.17 最佳归并树
- 构造哈夫曼树(将段长作为权值,度为归并路数K,叶节点为归并段数r)
- 当段个数不够时设置虚段