cs学硕上岸学长的复习笔记——数据结构狂背

以初试第四上岸川大计算机学硕,这篇文章在我复习过程中,给了我很大帮助,当然不一定适合做你的复习资料。发布出来仅提供我的学习、笔记方式。


目录

  • 基本定义和时间、空间复杂度
  • 线性表
  • 栈和队列
  • 树与二叉树杂碎概念、计算公式和存储结构
    • 杂碎概念
    • 计算公式
    • 存储结构
    • 习题小结
    • 二叉树遍历和线索二叉树
    • 重要习题
    • 树、森林、并查集
    • 重要习题
    • 二叉排序树、平衡二叉树、哈夫曼树
    • 重要习题
  • 图的定义和存储结构
    • 一些杂碎定义
    • 重要计算
    • 四种图的存储结构及其特点
    • 习题小结
  • BFS和DFS
    • BFS性能和实用性分析
    • DFS性能和实用性分析
    • 利用DFS和BFS官方代码模板探测图的连通性
  • 图的若干应用(必考的重点部分)
    • 最小生成树(MST)
    • 习题小结
  • 查找
    • 顺序和折半查找
    • 习题小结
    • B树和B+树
    • 习题小结
    • Hash和KMP
  • 排序的细碎知识

基本定义和时间、空间复杂度

先挖个坑,最后再总结这个
⚠️求斐波那契数列递归版的空间复杂度需要重点总结,见P11标注

线性表

1,单链表中引入头结点的优点:
(1)开始结点的位置被存入了头结点的指针域中,所以链表第一个位置的操作和其他位置的操作一样;
(2)无论链表是否为空,头指针都是指向头结点的非空指针,所以空表和非空表的处理也统一了。
正确的说法是:增加头结点是为了方便运算的实现。
2,判断循环单链表是否为空的方法是头结点的next指针是否指向头结点本身;循环双链表为空的时候则是头结点的prior和next指针都只想头结点本身。
3,静态链表和顺序表一样要预先分配一块连续的存储空间,增删和动态链表一样不用移动数据元素,只需要修改“指针”。
4,顺序表支持顺序存取和随机存取,链表只支持顺序存取。顺序表有序时,按查找可以采用折半查找法,复杂度是 O ( l o g 2 n ) O(log_2n) O(log2n).
5,由于链表的每个结点都有指针域,所以存储空间要比顺序表付出的代价大,存储密度不够大。存储密度大是顺序存储结构的优点。
6,链式存储比顺序存储结构能更方便地表示各种逻辑结构。顺序存储只能用物理上的邻接关系来表示逻辑结构。
7,线性表是一个有序序列,所以所有整数组成的序列不是线性表,因为是无限的。

栈和队列

1,栈和队列都是线性表,所以栈和队列有相同的逻辑结构是正确的。栈又叫后进先出线性表,队列又叫先进先出线性表。
2,若链表不带头结点且所有操作都在表头进行,则只有表头结点,没有表尾结点的单向循环链表不适合作为链栈。
3,Catalan数:
1 n + 1 C 2 n n \frac{1}{n+1}C_{2n}^n n+11C2nn
用来计算n个不同元素依次进栈,有多少不同的出栈序列。
4,C语言标识符:只能由数字字母和下划线组成,首字符不能为数字。
5,栈的插入、删除操作都在栈顶,只可能发生上溢,不可能发生下溢。
6,一个错误说法:非递归方式重写递归程序的时候必须要用到栈。
错误原因是也可以用循环,比如求阶乘、求斐波那契数列。
7,函数调用时,系统要用栈保存必要的信息。
8,将链式队列设计成带头结点的单链表会比较方便,如果不设置头结点可能会比较麻烦。
(带头结点则插入和删除操作就统一了)。
9,栈和队列的主要区别是插删元素操作的限定不一样,而它们的逻辑结构是一样的。
10,⭐️对于循环队列而言,如果不专门设置一个数据变量来判断队列是否为空,则需要牺牲一个存储单元判断队满队空,此时队空的条件是:
Q . f r o n t = = Q . r e a r Q.front==Q.rear Q.front==Q.rear
队满的条件是:
( Q . r e a r + 1 ) % M a x S i z e = = Q . f r o n t (Q.rear+1)\%MaxSize==Q.front (Q.rear+1)%MaxSize==Q.front
11,思考一下为什么带队首指针和队尾指针的循环单链表不适合做链队:
因为入队的时候还要将rear->next指向队首,这对于队列是多余的。不过,那道题是有比这个更好的选项,如果没有比这更好的选项,还是要选这个。
12,队列的链式表示成为链队列,它是同时带有队头指针和队尾指针的单链表。
使用单链表实现队列时,队头在链表的链头位置,因为队尾只方便增加结点,不方便删除结点(删除最后一个结点需要改变倒数第二个结点的next指针,尾指针不指向这个结点,所以要先从头查找到倒数第二个结点才能完成删除操作)。
13,链队列删除元素的操作(由12知是在队头删除),尾指针可能也是需要修改的,这个可以举例如果删之前只有一个数据结点。
14,⚠️中缀表达式和后缀表达式考点:
分别是中序、后序遍历表达式树。后缀表达式已经考虑了表达式的优先级,所以没有括号,只有操作数和运算符。

后续表达式可以利用栈来求表达式的值:
1,从左到右扫描后序表达式;
2,如果扫到的是数,就将其压栈;
3,如果扫到的是操作符,就依次出栈两个元素分别作为此运算符的右操作数和左操作数,计算结果压栈。

中缀表达式转化为后缀表达式的方法见P91标注,有个isp(栈内优先级)和icp(栈外优先级)的表格是执行以下算法必须背的(⚠️真题中出现过考试记录)
1,从左至右扫描中缀表达式;
2,遇到数就直接输出;
3,遇到操作符,则将其icp(栈外优先级)和栈顶元素的isp(栈内优先级)相比,若大于,则压栈;若小于,则栈顶元素出栈并输出之,然后操作符继续和新栈顶比较;若等于,⚠️则栈顶元素和这个操作符都扔掉(不输出),然后继续扫描中缀表达式。

(中缀表达式转后缀表达式的另一个方法是根据中缀表达式画出表达式树,然后后序遍历之)

15,栈在递归中的应用

递归优缺点:将大型复杂问题层层转化为简单的规模较小的问题,这样只需要使用少量代码就能够进行描述;缺点是通常效率不是很高。递归的精髓是能否将原始问题转换为属性相同但规模较小的问题。

递归工作栈:递归调用的过程中,系统为每一层的返回点、局部变量、传入实参等开辟递归工作栈进行数据存储,递归此处过多容易造成栈溢出
(将递归算法转化为非递归算法通常也需要借助栈来实现,但说只能借助栈来实现是错误的,比如递归求阶乘可以转换为用while循环的非递归。

16、尾递归、线性递归和单向递归特点和优缺点:

线性递归:
e.g.递归求斐波那契数列第n项的值

int Fib(int n){
	if(n==0)return 0;
	else if(n==1)return 1;
	else return Fib(n-1)+Fib(n-2);
}
⚠️:这个是线性递归而不是尾递归,因为其最后一步操作是加操作

尾递归:最后一步操作是递归,
e.g.

int F(n){
	if(n==1)return 1;
	return F(n-1);
)

⚠️尾递归的优点:**没有必要保存栈帧,而是可以覆盖栈帧。**就拿上例为例,求F(5)就是求F(4),求F(4)就是求F(3),后者覆盖前者,而不用像上个代码例子斐波那契数列一样保留Fib(4),Fib(3)……的信息。

单向递归:向“一个方向“递归,比如求阶乘,这样消除递归也不一定用栈保护现场,比那一起可优化代码,直接用一个循环解决。

⚠️对于单向递归和尾递归,可以利用迭代的方法消除递归,所以正确说法是:消除递归不一定需要使用栈)

17,队列在层次遍历中的使用:

1,根结点入队,
2,若队空则结束遍历,否则循环操作3,
3,队首元素出队遍历之,依次将其左右孩子入队(若有),然后返回2。

18,栈的应用:递归、⚠️进制转换、迷宫求解(DFS)。
迷宫求解只是求怎么通过迷宫,而不是求迷宫最短路,求迷宫最短路(BFS)需要用到队列。
19,页面替换算法用到了队列(FIFO),其他调度/管理算法的先来先处理都是可以用队列的。
20,正确说法:非递归算法通常效率高一些,因为递归通常有重复计算(使用剪枝法或dp数组可以解决)。
21,执行函数时,其局部变量一般采用栈结构进行存储。
(调用函数时,系统会为调用者构造一个由参数表和返回地址组成的活动记录,此活动记录和被调用函数的局部变量都要压入栈中
22,三元组表十字链表都可以用来存储稀疏矩阵
三元组:(行row, 列col, 值value)
十字链表:将行单链表和列单链表组合起来可以存储稀疏矩阵

树与二叉树杂碎概念、计算公式和存储结构

杂碎概念

1,树中结点最大度数是树的度数;树的高度(深度)是树中结点的最大层数;有序树是指结点的子树从左至右不能交换。
二叉树是有序树,结点的左右子树交换后就是新的树。
2,路径长度是路径上所经过的边的个数。⚠️同一结点的两个孩子之间没有路径,因为树的路径只能由双亲指向孩子的有向边构成。
树的路径长度:树根到每个结点路径长度的总和。
3,完全二叉树只可能有一个度为1的结点(或者没有),该结点只有左孩子没有右孩子。

计算公式

1,度为m的树第i曾最多结点数: m i − 1 m^{i-1} mi1
2,高为h的m叉树最多结点数:
m h − 1 m − 1 \frac{m^h-1}{m-1} m1mh1
3,具有n个结点的m叉树最小高度(命n小于等于上式就能证出):
⌈ l o g m ( n ( m − 1 ) + 1 ) ⌉ ⌈log_m(n(m-1)+1)⌉ logm(n(m1)+1)
4,对满二叉树或完全二叉树按层序编号,根结点编号为 1 1 1,则编号为 i i i的结点左孩子是 2 i 2i 2i,右孩子是 2 i + 1 2i+1 2i+1(前提是 i i i有左/右孩子)。
⚠️错误说法:完全二叉树中第 i i i个结点的左孩子编号为 2 i 2i 2i,错因是不一定有左孩子,可能是叶结点。
⚠️对于满二叉树或完全二叉树这样编号,若i小于等于 ⌊ n / 2 ⌋ ⌊n/2⌋ n/2,则其是分枝结点而非叶结点。树的结点数n为奇数则每个分枝结点都有左右孩子,偶数则最后一个分枝结点只有左孩子。
5,完全二叉树结点n所在的深度或者有n个结点的完全二叉树的高度:
⌈ l o g 2 ( n + 1 ) ⌉ 或 ⌊ l o g 2 n ⌋ + 1 ⌈log_2(n+1)⌉ 或 ⌊log_2n⌋+1 log2(n+1)log2n+1
6,非空二叉树度为0的结点 n 0 n_0 n0和度为2的结点 n 2 n_2 n2的关系:
n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1

存储结构

1,顺序存储:

  • 优点:对于完全二叉树和满二叉树比较合适;树中结点序号可以唯一反映结点之间的逻辑结构。
  • 缺点:空间利用率较低,对于一般的二叉树,只能添加一些并不存在的空结点,以维持数组下表反映结点间逻辑关系的特性。
    (⚠️这种存储结构要从数组下表1开始存储树中的结点)

2,链式结构:
用结构体表示结点,结构体中有数据、左孩子指针和右孩子指针。这种结构称为二叉链表。
⚠️重要结论:n个结点的二叉链表中,含有n+1个空链域。(证明:有n-1个结点需要被指针指向(根结点不需要被指针指向),则还剩n+1个指针不指向任何结点,即为NULL)。

习题小结

1,一棵完全二叉树有768个结点,则叶结点个数为:384.
计算注意事项:完全二叉树度为1的结点数为0或者1,具体为几可直接由结点总个数确定。
2,有124叶子结点的完全二叉树最多有几个结点:248.
注意事项:由 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1知度为2的结点有123个,故总结点数为 n 0 + n 1 + n 2 = 247 + n 1 n_0+n_1+n_2=247+n_1 n0+n1+n2=247+n1,而完全二叉树度为1的结点最多有1个,所以答案248.
3,完全二叉树根结点的序号为1,则判断序号为p和q的结点是否在同一层的方法是:
算结点数为p(q)的完全二叉树的高度公式。
4,⚠️特殊分析法:一棵2011个结点的树有116个叶子,该树对应的二叉树中无右孩子的结点个数有:1896个。
方法:不妨认为这个树最后一层有116个叶子,前面1895层每层1个结点。
5,树对应的二叉树中,无右孩子的结点个数:
所有分枝结点的最右孩子无右孩子,跟结点转换后也无右孩子,所以个数为原树中:
分 枝 结 点 数 + 1 分枝结点数+1 +1

二叉树遍历和线索二叉树

1,对于先序中序后序,采用递归时,递归工作栈的深度都是树的深度,最坏情况下空间复杂度为 O ( n ) O(n) O(n),树是深度为n的单支树。
2,遍历算法递归和非递归的转化:借助栈就可以实现。
非递归算法的执行效率高于递归算法。
3,层次遍历算法实现只需要借助一个队列。
4,先序/后序/⚠️层次序列和中序序列都可以唯一确定一棵二叉树。
5,线索二叉树:存储结构被成为线索链表。
线索二叉树主要是为了访问运算服务的,这种遍历不需要借助栈,因为结点中隐含了前驱和后继的信息。

重要习题

1,二叉树中有两个结点m和n,m是n的祖先,则可使用后序遍历找到从m到n的路径。
⚠️原因:后序遍历退回时访问根结点,就可以自下而上把从n到m的路径上的结点输出;若采用非递归算法,则后序访问到n时,栈中把从根到n的副指针的路径上的结点都记录了下来,也可以由此找到从m到n的路径。
2,⚠️前序中序后序中,所有叶子结点的访问顺序完全相同
3,层次序列和中序序列也可以唯一确定一棵二叉树。
4,线索二叉树是一种物理结构。⚠️不是逻辑结构。
5,⚠️二叉树线索化后,仍不能解决的问题是后序线索二叉树中求后序后继。
(先序线索求后继和中序线索求前驱、后继都是可以解决的)
所以错误说法:每个结点通过线索都可以直接找到它的前驱和后继。
反例就是后序线索二叉树查后继有时不可以。
⚠️所以后序线索树的遍历需要栈的支持。
6,线索二叉树用二叉树n+1个空指针存放结点的前驱和后继信息。
7,前序和中序序列的关系相当于是:
以前序序列入栈,出栈顺序就是中序序列。所以给定长度为n的前序序列,计算其可以对应几种中序序列,可以使用Catalan数(内容在栈和队列那里):
1 n + 1 C 2 n n \frac{1}{n+1}C_{2n}^n n+11C2nn
8,非空二叉树的先序和后序序列正好相反,则二叉树的形态是每层只有一个结点。

树、森林、并查集

1,树的双亲表示法就是用伪指针(数组下标)指向双亲结点,跟结点的伪指针是-1,
⚠️双亲表示法的缺点:查找结点x的孩子的时候,需要遍历整个结构,看谁的伪指针指向x,谁就是x的孩子。
2,树的孩子表示法,结构是n个单链表,每个单链表链接此结点的所有孩子;
⚠️孩子表示法的缺点:寻找双亲的操作需要遍历n个结点中的孩子链表。
3,孩子兄弟表示法,“左孩子右兄弟”,
⚠️孩子兄弟表示法的缺点:查找双亲结点比较麻烦(可用的解决方法:为每个结点增设一个parent域用于指向双亲)。
4,树和二叉树之间相互转换:“左孩子右兄弟”。⚠️二叉树转换为树或森林是唯一的。
5,树和森林的遍历:

  • 树的遍历(顾名思义)
    • 先根遍历
    • 后根遍历
  • 森林的遍历
    • 先序遍历森林:访问第一棵树的根结点——先序遍历第一棵树根结点的子树森林——去掉第一棵树后继续遍历剩下的,依此类推;
    • 中序遍历森林:中序遍历第一棵树的子树森林——访问第一棵树的根——中序遍历剩下的森林。⚠️中序遍历森林的访问序列就是森林对应的二叉树的中序序列,这里有个易错点就是直接在森林中观察其中序访问序列会感觉这个像是在做后根遍历。

6,树、森林、二叉树遍历序列对应关系

  • 二叉树的先序遍历对应:树的先根遍历、森林的先序遍历;
  • 二叉树的中序遍历对应:树的后根遍历、森林的中序遍历;

⚠️这里的二叉树是指从树和森林转化而来的。

7,树的应用——并查集
存储结构:树的双亲表示法。

适用范围:指向(直接或间接)同一个根结点的结点相当于一个类,所以可用于“等价类”。

细节:并查集是一个森林,每个结点有个序号(从0开始),对应数组下标。自己在数组中的位置用于存储本树的结点树(对于根结点来说)或者自己指向的结点的序号。

重要习题

1,森林中有n个非终端结点,即n个分枝结点。求其转化成的二叉树中右指针域为空的个数:n+1.
理由:每个分枝结点的最右孩子没有右孩子;⚠️最后一棵树的根结点没有右孩子。
2,⚠️利用树的先根序列和后根序列能否唯一确定一棵树:
能,因为树的后根序列对应转化为的二叉树的中序序列,先根序列对应二叉树的先序序列,二叉树的先序和中序能唯一确定一棵二叉树,二叉树和原树是一一对应关系。

二叉排序树、平衡二叉树、哈夫曼树

1,二叉排序树(BST):比较重要的知识就是元素的删除和平均查找长度的计算。
最坏查找长度 O ( n ) O(n) O(n),平均查找长度为 O ( l o g 2 n ) O(log_2n) O(log2n)。⚠️插入和删除单个元素的操作复杂度也是这样,主要时间在查找上,插删只需要修改指针。

⚠️相同关键字插入顺序不同可能得到不同的排序树。

⚠️元素的删除:“右子树空用左子女填补、左子树空用右子女填补、左右子树都非空则用右子树中序第一个元素填补”。

⚠️画出二叉排序树来计算查找成功和失败的平均查找长度:

⚠️静态查找表和动态查找表的说法:
若有序表是静态查找表(意思是只查不增删),宜用顺序表作为存储结构,配套使用二分查找;若有序表是动态查找表(边查边增删),宜用二叉排序树作为逻辑结构。

2,平衡二叉树(AVL,Balanced Binary Tree):
左子树和右子树高度差为结点的平衡因子。所有结点的平衡因子绝对值都不超过1则为平衡二叉树。

⚠️四种平衡旋转:用途就是每次增删操作后,若此操作导致了平衡树的不平衡,就进行平衡旋转进行调整。⚠️每次调整的范围都是最小的不平衡子树。

一共有LL(右单旋转)、RR(左单旋转)、LR(先左后右双旋转)、RL(先右后左双旋转)。图示给出LL和LR,其他照葫芦画瓢自己想:

⚠️平衡二叉树的重要计算:计算深度为h的平衡树最少结点数
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,n_h=n_{h-1}+n_{h-2}+1 n0=0,n1=1,n2=2,nh=nh1+nh2+1,思想就是一个跟结点挂接两个高为n-1和n的组成一个n+1的。

含有n个结点的平衡二叉树的最大深度为 O ( l o g 2 n ) O(log_2n) O(log2n),平均查找长度也是这个。

3,哈夫曼树(最优二叉树)
是带权路径长度(WPL)最小的树。⚠️没有度为1的结点。

带权路径长度计算细节和哈夫曼树构造过程见图:


哈夫曼编码:一种可变长度编码
可变长度编码比固定长度编码好得多,因为可以对频率高的字符用短编码,对频率低的字符用长编码。

哈夫曼编码是一种被广泛应用且非常有效的数据压缩编码。

⚠️前缀编码:没有一个码是其他码的前缀。
哈夫曼编码是前缀编码。

下图内容仔细看,图中方框内数字是字符频率:

⚠️利用哈夫曼树可以设计出总长度最短的二进制前缀编码。

重要习题

1,正确说法:二叉排序树是动态树表,查找失败时插入新结点。
2,错误说法:二叉排序树插入新结点时引起树的重新分裂和组合
错因:删除结点时才会这样。
3,中序遍历二叉排序树的到的序列是有序的。
4,二叉树的高度为6,所有非叶子结点的平衡因子均为1,则该平衡二叉树的结点总数是:20.
方法:⚠️所有非叶结点的平衡因子均为1,就是在说求高为6的平衡二叉树的最少结点数。
5,平衡旋转每次调整的是失衡的最小二叉子树。
6,⚠️哈夫曼树的正确说法:
树中两个权值最小的结点一定是兄弟结点(想想哈夫曼树的构造过程,始终选最小的两个先结为兄弟);
树中任一非叶结点的权值一定不小于下一层任一结点的权值(非叶结点的权值是自己两个孩子权值的总和);
对应一组权值构造出来的哈夫曼树一般是不唯一的(因为二叉树是有序树,左右子树颠倒,则为不同树)。
7,度为m的哈夫曼树中,只有度为m和0的结点。分枝结点一定度为m。
8,哈夫曼树中左右孩子的权值等于父结点的权值。⚠️如果给出两个从根到叶结点的权值序列,要判断是不是同属于一个哈夫曼树,则可以用这个方法。
9,大根堆就是父结点大于左右子女,既符合大根堆又符合二叉排序树的要求,这种树要会画(P168)。
10,⚠️⚠️哈夫曼树的重要应用举例:
六个有序表分别有10,35,40,50,60和200个元素,各表元素统一按照升序排列,通过五次两两合并操作,将这六个有序表合并成一个有序表。要求最坏情况下的总比较次数最小。
解答:⚠️入手点就是:先合并的表中的元素在后序的合并操作中也会再次参与比较,所以先将短表两两合并。想到哈夫曼树是将两个权值最小的结点两两合并,所以可以使用哈夫曼树作为题目的解答模型。(P178可看最少比较总次数)

图的定义和存储结构

线性表可以是空表,树可以是空树,图不可以是空图,图起码有一个顶点。

一些杂碎定义

1,简单图:不存在重复边、不存在顶点到自身的边。王道考研说数据结构只讨论简单图;
2,极大连通子图是无向图的连通分量,包含图中所有的边;极小连通子图是保持连通且边最少;
3,强连通图:仅为有向图的概念,任意一对顶点间都有路径(而不是弧)。
有向图中极大强连通子图是有向图的强连通分量。
4,连通图的生成树:包含图中所有顶点的极小连通子图。
5,网:就是带权图,指边上带有权值。
6,稀疏图: ∣ E ∣ < ∣ V ∣ l o g ∣ V ∣ |E|<|V|log|V| E<VlogV
7,简单路径:顶点不重复出现;
8,简单回路:除了第一个和最后一个顶点,其余顶点不重复出现。
9,正确说法:对无向连通图做一次深度优先搜索,可以访问到连通图的所有顶点。
⚠️这里说的一次dfs,不是说不靠递归的“一次”,只要是无向连通图就可以一次访问完所有结点;
10,⚠️有向图中存在拓扑序列说明图不存在回路;反之存在回路的图没有拓扑序列;
11,错误说法:强连通有向图的任何顶点到其他顶点都有弧。
错因:是都有路径而不是都有弧。

重要计算

1,无向图有7个顶点,保证任何情况下图都是连通的,需要的边数最少是:16.
注意事项:6个点的完全图再加一条边就可以了。
2,无向完全图边数: n ( n + 1 ) / 2 n(n+1)/2 n(n+1)/2;有向完全图边数是这个两倍。
3,28条边的非连通无向图至少有几个顶点:9.
注意事项:8个点的完全无向图有28条边,再加1个顶点就能非连通。
4,无向图有23条边,度为4的顶点5个,度为3的顶点4个,其他都是度为2的顶点,求图最多有多少顶点:16.
注意事项:23条边对应46个度数,就是这么简单。
⚠️不要去纠结题目给的图能不能画出来,直接按照公式计算就行了。
5,无向图有16条边,度为4的顶点为3个,度为3的顶点为4个,其他顶点的度数小于3,求图最少有多少顶点:11.
注意事项:还是边数的2倍为度数总和,可得度数小于3的结点共占8度,最少4个结点(都是2度)可以占8度。3+4+4=11.
6,无环有向图一定存在拓扑序列,但可能不唯一。
如何重排列无环有向图中的顶点序号以达到使得图的邻接矩阵所有为1的元素都在对角线以上:按照拓扑序列对顶点重新编号。⚠️对角线以上的特性就是i

四种图的存储结构及其特点

1,邻接矩阵
空间复杂度 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

  • 优点:很容易确定两个顶点之间是否有边相连(根据数组下标随机访问)
  • 缺点:要确定图中有多少条边比较麻烦,需要逐个遍历每个元素。

注意:无向图的邻接矩阵是对称阵;稠密图适合用这个方法存储;规模特大的邻接矩阵可用压缩存储。

⚠️重要公式: i 到 j 长 度 为 n 的 路 径 数 目 i到j长度为n的路径数目 ijn
邻 接 矩 阵 n 邻接矩阵^n n
2,邻接表
空间复杂度:无向图为 O ( ∣ V ∣ + 2 ∣ E ∣ ) O(|V|+2|E|) O(V+2E),有向图为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)

  • 优点:
    • 对于稀疏图,极大节省空间;
    • 给定一顶点,很容易找到它的所有邻边;
    • 计算给定顶点的出度很方便
  • 缺点:
    • 判断两个顶点之间有无边很麻烦,需要在顶点的边表中遍历含另一顶点的边表结点;
    • 计算给定结点的入度较麻烦,需要遍历整个邻接表

注意:邻接表表示方法不唯一;
⚠️逆邻接表则是方便求给定结点的入度而不方便求给定点的出度。

3,十字链表(有向图专用)
空间复杂度 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)

有向图的十字链表不唯一,但十字链表可以确定唯一的有向图。
P192红笔标注可看十字链表结构。

4,邻接多重表(无向图专用)
空间复杂度 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)

⚠️邻接多重表和邻接表中删除一条边的边结点的不同:
1,无向图的邻接表中一条边对应两个边结点。邻接表从两个顶点的边表中分别找到相应的边结点并删除,各修改相应的指针;
2,无向图的邻接多重表中一条边对应一个边结点。邻接多重表从一个边表中找到边结点,修改指针,再修改另外一个边表中对应的指针,然后释放这一个边结点的空间就行了。
⚠️所以有说法是邻接多重表删除边的操作比邻接表更简化,更简化的理由仅仅是前者是需要删除一个边结点而已,但两者都得在两个边表中进行遍历。

习题小结

1,正确说法:无向网的邻接矩阵是对称矩阵。(无向网就是无向带权图)
2,⚠️一个图的邻接矩阵表示是唯一的,不要认为因为结点编号可以导致矩阵也不同,就认为表示法不唯一;邻接表表示才是不唯一的。
3,邻接表存储图所用空间的大小于图的顶点数和边数有关。
4,⚠️n个顶点e条边的有向图用邻接表表示,删除于某个顶点v有关的所有边的时间复杂度: O ( n + e ) O(n+e) O(n+e),理由是先删除v结点的单链表中边表结点,最多 n − 1 n-1 n1个,复杂度为 O ( n ) O(n) O(n),然后遍历所有边表,删除入边是v的结点,复杂度为 O ( e ) O(e) O(e),加起来就是答案。

BFS和DFS

BFS性能和实用性分析

1,BFS需要借助辅助队列,n个结点均需要入队一次,最坏情况下空间复杂度是 O ( ∣ V ∣ ) O(|V|) O(V)
⚠️空间复杂度是需要的额外空间。
2,采用邻接表存储需要的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E),采用邻接矩阵的存储方式需要的时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)
3,广度生成优先树:⚠️如果用图用邻接矩阵存储,则广度生成优先树是唯一的;如果用邻接表存储,则广度生成优先树是不唯一的。

⚠️基于邻接表存储的图,广度/深度生成优先树都是不唯一的。

⚠️从有些角度来想,其实上述结论有瑕疵,但从考试角度,先记住上面的结论(包括图的邻接矩阵表示法唯一也是如此,考试的角度不考虑图中结点不同编号情况,但工业界考不考虑我不知道)。

⚠️BFS和DFS编程时要给每个结点设置一个访问标志,如果访问过就置true,只访问不是true的结点。

DFS性能和实用性分析

1,DFS是一个递归算法,需要递归工作栈,空间复杂度为 O ( ∣ V ∣ ) O(|V|) O(V)
2,邻接矩阵表示时,时间复杂度为 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2),邻接表表示时,时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)。时间复杂度是BFS是一样的。
⚠️如果只是设想一下for循环的次数,则会认为时间复杂度远远不止上面所述。实际上编程时为每个结点设置了访问标志,如果为true表示已经访问过了,就不会进入for循环执行语句,所以决定时间复杂度的循环语句的执行次数(进入for才算是执行)是要小于for循环条件判断次数的。

利用DFS和BFS官方代码模板探测图的连通性

⚠️就拿DFS来说,DFSTraverse()是遍历整个图的函数,它调用DFS()

1,知道了上面这句话后,对于无向图,一次DFS()遍历的是图的一个连通分量的所有顶点;对于有向图,若从初始点到每个点都有⚠️路径,则一次DFS()能够访问图中所有结点。
2,⚠️对于无向图来说,在DFSTraverse()中调用DFS()的次数等于连通分量数。

⚠️DFS和BFS都能够用来计算图的联通分量数。

3,⚠️使用DFS算法递归遍历无环有向图,在退栈时输出相应结点,则的到的顶点序列是逆拓扑序列。
要点:无环有向图一定有拓扑序列;退栈结点总为当前只有前驱没有后继的结点,所以是逆拓扑排序。

图的若干应用(必考的重点部分)

最小生成树(MST)

1,Prim算法:
复杂度 O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)。每一轮加入一个结点,每一轮比较的数量级是 O ( ∣ V ∣ ) O(|V|) O(V)(不信可以用书上图手推);
不依赖于边数,适合边稠密的图。
2,Kruskal算法:
复杂度 O ( ∣ E ∣ l o g ∣ E ∣ ) O(|E|log|E|) O(ElogE)

复杂度推导:
1,初始化生成树的边集为空集O(1);
2,对于图中每个结点,将其集合初始化为自身O(|V|);
3,将边按照权值排序O(|E|log|E|),这是因为采用堆来存放边的集合;
4,排好序的边由小到大,若相连两顶点不在一个集合中,则将边加入边集,将所连两顶点所在集合合并。(这一步的算法复杂度自行查《算法导论》并查集的内容)
上述最复杂的部分是O(|E|log|E|),证毕。

3,Dijkstra算法:
无论是邻接表存储还是邻接矩阵存储,复杂度都一样。
单源最短路: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2),所有结点间最短路: O ( ∣ V ∣ 3 ) O(|V|^3) O(V3)

功能是求单源最短路问题,求某一点到其他各点的最短路径。如果求所有结点间最短路,则对每个结点都运行一次此算法。

⚠️边上有负权值不适用。

对于无向带权图也可以用(相当于每对结点间有来回两条cost相同的路径)。

4,Floyd算法
算法复杂度 O ( ∣ V ∣ 3 ) O(|V|^3) O(V3)

⚠️允许边带负权值,但不允许回路中含带负权值的边。

对于无向带权图也可以用(相当于每对结点间有来回两条cost相同的路径)。

5,拓扑排序
有向无坏图的简称:DAG。AOV和AOE都是DAG。

AOV网是有向无环图,用顶点表示活动,边表示活动之间的前驱后继关系。

拓扑排序的时间复杂度为 O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(V+E)。书上P217我写了推导

拓扑排序执行流程:
1,将所有入度为0的结点压栈;
2,从栈中弹出一个结点输出,若栈空,算法结束;
3,对于输出的结点,删除以它为出发点的边;
4,返回1.

6,关键路径
AOE网是边上带权的有向无环图,用顶点表示事件,用边表示活动。

⚠️关键路径不唯一,只有缩短含于所有关键路径的关键活动,才能缩短工期。
(如果是缩短若干关键活动,则只要这些关键活动涉及到了所有关键路径,也能缩短工期)
⚠️关键活动缩短到一定程度会转化为非关键活动,所以不能任意缩短关键活动。

习题小结

1,任何无向连通图都有一棵或多棵最小生成树;
⚠️只要无向连通图中没有权值相同的边,则其最小生成树唯一(不要考虑“谁是树根”);
2,⚠️Prim和Kruskal算法构成的最小生成树可能相同,可能不同;
3,错误说法:有相同权值的无向图中,最小生成树一定不唯一。
错因:如果图本身就是树,则其最小生成树唯一,不要考虑换个树根就不唯一了。
4,使用Prim算法从不同顶点开始执行,得到的最小生成树不一定相同;
5,最小生成树代价唯一;
6,⚠️错误说法:所有权值最小的边一定会出现在所有的最小生成树中。
错因:如果所有权值最小的边组成了一个环,则总有一条最小的边不在生成树中(题意有歧义,不要理解成别的意思)。
7,求最短路径不能判断有没有环,因为最短路径是允许有环的。
8,关键路径能否判断图中有无环有争议:关键路径算法本身不能判断有没有环,如果是先判断图是否能拓扑排序的关键路径算法,则能判断有没有环。
9,⚠️若一个有向图的顶点不能排在一个拓扑序列中,则说明这个图有环,则进一步说明这个图有顶点数大于一的强连通分量(单看环,其为强连通的)。
10,若有向图中有环,则其不存在拓扑序列;
11,在拓扑排序算法中暂存入度为0的顶点,可以使用栈,也可以使用队列;
12,图的在最小生成树中的边的权值可以大于不在最小生成树中边的权值。
13,关键活动一定位于关键路径上;
14,⚠️对于所有关键路径上的公共活动,延长之不会导致产生不同关键路径,适当缩短之可以缩短工期,过度缩短之会使之变为非关键活动。
15,破圈法:生成最小生成树的方法之一,从图中找到一个环,去掉权值最大的边,循环这一步直到没有环。
破圈法是正确的方法,因为:首先,破圈法得到的树是无环连通图,即生成树;其次假如T不是最小生成树,则设T0是最小生成树,显然T和T0并集中有环,而这和“去掉环中最大一条边得到T”矛盾,所以T不仅是生成树,还是最小生成树。

查找

顺序和折半查找

1,顺序查找

一般线性表的顺序查找:

  • 查找成功的查找长度: n + 1 2 \frac{n+1}{2} 2n+1
  • 查找失败的查找长度: n + 1 n+1 n+1,原因是在数组下标为0的位置增设了“哨兵”,查找顺序是从后往前找。

有序表的顺序查找:

  • 查找成功的查找长度: n + 1 2 \frac{n+1}{2} 2n+1
  • 查找失败的查找长度: 1 + 2 + . . . + n + n n + 1 \frac{1+2+...+n+n}{n+1} n+11+2+...+n+n,推导过程可参考之后所讲“查找判定树”的内容。

2,折半查找(又叫二分查找)
重点掌握折半查找判定树。

关于查找判定树
判定树中有方形结点,它代表着查找失败的情况,但需要注意:
⚠️问判定树的高度时不把方形结点算入,因为方形结点是虚构的结点;
⚠️计算查找失败的ASL时才需要用到方形结点,计算方式和查找成功的ASL有差异,看书P243图6.2在倒数第九行的例子即可弄明白。

折半查找判定树是平衡二叉树,树高为:
h = ⌈ l o g 2 ( n + 1 ) ⌉ 或 ⌊ l o g 2 n ⌋ + 1 h=⌈log_2(n+1)⌉ 或 ⌊log_2n⌋+1 h=log2(n+1)log2n+1
注意到上述公式和二叉排序树是一样的。
⚠️查找失败时,比较次数最多为 h h h,最少为 h − 1 h-1 h1.

3,分块查找(索引顺序查找)
既有动态结构,又能够快速查找。

块内无序、块间有序;看张示意图了解基本结构:

⚠️计算方法:长度为n的查找表均匀分为b块,每块有s个记录,块内和索引内(块间)可以都用顺序查找或者都用折半查找,或者混合使用。其中ASL最佳的方式是 s = n s=\sqrt{n} s=n 时,此时 b = n b=\sqrt{n} b=n ,然后块内块间都用折半查找,得到最佳ASL:
⌈ l o g 2 ( s + 1 ) ⌉ + ⌈ l o g 2 ( b + 1 ) ⌉ ⌈log_2(s+1)⌉+⌈log_2(b+1)⌉ log2(s+1)+log2(b+1)

顺序查找、折半查找和索引查找的ASL计算一定要掌握!

习题小结

1,折半查找和二叉排序树时间性能有时相同(树是平衡的时候)有时不同;
2,折半查找用mid将分区分成两半,对每一半各自再进行查找;
上面这句话我会,但需要注意的地方是新的两个小区域不包含mid
3,长度为n的有序顺序表,采用折半查找一个不存在的元素,最多比较次数就是树的高度,最小比较次数就是树高减一;

B树和B+树

1,B树又叫多路平衡查找树;
2,m阶B树的根结点的关键字个数为 [ 1 , m − 1 ] [1,m-1] [1,m1]
非根结点关键字个数为 [ ⌈ m / 2 ⌉ − 1 , m − 1 ] [⌈m/2⌉-1,m-1] [m/21,m1]

或者另一种说法:除了终端结点外,每个结点最多 m m m个子树,根结点最少 2 2 2个子树,非根结点最少 ⌈ m / 2 ⌉ ⌈m/2⌉ m/2个子树。

3,本节重点是B树的高度计算
(关注这个问题的原因:B树的大部分操作所需的磁盘存取次数与B树的高度成正比)

⚠️e.g. 拥有n个关键字的m阶B树的高度h的范围:
n ≤ ( m − 1 ) ∗ ( 1 + m + m 2 + . . . + m h − 1 ) n≤(m-1)*(1+m+m^2+...+m^h-1) n(m1)(1+m+m2+...+mh1)
可以推出
h ≥ l o g m ( n + 1 ) h≥log_m(n+1) hlogm(n+1)
第一层最少一个关键字,从而第二层最少 2 2 2个点,每个点中最少 ⌈ m / 2 ⌉ − 1 ⌈m/2⌉-1 m/21个关键字,从而第三层最少 2 ⌈ m / 2 ⌉ 2⌈m/2⌉ 2m/2点,依此类推,第 h h h层最少 2 ⌈ m / 2 ⌉ h − 2 2⌈m/2⌉^{h-2} 2m/2h2个结点。
然后在第 h + 1 h+1 h+1层(叶结点下面一层,以方块表示查找失败的那一层,一共有 n + 1 n+1 n+1个结点),s结点数最少为: 2 ⌈ m / 2 ⌉ h − 1 2⌈m/2⌉^{h-1} 2m/2h1,即
n + 1 ≥ 2 ⌈ m / 2 ⌉ h − 1 n+1≥2⌈m/2⌉^{h-1} n+12m/2h1
可以推出
h ≤ l o g ⌈ m / 2 ⌉ ( n + 1 2 ) + 1 h≤log_{⌈m/2⌉}(\frac{n+1}2)+1 hlogm/2(2n+1)+1

4,本节的另一个重点是B树的插入和删除,掌握以下图片:
(注意插入导致结点分裂时,从中一分为二,中间结点加入父结点,⚠️这里的中间结点是指除以二向上取整)


5,B+树每个结点的关键字个数和子树个数相同,所有非叶结点只起索引作用,叶结点包含全部关键字,可以多路查找,也可以顺序查找(对比B树只能多路查找),每个结点关键字个数(同时也是子树个数)范围是 [ ⌈ m / 2 ⌉ , m ] [⌈m/2⌉,m] [m/2,m],根结点是 [ 1 , m ] [1,m] [1,m]

6,⚠️一个重要易错点:
只要符合m阶B树对每个结点的范围要求,就是m阶B树,而不用至少一个结点有m个分支。
这样一来,如果让判断“以下B树是几阶的”也要小心,不能只看最大的度数为m就判定为m阶的,还要看结点中关键字有没有超过m阶B树对结点中关键字个数的要求。
7,B树和B+树的一些特性:
B树和B+树都能有效地支持随机查找;
B树和B+树都可以用于文件索引结构;

⚠️适合B树的是:关系数据库系统中的索引;
适合B树的是:
编译器中词法分析(用有穷自动机和语法树);
网络中的路由表快速查找(靠高速缓存、路由表压缩技术和快速查找法);
操作系统的磁盘空闲块管理(用空闲空间链表)。

习题小结

1,有些材料会把B树最后一行表示查找失败的方形结点称作叶结点,不过408真题没有这样干过,874真题不清楚会不会这样做;(真题中B树的高度也不包括方形结点那一层)
2,含有n个关键字的B树的最后一行查找失败结点共有n+1个(注意有材料将这个称为叶结点,直接问叶结点有多少个;有材料直接问n个非叶结点的B树中含有多少关键字也是在说,除了方形结点以外,B树共有n个结点);
3,插入一个关键字导致结点分裂多次的例子:

Hash和KMP

6.4和6.5,用书上标注结合笔记本上P52-55.

记录一些关键问题:
1,如何在散列表中删除一个元素:
如果是拉链法,则可以直接物理删除;
如果是开放定址法,就只能先做删除标记,因为该地址可能是该记录的同义词查找路径上的地址,如果物理删除,则中断了查找路径(在查找时遇到了空地址就认为是查找失败)。

11月24日:4.3代码模板抄写,概念和习题要背的总结。5.2概念总结,代码先不抄;
11月25日:4.3代码模板从P118中序非递归开始。4.4和5.3概念总结,代码先不抄;
11月26日:昨日有学校的专业课辅导,完成昨日的;
11月27日:4.5认真复习,比较淡忘了。5.3.3和5.3.4复习。抄代码模板从4.3的P118中序非递归开始。
11月28日:4.5.4;5.3.3和5.3.4复习;抄代码模板从4.3的P118中序非递归开始。
11月29日:深入理解5.4所有算法,除习题外部分完成总结;抄代码模板从P119层次遍历开始。
11月30日:搞定第5.4习题;6.1和6.2复习;抄代码模板从P119层次遍历开始。
12月4日:6.2.4和6.3全部搞定。
12月5日:理解第7章所有算法(要求能手动模拟)。
12月6日:理解第7章所有算法(要求能手动模拟)。⚠️本日另一项任务是OS,由于今日始要均衡复习,所以不看计网,不然时间不够;明天开始做真题,一天两套,最晚10号完成408真题除了数据结构大题的部分。

(这是考研期间,手忙脚乱的计划,图方便直接写在了这里,做个纪念,不删了)

并查集代码总结的时候要画一下辅助图,这个感觉容易忘掉。

排序的细碎知识

1,从1000个关键字中选出前5个最小的:堆排序最快;
2,交换类排序,其趟数和原始序列有关。交换类排序:冒泡排序、快速排序;
3,交换类和选择类排序,每趟排序结束都有一个关键字放在了最终位置;
4,关键字比较次数和初始排列次序无关的是:简单选择排序;
⚠️希尔排序比较次数和初始次序有关,不要忘了它是插入类排序(间距不为一的直接插入排序)
5,归并排序有两个基本阶段,第一个阶段是生成初始归并段;
6,归并排序中归并的趟数是: ⌈ l o g 2 n ⌉ ⌈log_2n⌉ log2n
7,文件有m个初始归并段,采用k路归并的时候,所需的归并趟数是 ⌈ l o g k m ⌉ ⌈log_km⌉ logkm

你可能感兴趣的:(CS考研和读研笔记/心得,数据结构,计算机考研,四川大学,考研)