数据结构考研笔记
一、概念篇:
-
数据结构的定义:相互之间存在一种或多种特定关系的数据元素的集合,由逻辑结构、存储结构和数据运算三部分组成;
-
数据的逻辑结构:
- 线性结构:一般线性表;栈和队列;串;数组和广义表;
- 非线性结构:一般树和二叉树;有向图和无向图;集合;
-
数据的存储结构:
存储结构 优点 缺点 顺序存储 可以随机存取,元素占用空间最小 只能使用相邻的一整块存储单元,产生较多的外部碎片 链式存储 没有碎片,充分利用所有存储单元 指针占用额外空间,只能顺序存取 索引存储 检索速度快 索引表占用额外空间,增删数据还需要修改索引表 散列存储 增删改查速度都快 散列函数选择不好则产生冲突,解决冲突增加时空开销 -
算法原地工作:算法所需的额外辅助空间是常数O(1),而非不需要任何额外的辅助空间;
-
静态链表:借助数组来描述线性表的链式存储结构,需要预先分配一块连续的内存空间,其插入、删除操作只需要修改指针,而不需要移动元素;
-
循环链表的判空条件:
- 循环单链表:头指针是否指向自己;
- 循环双链表:头指针的pre和next指针都指向自己;
-
n个不同的元素进栈,总共有卡特兰数: C 2 n n n + 1 \cfrac{C_{2n}^n}{n+1} n+1C2nn种不同的出栈顺序;
-
共享栈:
- 为了更有效地利用存储空间,两个栈的空间相互调节;
- 只有在整个存储空间占满时才发生上溢,且栈满的条件是:两个栈顶指针相邻;
-
循环队列:
- 出队:front = (front+1)%n;
- 入队:rear = (rear+1)%n;
- 队空的条件:front == rear(此时意味着front指向队头元素,rear指向队尾下一个元素);
- 队满的条件:(rear+1)%n == front(牺牲一个单元);
- 队列长度:(rear-front+n)%n;
- 注意front和rear的含义(指向队头/队尾,还是队头前一个元素/队尾后一个元素);
-
双端队列:
- 输入受限的双端队列:前端push和pop,后端只能pop(前端I/O为栈,前后端I/O则是队列);
- 输出受限的双端队列:前端push和pop,后端只能push(前端I/O为栈,后前端I/O则是队列);
-
稀疏矩阵:
- 两种存储结构:三元组表(数组)和十字链表(链表);
- 优点:节省存储空间;
- 缺点:失去随机存取特性;
-
树的相关概念:
- 树中结点的度数是指结点的孩子个数;
- 树的度是指树中结点的最大度数;
- 结点的层次是从树根为第1层开始定义的;
- 树的高度是指树中结点的最大层数;
- 树中两个结点之间的路径的路径长度是指所经过的边的个数;
- 树的路径长度是指从树根到每个结点的路径长度的总和;
- 有序树:树中任意节点的子结点之间有顺序关系,即使只有一个孩子也要指明(比如是左还是右);
-
树的相关数学性质:
- 若树的结点数为n,则树的所有结点的度数之和/边的条数为n-1;
-
二叉树的相关概念:
- 满二叉树:二叉树中的每层的结点个数都达到了最大值的树,约定从根节点编号为1开始,从左到右,从上到下,逐层为每个结点编号;
- 完全二叉树:树中每个结点都可以与满二叉树的结点一一对应的二叉树;
- 平衡二叉树:二叉排序树BST的左右子树的高度之差(平衡因子)的绝对值不超过1,则称其为平衡二叉树;
- 最优二叉树:带权路径长度(WPL)最小的二叉树,也称霍夫曼树;
- 二叉排序树:左右子树均是二叉排序树,且左子树的所有结点的权值小于根结点的权值,右子树的所有结点的权值大于根结点的权值;
- 正则二叉树:结点的度数只有0或者2;
-
二叉树的相关数学性质:
- 完全二叉树中,编号为 i i i的结点,其父亲编号为 ⌊ i / 2 ⌋ \lfloor i/2\rfloor ⌊i/2⌋,左孩子编号为 2 i 2i 2i,右孩子编号为 2 i + 1 2i+1 2i+1,其所在的层数为 ⌈ l o g 2 ( i + 1 ) ⌉ \lceil log_2(i+1)\rceil ⌈log2(i+1)⌉,最后一个非叶结点(唯一可能度为1的结点)的编号/非叶结点的个数是 ⌊ n / 2 ⌋ \lfloor n/2\rfloor ⌊n/2⌋,叶结点个数为 n − ⌊ n / 2 ⌋ n-\lfloor n/2\rfloor n−⌊n/2⌋,树的高度为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n+1) \rceil ⌈log2(n+1)⌉或 ⌊ l o g 2 ( n ) ⌋ + 1 \lfloor log_2(n)\rfloor+1 ⌊log2(n)⌋+1;
- 完全二叉树的左右子树至少有一个是满二叉树;
- 相同结点数的二叉树中,完全二叉树的路径长度最短;
- 二叉树的叶子结点个数等于度为2的结点个数+1,即 n 0 = n 2 + 1 n_0 = n_2 + 1 n0=n2+1;
- 高度为h的二叉树至多有 2 h − 1 2^h-1 2h−1个结点,二叉树第h层至多有 2 h − 1 2^{h-1} 2h−1个节点;
- 高度为h的正则二叉树的结点数至少为 2 h − 1 2h-1 2h−1;
- n个结点的二叉树链表中,有n+1个空指针域;
-
二叉树的遍历:
- 先序遍历:NLR;中序遍历:LNR;后序遍历:LRN;层次遍历:BFS;
- 确定一棵二叉树的序列组合:
- 思想:能找到根,并且能划分左右子树;
- 可以唯一确定:中序+前序/后序/层次序;
- 不能唯一确定:先序+后序/层次序(有根但无法划分左右子树);
- 深度优先遍历的三种遍历访问叶结点的次序是一样的;而层次遍历则可能不同;
- 任意一种包含空指针信息的遍历顺序均能唯一确定一棵二叉树,除了中序(不能确定根在哪里);
- 若二叉树的先序遍历和后序遍历互为逆序,则:二叉树是退化的链状二叉树,每个结点要么是叶子结点(末尾),要么只有一个孩子结点,其高度等于结点个数;
- n个结点组成的先序序列,对应 C 2 n n n + 1 \cfrac{C_{2n}^n}{n+1} n+1C2nn个不同的二叉树(先序序列相当于入栈序列,中序序列相当于出栈序列);
-
线索二叉树:
- 若无左子树,则令左孩子结点指向前驱结点(ltag=1);若无右子树,则令右孩子结点指向后驱结点(rtag=1);
- 引入线索二叉树的目的是为了加快查找结点前驱和后继的速度;
- 建立后序线索二叉树需要栈的支持;
- 线索二叉树找后继:
- 先序:有左孩子就是左孩子,没有左孩子有右孩子就是右孩子,叶子结点直接看右链域;
- 中序:若rlag=1,直接看右链域,否则为右子树的最左下结点;
- 后序:根没有后继,否则若是双亲的右孩子或者是没有右兄弟的左孩子,则是双亲,否则是右兄弟的最左下结点;
-
一般树和森林:
- 孩子兄弟表示法:左孩子结点,右兄弟结点/兄弟树;
- 一般树的遍历:先根遍历(相当于二叉树的先序遍历);后根遍历(相当于二叉树的中序遍历);
- 森林的遍历:先序遍历(同二叉树的先序遍历);中序遍历(同二叉树的中序遍历);
- 森林中树的棵数等于转化成的二叉树的根节点的兄弟个数+1(加上根节点自身);
- 若树或者森林总共有n个叶结点,则转化而成的二叉树总共有n个无左孩子的结点;
- 若树或者森林总共有n个非叶结点,则转化而成的二叉树总共有n+1个无右孩子的结点(加上最后一棵树的根节点自身);
- 具有n个顶点和e条边的森林,则该森林有n-e棵树;
-
k-正则树:
- n 0 = ( k − 1 ) n k + 1 n_0 = (k-1)n_k+1 n0=(k−1)nk+1;
- 高度为h的k-正则树最多有 k h − 1 k − 1 \cfrac{k^h-1}{k-1} k−1kh−1个结点(对应满k叉树),最少有 ( h − 1 ) k + 1 (h-1)k+1 (h−1)k+1个结点(每个非叶结点的k个孩子中,只有一个孩子为非叶结点);
-
二叉排序树:
- 对二叉排序树进行中序遍历能得到一个递增的有序序列;
- n个结点的二叉排序树中查找某个关键字的最多比较次数是n;
- n个结点的二叉排序树最小深度为 ⌈ l o g 2 ( n + 1 ) ⌉ \lceil log_2(n+1)\rceil ⌈log2(n+1)⌉;
- 在二叉排序树中删除某叶子结点再插入该叶子结点,树前后相同;
- 在二叉排序树中删除某非叶子结点再插入该非叶子结点,树前后不同;
-
平衡二叉树:
- n个结点的平衡二叉树的最大深度为 O ( l o g 2 n ) O(log_2n) O(log2n);
- 在平衡二叉排序树中删除某叶子结点再插入该叶子结点,树前后可能不相同;
- 在平衡二叉排序树中删除某非叶子结点再插入该非叶子结点,树前后可能相同;
- 深度为h的平衡二叉树中含有的最少结点数(所有非叶结点的平衡因子均为1)的递推公式: n h = n h − 1 + n h − 2 + 1 , n 1 = 1 , n 2 = 2 n_h = n_{h-1}+n_{h-2}+1,n_1=1,n_2=2 nh=nh−1+nh−2+1,n1=1,n2=2;
-
霍夫曼树:
- 是一棵正则二叉树,满足 n 0 = n 2 + 1 , 且 n 0 + n 2 = n n_0=n_2+1,且n_0+n_2=n n0=n2+1,且n0+n2=n;
- 上层结点的值一定大于任何下层结点的值;
-
堆:
- 是一棵完全二叉树;利用完全二叉树的顺序存储;
- 下标为 i i i的结点所在二叉树的第 ⌈ l o g 2 ( i + 1 ) ⌉ \lceil log_2(i+1)\rceil ⌈log2(i+1)⌉层;
- 大根堆中第k大的元素可以在第 ⌈ l o g 2 ( k + 1 ) ⌉ \lceil log_2(k+1)\rceil ⌈log2(k+1)⌉层的任意位置;
-
图的相关概念:
-
简单路径:在路径序列中,顶点不重复出现的路径;
-
简单回路:除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路;
-
连通:无向图中,v和w之间存在路径;强连通:有向图中,v到w和w到v都存在有向路径;
-
连通分量/强连通分量:无向图/有向图的极大连通子图;
-
生成树:包含连通图中所有顶点的极小连通子图;
-
有向完全图:两个顶点之间都有两条方向相反的边连接的图;
-
竞赛图:底图是无向完全图的有向图;
-
邻接矩阵:存储顶点之间邻接关系的二维数组;无权图的邻接矩阵中,1表示两顶点之间有边,0表示无边;有权图的邻接矩阵中,w表示两顶点之间边的权值,0或者 ∞ \infin ∞表示两顶点之间不存在边;
-
邻接表:对图中每个顶点v建立一条单链表,称为边表,边表结点存储了从顶点v出发的边的终点;边表的头指针和顶点的数据采用顺序存储,称为顶点表;
-
十字链表:有向图的一种链式存储结构,每条边由5域弧结点表示,每个顶点由3域顶点结点表示;
弧结点:[出点,入点,同入点的下一边,同出点的下一边,info] 顶点结点:[data,首出边,首入边]
-
邻接多重表:无向图的一种链式存储结构,每条边由6域边结点表示,每个顶点由2域顶点结点表示;
边结点:[mark,i顶点, i顶点的下一边,j顶点,j顶点的下一边,info] 顶点结点:[data,该顶点的首边]
-
-
图的相关性质:
- 握手定理:无向图: ∑ d ( v ) = 2 e \sum d(v) = 2e ∑d(v)=2e;有向图: ∑ d i n ( v ) = ∑ d o u t ( v ) = e \sum d_{in}(v) = \sum d_{out}(v) = e ∑din(v)=∑dout(v)=e;
- 一个无向图有n个顶点,和多于n-1条边,则该图一定有环;
- 对于n个顶点的无向图,则至少需要n-1条边才能连通;对于n个顶点的有向图,则至少需要n条边才能连通;
- 某图为有向无环图当且仅当该图存在拓扑路径;
- n个顶点的无向完全图总共有 C n 2 C_n^2 Cn2条边;n个顶点的有向完全图总共有 2 C n 2 2C_n^2 2Cn2条边;
- 对于有n个顶点的无向图,至少需要 C n − 1 2 + 1 C_{n-1}^2+1 Cn−12+1条边才能保证其连通性;
-
图的遍历:
- 一个图的邻接矩阵表示唯一,但邻接表表示不唯一,取决于插入边的顺序;
- 一个图的邻接矩阵的空间复杂度: O ( n 2 ) O(n^2) O(n2);邻接表的空间复杂度为: O ( n + e ) O(n+e) O(n+e);
- 图的DFS和BFS的时间复杂度,以邻接矩阵存储则是 O ( V 2 ) O(V^2) O(V2),以邻接表存储则是 O ( V + E ) O(V+E) O(V+E),空间复杂度都是 O ( V ) O(V) O(V);
- 图的DFS遍历序列,基于邻接矩阵则是唯一的,基于邻接表则是不唯一的;
- 图的BFS生成树的树高总是不大于图的DFS生成树的树高;
-
最小生成树:
- Prim算法的时间复杂度为 O ( V 2 ) O(V^2) O(V2),空间复杂度为 O ( V ) O(V) O(V),适合于求解边稠密的图的MST;
- Kruskal算法的时间复杂度为 O ( E l o g E ) O(ElogE) O(ElogE),空间复杂度为 O ( V ) O(V) O(V),适合于求解边稀疏而顶点较多的图的MST;
-
最短路径:
- Dijkstra算法的时间复杂度为 O ( V 2 ) O(V^2) O(V2),空间复杂度为 O ( V ) O(V) O(V),但不能求解有负权边的图;
- Floyd算法的时间复杂度为 O ( V 3 ) O(V^3) O(V3),空间复杂度为 O ( V 2 ) O(V^2) O(V2),但不能求解有负权环的图;
-
拓扑排序和关键路径:
- 若有向图存在拓扑序,则按照拓扑序排列的邻接矩阵对角线以下均为零;
- AOV网络:顶点表示活动,有向边<Vi,Vj>表示活动Vi必须先于活动Vj进行,是无权DAG图;
- AOE网络:顶点表示事件,有向边表示活动,边上权值表示完成该活动的开销,是有权DAG图;
- 事件 v k v_k vk的最早发生时间: v e k = m a x ( v e j + w j k ) , v e 0 = 0 ve_k = max(ve_j+w_{jk}), ve_0 = 0 vek=max(vej+wjk),ve0=0,表示从源点到该顶点的最长路径长度;
- 事件 v k v_k vk的最迟发生时间: v l k = m i n ( v l j − w k j ) , v l n = v e n vl_k = min(vl_j-w_{kj}),vl_n=ve_n vlk=min(vlj−wkj),vln=ven,表示保证它的后继事件在其最迟发生时间发生时,该事件最迟发生的时间;
- 活动 a i a_i ai的最早开始时间: a e i = v e k , a i = < v k , v j > ae_i = ve_k, a_i = <v_k,v_j> aei=vek,ai=<vk,vj>,表示活动 a i a_i ai的最早开始时间就是活动弧起点事件的最早发生时间;
- 活动 a i a_i ai的最迟开始时间: a l i = v l k − w j k , a i = < v j , v k > al_i = vl_k-w_{jk},a_i = <v_j,v_k> ali=vlk−wjk,ai=<vj,vk>,表示活动 a i a_i ai的最迟开始时间就是活动弧终点事件的最迟发生时间-活动的进行时间;
- 关键活动:最早开始时间和最迟开始时间相等的活动;增加任一关键活动的耗时,整个工期增长;不能任意缩短关键活动,一旦缩减到一定程度,就可能变成非关键活动;
- 关键路径:从源点到汇点的最长路径,其上的活动边称为关键活动;完成整个工程的最短时间就是关键路径的长度;关键路径并不一定唯一,只有加快出现在所有关键路径上的公共关键活动才能达到缩减工期的目的;
- 拓扑排序的时间复杂度:邻接表为 O ( V + E ) O(V+E) O(V+E),邻接矩阵为 O ( V 2 ) O(V^2) O(V2);空间复杂度为 O ( V ) O(V) O(V);
- 若一个有向图的顶点不能构成一个拓扑序,则说明该有向图含有顶点数大于1的强连通分量(即有环);
-
查找:
- 顺序查找的平均查找长度为 n + 1 2 \cfrac{n+1}{2} 2n+1;折半查找的平均查找长度为 O ( l o g 2 n ) O(log_2n) O(log2n);
- 折半查找的判定树是一棵平衡二叉树;
- 折半查找仅适合顺序存储结构,不适合链式存储结构,且要求关键字有序排列;
- 折半查找的比较次数最多不超过树的高度,即平均/最多比较$\lceil log_2(n+1) \rceil 次 ; 若 查 找 不 存 在 的 元 素 , 由 于 判 定 树 是 平 衡 树 , 则 最 少 的 比 较 次 数 为 次;若查找不存在的元素,由于判定树是平衡树,则最少的比较次数为 次;若查找不存在的元素,由于判定树是平衡树,则最少的比较次数为\lceil log_2(n+1) \rceil -1$;
- 分块查找:
- 查找表分成若干块,块内无序,块间有序(每块的最大关键字作为该块大小的度量);
- 建立一个索引表,表项按序存储每块的最大关键字和块中的首地址;
- 查找过程:首先顺序/折半查找索引表,找到查找元素所在的块,然后块内顺序查找;
- 插入过程:找到所要操作的块,直接尾部插入;
- 删除过程:找到对应的块内的结点,直接删除;
-
B树:
-
B树是平衡的多叉树,所有结点的平衡因子均为0;适合于数据库的存储结构;
-
B树的阶:树中所有结点的孩子个数的最大值,用m表示;
-
每个结点至多有m棵子树,非空树的根结点至少有2棵子树,所有非根结点至少有 ⌈ m / 2 ⌉ \lceil m/2\rceil ⌈m/2⌉棵子树;
-
结点的子树个数是关键字个数+1,即每个结点的关键字个数范围为 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] [\lceil m/2\rceil-1,m-1] [⌈m/2⌉−1,m−1];
-
结点内部关键字从左到右递增有序;
-
所有叶结点都出现在同一层,且不带信息,视为外部结点;故n个关键字的B树总共有n+1个叶结点;
-
B树的高度:不包括最后的叶结点所处层,设一棵B树有n个关键字,阶数为m,则高度h的取值范围为: l o g m ( n + 1 ) ≤ h ≤ l o g ⌈ m / 2 ⌉ ( n + 1 2 ) + 1 log_m(n+1) \le h \le log_{\lceil m/2\rceil}(\cfrac{n+1}{2})+1 logm(n+1)≤h≤log⌈m/2⌉(2n+1)+1;磁盘存取次数至多为B树的高度;
-
-
B+树:
- B+树是平衡的多叉树,比B树的磁盘读写代价更低,查询效率更稳定,适合于数据库的存储结构;
- B+树的阶:树中所有结点的孩子个数的最大值,用m表示;
- B+树的高度:包括叶结点,即叶结点所在层的层数;
- 非叶根结点至少有两棵子树,分支结点至少有 ⌈ m / 2 ⌉ \lceil m/2\rceil ⌈m/2⌉棵子树;
- 结点的子树个数与关键字个数相等,即每个结点的关键字个数范围为 [ ⌈ m / 2 ⌉ , m ] [\lceil m/2\rceil,m] [⌈m/2⌉,m];
- 一个结点中的每个关键字指向一个以该关键字为最大值的子结点;
- 所有信息都存储在叶结点中,所有非叶结点仅起索引作用,即叶结点包含全部关键字及指向记录的指针,叶结点中关键字按大小顺序排列成一个链表;
- B+树有两个头指针,一个指向树根,一个指向关键字最小的叶结点(关键字链表的头结点);因此B+树支持顺序查找;
- 无论查找成功与否,B+树的查找一定是从根结点到叶结点的路径;
-
哈希表:
- 常用散列函数:
- 直接定址法: H ( k e y ) = a × k e y + b H(key) = a\times key+b H(key)=a×key+b,适合于关键字分布基本连续的情况;
- 除留取余法: H ( k e y ) = k e y m o d p H(key) = key \mod p H(key)=keymodp,选好 p p p,使得关键字等概率地映射到散列空间;
- 平方取中法:取 k e y 2 key^2 key2的中间几位,适合于关键字的每位取值不均匀的情况;
- 数学分析法:将关键字的数码中,分布均匀的若干位作为散列地址,适合于已知的关键字集合;
- 处理冲突的方法:
- 开放定址法:
H
i
=
(
H
(
k
e
y
)
+
d
i
)
m
o
d
m
H_i = (H(key)+d_i) \mod m
Hi=(H(key)+di)modm,
d
i
d_i
di为增量序列;
- 线性探测法: d i = 0 , 1 , 2 , . . , m − 1 d_i = 0,1,2,..,m-1 di=0,1,2,..,m−1;此法容易产生聚集现象;
- 平方探测法: d i = 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 , . . , k 2 , − k 2 d_i = 0^2,1^2,-1^2,2^2,-2^2,..,k^2,-k^2 di=02,12,−12,22,−22,..,k2,−k2, m = 4 k + 3 m = 4k+3 m=4k+3;此法可以有效避免聚集现象,但是不能探测到散列表上所有单元(至少能探测到一半单元);
- 再散列法: d i = i × H 2 ( k e y ) d_i = i\times H_2(key) di=i×H2(key);此法不易产生聚集现象;
- 伪随机序列法: d i = r a n d i d_i = rand_i di=randi;
- 链地址法:同义词存储在一个线性链表中;
- 开放定址法:
H
i
=
(
H
(
k
e
y
)
+
d
i
)
m
o
d
m
H_i = (H(key)+d_i) \mod m
Hi=(H(key)+di)modm,
d
i
d_i
di为增量序列;
- 装填因子:
- a = n m a = \cfrac{n}{m} a=mn,n是表中记录数,m是哈希表长度;
- 哈希表的查找效率取决于:散列函数、处理冲突方法和装填因子;时间复杂度为 O ( 1 ) O(1) O(1);
- 哈希表的平均查找长度依赖于哈希表的装填因子 α \alpha α, α \alpha α越大,装填的记录越满,发生冲突的可能性越大;
- 哈希表的删除:
- 对于开放定址法,只能逻辑删除(作删除标记),且需要定期维护哈希表,把删除标记的元素物理删除,否则会有很多无法利用的空位置;
- 对于链地址法,就是链表的删除算法;
- 常用散列函数:
-
排序算法:
-
一趟排序:对尚未确定最终位置的所有元素进行一遍处理称为一趟排序,取决于元素个数n;
-
算法复杂度与初始序列的关系:
- 比较次数 与序列初态 无关 的算法是:归并排序、简单选择排序、基数排序、折半插入排序;
- 比较次数 与序列初态 有关 的算法是:快速排序、直接插入排序、冒泡排序、堆排序、希尔排序;
- 移动次数 与序列初态 无关 的算法是:基数排序;
- 排序趟数 与序列初态 有关 的算法是:冒泡排序、快速排序;
-
内部排序的比较:
算法种类 时间复杂度 空间复杂度 稳定性 存储结构 特点 插入排序 最好: O ( n ) O(n) O(n)
平均: O ( n 2 ) O(n^2) O(n2)
最坏: O ( n 2 ) O(n^2) O(n2)O ( 1 ) O(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 ( 1 ) O(1) O(1) 稳定 顺序/链式 ① 排序中产生的有序子序列一定是全局有序的;
② 每趟排序可以确定一个元素的最终位置;
③ 适用于基本有序的序列;选择排序 最好: O ( n 2 ) O(n^2) O(n2)
平均: O ( n 2 ) O(n^2) O(n2)
最坏: O ( n 2 ) O(n^2) O(n2)O ( 1 ) O(1) O(1) 不稳定 顺序 ① 每趟排序可以确定一个元素的最终位置;
② 元素间的比较次数与序列的初始状态无关;
③ 适用于数据量不大的序列;希尔排序 最坏: O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1) 不稳定 顺序 ① 作为插入排序的扩展,适用于大规模数据排序;
② 目前未得到精确的渐进时间;快速排序 最好: O ( n l o g n ) O(nlogn) O(nlogn)
平均: 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)
最坏: O ( n ) O(n) O(n)不稳定 顺序 ① 内部排序算法中平均性能最优;
② 每趟排序会将基准元素放到最终位置;
③ 改进方法:选择头尾中三者之中的中位数作为基准元素;
或者随机选择一个作为基准元素;堆排序 最好: O ( n l o g n ) O(nlogn) O(nlogn)
平均: O ( n l o g n ) O(nlogn) O(nlogn)
最坏: O ( n l o g n ) O(nlogn) O(nlogn)O ( 1 ) O(1) O(1) 不稳定 顺序 ① 线性时间内将一个无序数组建成堆;
② 适合关键字较多且只需要前m个最小/最大元素的情景(最小/大只需要大小为m的大/小根堆);
③ 每趟排序可以确定一个元素的最终位置;归并排序 最好: O ( n l o g n ) O(nlogn) O(nlogn)
平均: O ( n l o g n ) O(nlogn) O(nlogn)
最坏: O ( n l o g n ) O(nlogn) O(nlogn)O ( n ) O(n) O(n) 稳定 顺序 ① 对N个元素进行k路归并排序,排序的趟数为: ⌈ l o g k N ⌉ \lceil log_k N \rceil ⌈logkN⌉;
② 元素间的比较次数与序列的初始状态无关;基数排序 最好: O ( d ( n + r ) ) O(d(n+r)) O(d(n+r))
平均: O ( d ( n + r ) ) O(d(n+r)) O(d(n+r))
最坏: O ( d ( n + r ) ) O(d(n+r)) O(d(n+r))O ( r ) O(r) O(r) 稳定 顺序+
链表① 一趟排序分为分配和收集两个过程;
② 可以从最高位(MSD)开始,也可以从最低位开始(LSD);
③ 不基于比较和移动元素,而基于关键字各位的大小;
④ 适用于记录的关键字位数较少且可以分解的情景; -
外部排序:
-
k路归并:
- ① 假设外存文件的数据量为 N N N,根据内存缓冲区的大小(设为 M M M),将外存文件分为 r = N / M r = N/M r=N/M个初始归并段,依次读入内存进行内部排序,然后重新写回外存;
- ② 对这些初始归并段进行逐趟归并,假设执行 k k k路归并(假设磁盘块大小为 S S S,则内存中每个输入/输出缓冲区大小亦为 S S S,又需要至少 k k k个输入缓冲区,1个输出缓冲区,故 k + 1 ≤ M / S k+1\le M/S k+1≤M/S),则需要 ⌈ l o g k r ⌉ \lceil log_k r \rceil ⌈logkr⌉趟归并;
- 每一趟归并都要对所有 N / S N/S N/S个磁盘块读、写各一次,因此增大归并路数 k k k,或者减少初始归并段个数 r r r,都能减少趟数,进而减少读写磁盘的次数;
- 但是增加归并路数 k k k时,每次从 k k k个关键字选出最小值需要 k − 1 k-1 k−1次比较,每一趟归并需要 ( N − 1 ) ( k − 1 ) (N-1)(k-1) (N−1)(k−1)次比较,则总共需要比较: ⌈ l o g k r ⌉ ( N − 1 ) ( k − 1 ) \lceil log_k r \rceil(N-1)(k-1) ⌈logkr⌉(N−1)(k−1)次,内部归并时间增加;且当 k k k的增加需要增加输入缓冲区的个数,受内存空间限制, k k k值太大势必会减少每个输入缓冲区的大小,使得内外存交换数据的次数增大;
-
败者树:
- 总共有k个叶结点,分别存放k个归并段在归并过程中当前参加比较的记录;
- 内部结点用来存放左右子树的败者,而让胜者继续往上继续比较,一直到根节点(指向最终胜者,即最小的数);
- 败者树高为 l o g 2 k log_2k log2k,因此k个关键字选最小最多需要 ⌈ l o g 2 k ⌉ \lceil log_2 k \rceil ⌈log2k⌉次比较;
- 使用败者树后,内部归并的总比较次数为 ⌈ l o g k r ⌉ ( N − 1 ) ⌈ l o g 2 k ⌉ = ⌈ l o g 2 r ⌉ ( N − 1 ) \lceil log_k r \rceil(N-1)\lceil log_2 k \rceil = \lceil log_2 r \rceil(N-1) ⌈logkr⌉(N−1)⌈log2k⌉=⌈log2r⌉(N−1)次,与k无关;
- 因此使用败者树后,只要内存允许,增大归并路数k可以有效减少磁盘读写次数;
-
置换-选择排序:
-
用于产生更长的归并段,进而减少初始归并段的个数 r r r;
-
算法步骤:
1) 定义初始待排输入文件FI,初始归并段输出文件FO,内存工作区W,W可容纳w个记录,FO和W初始为空 2)从FI中输入w个记录到W; 3)从W中用败者树选择关键字最小的记录,记为min,并将min输出到FO; 4)若FI不空,从FI输入下一个记录到W; 5)从W中选择比min大的关键字中最小的关键字,作为新的min; 6)重复3~5,直到选不出新的min,此时得到一个初始归并段,输出一个归并段的结束标志到FO; 7)重复2~6,直到W为空;
-
-
最佳归并树:
-
使用置换-选择排序算法后,得到了长度不等的初始归并段,因此需要组织初始归并段的归并顺序,使得磁盘读写次数最少;
-
采用霍夫曼树的思想,在归并树中让记录数少的初始归并段先归并,记录数多的初始归并段最晚归并,即可建立总磁盘读写次数最少的最佳归并树;
-
若采用k路归并,而初始归并段不足以形成严格的k叉正则归并树时,需要添加长度为0的虚段,这些虚段需要添加在叶子结点;
-
对于k叉正则树, n 0 = ( k − 1 ) n k + 1 → n k = ( n 0 − 1 ) / ( k − 1 ) n_0 = (k-1)n_k+1 \rightarrow n_k = (n_0-1)/(k-1) n0=(k−1)nk+1→nk=(n0−1)/(k−1),因此 r r r个初始归并段作为叶结点,需要 ( r − 1 ) / ( k − 1 ) (r-1)/(k-1) (r−1)/(k−1)个内结点;
-
所需添加虚段个数:
-
若 ( r − 1 ) % ( k − 1 ) = 0 (r-1)\%(k-1)=0 (r−1)%(k−1)=0,说明刚好可以构成k叉正则归并树;
-
若 ( r − 1 ) % ( k − 1 ) = u (r-1)\%(k-1)=u (r−1)%(k−1)=u,则说明 u u u个长度最小的初始归并段是多余的,因此将长度第 u + 1 u+1 u+1小的那个叶结点变成内结点,然后该原叶结点、 u u u个多余的归并段和 k − u − 1 k-u-1 k−u−1个空段作为该内结点的 k k k个叶子结点,便可以构成一棵k叉正则归并树;
-
-
-
-
二、技术篇:
-
分析算法的时空复杂度:
- Master定理:
- 递归树:根据递推方程,写出每一层的代价数列,然后求和得到总代价;
-
顺序表和链表:
- 线性表基本操作:
-
交换顺序表的两个子划分顺序表;
-
求顺序表的主元素;
-
求两个升序顺序表的中位数;
-
原地逆置链表;
-
求两个单链表的公共结点;
-
判断链表是否有环,并找到环的入口;
-
栈和队列:
- 栈基本操作:
- 队列基本操作:
-
给定入栈序列,判断可能的合法出栈序列;
-
给定双端队列入队序列,判断可能的合法出队序列;
-
利用栈进行括号匹配;
-
利用栈将中缀表达式转化为后缀表达式(如a+b-a*((c+d)/e-f)+g => ab+acd+e/f - * - g+);
-
利用栈求解后缀表达式的值;
-
利用双栈求解中缀表达式的值(一边将中缀表达式转化为后缀,同时求解后缀);
-
稀疏矩阵:
- 计算特殊矩阵压缩后的一维索引和二维索引的转换公式(注意一维/二维分别的起始下标是0还是1);
-
字符串:
-
模式匹配的KMP算法的执行过程;
-
next数组的生成过程(字符串下标从1开始):
通 项 公 式 : n e x t [ j ] = { 0 , j = 1 m a x { k ∣ 1 < k < j 且 p 1 . . . p k − 1 = p j − k + 1 . . . p j − 1 } , 存 在 k 1 , 不 存 在 k 递 推 公 式 : n e x t [ j + 1 ] = k + 1 , k = n e x t [ [ . . [ n e x t [ j ] ] , 且 p k = p j ( 不 存 在 k 则 k = 0 ) 通项公式:next[j] = \begin{cases}0,&j=1\\max\{k|1<k<j且p_1...p_{k-1}=p_{j-k+1}...p_{j-1}\},&存在k\\1,&不存在k \end{cases}\\ 递推公式:next[j+1]=k+1, k=next[[..[next[j]],且p_k=p_j(不存在k则k=0) 通项公式:next[j]=⎩⎪⎨⎪⎧0,max{k∣1<k<j且p1...pk−1=pj−k+1...pj−1},1,j=1存在k不存在k递推公式:next[j+1]=k+1,k=next[[..[next[j]],且pk=pj(不存在k则k=0) -
nextval数组的生成:若 p j = p k p_j=p_k pj=pk,则将 n e x t [ j ] next[j] next[j]修改成 n e x t [ k ] next[k] next[k]即可,即保证 p j ≠ p k p_j\neq p_k pj=pk;
-
-
树:
- 计算高度为h的树/二叉树/完全二叉树至多/至少可以拥有的结点/叶结点个数;
- 计算结点数为n的树/二叉树/完全二叉树的高度的最大值/最小值;
- 根据两种二叉树的遍历序列还原二叉树;
- 二叉树的先序/中序/后序/层次遍历算法;
- 求二叉树的高度/直径;
- 判定一棵二叉树是否是完全二叉树;
- 求(完全)二叉树中两结点的公共祖先;
- 并查集的实现;
-
二叉排序树:
-
插入过程:插入的结点一定是新添加的叶结点;
-
删除过程:
1) 若是叶结点,直接删除; 2) 否则,若只有一棵子树,则用该子树替代自己; 3) 若有两颗子树,则找自己的直接后继(右子树的最左下结点)或者直接前继(左子树的最右下结点)替代自己,转而删除后继或前继,转化为1)、2)情况;
-
查找过程;
-
判定一棵二叉树是否是二叉排序树;
-
-
平衡二叉树的旋转:
-
右单旋转(LL):结点A的左孩子的左子树插入了新结点导致A的平衡因子从1变成了2;
-
左单旋转(RR):结点A的右孩子的右子树插入了新结点导致A的平衡因子从1变成了2;
-
先左后右双旋转(LR):结点A的左孩子的右子树插入了新结点导致A的平衡因子从1变成了2;
-
先右后左双旋转(RL):结点A的右孩子的左子树插入了新结点导致A的平衡因子从1变成了2;
<左单旋转>
-
<先左后右双旋转>
-
最优二叉树:
- 霍夫曼算法:求最优二叉树的过程/建立霍夫曼树的过程;
-
堆:
-
堆的向下调整:(以下均以大根堆为例)
1)若该结点值大于左右子结点,则已调整好,退出;否则,执行2; 2)将左右子结点中较大者与该结点交换; 3)对发生交换的子结点重复执行1;
-
建堆:依次对第 ⌊ n / 2 ⌋ , ⌊ n / 2 ⌋ − 1 , . . , 1 \lfloor n/2 \rfloor,\lfloor n/2 \rfloor-1,..,1 ⌊n/2⌋,⌊n/2⌋−1,..,1个非叶结点为根的子树调用堆的向下调整算法;
-
堆的插入:
1)将插入结点放在末端; 2)若插入结点小于其父结点,则插入完成,退出;否则,执行3; 3)将插入节点与父结点交换,重新执行2;
-
堆排序:将堆顶元素与最后一个元素交换(堆的长度减1),然后对新堆进行向下调整;重复至新堆为空
-
-
图:
- 图的DFS算法和BFS算法的执行过程;
- 最小生成树算法(Prim算法和Kruskal算法)的执行过程;
- 最短路径算法(Dijkstra算法和Floyd算法)的执行过程;
- 拓扑排序/逆拓扑排序算法的执行过程;
- 关键路径算法的执行过程;
- 用有向无环图描述表达式:先将表达式转化成有向二叉树(双亲是一个运算符,子女是参与该运算的两个操作数,箭头从双亲指向子女),然后在树中去除重复顶点;
-
查找:
-
折半查找的算法执行过程;
-
折半查找的平均查找长度(成功/失败)的计算:
- 平均查找成功:所有关键字的从根结点到关键字结点的路径上的结点数的平均值;
- 平均查找失败:所有失败结点的从根结点到失败结点的路径上的结点数的平均值;
-
分块查找的平均查找长度计算:
- 将长度为n的查找表均匀地分成b块,每块有s个记录,记索引查找表的平均查找长度为
L
i
Li
Li,块内的平均查找长度为
L
s
Ls
Ls,则:
- 若在索引表和块内均顺序查找,则 A S L = L i + L s = b + 1 2 + s + 1 2 → 当 s = b = n 时 , 取 最 小 值 n + 1 ASL = Li+Ls = \cfrac{b+1}{2}+\cfrac{s+1}{2}\rightarrow当s=b=\sqrt{n}时,取最小值\sqrt{n}+1 ASL=Li+Ls=2b+1+2s+1→当s=b=n时,取最小值n+1;
- 若在索引表中采取折半查找,则:
A S L = L i + L s = ⌈ l o g 2 ( b + 1 ) ⌉ + s + 1 2 ASL = Li+Ls = \lceil log_2(b+1)\rceil+\cfrac{s+1}{2} ASL=Li+Ls=⌈log2(b+1)⌉+2s+1
- 将长度为n的查找表均匀地分成b块,每块有s个记录,记索引查找表的平均查找长度为
L
i
Li
Li,块内的平均查找长度为
L
s
Ls
Ls,则:
-
B树的查找:
① 在B树中找结点(磁盘操作);② 在结点内查找关键字(内存);
-
B树的插入:
- 查找到最底层非叶结点,然后插入;
- 插入之后,若插入的结点关键字个数小于m,则插入结束,否则,需要对该结点进行分裂:
- 将第 ⌈ m / 2 ⌉ \lceil m/2\rceil ⌈m/2⌉个关键字(从1开始)移到父结点,然后左右两部分关键字分成左右两个子结点;
- 再检查父结点是否需要分裂,依次递推至根结点,进而可能导致B树高度+1;
-
B树的删除:
- 若被删关键字不在最底层非叶结点中,则用其前驱/后驱关键字代替被删关键字,然后到下一层删除前驱/后驱关键字,则最终都归结于删除最底层非叶结点;
- 若被删关键字所在结点个数不小于
⌈
m
/
2
⌉
\lceil m/2\rceil
⌈m/2⌉,则直接删除,否则删后需要对该结点进行合并:
- 如果兄弟够借,即左/右兄弟的结点个数不小于 ⌈ m / 2 ⌉ \lceil m/2\rceil ⌈m/2⌉,则让左/右父亲关键字进入被删结点,再用左/右兄弟关键字代替左/右父亲关键字进入父亲结点;
- 如果兄弟不够借,即左/右兄弟的结点个数均为 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil-1 ⌈m/2⌉−1,则将关键字的左/右父亲关键字和左/右兄弟结点合并一起进入被删结点组成新结点,此时会引起父亲结点关键字数量减1;
- 第二种情况下,再检查父结点是否因为关键字数量减1而需要合并,依次递推至根结点,若根结点因合并而关键字减为0,则直接将合并的新结点作为新根结点;
-
B树的阶数m的计算:
- B树的一个结点应该满足操作系统一次读写的物理记录大小,因此结点大小应该小于等于且最接近一个磁盘块的大小M;
- 设一个关键字占K,磁盘块地址指针占S,整数占T;
- 而B树一个结点的结构为:至多m-1个关键字,每个关键字都包含一个指向记录的指针,至多m个子树结点指针,和表示一个实际关键字个数的整数n;
- 因此B树的阶应该满足: ( m − 1 ) K + [ ( m − 1 ) + m ] S + T ≤ M (m-1)K+[(m-1)+m]S+T \le M (m−1)K+[(m−1)+m]S+T≤M;
-
哈希表的查找过程;
-
哈希表的平均查找长度(成功/失败)的计算:
- 查找成功是针对所有记录的查找的次数的平均值,因此分母是记录数;
- 查找失败是针对散列空间,因此分母是所有可能映射到的位置总数(注意不一定是表长,散列空间小于等于表长);
-
-
排序:
- 各种内部排序算法的执行过程;
- 外部排序的k路归并算法的执行过程,以及磁盘读写次数、归并趟数的计算;
- 败者树的选择排序过程;
- 置换-选择排序的执行过程;
- 最佳归并树的生成过程;