本篇为个人准备复试过程中的专业课复习笔记,全篇侧重于概念、对比,无具体代码涉及,复习时参考为王道考研408.
概论
复杂度相关:
f(n)、T(n)、O(n)
f(n)为语句重复执行的次数(频度)
对不同算法的基本操作的频率f求和,得到算法的时间复杂度T。
因为一般情况下更关注数量级,所以这里看的是最大频度的数量级O
T(n)=O(maxlevel(f(n)))
T与f是正整数函数,当n达到一定程度,总满足O<T<cf,c为常数,所以只要求出T的最高阶,忽略低阶和常数,就可以反映算法时间性能
思想
递归
一个函数或子程序是由自身所定义或调用的(一个可以反复执行的递归过程与一个跳出执行过程的出口)
应用:阶乘、斐波那契数列、函数调用栈
分治
将一个难以直接解决的大问题依照相同概念分割成两个或者更多子问题(问题之间相互独立),以便各个击破。最好使子问题的规模大相同,即将一个问题分割成大小大致相等的k个子问题的处理方法行之有效。
设计方法:分治法设计的程序一般是递归算法,因此可以用递归方程进行分析。
应用:归并排序
动态规划
如果一个问题的答案与子问题相关,即经分解得到的子问题往往不是互相独立的;将大问题拆解,将每个子问题答案存起来,以供下次求解时直接取用。
前提:需要有最优子结构性质和子问题重叠性质。
最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性质。
重叠子问题 :对每个子问题只解一次,然后将解保存在一个表格中,当再次需要解此问题时,只是简单地用常数时间查看所保存的结果即可。
设计方法:基于动态规划法的算法设计通常按以下几个步骤进行:
(1) 找出最优解的性质,并描述其结构特征;
(2) 递归定义最优值;
(3) 以自底向上的方式计算最优值;
(4) 根据计算最优值时得到的信息构造一个最优解。
贪心
从一点开始,在解决问题的步骤使用贪心原则(采取在当前状态下最有利或最优化的选择,不断改进,持续在每一步选择最佳方法,逐步逼近给定目标,当无法前进时停止)
贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优解的选择来实现(贪心选择)。
证明的具体步骤是:
考察问题的一个全局最优解,并证明可修改该最优解,使其以贪心选择开始;
做贪心选择后,原问题简化为一个规模更小的类似子问题;
用数学归纳法证明,通过每一步作贪心选择,最终可导致问题的一个全局最优解。
最优子结构是指当一个问题的最优解包含其子问题的最优解性质。
应用:图结构的最小生成树、最短路径、哈弗曼编码
动态规划与贪心对比
在动态规划算法中,每步所作出的选择往往依赖于相关子问题的解,因此只有在解出相关子问题的解后才能做出选择;
贪心算法所作的贪心选择仅在当前状态下做出的最好选择,即局部最优选择,然后再求解作出该选择后所产生的相应子问题的解,即贪心算法所作出的贪心选择可以依赖于“过去”所作出的选择,但绝不依赖于将来所作出的选择,也不依赖于子问题的解。
贪心算法和动态规划算法都具有最优子结构性质。
线性表
有限序列(元素可以为空)
线性与链式对比
线性表 | 链式表 | |
---|---|---|
定义 | 按照顺序在连续地址空间存 | 结点包括数据域和指针域 |
特点 | 随机存取,查找效率高 需要预先申请足够的空间 插入删除效率低 | 非随机存储,查找效率低 节约空间,不用提前申请 插入删除效率高 |
时间复杂度 | 查找为O(1) O(n) | 查找为O(n) 一旦找到目标位置,无需修改其余结点 |
头指针与头结点对比
头指针 | 头结点 | |
---|---|---|
定义 | 指向链表中第一个结点的存储位置 | 在链表第一个结点前加一个辅助结点,此时头指针指向头结点 |
目的 | 统一空表与非空表操作,简化实现 |
此外可以使最后结点指向头结点组成环,形成循环链表,也可以增加指针域形成双向、双向循环链表
链表环检测(快慢指针)
fast与slow指针初始化为head
fast迭代为.next.next,一次两步
slow迭代为.next,一次一步
若在遇到链表尾部前两指针相等,则存在环
此时,令cur指针为发现位置,与slow同时一步步迭代,相等的位置即为环入口
原理链接
栈和队列
定义:操作受限的线性表
栈与队列比较
栈 | 队列 | |
---|---|---|
特点 | 后进先出LIFO | 先进先出FIFO |
应用 | 数制转换 括号匹配 迷宫求解 | 树的层次遍历 划分子集 图的广度优先遍历 |
栈的实现
顺序栈 | 链栈 | |
---|---|---|
栈空标志 | S.top=S.base | S=NULL |
栈满标志 | S.size ≤ \leq ≤S.top - S.base | 无满栈情况 |
说明 | 无需设置头结点 头指针即栈顶指针 |
队列实现
说明:front指向队列头部,指向即将出队列的元素结点
rear指向队列尾部,指向下一个
顺序(循环)队列 | 链形式队列 | |
---|---|---|
空判断 | Q.front=Q.rear | Q.front=Q.rear;有头结点 Q.front=NULL;无头结点 |
满判断 | (Q.rear+1)%MAXSIZE=Q.front | 无满队列情况 |
rear | rear指向下一个待插入元素的空位置 | rear指向队列最后一个插入的元素 |
说明 | 可以额外在Queue中加入额外的flag标记位判断是否未满,以充分理由被丢弃的一个空白 |
字符串
取值受限的线性表
字符串匹配算法
朴素模式匹配
主串与子串第一个比较
- 若相同,则继续比较之后的
- 若中途不同,则与刚开始就不同的处理一致
- 若不同,则主串的下一个字符重新与子串第一个比较
改进的模式匹配算法
当匹配子串其他字符过程中若失败,不用像之前一样回溯主串位置。
这一改进主要是引入了next数组,通过观察子串特征构建,应用方法:当匹配失败时,不回溯主串,直接使用next数组对应子串失败下标的值所谓下一次子串匹配的索引值,主串位置不变。
构建方法:
下标值等于:分别以子串首部为起点、以子串匹配失败点前一位为终点的两个小子串的最大相同位数(初始化next[0]=0,next[1]=1,若首位失败则主串+1,其余情况主串不变直接与对应索引子串比较即可)
两种匹配算法对比
主串n,子串m,成功的可能位置(1到n-m+1)
朴素算法 | 改进算法 | |
---|---|---|
最好的情况 | 平均时间复杂度O(n+m) | 平均时间复杂度O(n+m) |
最坏的情况 | 平均时间复杂度O(n*m) | 平均时间复杂度O(n+m) |
空间复杂度 | 无 | O(m) |
说明 | 大多情况下,依然用此方案 | 对于特殊子串效果显著 优势为主串不用回溯,在处理外设输入的大文件十分有效 |
树
树、m叉树、森林定义
树:n个结点的有限集合T,可以为空:
- 有且仅有一个无前驱的称为根的结点
- 其余结点可以分成m个互不相交的有限集合,其中每个集合又是一棵树
二叉树:空树、一个根节点以及最多两棵互不相交的子二叉树构成
森林:互不相交的树构成的集合
二叉树
存储
顺序:下标为i的子树,2i为其左子树,2i+1为其右子树
链式:两个指针域,一个指向左孩子,一个指向右孩子
特殊二叉树
平衡二叉树:叶子节点的深度之间最多相差1
二叉排序树:左子树<根<右子树
完全二叉树:按照顺序存储树结构,从根节点到最后一个节点之间没有空节点(即不存在一个结点只有右子树没有左子树)
线索二叉树:所有的结点的空指针域指向遍历方案的前驱/后继结点,另外为了区分存储结构,结点额外增加两个标记位
遍历与恢复
- 层次遍历:额外需要一个队列结构
- 根节点加入队列
- 从队列提出一个节点遍历,同时将其所有孩子结点加入队列
- 反复第二步直到队列为空
- 先序遍历:根->左子树->右子树
- 中序遍历:左子树->根->右子树
- 后序遍历:左子树->右子树->根
恢复必须有中序遍历结果,再配合先序遍历或后序遍历结果,以先序为例:
先序第一个结果为根,对应中序找到,中序左侧的结点即全为左子树,右侧为右子树;不断递归即可恢复二叉树
二叉树线索化
遍历过程中,额外使用一个指针指向当前遍历结点的前驱,检测树当前孩子指针域是否为空,若空则按照顺序完善线索,并修改标记。
哈弗曼树
保证最小带权路径和,所有的带排结点作为叶子节点,初始化为不同的树,每次选择两个最小的权值合并加上根节点(权值为两孩子之和),知道最后形成一棵树。
树的一般化存储结构
- 孩子表示法,一个指针指向该结点的孩子链表
- 双亲表示法,记录唯一双亲位置
- 孩子双亲表示法,上面两种方法合并
- 孩子兄弟表示法,一个记录第一个孩子,另一个指针指向该节点同双亲的兄弟
树、二叉树、森林的转化
- 树->二叉树
树的兄弟结点间连接
只保留与左孩子的连线
将结果扭正(根节点无右子树)
- 二叉树->树
将A的右孩子、右孩子的右孩子…与A的双亲相连,删掉原有的双亲关系
- 森林->二叉树
每个树先变成二叉树
第一个二叉树为基准,第二个二叉树接入为第一个二叉树右子树,第三个为第二个右子树…
图
由非空顶点集合V与边集合E组成,记为G
存储形式
- 邻接矩阵:容易确定顶点间是否有边,但不易对边计数
- 邻接表:找出边容易,入边需要遍历整个表
- 十字链表:除了指向下一个出边的链表外,还有一个指向相同入边的链表
- 邻接多重表:与十字链表相似,用于无向图
遍历
- 广度优先BFS:与树的层次遍历原理一致
- 深度优先DFS:与树的先序遍历类似
最小生成树
Prim算法 | Kruskal算法 | |
---|---|---|
思想 | 从顶点出发 | 从边出发 |
初始化 | U = { u 1 } , T E = { } U=\{u_1\},TE=\{\} U={u1},TE={} | U = V , T E = { } U=V,TE=\{\} U=V,TE={} |
重复 | 所有
u
∈
U
,
v
∈
V
−
U
u\in U,v\in V-U
u∈U,v∈V−U的边中选一个代建最小边(u,v),纳入TE,并将v纳入U 直至U=V | 选取E中权值最小的边(u,v),判断与已选边是否形成回路(并查集思想,原本默认每个点属于各自类,一但边连到一起,就合并为一个类,一类别区分),若不构成则纳入,否则删除 直到E为空或边数足够 |
复杂度 | O ( n 2 ) O(n^2) O(n2) | 边已经增序排列
O
(
n
2
)
O(n^2)
O(n2) 若用堆排序 O ( e l o g e ) O(eloge) O(eloge) |
使用场景 | 边稠密网 | 边稀疏网 |
最短路径
Dijkstra算法 | Floyd算法 | |
---|---|---|
应用 | 带权图的单源最短路径 | 带权图的各节点间最短路径 |
思想 | 贪心算法,路径递增次序产生最短路径,使用局部最优计算全局最优 | 动态规划思想,将问题求解分为多个阶段。对某一对结点之间,先不允许其他节点中转,再逐渐引入其他节点 |
问题 | 由于贪心算法,所以负权值的出现会使得结果不是最优 | 无法解决带负值权值环的图 |
初始化 | final[0]=true;dist[0]=0;path[0]=-1 | 初始化A矩阵nn规模,为直接距离,path矩阵nn记录下一个中转结点 |
处理 | 遍历顶点,对于未确定路径且dist最小的的顶点,final[i]=true,更新path以及dist | 一步一步加入结点作为中转 每一步加入,考虑以该节点为中转结点是否会使得最短距离改变,若是,则修改对应位置 |
空间复杂度 | O(n) | O ( n 2 ) O(n^2) O(n2) |
时间复杂度 | O ( n 2 ) O(n^2) O(n2) | O ( n 3 ) O(n^3) O(n3) |
拓扑排序
顶点表示活动的网(AOC网),弧为先后顺序
核心思想:考虑入度情况
- 首先选择第一个入度为零的结点输出它
- 删除该顶点与所有以它为头的弧
- 重复上述两步,直到全部输出或不存在入度为0的点
相反的可以使用出度为零的点逆序得到逆拓扑排序
对于无环图,可以使用DFS实现
应用:判断是否存在环
关键路径
边表示活动的网(AOE网),结点为事件点,弧的权值为活动所需时间
关键路径:具有最大路径长度的路径,经过的活动为关键活动
定义:
- 事件v的最早发生时间ve
- 按照拓扑排序依次计算,对于多个入度的事件,取时间最大值
- 事件v最迟发生时间vl
- 按照逆拓扑排序依次计算,汇点的时间等于对应ve值,对于多个出度的事件,取时间最小值
- 活动a的最早开始事件e
- 对应弧尾事件的最早发生时间
- 活动a的最迟开始时间l
- 对应弧头事件的最迟发生时间 — 该活动所需时间
- 活动a的时间余量d=l-e
关键活动即为:活动余量为0的活动集合
查找
一般策略对比
顺序查找 | 折半查找 | 分块查找 | |
---|---|---|---|
思想 | 从头到尾依次找 | 每次检查low与high的平均位置 | 索引表记录每个分块最大关键字以及其存储空间,再在块内顺序查找 |
应用场景 | 顺序表、链表、有序无序均可用 | 有序顺序表 | 数据分块存储,块内无序,块间有序 |
优化 | 元素有序 按照元素查找概率存 | 按照需求预先构造平衡的二叉排序树 | 一般按顺序查找索引表 $ASL=\frac{b+1}{2}+\frac{s+1}{2} |
其中n=sb,所以当s=\sqrt{n}时 | |||
ASL最小为\sqrt{n}+1$ | |||
时间复杂度 | O(n) | O ( l o g 2 n ) O(log_2n) O(log2n) | O(b+s) |
B树
定义:又称多路平衡查找树,所有结点的孩子个数的最大值称为B树的阶,m表示,可以为空树,否则性质如下:
- 除了根节点之外,其余任何结点至少有m/2(上取整)个分叉,即至少含有分叉减一个关键字
- 对于任何一个结点,其所有子树高度都相同
- 所有叶子节点在同一层上,不携带信息(查找失败的点,一般这一层不算为B树高度)
插入
- 查找确定插入位置
- 判断是否超过关键字上限
- 未超过,完成
- 超过,当前节点中间元素上提到父节点,直至符合关键字个数限制
删除
- 非终端结点
- 使用直接前驱/后继代替位置(当前关键字左/右指针子树中最右下/左下的元素)
- 终端节点
- 删除后判断是否低于关键字下限
- 若低于
- 右兄弟够借,使用后继、后继的后继填补
- 左兄弟够借,使用前驱、前驱的前驱填补
- 都不够,与父节点关键字、左右兄弟合并,可能使得父节点关键字减一,需要继续合并
- 若低于
- 删除后判断是否低于关键字下限
B+树
定义:
- 每个分支结点最多有m棵子树
- 非叶根节点至少有两棵子树,其他每个分直接点至少有m/2的上取整棵子树
- 结点的子树个数与关键字个数相等
- 所有叶子节点包含全部关键字以及指向记录的指针,叶结点中将关键字顺序排列,相邻叶结点顺序链接
B树与B+树对比
m阶B树 | m阶B+树 | |
---|---|---|
类比 | 二叉查找树的拓展 | 分块查找的拓展 |
子树数量 | 节点中n个关键字对应n棵子树 | 节点中n个关键字对应n+1棵子树 |
关键字数目 | 根节点的关键字数
n
∈
[
1
,
m
]
n\in [1,m]
n∈[1,m] 其余结点关键字数[m/2,m] | 根节点关键字数[1,m] 其余结点关键字数[m/2,m-1] |
结点信息 | 叶子节点包含全部关键字,非叶子结点的关键字也在叶子节点中 | 各节点中包含的关键字不重复 |
结点信息 | 叶结点包含信息,非叶结点起到索引作用,每个索引项只有对应子树最大关键字以及指向子树的指针,无关键字对应记录的地址 | 结点包含关键字对应的记录的存储地址 |
查找方式 | 不支持顺序查找,查找速度不稳定,可能停在任何一层 | 可以顺序查找,不管失败成功都会停在最下层 |
相同 | 除根节点外,最少m/2个分叉 任何一个结点的子树都要一样高 |
散列查找
定义
对于关键字通过散列函数处理计算得到存储位置信息
常用散列函数
- 除留余数法:p不大于散列表的最大质数
- 直接定值法:关键字的线性变换
- 数字分析法:取特殊的某几位
- 平方取中法:平方后的中间几位
常用冲突处理
- 拉链法:后续冲突直接连在链表上
- 开放地址法:
- 线性探测
- 平方探测
- 多散列函数法
查找效率
与散列函数、冲突处理、装填因子有关
排序
内存排序
方案对比
思想 | 适用范围 | 优化可能 | 空间复杂度 | 时间复杂度 | 稳定性 | |
---|---|---|---|---|---|---|
插入排序 | 每次从待排序列选一个插入到排好的子序列中 | 顺序表、链表 | 寻找目标插入点时时折半查找 | O(1) | 最好
O
(
n
)
O(n)
O(n) 最差 O ( n 2 ) O(n^2) O(n2) 平均 O ( n 2 ) O(n^2) O(n2) | 稳定 |
希尔排序 | 首先下标d取余值相同的为同一子序列,排序后不断缩小d值重新排,直至d=1排完 | 顺序表 | O(1) | 优于直接插入排序 | 不稳定 | |
冒泡排序 | 从前往后,依次两两比较,若逆序则交换,最多执行n-1轮 | 顺序表、链表 | O(1) | 最好
O
(
n
)
O(n)
O(n) 最差 O ( n 2 ) O(n^2) O(n2) 平均 O ( n 2 ) O(n^2) O(n2) | 稳定 | |
快速排序 | 取任意元素为基准,左右两端不断比较,使得左边小,右边大,即确定了基准位置,接着左右两边递归调用重复 | 顺序表,链表 | 取决于递归深度,划分越均匀,效率越高 | 最好
O
(
l
o
g
n
)
O(logn)
O(logn) 最差 O ( n ) O(n) O(n) | 最好
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn) 最差 O ( n 2 ) O(n^2) O(n2) 平均 O ( n l o g n ) O(nlogn) O(nlogn) | 不稳定 |
简单选择排序 | 每一趟选择最小或最大的元素加入有序子序列 | 顺序表、链表 | 必须n-1轮 | O(1) | O ( n 2 ) O(n^2) O(n2) | 不稳定 |
堆排序 | 建立大根堆,依据大根堆排序 | 顺序表、链表 | O(1) | 建堆
O
(
n
)
O(n)
O(n) 排序 O ( n l o g n ) O(nlogn) O(nlogn) | 不稳定 | |
归并排序 | 把两个或者逗哥有序子序列合并,首先递归划分左右序列,接着排序合并 | 顺序表、链表 | O(n) | O ( n l o g n ) O(nlogn) O(nlogn) | 稳定 | |
基数排序 | 将整个关键字拆分d组,d次分配与收集,若当前组可能取r个值,则要r个队列 分配:根据关键字组值放入对应队列 收集:把队列结点依次出队链接 | 关键字便于拆分,d小 每组关键字取值范围不大,r小 数据元素n大 | 关键字便于拆分,d小 每组关键字取值范围不大,r小 数据元素n大 | O® | O(d(n+r)) | 稳定 |
堆排序大致细节
堆:左右结点小于或大于自己
- 建立堆
依据原则:顺序来看,下标小于等于2/n的结点均为非叶子结点,重点是维护它们的位置
方案:循环处理非叶子结点,与他们的孩子比较,若与需求相反则与孩子交换
- 排序
不断取出堆顶元素,使用堆底元素代替堆顶位置,通过下坠调整堆使其符合需求
重复上述过程n-1次
- 结点插入删除
- 插入:新元素放在堆底,上升到合理位置
- 删除:使用堆底代替,下坠到合理位置
磁盘排序
方案迭代优化
需要多个输入缓冲区以及一个输出缓冲区
读写外存+内部排序+内部归并
思想 | 优化/问题 | |
---|---|---|
外部排序 | k路归并,内存分配k个输入缓存区和一个输出缓存区 r个初始归并段,S趟k路归并 | 增加归并段路数k,多路平衡归并(增加输入缓冲区,每次选择进行k-1次对比) 减少初始归并段数量r |
败者树 | 使用多路平衡归并使建立败者树减少关键字对比次数,可以看做一个完全二叉树 | 优化了外部排序增加归并段导致的对比次数增加问题,使得每次对比最多仅需要深度次对比 |
置换选择排序 | 使用更大的内存区域内部排序,最多存
l
l
l个记录 初始一次性取出 l l l个记录,选出最小的放到缓冲,在取出一个待排值,找到大于输出缓冲的最小值写入,重复直至输出缓冲慢或者内存中没有比缓冲大的值则写出归并段 | 优化归并段数量,使得其数量可以减少,但是每个归并段的长度不一 |
最佳归并树 | 每个初始归并段对应一个叶子节点,归并段的块数作为叶子的权值 构建最小权值树(哈弗曼树),从低向上归并 首先补充虚段,再构造对应哈弗曼树 | 由于置换选择排序的归并段数据量不同,所以构造归并树优化归并次数 |