数据结构复习点

链表

  1. 带头结点的链表注意头结点不存储数据。空表判断用 L->next == NULL 。
  2. 不带头结点的空表判断是 L == NULL 。
  3. 链表的建立有头插法、尾插法两种。

栈和队列

  1. 卡特兰数公式为 1 n + 1 C 2 n n \frac{1}{n+1}C_{2n}^{n} n+11C2nn
  2. 栈、队列的顺序存储都依赖于指针。
    (1.1)栈要定义栈顶指针 top,指向栈顶元素的位置(定义不一定,也有可能特殊出题)。
    (1.2)初始化时,令 top = -1,代表栈为空。top = N-1 时,代表栈满。
    (2.1)队列要定义队头指针 front 和队尾指针 rear,front 指向队头元素的位置,rear 指向队尾元素的后一个位置(定义不一定,也有可能特殊出题)。
    (2.2)初始化时,可以让 front 和 rear 都等于0。front == rear 时,代表队空。
    (2.3)循环队列中入队、出队对指针操作后需要取余,%MaxSize。
    (2.4)(rear+1)%MaxSize = front 用于判断队满,即即将插入数据的位置的下一个位置如果是队头,则队满,因此会浪费一个位置空间。
    (2.5)不浪费存储空间有两种方法。一:定义个size表示当前队列长度。二:定义个tag代表最后一次操作的类型。
    (2.6)判断当前元素数量的公式为 (rear-front)%MaxSize。
  3. 链栈需要定义栈顶指针,创建有头插法、尾插法。链队列需要记录头、尾两个指针。
  4. 验证栈、队列、双端队列、受限的双端队列的输出合法性时,核心思想是:一个数输出了,代表在它之前的所有数都已经进入了。(尤其要小心受限的双端队列)
  5. 括号匹配问题,注意区别不同的括号 “( )”、“[ ]”、“{ }”。

难点:表达式求值

表达式由操作数、运算符、界限符 组成。
人类能直观理解的是带括号的中缀表达式,前缀表达式又叫波兰表达式,后缀表达式又叫逆波兰表达式。
不同表达式中,操作数的顺序不变,运算符的顺序改变。

中缀表达式后缀表达式(括号内非机算)前缀表达式(括号内非机算)
a+bab++ab
a+b-cab+c-(或abc-+)+a-bc(或-+abc)
a+b-c*dab+cd*-+a-b*cd

中缀表达式转后缀、前缀表达式结果不唯一,但机算结果唯一。机算要求转后缀表达式时,左侧运算符能优先计算必须优先计算,转前缀表达式时,右侧运算符能优先计算必须优先计算。在这种情况下,

  • 中缀表达式的运算符计算顺序为后缀表达式中运算符从左到右排列的顺序。
  • 中缀表达式的运算符计算顺序为前缀表达式中运算符从右到左排列的顺序。

后缀表达式的计算可用栈实现,具体按如下规则:

  1. 扫描
  2. 扫描到操作数,则入栈
  3. 扫描到运算符,则从栈中取出两个操作数,第一个取出的操作数放在侧,第二个取出的操作数放在侧,进行运算,将运算结果压回栈。
  4. 如果表达式合法,最终栈内只剩一个元素,为表达式的值。

前缀表达式的规则类似,只需将上述步骤的两对“左”、“右”互换:

  1. 扫描
  2. 扫描到操作数,则入栈
  3. 扫描到运算符,则从栈中取出两个操作数,第一个取出的操作数放在侧,第二个取出的操作数放在侧,进行运算,将运算结果压回栈。
  4. 如果表达式合法,最终栈内只剩一个元素,为表达式的值。

难点中的难点:中缀表达式转后缀表达式

(为方便理解,笔记中的运算符只包括加减乘除。)
这个算法,需要根据运算优先级(括号优先级>乘除法优先级>加减法优先级),和左侧运算符能优先计算必须优先计算的原则设计,此外要注意括号内可以有大量的运算符计算,因此括号内的运算规则等同于表达式的运算规则。具体算法需要用栈辅助实现,按如下规则:

  1. 从左到右扫描中缀表达式。
  2. 遇到操作数,直接加入后缀表达式。
  3. 遇到界限符,左括号 “(” 直接入栈,右括号 “)” 则依次弹出栈内运算符并加入后缀表达式,直到弹出第一个左括号。
  4. 遇到运算符,依次弹出栈中优先级高于或等于当前运算符的所有运算符,并加入后缀表达式,遇到优先级更低的运算符或左括号 “(” 或栈空则停止。之后把当前运算符压入栈中。
  5. 按上述操作处理完所有字符后,将栈中剩余字符依次弹出并加入后缀表达式。

根据这个规则,个人总结一些方便理解的结论:

  1. 根据规则3,栈内可以出现多个左括号,但每次扫描到右括号,弹出的元素只包含运算符。且至多弹出两个运算符。根据规则4,若弹出两个运算符,则第一个弹出的是乘除号,第二个弹出的是加减号。
  2. 根据规则4,一次连续弹出操作中,不会弹出两个相同优先级的运算符,因为在遇到第二个符号运算符之时或之前,第一个运算符就一定会被弹出。

为方便理解,可以尝试这个例子:A+B*(C-D)-E/F。

结合后缀表达式的计算算法,和中缀转后缀算法,自然可以设计出一个,无需存储后缀表达式的,实现中缀表达式计算的算法。只需同时定义运算符栈和操作数栈,将原本加入后缀表达式和扫描后缀表达式的操作合并。

  1. 结点的度,即结点有几个孩子(分支)。树的度,即树的各结点的度的最大值。
  2. 有序树or无序树,由各结点的子树是否有序(位置是否可互换)决定。
  3. 树的结点数=总度数+1。
  4. m叉树,即规定每个结点最多只能有m个孩子的树,但树的度可以小于m。(度为m的树 ≠ \neq =m叉树)
  5. 度为 m 的树第 i 层最多有 m i − 1 m^{i-1} mi1个结点( i ≥ 1 i\ge1 i1),高度为 h 的m叉树最多有 m h − 1 m − 1 \frac{m^h-1}{m-1} m1mh1个结点。
  6. 高度为h的m叉树至少有 h h h 个结点,高度为h的度为m的树至少有 h + m − 1 h+m-1 h+m1 个结点。
  7. 具有 n 个结点的 m 叉树的最小高度为 ⌈ log ⁡ m ( n ( m − 1 ) + 1 ) ⌉ \lceil \log_m{(n(m-1)+1)}\rceil logm(n(m1)+1)
  8. 满二叉树,指高度为h,且含有 2 h − 1 2^h-1 2h1个结点的树,其每个结点的度不是0就是2。
  9. 对满二叉树,按照层序从1开始编号,第i个结点的左子结点编号为2i,右子结点编号为2i+1,父结点编号为 ⌊ i / 2 ⌋ \lfloor{i/2}\rfloor i/2
  10. 完全二叉树,即层序编号和满二叉树中 1~n 编号相对应的二叉树。其最多只有一个度为1的结点。
  11. 二叉排序树(BST,Binary Sort Tree),满足左子所有结点的关键字均小于根结点的关键字,右子所有结点的关键字均大于根结点的关键字,且左子树和右子树又各是一棵二叉排序树。
  12. 平衡二叉树,树上任一结点的左子树和右子树深度之差不超过1。
  13. 记非空二叉树中,度为0、1、2的结点分别有 n 0 n_0 n0 n 1 n_1 n1 n 2 n_2 n2个,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1

难点:树和森林

  1. 二叉树的先序、中序、后序遍历,都要求先遍历左子树,再遍历右子树,先中后是根结点的遍历顺序。此外还有层序遍历。
  2. 树只有先根、后根、层次遍历。其中后根遍历相当于深度优先遍历,层次遍历相当于广度优先遍历。
  3. 树的孩子兄弟表示法,可将其表示为二叉链表的结构,形式上等价于二叉树。方法是,保留每个结点和其第一个(最左侧)子结点的连接,第一个子结点的兄弟结点按顺序接在第一个子结点的右侧(每个亲兄弟结点依次相连)。
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
4. 树在表示成二叉链表(转化为二叉树)后,可以看出,根结点是没有右子树的。
5. 森林是若干棵互不交叉的树的集合,也可用上述表示方法转化为一颗二叉树,多棵树的森林转化为的二叉树的根结点是有右子树的(可以想想在上图中加上一棵只有一个结点的树,那么对应二叉树的根结点就有向右的分支了)。
6. 森林只有先序遍历和中序遍历,相当于对应二叉树的先序遍历和中序遍历。
7. 森林的先序遍历效果等价于依次对各棵树进行先根遍历。
8. 森林的中序遍历效果等价于依次对各棵树进行后根遍历。
9. 树的后根遍历效果等价于对应二叉树的中序遍历。
10. 上述几条记忆时,记住树的遍历是先根、后根而不是先序、后序,而森林的遍历却同样用先序、中序,其原因也就在于此。

森林二叉树
先根遍历先序遍历先序遍历
后根遍历中序遍历中序遍历

难点:哈夫曼树

  1. 结点的权:有某种显示含义的数值(例如各个编码中字母出现的频率)。
  2. 结点的带权路径长度:从树的根到该结点的路径长度(深度-1)与该结点上权值的乘积。
  3. 树的带权路径长度(WPL,Weighted Path Length):树中所有叶子结点的带权路径长度之和。
  4. 哈夫曼树定义为,在含有n个带权叶结点的二叉树中,带权路径长度(WPL)最小的二叉树。
  5. 哈夫曼树的构造:使权值最小的两个结点或子树合成一棵子树,这棵子树的权值为其所有叶子结点权值之和,重复此过程直到只剩下最后一棵树。(注:哈夫曼树不唯一)
  6. 哈夫曼树的应用:哈夫曼编码。相关概念有,前缀编码(没有一个编码是另一个编码的前缀,即所有编码都对应二叉树中的叶子结点),可变长度编码(允许对不同字符用不等长的二进制位表示)。

难点:AVL树(平衡二叉树)

  1. 平衡因子:针对各个结点,其左子树深度减去右子树深度为结点的平衡因子。
  2. 平衡二叉树,简称平衡树(AVL树)定义为,树上任一结点的左子树和右子树高度之差不超过1。即平衡二叉树的每个结点的平衡因子只可能是-1、0或1。
  3. 将二叉排序树构造为平衡二叉树,即可实现时间复杂度为 O ( log ⁡ 2 n ) O(\log_2{n}) O(log2n)的查找算法。构建这种平衡二叉树的主要步骤是,在二叉排序树中插入新的结点。
  4. 为保证插入结点前后,二叉排序树都为平衡树,每次插入结点需要进行调整保持平衡。每次需要调整的对象都是“最小不平衡子树”,即从叶子结点往上,第一个根结点平衡因子为 ± 2 \pm2 ±2 的子树。
  5. 最小不平衡子树分为四种类型:LL,RR,LR,RL,具体如下表(记最小不平衡子树的根结点为A)。
类型说明
LL在A的左孩子的左子树中插入导致不平衡
LR在A的左孩子的右子树中插入导致不平衡
RR在A的右孩子的右子树中插入导致不平衡
RL在A的右孩子的左子树中插入导致不平衡
  1. 具体如何调整上述四种类型的最小不平衡子树,只需记住:LL要右旋,RR要左旋,LR要先左旋变成LL,RL要先右旋变成RR,如下图。
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
  2. 二叉排序树的删除操作,若删除的是叶结点,则直接删除,若删除的是分支结点,则用其左子树上最右侧(值最大)的结点的值替代原需删除结点的值,再删除左子树上的该结点,或者用其右子树上最左侧(值最小)的结点的值替代原需删除结点的值,再删除右子树上的该结点。
  3. 平衡二叉树的删除操作如下:
    (1) 按照二叉排序树删除结点的方法删除对应叶子结点。
    (2) 从被删除结点的位置不断往根结点方向迭代,直到找到第一个平衡因子为 ± 2 \pm2 ±2 的子树(最小不平衡子树)。
    (3) 根据最小不平衡子树根节点的最大高度孩子、孙子结点,判断该子树的类型(LL、LR、RR、RL)。
    (4) 平衡最小不平衡子树。
    (5) 平衡后,从最小不平衡子树的根结点继续往其根结点方向迭代,重复 (2) 开始的步骤,直到整棵树的根结点。
  4. 对上一条中,为什么进行一次平衡操作后,整棵树仍可能不平衡的解释:平衡操作一般会使根结点的高度-1,如果根结点的兄弟结点高度原本就比该根结点多1,则会产生不平衡。

  1. 图G由顶点集V和边集E组成,记为 G = { V , E } G = \{V, E\} G={V,E} V { G } V\{G\} V{G}表示图G中顶点的有限非空集, E { G } E\{G\} E{G}表示图G中顶点之间的关系(边)的集合。 ∣ V ∣ |V| V表示图G中顶点的个数,也称图G的阶 ∣ E ∣ |E| E表示图G中边的条数。
  2. 图的顶点集必须非空,图的边集可以为空。
  3. 无向图的边为无向边,简称为边,记为 ( v , w ) (v,w) (v,w)。有向图的边为有向边,也称为弧,记为 < v , w > <v,w> <v,w> v v v为弧头, w w w为弧尾。
  4. 简单图,定义为既不存在重复边,也不存在顶点到自身的边,否则为多重图。后续提到的图默认为简单图。
  5. 对于无向图,顶点v的定义为依附于该顶点的边的条数,记为 T D ( v ) TD(v) TD(v)。对于有向图,入度定义为以顶点v为终点的有向边的数目,记为 I D ( v ) ID(v) ID(v)出度定义为以顶点v为起点的有向边的数目,记为 O D ( v ) OD(v) OD(v)
  6. 路径,定义为顶点 v p v_p vp 到顶点 v q v_q vq 之间的顶点序列 v p , v i 1 , . . . , v q v_p, v_{i_1}, ..., v_q vp,vi1,...,vq。若路径的第一个和最后一个顶点相同,则称为回路
  7. 若路径中所有顶点不重复出现,则为简单路径。若回路中除首尾顶点外其他顶点不重复出现,则为简单回路
  8. 顶点到顶点之间的距离定义为最短路径的长度,若不存在路径则记为 ∞ \infin
  9. 无向图中,若顶点v到顶点w有路径存在,则称v和w是连通的。有向图中,若从顶点v到顶点w和从顶点w到顶点v都有路径,则称v和w是强连通的。
  10. 无向图中,若任意两个顶点之间都连通,则称为连通图。有向图中, 若任意两个顶点之间都强连通,则称为强连通图
  11. 对于n个顶点的无向图G,若G是连通图,则至少有 n − 1 n-1 n1 条边,若G是非连通图,则至多有 C n − 1 2 C_{n-1}^2 Cn12条边。
  12. 对于n个顶点的有向图G,若G是强连通图,则至少有 n n n条边(形成回路)。
  13. 对于图 G = { V , E } G=\{V,E\} G={V,E}和图 G ′ = { V ′ , E ′ } G'=\{V',E'\} G={V,E},若 V ′ V' V V V V的子集, E ′ E' E E E E的子集,则称 G ′ G' G G G G的子图。注意:子图的前提是图,所以不是任取顶点和边都能构成子图。
  14. 满足 V { G ′ } = V { G } V\{G'\}=V\{G\} V{G}=V{G}的子图 G ′ G' G,称为图 G G G生成子图
  15. 无向图中的极大连通子图称为连通分量。一个无向图可以分为若干个连通分量。有向图中的极大强连通子图称为强连通分量。一个有向图可以分为若干个强连通分量,但可能需要丢弃部分弧。
  16. 连通图的生成树,定义为包含图中全部顶点的一个极小连通子图。生成树的构造并不唯一。
  17. 非连通图中,连通分量的生成树构成了非连通图的生成森林
  18. 图的每个边都可以标上具有某种含义的数值,即权值。边上带有权值的图称为带权图,也称。路径也可以重新定义为带权路径长度,即路径上所有边的权值之和。
  19. 无向完全图,即无向图中,任意两个顶点之间都存在边。有向完全图,即有向图中,任意两个顶点之间都存在两条相反方向的弧。
  20. 稀疏图,即边数很少的图,对边数没有绝对的界限,一般用 ∣ E ∣ < ∣ V ∣ log ⁡ ∣ V ∣ |E|\lt |V|\log{|V|} E<VlogV,反之称为稠密图。
  21. 树,即不存在回路,且连通的无向图。
  22. 有向树,可理解为只有一个顶点的入度为0,其他顶点的入度都为1的有向图(不是强连通图)。
  23. 邻接矩阵法存储有向图时,(行号,列号)对应于 <弧头,弧尾> 。1代表有边,0代表无边。邻接矩阵适合存储稠密图。
  24. 邻接矩阵法存储带权图(网)时,没有边用无穷大(代码中定义一个较大的数值)表示权值,对角线上的元素可用无穷大或0表示。
  25. 记无向图G的邻接矩阵为A, A n A^n An的元素 A n [ i ] [ j ] A^n[i][j] An[i][j] 等于由顶点 i i i 到顶点 j j j 的长度为 n n n 的路径的数目。
  26. 图的邻接表法,相当于用顺序+链表的方式存储(相当于树的孩子表示法)。需要定义一个表,每行第一个元素存储一个顶点名,第二个元素指向这个顶点连接的第一条边,每个边都包含这条边指向的顶点编号,和下一条边的指针(这里边的定义和图的边的定义不一样,一个无向图的边需要定义两个邻接表中的边)。这里的每行也可以定义为一个顶点,包含两个元素。具体实现如下图。
    在这里插入图片描述
  27. 邻接表存储无向图的空间复杂度只有 O ( ∣ V ∣ + 2 ∣ E ∣ ) O(|V|+2|E|) O(V+2∣E),存储有向图的空间复杂度只有 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E),但计算有向图某个顶点的入度时需要遍历整个表。邻接表表示方式不唯一,适合存储稀疏图。
  28. 十字链表法只能用于存储有向图,解决了邻接表存储无向图时,计算顶点入度较复杂的问题。其特点是,在邻接表的边的定义上,除了该有向边的出发顶点(弧头)编号、指向下一条以该顶点作为弧头的边的指针外,加上了指向下一条以该点作为弧尾的指针、指向结点的编号。具体如下图(其中tailvex就是原本存储的编号,不用在意图中左上角的注释)。
    在这里插入图片描述
  29. 临界多重表只能用于存储无向图,解决邻接表存储无向图时,同一条边需要存储两份数据和删除操作较复杂的问题。其特点是,在邻接表的边的定义上,除了该边的出发结点编号、指向下一条以该出发结点作为一个顶点的边的指针外,加上了该边另一头的结点编号,和下一条以另一头结点编号作为一个顶点的边的指针。具体如下图。
    在这里插入图片描述
  30. 广度优先遍历(BFS),有如下注意点:(1) 需要有一个 visit 数组标记每个结点是否被访问过。(2) 需要一个辅助队列。(3) 非连通图无法一次遍历所有结点,每次遍历结束需要检查 visit 数组。
  31. BFS空间复杂度来自于辅助队列,为 O ( ∣ V ∣ ) O(|V|) O(V)。BFS时间复杂度,如果用邻接矩阵存储为 O ( ∣ V ∣ 2 ) O({|V|}^2) O(V2)(由 O ( ∣ V ∣ ) + O ( ∣ V ∣ 2 ) O(|V|)+O({|V|}^2) O(V)+O(V2) 推出),如果用邻接表存储为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  32. 按照广度优先遍历访问连通图,可以得到一棵与之对应(遍历序列相同)的树,叫广度优先生成树。如果图的存储方式为临界矩阵,从同一个结点出发的广度优先遍历序列/广度优先生成树唯一,如果图的存储方式为邻接表,从同一个结点出发的广度优先遍历序列/广度优先生成树不唯一。按照广度优先遍历访问非连通图,则可以得到对应的广度优先生成森林。
  33. 广度优先遍历(DFS),有如下注意点:(1) 需要有一个 visit 数组标记每个结点是否被访问过。(2) 需要利用递归函数。(3) 非连通图无法一次遍历所有结点,每次遍历结束需要检查 visit 数组。
  34. DFS空间复杂度来自于函数调用栈,为 O ( ∣ V ∣ ) O(|V|) O(V)。DFS时间复杂度,如果用邻接矩阵存储为 O ( ∣ V ∣ 2 ) O({|V|}^2) O(V2)(由 O ( ∣ V ∣ ) + O ( ∣ V ∣ 2 ) O(|V|)+O({|V|}^2) O(V)+O(V2) 推出),如果用邻接表存储为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)
  35. 按照深度优先遍历访问连通图,可以得到一棵与之对应(遍历序列相同)的树,叫深度优先生成树。如果图的存储方式为临界矩阵,从同一个结点出发的深度优先遍历序列/深度优先生成树唯一,如果图的存储方式为邻接表,从同一个结点出发的深度优先遍历序列/深度优先生成树不唯一。按照深度优先遍历访问非连通图,则可以得到对应的深度优先生成森林。
  36. 对无向图进行BFS/DFS,需要调用BFS/DFS函数的次数等于连通分量数。而对有向图而言,则和起始顶点选择有关。对连通图或强连通图,只需要调用一次BFS/DFS。

难点:最短路径问题

  1. 最短路径问题分为单源最短路径(求一个顶点到其他各个顶点的最短路径)和各顶点间的最短路径。其中求单源最短路径的算法有BFS算法(适用于无权图)和Dijkstra算法(适用于无权图和带权图)。求各顶点间最短路径的算法有Floyd算法(适用于无权图和带权图)。
  2. BFS算法求最短路径,需要定义两个数组,一个数组用于记录每个顶点到起始点的距离,第二个数组用于记录每个顶点的直接前驱。
  3. Dijkstra算法求最短路径,需要定义三个数组,第一个数组 final[] 用于记录每个顶点是否已找到最短路径,第二个数组 dist[] 用于记录每个顶点当前找到的最短路径长度,第三个数组 path[] 用于记录每个结点当前最短路径的前驱结点。
  4. Dijkstra算法中数组的初始化:final[] 中起始点(记为A)标为True,其余标为False;dist[] 中起始点标为0,其他点若有A到该点的直接路径,标记为该路径长度,否则标记为 ∞ \infin ;path[] 中起始点标记为-1,其他点若有A到该点的直接路径,标记为起始点,否则标记为-1。
  5. Dijkstra算法每一步更新数组的核心是,只增加一个已找到最短路径标记,即 dist[] 中没找到最短路径点的最小值对应的点(记为X)。之后 dist[] 更新所有没找到最短路径的点,直接前驱为X时的路径长度(dist[x]+arcs[x][i])和原本记录路径长度(dist[i])中的最小值,若X为前驱结点的路径更短,则同时更新 path[] 数组中对应的值 path[i] 为X。
  6. Dijkstra算法时间复杂度为 O ( ∣ V ∣ 2 ) O({|V|}^2) O(V2)。 Dijkstra算法不适用于权值有负数的带权图。

难点:拓扑排序

(第一条为有向无环图描述表达式相关知识点)

  1. 有向无环图(DAG,Directed Acyclic Graph),可用于描述表达式。将表达式对应的树画出来,再合并成有向无环图(DAG),并注意每个操作数只会出现一次即可。此外,DAG可以是多重图,例如 a+a 表示为 + 有两条指向 a 的边。
  2. AOV网(Activity On Vertex Network),用DAG来表示一个工程,顶点表示活动,有向边 < V i , V j > <V_i,V_j> <Vi,Vj>表示活动 V i V_i Vi必须先于活动 V j V_j Vj进行。
  3. AOV网可以有多个入度为0的点,和多个出度为0的点。
  4. 拓扑排序,即在AOV网中找到活动的先后顺序,使得排在后面的活动不存在到排在其前面的活动的路径。也就是说,按从左到右的顺序可以正常执行这个工程(任何一个结点的所有前继结点,必定排在其前面)。拓扑排序不唯一。
  5. 拓扑排序的一种手动实现思路是:
    (1) 从AOV网中选择一个没有前驱结点的顶点并输出。
    (2) 从网中删除该结点和所有以该结点为起点的边。
    (3) 重复前两个操作,直到AOV网为空(执行完毕),或者当前网中不存在入度为零的结点(有回路,报错)。
  6. 逆拓扑排序,可理解为将AOV网中所有有向边反向后得到的拓扑排序,实现算法中,把找到入度为零的点改成出度为零的点,删掉以该结点为起点的边改为删掉以该结点为终点的边即可。
  7. 逆拓扑排序还可以用DFS算法实现,只需要在顶点退出栈时,输出这个结点。由于DFS算法递归的特性(或者说后根遍历的特性),在输出完一个根结点的所有后继结点前,是不会弹出这个根结点的。
  8. DFS算法实现逆拓扑排序时,用visited数组保存每个结点是否访问过(已进入栈),可防止图中有回路(不是DAG图)。
  9. 注意,逆拓扑排序可以用DFS算法实现,但拓扑排序不可以用BFS算法实现。
  10. AOE网(Activity On Edge Network),即在带权有向图中,用有向边表示活动,以边上的权值表示该活动的开销的图。
  11. AOE网中,有且仅有一个入度为0的结点,称为源点,有且仅有一个出度为0的结点,称为汇点。
  12. AOE网中,从源点到汇点的所有路径中,具有最大带权路径长度的路径称为关键路径。完成整个工程的最短时间就是关键路径的长度。关键路径上的活动称为关键活动

查找

  1. 查找表定义为用于查找的数据集合,可以是任何数据结构。
  2. 关键字定义为数据元素中唯一标识该元素的某个数据项的值。
  3. 静态查找表,只需要进行查找操作。动态查找表,还需要进行插入、删除操作。
  4. 查找长度定义为,在查找运算中,需要对比关键字的次数。平均查找长度ASL, Average Search Length),定义为所有查找过程中进行关键字比较次数的平均值。
  5. 评价查找算法的效率时,一般分开考虑查找成功、失败两种情况的ASL。计算查找失败情况下的ASL,一般会假设每个区间概率相等。
  6. 顺序查找,应用于线性表,即从头到位遍历的查找。若查找表有序,则对查找失败的情况,可进行优化(不需要扫到尾)。

折半查找

  1. 折半查找,又称“二分查找”,仅适用于有序的顺序表(不适合链表)。
  2. 折半查找需定义首尾指针 low、high,和中间指针 mid=(low+high)/2。每次比较查找目标和 mid 指针对应的值,未查找成功的情况下,若目标值大于 *mid,则令 low = mid+1,若目标值小于 *mid,则令 high = mid-1。重复此过程直到 tag==*mid(查找成功)或 low>high(查找失败)为止。
  3. 折半查找的查找判定树,查找成功情况下,按照任何一结点的右子树比左子树多0~1个结点的规律构造。查找成功/失败的查找判定树大致结构如图。(下图中,如果少一个结点,应该先少10这个位置,再少33这个位置,再少19这个位置……)
    在这里插入图片描述
    在这里插入图片描述
  4. 折半查找树高 h = ⌈ log ⁡ 2 ( n + 1 ) ⌉ h=\lceil \log_2{(n+1)}\rceil h=log2(n+1)(不包含失败结点),ASL ≤ h \le h h。时间复杂度为 O ( log ⁡ 2 n ) O(\log_2n) O(log2n)

红黑树

  1. 平衡二叉树(AVL Tree)在插入/删除一个结点时,很容易破坏平衡性,调整时间开销大。因此引入红黑树(RBT,Red-Black Tree),其查找、插入、删除操作的时间复杂度和AVL树一样都为 O ( log ⁡ 2 n ) O(\log_2n) O(log2n),但插入/删除操作不容易破坏“红黑”特性,即使需要调整也能在常数级时间内完成。
  2. 红黑树是一种二叉排序树(BST),其要求如下:
    (1) 每个结点或是红色,或是黑色。
    (2) 根结点和叶子结点(这里指红黑树的外部结点、NULL结点、失败结点)均是黑色的。
    (3) 不存在两个相邻的红结点,即红结点的父结点和孩子结点均是黑色的。
    (4) 对每个结点,从该结点到任意叶子结点的简单路径上,所含黑结点的数目相同。
  3. 红黑树的结点包含元素有,关键字的值,父结点指针,左孩子指针,右孩子指针,结点颜色(可用0/1表示黑/红)。
    在这里插入图片描述
  4. 结点的黑高,定义为从该结点出发到叶结点所经过的黑结点的数量(不包括自身)。
  5. 红黑树的几个性质:
    (1) 从根结点到叶结点的最长路径不大于最短路径的2倍。
    (2) 有n个内部结点的红黑树高度 h ≤ 2 log ⁡ 2 ( n + 1 ) h\le 2\log_2{(n+1)} h2log2(n+1)
  6. 红黑树的查找与BST、AVL相同。
  7. 红黑树的插入操作,步骤中一些名词含义为,染色(红变黑、黑变红),儿父爷叔(分别指当前插入的新结点、该结点的父亲结点、爷爷结点、父亲结点的亲兄弟结点,对应结点都唯一),LL、LR、RR、RL(和平衡树中的定义类似,在L/R子结点的L/R子树上插入导致红黑性不满足)及其对应的旋转操作(旋转后,非红黑性在某些情况下会往爷结点传递,详见具体步骤)。
  8. 红黑树插入具体步骤:
    (1) 查找,确定插入位置,插入新结点。
    (2) 若新结点是根结点,则染为黑色,若不是根结点,则染为红色。
    (3) 若插入新结点后,仍满足红黑树定义,则插入结束。否则按如下规则调整。
    Ⅰ新结点的叔叔为黑色。则①判断LL、LR、RR、RL类型,并进行旋转。②若为LL、RR型,旋转后的原父结点、原爷结点染色,若为LR、RL型,旋转后的原儿结点、原爷结点染色(因为要先变成LL、RR)。
    Ⅱ 新结点的叔叔为红色。则①父结点、叔结点、爷结点染色。②爷作为新结点,重新执行步骤(2)、(3)。
  9. 由于除根结点外,红黑树中插入的新结点都为红色,因此不可能违反第2条中的(1)(2)(4),即只可能发生出现连续两个红色导致红黑性不满足。
  10. 具体步骤若无法理解,可自行在红黑树模拟网站上实验。
  11. 红黑树的删除暂时不记笔记。

B树、B+树

  1. 将二叉排序树拓展为m叉排序树,树中每个结点保存最多m个指向子树的指针和m-1个关键字。如果再要求除了根结点外,每个结点至少有 ⌈ m / 2 ⌉ \lceil m/2\rceil m/2个分叉( ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil-1 m/21个关键字),且每个结点的所有子树高度均相等的条件,则称之为B树。
    在这里插入图片描述
    在这里插入图片描述
  2. B树的高度一般指不包含叶子结点的高度。可证明包含n个关键字的B树的高度h满足 log ⁡ m ( n + 1 ) ≤ h ≤ log ⁡ ⌈ m / 2 ⌉ n + 1 2 + 1 \log_m{(n+1)} \le h \le \log_{\lceil m/2\rceil}{\frac{n+1}{2}}+1 logm(n+1)hlogm/22n+1+1
  3. B树的插入要按照如下规则:
    (1) 新元素一定是插入到终端结点,用查找确定插入位置。
    (2) 插入操作中,若发生了某一结点关键字数超出上限(m-1),则将第 ⌈ m / 2 ⌉ \lceil m/2\rceil m/2个关键字提升到父结点中(若没有父结点则新建一个结点作为父结点,此时B树高+1),并将两侧关键字分裂为两个兄弟结点。父结点关键字数超出上限时,迭代执行此操作。
    在这里插入图片描述
    在这里插入图片描述
  4. B树的删除操作,若删除的是终端结点内关键字,有如下情况:
    (1) 删除后,终端结点内关键字数仍大于或等于 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil-1 m/21,则无需另外操作。
    (2) 删除后,终端结点内关键字数小于 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil-1 m/21,且其直接的左/右兄弟结点内关键字数充足(大于或等于 ⌈ m / 2 ⌉ \lceil m/2\rceil m/2),则让左/右兄弟的最右/左侧关键字移至父结点,并将父结点中对应关键字移入该终端结点。注意,此操作不会改变父结点关键字个数。
    (3) 输出后,终端结点内关键字数小于 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil-1 m/21,且其直接的左/右兄弟结点内关键字数不充足(等于 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil-1 m/21),则让其和左/右兄弟结点,以及父结点中对应关键字合并。注意,此操作会使父结点关键字数减一,因此需要对父结点迭代(1)-(3)的操作,若父结点为空(只有根结点会发生这种情况),B树高-1。
  5. B树的删除操作,若删除的是非终端结点内关键字,则用该关键字左/右侧子树下最大/小的关键字(一定在终端结点上)代替删除位置,重新看作对终端结点中关键字的删除。
  6. 由B树的删除操作,可以理解B树中的结点中,必须要保存一个 n 用于记录当前关键字数量。此外要记住,插入和删除,非根结点关键字数要控制在大于或等于 ⌈ m / 2 ⌉ − 1 \lceil m/2\rceil-1 m/21,其中 m 是最大分支数,不是最大关键字数。
  7. B+树具有B树和分块查找的特点,和B树有如下区别:
    (1) B树保存关键字的最后一层称为终端结点,失败结点称为叶子结点。B+树保存关键字的最后一层称为叶子结点。
    (2) B树每个分支结点,分支数=关键字数+1,B+树每个分支结点,分支数=关键字数,每个关键字等于对应子树的关键字中的最大值。
    (3) B树每个分支结点都保存关键字和关键字对应记录,而B+树每个分支结点的关键字只起到索引作用,叶子结点用于保存所有关键字和对应记录。
    (4) B树所有结点包含的关键字是不会重复的,B+树中分支结点中出现的关键字都会在下一层出现。
    (5) B树根结点关键字数 n ∈ [ 1 , m − 1 ] n\in[1,m-1] n[1,m1],其他结点关键字数 n ∈ [ ⌈ m / 2 ⌉ − 1 , m − 1 ] n\in[\lceil m/2\rceil-1,m-1] n[⌈m/21,m1]。B+树根结点关键字数 n ∈ [ 1 , m ] n\in[1,m] n[1,m],其他结点关键字数 n ∈ [ ⌈ m / 2 ⌉ , m ] n\in[\lceil m/2\rceil,m] n[⌈m/2,m]
    在这里插入图片描述
    在这里插入图片描述

散列(哈希)查找

  1. 散列表(Hash Table),又称哈希表,特点是数据元素的关键字与其存储地址直接相关。
  2. 哈希函数,即关键字到地址的映射函数,Addr = H(Key) 。
  3. 若不同的关键字通过哈希函数映射到同一个值,则称它们为“同义词”。通过哈希函数确定的位置已存放了其他元素,则称这种情况为“冲突”。
  4. 处理冲突的第一种方法是,拉链法(链地址法),把所有同义词存储在一个链表中。
  5. 要注意ASL(平均查找长度,定义一般为查找过程进行关键字比较的次数)在成功和失败两种情况下的计算,成功情况下假设所有成功情况概率相等,失败情况下假设关键字对应每个地址概率相等。 A S L 失败 = α ASL_{失败}=\alpha ASL失败=α,其中 α \alpha α为装填因子,定义为表中记录数除以散列表长度,例如长度为13的散列表内记录了12个数据,装填因子就为12/13=0.92。
  6. 为使不同关键字的冲突尽可能的少,有以下几种常用哈希函数:
    (1) 除数留余法,H(key) = key % p,p为不大于散列表长度m的最大质数(之所以这么取是考虑到关键字可能具有规律性)。
    (2) 直接定址法,H(key) = key 或 H(key) = a*key + b。
    (3) 数字分析法,观察关键字中分布较为均匀的若干位作为散列地址,例如取手机号后四位。
    (4) 平方取中法,取关键字平方值的中间几位。
  7. 处理冲突的第二种方法是,开放地址法,即哈希表中的空闲地址可向非同义词开放。此时地址和关键字的关系可表示为 H i = ( H ( k e y ) + d i ) % m H_i=(H(key)+d_i)\%m Hi=(H(key)+di)%m 。其中 d i d_i di为增量序列,i 可理解为第 i 次发生冲突。
  8. d i d_i di 的取值方法有如下几种:
    (1) 线性探测法, d i = 0 , 1 , 2 , . . . , m − 1 d_i=0,1,2,...,m-1 di=0,1,2,...,m1。即发生冲突时,依次往后探测相邻的下一个位置是否为空。(注:线性探测法很容易造成同义词、非同义词的“聚集/堆积”现象,严重影响查找效率)
    (2) 平方探测法(二次探测法), d i = 0 2 , 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , k 2 , − k 2 , k ≤ m / 2 d_i=0^2,1^2,-1^2,2^2,-2^2,...,k^2,-k^2,k\le m/2 di=02,12,12,22,22,...,k2,k2,km/2。(注:使用平方探测法,哈希表长度 m 必须是满足 4j+3 的质数,否则无法探测到所有位置)
    (3) 伪随机序列法,自定义一个伪随机序列 d i d_i di
  9. 注意,开放地址法中的取模运算是对m取模,和哈希函数中除数留余法不同,因此同时用除数留余法和开放地址法,数据是可以存储在大于p的位置中的。
  10. 开放地址法的查找,需要先确定第一个查找位置H(key),再依次按照 d i d_i di 往后查找,如果查找到空位置(或表满查找到最后一个位置),则查找失败。算查找失败长度时,空位置也算查找长度。
  11. 开放地址法,在删除元素时,不能直接删除,而是应该做一个标记,标记为删除。
  12. 开放地址法,计算 A S L 失败 ASL_{失败} ASL失败 时,假设 H(key) 落到 0到p-1 这 p 个位置的概率相同,即最后公式中的分母为 p。
  13. 处理冲突的第三种方法是,再散列法,需要定义多个哈希函数,当前一个哈希函数冲突时,使用下一个哈希函数,直到不冲突为止。

排序

  1. 排序算法的稳定性,指若相同值的元素排序后顺序不变,则称该排序算法是稳定的。
  2. 排序算法分为内部排序和外部排序,内部排序数据都在内存中,外部排序针对数据太多,无法放入内存的情况。
  3. 算法可视化演示网站
  4. 插入排序:取出第n个元素(n 从2开始取到最后一个),依次比较第 n-1 到第1个元素,如果比第n个元素大,则往后移一个位置,否则将第n个元素插入后一个位置中。(即第k次循环,假设前k个元素有序,目的是让前k+1个元素有序)
  5. 插入排序中,为防止每次循环,除了要判断 temp > A[i],还需要判断 i >= 0,可以让 A[0] 不存储任何元素,每次循环时,令A[0] = A[n](temp),则只需要比较 A[0] > A[i]。
  6. 插入排序,空间复杂度为 O ( 1 ) O(1) O(1),时间复杂度为 O ( n 2 ) O(n^2) O(n2),稳定。
  7. 插入排序可优化为折半插入排序,先用折半查找找到应该插入的位置,再移动元素。不过只降低了查找步骤的时间复杂度,没降低移动步骤的时间复杂度,所以总体时间复杂度不变,仍是 O ( n 2 ) O(n^2) O(n2)
  8. 冒泡排序,从后往前(或者从前往后)两两比较相同元素的值,若为逆序则交换。重复此操作(注意每次少比较一次),最多执行 n-1 趟排序完毕。若某一趟中,未发生元素交换,排序可提前结束。
  9. 冒泡排序,空间复杂度为 O ( 1 ) O(1) O(1),时间复杂度为 O ( n 2 ) O(n^2) O(n2),稳定。
  10. 冒泡排序和快速排序,都属于交换排序。
  11. 简单选择排序,思想是每一趟在待排序序列中,选取关键字最小的元素加入有序子序列。
  12. 简单选择排序,一种实现方法是,第k次迭代,寻找第k到第n位中最小的元素,与第k位元素交换。
  13. 简单选择排序,空间复杂度为 O ( 1 ) O(1) O(1),时间复杂度为 O ( n 2 ) O(n^2) O(n2)且不会因为初始状态改变),不稳定
  14. 简单选择排序和堆排序,都属于选择排序。

难点:快速排序

  1. 快速排序思想:在待排序表 L[1…n] 中任取一个元素pivot作为枢轴(或基准,通常取首元素), 通过一趟排序将待排序表分成两部分 L[1…k-1] 和 L[k+1…n],使得 L[1…k-1] 中的元素小于pivot, L[k+1…n] 中的元素大于pivot,这个过程称为一次“划分”。然后分别对两个子表递归进行上述操作,直到每部分只剩一个元素或为空。
  2. 对一个待排序表进行一次划分的步骤如下:
    (1) 首先标记首尾元素指针为 low,high,取出 low 中的元素,记录在 pivot 中。(此时low指针位置空出)
    (2) 比较 high 指针位置元素和 pivot 大小,若小于 pivot,则将 high 中元素放到 low 中,否则左移 high,重复此操作,直至 high 指向元素小于 pivot 停止移动(此时high指针位置空出)或满足(4)中的条件。
    (3) 比较 low 指针位置元素和 pivot 大小,若大于 pivot,则将 low 中元素放到 high 中,否则右移 low,重复此操作,直至 low 指向元素大于 pivot 停止移动(此时low指针位置空出)或满足(4)中的条件。
    (4) 循环(2)、(3)操作,直到 low 和 high 重合,把 pivot 放入这个位置,分出左右两个子表。
  3. 快速排序,时间复杂度最好 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n),最坏 O ( n 2 ) O(n^2) O(n2)(序列原本就有序),空间复杂度最好 O ( log ⁡ 2 n ) O(\log_2n) O(log2n),最坏 O ( n ) O(n) O(n)。不过实际应用中,快排往往趋于最好的时间复杂度,即平均时间复杂度为 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n),因此快排是综合来讲应用最广泛的排序算法。
  4. 快速排序的效率,取决于每次选取的 pivot 能否均匀地将序列分为两部分。因此一种优化思路是,调整 pivot 的选取规则。
  5. 快速排序,不稳定,例如 2、2、1。
  6. (考研408中,一趟排序指该算法递归的一层,可能包含多个序列同时进行的几次划分,而部分教材中,一趟排序等于一次划分)

难点:堆排序

  1. 若n个关键字序列 L[1…n],若满足 L [ i ] ≥ L [ 2 i ] L[i]\ge L[2i] L[i]L[2i] L [ i ] ≥ L [ 2 i + 1 ] L[i]\ge L[2i+1] L[i]L[2i+1] 1 ≤ i ≤ n / 2 1\le i\le n/2 1in/2),则称为大根堆,若满足 L [ i ] ≤ L [ 2 i ] L[i]\le L[2i] L[i]L[2i] L [ i ] ≤ L [ 2 i + 1 ] L[i]\le L[2i+1] L[i]L[2i+1] 1 ≤ i ≤ n / 2 1\le i\le n/2 1in/2),则称为小根堆。(即堆是满足根结点大于等于/小于等于孩子结点的顺序存储的完全二叉树)
  2. 堆排序是一种选择排序,因此思路仍是每次选择值最大/最小的元素加入有序序列。
  3. 根据一个序列建立大根堆的思路是,从后往前(自底向上)把所有非终端结点(编号满足 i ≤ ⌊ n / 2 ⌋ i\le \lfloor n/2\rfloor in/2)都检查一遍,是否满足大根堆要求,如果不满足则进行调整。
  4. 上一条中的调整,即将跟结点和左右孩子中较大的孩子互换,但这样导致小的元素下坠后,可能连带破坏大根堆的性质,因此需迭代检查互换后的孩子结点,若再发生下坠还需继续向下迭代(自顶向下)。
  5. 堆排序,先将整体待排序序列调整为大根堆,每一趟将堆顶元素加入有序子序列中( L [ 1 ] L[1] L[1]与待排序序列最后一个元素 L [ n − k + 1 ] L[n-k+1] L[nk+1]交换),之后将待排序元素序列再次调整为大根堆,重复操作直到只剩下一个待调整元素。
  6. 注意,堆排序中,除了最开始的调整为大根堆需要对第 ⌊ n / 2 ⌋ \lfloor n/2\rfloor n/2个结点到第1个结点调整外,其余的调整实际上只需要对第1个结点进行调整。
  7. 堆排序中,第一次建立大根堆的时间复杂度为 O ( n ) O(n) O(n),之后的 ⌊ n / 2 ⌋ − 1 \lfloor n/2\rfloor-1 n/21次调整,每次调整的时间复杂度为 O ( log ⁡ 2 n ) O(\log_2n) O(log2n),因此堆排序时间复杂度为 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n),空间复杂度为 O ( 1 ) O(1) O(1)不稳定
  8. 在堆中插入新元素,将新元素插入表尾,迭代父结点进行调整即可(不需要考虑第4条中的下坠情况)。
  9. 在堆中删除某个元素,将堆底元素替代删除的位置,再对该位置元素进行调整(需要考虑第4条中的下坠情况)。

难点:基数排序

  1. 基数排序,特点是不依赖与比较数字间的大小进行排序。以0-999之间的待排序元素按大到小排序为例,共需进行最大数位长度次数的操作(3趟)。按从低位到高位(个、十、百)的顺序进行。每一趟操作分为“分配”和 “收集”两步。
  2. 分配是将所有数据按指定位的值,分配到基数个队列中(这里基数尾进制数,即9876543210),排在前面的元素在队列中更靠近队头。
  3. 收集是将所有队列,按余数从大到小(目的从小到大,则按从小到大),从队头到队尾,重新连成一个序列。
  4. 基数排序规范定义:对待排序元素,假设长度为 n 的线性表中,每个结点 α j \alpha_j αj的关键字由d元组 ( k j d − 1 , k j d − 2 , . . . , k j 0 ) (k_j^{d-1},k_j^{d-2},...,k_j^0) (kjd1,kjd2,...,kj0)组成,其中 0 ≤ k j i ≤ r − 1 0\le k_j^i\le r-1 0kjir1,r称为“基数”。基数排序得到递减序列的过程如下,
    初始化:设置r个空队列, Q r − 1 , Q r − 2 , . . . , Q 0 Q_{r-1},Q_{r-2},...,Q_0 Qr1,Qr2,...,Q0。按照各个关键字权重递增的次序,对d个关键字分别做“分配”和“收集”。
    分配:顺序扫描各个元素,若当前处理的关键字位 = x =x =x,则将元素插入 Q x Q_x Qx队尾。
    收集:把 Q r − 1 , Q r − 2 , . . . , Q 0 Q_{r-1},Q_{r-2},...,Q_0 Qr1,Qr2,...,Q0各个队列中的结点依次出队并链接。
  5. 基数排序基于队列的空间复杂度 O ( r ) O(r) O(r),时间复杂度 O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)),稳定。
  6. 基数排序适用于,数据可方便拆分为d元组,且d较小,每组关键字取值范围不大(r较小),数据元素个数n较大的情况。

非本人考点知识点(暂时不记笔记)

并查集

最小生成树

希尔排序

归并排序

  1. 归并:把两个或多个有序的序列合并成一个。在内部排序中一般采用2路归并。对长度为 m 的序列,先分为 m 个有序子序列,每次归并第2i-1、2i个子序列,直到最后只剩下一个子序列。
  2. 归并排序的实现中,需要一个辅助数组B,B复制待排序数组A中的所有元素。因此归并排序空间复杂度 O ( n ) O(n) O(n)。每趟对所有序列进行归并的时间复杂度为 O ( n ) O(n) O(n),因此归并排序时间复杂度 O ( n log ⁡ 2 n ) O(n\log_2n) O(nlog2n)。归并排序,稳定。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值