数据结构复习笔记(西南交通大学2020秋季学期数据结构A)-Keller_Wang

0.写在前面

本文作者:Keller Wang

用于西南交通大学2020秋季学期的数据结构A课程的期末考试复习

​ 写这个笔记的初衷是因为老师发的ppt中并没有对于各个例题的过多解答,而且考察的很多知识点其实不需要对算法和数据结构本身有很深入的了解就可以解决,因此就考虑写这么一篇笔记,主要希望能够帮助大家快速并全面的应对数据结构A课程的期末考试。

​ 本文更侧重与对各个知识点对应的选择填空题的应对策略,其中很多数据结构和算法介绍的并非详细,建议有时间的同学查阅更详细的资料以加深理解。

​ 其中知识点顺序采用了赵宏宇老师"课程总结.ppt"中"考试要点"部分中列举的27个知识点。(其中对 7.中序线索化12.中序穿线二叉树 进行了合并,以及 21.希尔排序26.希尔排序给定增量序列写出各趟增量排序结果 进行了合并)。

​ 各知识点例题采用了赵宏宇老师"数据结构-习题讲评汇总28个知识点.ppt"、"习题讲评1(线性表-堆栈队列-数组广义表-串).pptx"和"习题讲评2(树-图-查找-内部排序).pptx"三个ppt中的几乎全部题目(删除了极个别重复度极高的题目)。

​ 由于时间原因和作者本身能力问题,本文难免会出现部分纰漏,还希望大家多多批评指正。
同时也上传了更易阅读的pdf版本点击这里下载! (PDF版本随本文的更新也在更新,更新日志见本文末尾)


复习大纲


1.分析算法的T(n)和S(n)

T(n)即为算法的时间复杂度,S(n)为算法的空间复杂度

例1:分析@标注语句执行频度
for(i=1;i<=n;i++)
	for(j=1;j<=i;j++) 
		for(k=1;k<=j;k++)
			@ x+=delta;

显然只需要根据各层循环分别计算即可:
m = ∑ i = 1 n ∑ j = 1 i ∑ k = 1 j 1 = n ( n + 1 ) ( n + 2 ) 6 m=\sum_{i=1}^{n}\sum_{j=1}^{i}\sum_{k=1}^{j}1=\frac{n(n+1)(n+2)}{6} m=i=1nj=1ik=1j1=6n(n+1)(n+2)

例2:分析@标注语句执行频度
i=1;j=0;
while(i+j<=n)
	@ if(i>j)j++; else i++;

虽然这题看似是一个循环的条件由两个变量的值决定,但实际上,每次循环 i+j 的值只增加1,因此@的执行次数为n

例3:分析@标注语句执行频度
x=n;y=0;
while(x>=(y+1)*(y+1))
	@y++;

这里首先可以看到x是个唬人的变量,实际上直接把它看作n就可以。然后令m=y+1,则有 m 2 ≤ n ⇒ m ≤ n m^2\leq n \Rightarrow m\leq \sqrt n m2nmn m每次增加1,故@的执行次数为 ⌊ n ⌋ \left\lfloor \sqrt n \right\rfloor n

例4:分析时间复杂度及变量count的值
int Time(int n){  // n>2
	count=0;x=2;
	while(x<n/2){
		x*=2;
		count++;
	}
}

计算时间复杂度时,需要使用大O表示法,即 T ( n ) = O ( f ( n ) ) T(n) = O(f(n)) T(n)=O(f(n)) ,推导大O只需要以下三种规则:

1、用常数1取代运行时间中的所有加法常数
2、只保留最高阶项
3、去除最高阶的常数

那么看这道题,从第一次时,x=2,随后每次x自乘2,假设执行了k次,则最终 x = 2 k x= 2^k x=2k 。那么显然有
2 k < n 2 ⇒ k < l o g 2 n − 1 2^k < \frac {n}{2} \Rightarrow k<log_2 n -1 2k<2nk<log2n1
故时间复杂度为 O ( l o g 2 n ) O(log_2 n) O(log2n) ,count的值为 ⌈ l o g 2 n ⌉ − 2 \left\lceil log_2 n \right\rceil -2 log2n2

2.堆栈与队列(特别是循环队列)

堆栈是一种先入后出的数据结构,可以当成一个杯子,往里面放小球,后放入的小球在上面,可以先出来,而先放入的小球只有等上面的小球都拿出来之后才可以出来。

队列是一种先入先出的数据结构,可以当成一个管子,往里面放小球,就像排队一样先排队的可以先出队。

例1:若元素入栈次序为ABC,写出所有可能的出栈序列。

这种问题只需要考虑在3次入栈操作中插入出栈操作即可,只要保证不重不漏的统计完所有出栈位置的可能性即可。

ABC			(PXPXPX)		P-入栈操作, X-出栈操作
ACB			(PXPPXX)
BAC			(PPXXPX)
BCA			(PPXPXX)
CBA			(PPPXXX) 
例2:长度为n且设有队头、队尾指针的链式队列中,出队操作的时间复杂度为 ____

设有队头指针的链式队列出队操作只需要将队头后移即可,因此时间复杂度为O(1)

例3:试说明以下算法的功能。
Status algo1(Stack S){ 
	int i,n,A[255];
	n=0;
	while(!StackEmpty(S)){
		n++;
		Pop(S,A[n]);
	}
	for(i=1;i<=n;i++)
		Push(S,A[i]);
}

algo1中首先通过一个while循环让栈S中的元素依次出栈并存储在A数组中,然后一个for循环让A数组依次进栈,故实现的功能是使堆栈S中元素逆序存储

对于类似的问题,若看代码分析有困难,只需要构造一个3~5个元素的例子按照程序要求执行一遍即可分析出功能。

例4:写出下面的递归算法,并消除递归。

F ( n ) = { n + 1 n = 0 n F ( n / 2 ) n > 0 F(n)=\left \{ \begin{matrix} n+1 &n=0 \\ nF(n/2) & n>0 \end{matrix} \right. F(n)={n+1nF(n/2)n=0n>0

首先很容易写出递归算法:

int F(int n){
	if(n==0) return 1;
	return n*F(n/2);
}

消除递归即使用数组或堆栈将递归结构消除,从而防止系统栈溢出。

(1)方法一:使用数组消除递归

思路:对于这种参数只有一个的函数,只需要将参数作为数组下标,即可转为递推计算

int F1(int n){
    if(n<=1) return 1;
    int *a=new[n/2+1]; //最大仅需要 n/2+1 项
    a[0]=1;
    for(int i=1;i<=n/2;i++){
        a[i]=i*a[i/2];
    }
    int ans=n*a[n/2];
    delete []a;
    return ans;
}

(2)方法二:使用堆栈消除递归

思路:使用堆栈即相当于手动模拟系统执行递归的过程,入栈过程即为正向递归的过程,然后依次出栈表示将值逐层返回。

int F2(int n){
	if(n==0) return 1;
	stack<int> s;
	int k=n/2;
	while(k){
		s.push(k);
		k=k/2;
	}
	int fnow=1; // 初始时fnow=F(0)=1
	while(!s.empty()){
		r=r*s.top();
		s.pop();
	}
	return n*r;
}

3.前/中/后缀表达式的互化

(1)中缀表达式
我们平时缩写的表达式,将运算符写在两个操作数中间的表达式,称作中缀表达式。在中缀表达式中,运算符有不同的优先级,圆括号用于改变运算顺序,这使得运算规则比较复杂,求值过程不能直接从左到右顺序进行,不利于计算机处理。

例如:(4+1*(5-2))-6/3

(2)后缀表达式
将运算符写在两个操作数之后的表达式称作后缀表达式。后缀表达式中没有括号,并且运算符没有优先级。后缀表达式的求值过程能够严格按照从左到右的顺序进行,有利于计算机处理。

例如:4 1 5 2 - * + 6 3 / -

(3)前缀表达式
前缀表达式是将运算符写在两个操作数之前的表达式。和后缀表达式一样,前缀表达式没有括号,运算符没有优先级,能严格按照从右到左的顺序计算。

例如:- + 4 * 1 - 5 2 / 6 3

(4)中缀表达式转后缀表达式
step1:初始化一个栈和一个后缀表达式字符串
step2:从左到右依次对中缀表达式中的每个字符进行以下处理,直到表达式结束

  • 如果字符是‘(’,将其入栈
  • 如果字符是数字,添加到后缀表达式的字符串中
  • 如果字符是运算符,先将栈顶优先级不低于该运算符的运算符出栈,添加到后缀表达式中,再将该运算符入栈。注意,当‘(’在栈中时,优先级最低
  • 如果字符是‘)’,将栈顶元素出栈,添加到后缀表达式中,直到出栈的是‘(’

step3:如果表达式结束,但栈中还有元素,将所有元素出栈,添加到后缀表达式中

(5)中缀表达式转前缀表达式

中缀表达式转换到前缀表达的方法和转换到后缀表达式过程一致,细节上有所变化
step1:初始化两个栈s1 和s2
step2:从右到左依次对中缀表达式中的每个字符进行以下处理,直到表达式结束

  • 如果字符是‘)’,将其入栈s1
  • 如果字符是数字,添加到s2中
  • 如果字符是运算符,先将s1栈顶优先级不低于该运算符的运算符出栈,添加到s2中,再将该运算符入栈s1。当‘)’在栈中时,优先级最低
  • 如果字符是‘(’,将栈顶元素出栈,添加到s2中,直到出栈的是‘)’

step3:如果表达式结束,但栈中还有元素,将所有元素出栈,添加s2中
step4:将栈s2中元素依次出栈,即得到前缀表达式

(6)前/后缀表达式转中缀表达式

前/后缀表达式转中缀表达式就相当于对前/后缀表达式求值。

即:将后缀表达式从左到右依次入栈(若是前缀表达式则从右向左),若遇到运算符,则将栈顶两个元素弹出并进行对应操作,手动翻译成中缀表达式中即可。

例1:写出中缀表达式 a + ( b − c ) ∗ d a+(b-c)*d a+(bc)d对应的前缀表达式和后缀表达式。

按照(4)(5)方式即可,注意前缀表达式需要两个栈,且顺序是从右向左。

例2:后缀式3, 4, X, *, +, 2, Y, *, 5, /, - (表达式中逗号表示空格)对应的中缀式为

按照(6)方式即可。

4.写出KMP或改进KMP算法的next数组元素值

如果了解KMP算法的话这部分就不会有什么问题,不过由于本文希望可以帮助大家速成,所以这里直接讲一下如何直接求next数组和nextval数组的元素值。

step1:求解原字符串的前缀后缀最长公共元素长度

​ 首先说啥是前缀后缀公共元素:对于字符串P=p0 p1 …pj-1 pj ,如果存在(p0 p1 …pk-1 pk) = (pj- k pj-k+1…pj-1 pj),那么就说明P中有最大长度为k+1的相同前缀后缀。

​ 什么意思呢?举个例子,比如字符串"abcab",要找的是它的前缀和它的后缀相同的部分,也就是“ab”。

​ 然后对于整个字符串,我们可以求出来它的各个子串的前缀后缀的公共元素的最大长度,并且列出如下的表:

模式串abcab
最大前缀后缀公共长度00012

​ 比如对于表中第3列(c)的对应值为0,意思是说对于字符串"abc"来说,因为a!=c,所以前缀后缀公共长度为0;对于表中第4列(a)的对应值为1,意思是说对于字符串"abca"来说,因为a==a,但是 ab!=ca,所以前缀后缀公共长度为1。

step2:右移得到next数组

​ 得到刚刚的表之后,将表中最末一位舍弃,然后整体右移一位,在最左侧补上-1即可得到next数组!如下:

模式串abcab
next数组编号next[0]next[0]next[0]next[0]next[0]
next数组值-10001

step3:逐一检查更新得到nextval数组

​ 得到next数组后,从左至右进行检查,设next[j]=k,若k>=0且p[j]==p[k],则令nextval[j]=next[k],否则nextval[j]=next[j]。

​ 举个例子,对于字符串"abaabcac",结果如下:

模式串abaabcac
next-10011201
nextval-10-1102-11

​ 注意nextval数组相对于next数组改动的值。

例1:写出"abcabaa"的子串的next和nextval数组元素值

利用上述方法可以很轻易得到:

下标0123456
next-1000121
nextval-100-1020

5.多维数组/半三角矩阵元素下标与顺序存储下标互算

存储下标的互算这一部分说白了就是让你数数的,一般分为按"行序为主序"和按"列序为主序",意思就是把矩阵一行一行数还是一列一列的数,问你某个元素是第几个数到的,然后再乘以每个元素的存储长度,就可以算出存储地址了~

例1:若某高级语言30行x20列二维数组(设数组元素下标从0开始)元素按行序为主序存储,每个元素存储长度为8字节。若该数组首地址为0,则存储地址为600(该地址为十进制数)的元素的行下标= ,列下标= 。

二维数组这种是最简单的,给出了存储地址600,可以算出来是第 ⌈ 600 + 1 8 ⌉ = 76 \left\lceil \frac{600+1}{8} \right\rceil = 76 8600+1=76个元素 (注意这里要加1是因为首地址为0),然后每行有20个元素,可以得出这个元素在第4行,然后余下16也就是第16个元素,因此得到行下标为3,列下标为15。

例2:某10阶对称方阵用一维数组按“行序为主序”顺序存储其下半三角元素(含主对角线元素),若所有下标从0开始,则行、列下标分别为6、7的矩阵元素在一维数组中的存储下标为?

第二种类型就是对称矩阵的存储,一般是存储包含对角线的上半三角或下半三角矩阵,这种类型的计算其实也很简单,只需要在草稿纸上简单画个草图,即可看出要求的元素前面有多少元素,再进行相应计算即可。

对于这道题要注意一个陷阱,因为下半三角矩阵中始终有 i ≥ j i\geq j ij的,所以他这里问 a 67 a_{67} a67的位置需要转换成 a 76 a_{76} a76的位置进行计算,可以得到存储下标为34 (时刻注意数组下标从0开始的问题)

6.广义表的表示;求表头和表尾

广义表也称为列表,是一种递归定义的线性表,记为 L S = ( a 0 , a 1 , a 2 , . . . , a n − 1 ) LS=(a_0,a_1,a_2,...,a_{n-1}) LS=(a0,a1,a2,...,an1),其中每个元素 a i a_i ai可以是单个数据原子或者一个广义表。

表头(Head): 非空广义表的第一个元素 a 0 a_0 a0

表尾(Tail): 非空广义表除表头外其余元素组成的广义表 ( a 1 , a 2 , . . . , a n − 1 ) (a_1,a_2,...,a_{n-1}) (a1,a2,...,an1)

例如:

广义表及图形表示举例图

了解了定义就很好解决关于广义表的问题:

例1:广义表 ( a , b , c , d ) (a,b,c,d) (a,b,c,d)的表尾?

根据定义显然为: ( b , c , d ) (b,c,d) (b,c,d)

例2:设有广义表 L = ( ( ) , ( ) ) L=((),()) L=((),()),则GetHead(L)= ;GetTail(L)= ?

根据定义,GetHead(L)=(),GetTail(L)=(())。这里要注意,因为表尾是一个广义表,所以这里GetTail(L)不是(),而是(())。

7.二叉树的前/中/后序以及层次遍历;各种计算问题

二叉树类的问题主要考察二叉树的基本性质,具体通过下面几个例题逐一体会好了~

例1:一棵深度为k且具有 2 k − 1 2^{k}-1 2k1个结点的二叉树称为 。这类二叉树的特点是:二叉树的每一层结点的个数都为上一层的_?

一棵深度为k且具有2k-1个结点的二叉树是满二叉树,也就是叶子节点全都填满的情况,每一层的结点个数都是上一层的二倍。

例2:已知完全二叉树的第7层(第7层为最后一层)有10个叶结点,则整棵二叉树的结点数为 ?

完全二叉树的定义是除了最后一层外为满二叉树,且最后一层的叶子结点都连续集中在最左边。

因此可以首先计算前6层的结点数为 2 6 − 1 = 63 2^6-1=63 261=63,再加上第7层的10个结点,即为73个结点。

例3:若完全二叉树(结点编号从1开始)中某结点编号为11,则该结点的右儿子编号为 ,双亲结点编号为 。

在完全二叉树中计算结点编号的方式为:若结点编号为k,则他的左右儿子结点编号为2k和2k+1。

因此11号结点右儿子编号为23,双亲结点编号为5。

例4:一棵二叉树中,度为2的结点数为15,则叶子结点数为 。

这里有个新的概念:度。在树中,结点的度是指结点拥有子树的数目。

这里需要用到一个二叉树的性质:**对任意一棵二叉树,若其叶子结点数为 n 0 n_0 n0,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1。**具体证明过程为了大家宝贵的复习时间不在这里展示,感兴趣可以查阅其他资料QAQ。

因此叶子节点数为16。

例5:二叉树如下图所示,写出先序、中序、后序遍历结点访问次序

例7-5题目

二叉树的层次遍历次序分为三种:

前序遍历(前根遍历):——>左——>右

中序遍历(中根遍历):左——>——>右

后序遍历(后根遍历):左——>右——>

计算访问次序的时候只需要像程序一样对二叉树进行递归遍历,即可得到对应的访问次序。

先序:ABDFGCE

中序:BFDGACE

后序:FGDBECA

8.中序线索化与中序穿线二叉树

穿线二叉树的意思就是:二叉树中会有一些结点的左右指针为NULL,将这些指针指向这个结点的遍历次序的前驱和后继。说白了还是找遍历次序的事。

例1:二叉树的中序遍历为DGBEAHFIC,画出中序线索化后的穿线二叉树。

根据定义得到结果如下:

中序线索化结果图

例2:假设二叉树已完成中序线索化,以下算法函数利用线索指针实现非递归的中序遍历,请填空使算法完整。

已知中序穿线二叉树结点及结点指针类型定义如下:

typedef struct bt_node{
    char data;
    int ltag,rtag;
    struct bt_node *lchild, *rchild;
} BTNode,*BT;

若ltag==0为真,则lchild指向中序遍历次序的前驱结点,否则lchild指向左儿子;

若rtag==0为真,则rchild指向中序遍历次序的后继结点,否则rchild指向右儿子。

LPtr first(LPtr root){
    if(!root)
        return NULL; 
 	LPtr p=root;
    while(p->ltag!=0)
        p=__(1)___;
   	return p;
}
void midtravel(LPtr bt){
    LPtr p=first(bt);
  	while(___(2)___){
        visit(p);  /* 访问p所指结点 */
  		if(____(3)____)
        	p=p->rchild;      /* 线索后继 */
    	else
        	p=first(___(4)____);    /* 非线索后继 */
	}  
}

思路:midtravel函数即非递归中序遍历的函数,首先传入根节点,然后通过first函数找到该根节点下需要最先访问的最左叶子结点,然后开始不断寻找后继,若rtag==0,说明没有右儿子,直接访问其后继结点,否则进入其右子树,重复该过程。

(1)p->lchild ; first函数的含义是找到当前子树的最左叶子结点,因此需要不断找左儿子。

(2)p ; p指针表示访问次序,只有当p==NULL时才表示访问结束

(3)!p->rtag ;若!p->rtag则表示当前结点没有右儿子,直接访问指向的线索后继结点。

(4)p->rchild ;进入else部分说明当前结点有右儿子,下一步需要访问其右子树的最左叶子结点。

9.已知前/中序遍历次序构造二叉树

思路:有了前序遍历,首先我们可以知道根节点的值,由此创建根节点。然后在中序遍历中找到根的值所在的下标,切出左右子树的前序和中序;再在左右子树中不断进行这个步骤,就可以得到完整的二叉树。

例1:设先序遍历某二叉树得到的结点访问序列为ABCD,中序遍历该二叉树的序列为BADC,则后序遍历该二叉树的序列为?

首先根据先序遍历可以知道二叉树的根节点为A,然后在中序遍历中以A为分界,得到左子树B和右子树DC,对于右子树根据先序遍历的顺序可以知道子树的根节点为C,那么D则是C的左儿子。因此可以写出二叉树的后序遍历为:BDCA。

示意图如下:

已知前/中序遍历次序构造二叉树示意图

例2:已知10个关键字组成的某二叉排序树的先序遍历关关键字次序为30, 9, 5, 18, 12, 24, 50, 39, 42, 60。画出该二叉排序树。

这个题看似只给出了先序遍历,但是还有一个关键字在于二叉排序树,相当与给出了二叉树的中序遍历为将这些元素由小到大排序。

然后再根据上面的方法即可得到结果:

例9-2结果图

10.树与森林的转换-孩子兄弟链表

森林可用孩子兄弟链表实现存储,孩子兄弟链表其实也是二叉树,只不过其左指针指向子树根结点的第1个孩子,右指针指向根结点的下一个兄弟。

例1:由三棵树组成的森林由广义表(A(B, C, D), E(F), G(H, I(J)))给出,请画出该森林;若用孩子兄弟链表将该森林表示为二叉树形式,画出该二叉树。

根据广义表和孩子兄弟链表的定义即可得到结果如下:

孩子兄弟链表结果图

左侧是森林,右侧是对应的孩子兄弟链表形式。

11.哈夫曼二叉树与哈夫曼编码, 平均码长的计算

(1)哈夫曼二叉树

给定n个元素及其对应的概率后,其哈夫曼二叉树的构造方法如下:

​ 构建n棵只含根节点的二叉树,其中第i棵二叉树的根节点权值为第i个元素的概率。

​ 然后每次选择其中根节点权值最小的两棵树 T i T_i Ti T j T_j Tj 作为左右子树,并增加一个权值为 T i T_i Ti T j T_j Tj 权值和的根节点,作为新的二叉树替换原有的两棵二叉树,不断重复这个过程直到只剩一棵完整的哈夫曼二叉树。

(2)哈夫曼编码

​ 哈夫曼编码是将通信过程中用到的字符构造成哈夫曼二叉树,然后将左分支和右分支标记上0和1,这样只需要给定一个01串,便可以在对哈夫曼二叉树进行遍历得到对应的原本信息(如果是0就往下一层走,如果是1就得到对应的字符)

(3)平均码长计算

​ 平均码长是:各个叶子结点的权值乘以其对应的深度的和

例1:六个符号A,B,C,D,E,F出现的概率分别是1/24, 9/24, 3/24, 5/24, 2/24, 4/24,建立一棵哈夫曼(Huffman)二叉树,给出各个符号对应的哈夫曼编码,并计算这六个字符的平均编码长度。

​ 首先根据(1)中的方式构造出哈夫曼二叉树如下:

在这里插入图片描述

同时根据哈夫曼编码方式得到各个字符的对应哈夫曼编码。

平均长度:

W P L = ( 1 24 + 2 24 ) ∗ 4 + ( 3 24 + 4 24 + 5 24 ) ∗ 3 + 9 24 ∗ 1 = 19 8 WPL=(\frac{1}{24}+\frac{2}{24})*4+(\frac{3}{24}+\frac{4}{24}+\frac{5}{24})*3+\frac{9}{24}*1=\frac{19}{8} WPL=(241+242)4+(243+244+245)3+2491=819

12.图的存储结构(画图)与DFS、BFS遍历

图一般有两种存储结构,一种比较简单的是邻接矩阵,也就是开一个二维数组,将每条从i到j的边储存在 a [ i ] [ j ] a[i][j] a[i][j]中。另外一种是邻接表。

邻接表由一个顶点表和一个边表构成,结点表中存储图中的各个顶点,其中表中每个结点都包括对应的顶点和由这个顶点出发的第一条边指针指向对应的边表。而边表存储各个顶点出发的所有边。

因为邻接表的性质注定了它存储的边是有向的,因此对于无向图则需要将每条边看作两条反向边即可。

例1:用邻接表存储n个顶点e条边的无向图,其头(顶点)结点和表(边)结点的总数是?

因为是无向图,因此对每条边都要建立两条反向边,所以结点总数为 n+2e。

例2:某DAG的邻接矩阵存储结构如下图所示,基于该存储结构:(1) 画出该DAG;(2) 写出从1号顶点出发的深度优先遍历顶点访问次序;(3) 写出从1号顶点出发的宽度优先遍历顶点访问次序;

例12-1题图
根据定义可以得到如下图:
例12-1题图
并且得到dfs序:1,4,5,2,3 ; 和 bfs序:1,4,3,2,5

13.用普里姆或克鲁斯卡尔算法求最小生成树

这一部分的两个算法都很好理解,而且只需要掌握生成的次序,只需要了解这两个算法的流程即可。建议百度了解一下~

14.AOV网络与拓扑排序

AOV网络其实就是一张有向无环图。

拓扑排序则是一种检验AOV是否有环的一种方式,是对有向图的顶点排成一个线性序列

拓扑排序的步骤:

step1: 首先用邻接表存储有向无环图,其中对于每个顶点统计其入度(指向它的边数),额外开一个栈存储所有入度为0的顶点。

step2: 每次将栈顶弹出,并删除这个顶点的所有出边,同时让出边对应的顶点入度减一,若此时对应的顶点入度变为0了,则将这个顶点入栈。

step3: 反复step2这个过程,直到没有入度为0的顶点。

step4: 若此时依然还有顶点存在,说明图中有环。

例1:某AOV网络如下图所示,若各顶点的邻接点次序为字母升序,写出采用队列和堆栈存储入度为零顶点时,拓扑排序算法得到的排序结果。

拓扑排序题图

在这个题目中,涉及到了使用队列存储入度为0的顶点,实际上不难发现效果和用栈的区别不大,只不过是排序顺序变化而已。

按照上述的拓扑排序步骤即可得到答案:

队列:ABCEDF

堆栈:ACEBDF

15.AOE网络(求顶点的最早开始/最晚开始时间,求关键活动及关键路径)

AOE网络:带权的有向无环图,其中仅有一个入度为0的顶点为源点,仅有一个出度为0的点为汇点。

顶点的最早开始/最晚开始时间: 指的是如果把每个顶点看作一个项目的话,完成这个项目之前不是需要完成一些前置的项目么,可以得到最早什么时候可以开始这个项目,以及最晚什么时候就必须要开始这个项目了。

关键活动: 指的是完成整个工程时不可以延迟的项目,一旦延迟整个工程就会延迟。而实际上,顶点的最早开始和最晚开始时间相同的顶点就是关键活动

关键路径: AOE网中,从源点到汇点的所有路径中,边权之和最大的路径为关键路径。对应的边权和也就是完成工程所需的最短时间。而实际上,由关键活动构成的即为关键路径。

求解关键路径的方法如下:

step1:利用拓扑排序求出所有顶点的最早开始时间。

step2:利用逆拓扑排序求出所有顶点的最晚开始时间。

step3:得到关键活动和关键路径。

例1:已知带权网络如下,边上的权重表示活动持续的时间,求解以下问题。(1) 写出各顶点的最早开始与最晚开始时间。(2) 写出关键路径(用顶点拓扑有序序列表示)。

关键路径题图

首先进行拓扑排序,对于每一个顶点需要注意,最早开始时间应取所有抵达这个点的情况中的最晚时间。也就是在拓扑排序每次对顶点k的入度进行减一操作的时候,比较当前到达k节点所用的时间是否比已经求得的到达k节点时间要长,如果更长的话就更新这个时间,最终就可以得到所有顶点的最早开始时间。

然后将拓扑序列进行逆向出栈操作,同样的对每个顶点来说,最晚开始时间应取所有反向抵达这个点的情况中的最早时间。思路与最早开始时间求解类似也是逐次更新。

这样就可以得到最早开始时间/最晚开始时间和关键路径为:ACBEF

关键路径结果图

16.最短路径(会填写两种算法的动态过程表格)

这一部分的两个算法(dijkstra和floyd)都很好理解,而且只需要掌握生成的次序,只需要了解这两个算法的流程即可。建议百度了解一下~

17.二叉排序树插入与删除结点;平衡二叉排序树插入结点;等概率成功查找时的ASL计算

(1)二叉排序树

二叉排序树 (也叫二叉判定树) 就是左子树上所有结点的值小于根结点的值,右子树上所有结点的值大于等于根节点的值的二叉树。

其重要的性质为:中序遍历二叉排序树,所得结点访问次序为结点值的递增序列。

(2)成功查找的ASL计算
A S L 成 功 = ∑ i = 0 n − 1 p i c i ASL_{成功}=\sum_{i=0}^{n-1}p_ic_i ASL=i=0n1pici
其中, p i p_i pi表示查找 a i a_i ai的概率, c i c_i ci表示成功找到 a i a_i ai的关键字比较次数。

题目一般会告诉我们每个元素等概率查找,这种情况下二叉排序树的ASL的计算可以化简为:
A S L 成 功 = 1 n ∑ i = 0 n − 1 d i ASL_{成功}=\frac{1}{n}\sum_{i=0}^{n-1}d_i ASL=n1i=0n1di
其中, d i d_i di表示第i个元素在二叉排序树中的深度。

例1:9个关键字输入次序为20, 8, 30, 15, 6, 40, 10, 25, 2。按此输入次序构造二叉排序树,写出其中序遍历次序;若每个元素等概率查找,试计算成功查找时的ASL。

首先根据二叉排序树的性质可以得到其中序遍历就是把这堆数从小到大排列,其次这个给定的输入顺序也就是二叉排序树的先序遍历,那么就可以根据先序遍历和中序遍历得到二叉排序树如下。
在这里插入图片描述

中序遍历:2,6,8,10,15,20,25,30,40

A S L 成 功 = 1 9 ( 1 ∗ 1 + 2 ∗ 2 + 3 ∗ 4 + 4 ∗ 2 ) = 25 9 ASL_{成功}=\frac{1}{9}(1*1+2*2+3*4+4*2)=\frac{25}{9} ASL=91(11+22+34+42)=925

18.分块顺序查找的最佳分块方法

这一部分只需要记住最佳分块长度为 n \sqrt{n} n 即可。

例1:900个元素的索引顺序表,分块内部和索引表均用顺序查找,则最佳分块长度为?

答案显然为 900 = 30 \sqrt{900}=30 900 =30

19.B-和B+树插入/删除结点的结果画图

貌似不考?懒得写了这个要写好多…不会的话建议放弃这个吧QAQ

20.哈希表(装填因子,同义词,构造原则,线性及二次探测、链表解决冲突的方法及对应的元素等概率成功查找时的ASL的计算)

(1)哈希表的定义

​ 由关键字K检索数据元素(对象)时,数据元素(对象)的存储地址可以由函数 H ( K ) H(K) H(K)直接计算得到,这样可以大大加快查找速度。若查找表中的所有数据元素均存储到函数 H ( ⋅ ) H(·) H()指定的地址,称为哈希表。

H ( K ) H(K) H(K)一般称为哈希函数或散列函数,因此,哈希表也常称为散列表

(2)装填因子
α = 表 中 数 据 元 素 数 / 哈 希 表 的 长 度 \alpha={表中数据元素数}/{哈希表的长度} α/
α \alpha α越大说明哈希表的存储空间利用率越高,但 α \alpha α越 大往往也意味着冲突概率越大,减少冲突的难度更高。

(3)同义词

​ 当 K i ! = K j K_i!=K_j Ki!=Kj时,若 H ( K i ) = H ( K j ) H(K_i)=H(K_j) H(Ki)=H(Kj),也就是发生了地址冲突时,称 K i K_i Ki K j K_j Kj为同义词。

(4)构造原则

​ 计算简单,地址分布均匀(发生冲突可能性小)。

(5)冲突处理-开放定址法

​ 当发生冲突时,就需要找"下一个位置"来插入,其中下一个位置的确定方法为:
H i = ( H ( k ) + d i ) m o d   m ( i = 1 , 2 , 3 , . . . 称 为 探 测 次 数 ) H_i=(H(k)+d_i)mod\ m\quad(i=1,2,3,...称为探测次数) Hi=(H(k)+di)mod m(i=1,2,3,...)
​ 其中 d i d_i di为求"下一个位置"的增量

a.线性探测再散列
d i = 1 , 2 , . . . , m − 1 d_i=1,2,...,m-1 di=1,2,...,m1
​ 若当 d i = m − 1 d_i=m-1 di=m1时仍未查到空闲单元,则说明哈希表已满,这时还要查找另外的溢出表。

b.二次探测再散列
d i = 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , ± k 2 ( k ≤ m / 2 ) d_i=1^2,-1^2,2^2,-2^2,...,\pm k^2 \quad (k\leq m/2) di=12,12,22,22,...,±k2(km/2)
(6)冲突处理-链地址法

​ 将所有关键字为同义词的数据元素存储在同一链表中。设哈希地址范围为0~m-1,设置一个指针向量: Chain chainhash[m];

​ 每个分量的初始状态为NULL指针,凡哈希地址为i的的记录则插入到chainhash[i]的链表中。

(7)成功查找时ASL的计算

对于已知哈希序列的情况下:
A S L 成 功 = ∑ i = 0 n − 1 p i c i ASL_{成功}=\sum_{i=0}^{n-1}p_ic_i ASL=i=0n1pici
其中, p i p_i pi表示查找 a i a_i ai的概率, c i c_i ci表示成功找到 a i a_i ai的关键字比较次数。

​ 如果只知道装填因子 α \alpha α时,可以利用下面的公式粗略计算时间复杂度:

​ a.线性探测再散列
A S L 成 功 ≈ 1 2 ( 1 + 1 1 − α ) ASL_{成功}\approx\frac{1}{2}(1+\frac{1}{1-\alpha}) ASL21(1+1α1)
​ b.二次探测
A S L 成 功 ≈ − 1 α l n ( 1 − α ) ASL_{成功}\approx-\frac{1}{\alpha}ln(1-\alpha) ASLα1ln(1α)
​ c.链地址
A S L 成 功 ≈ 1 + α 2 ASL_{成功}\approx1+\frac{\alpha}{2} ASL1+2α

例1:12个关键字为{19, 14, 23, 01, 68, 20, 84, 27, 55, 11, 10, 79}。设H(K)=K mod 13,地址空间范围0~15,用二次探测再散列解决冲突。画出哈希表;若各元素等概率查找,求成功查找时的平均查找长度。

首先可以写出二次探测的构造方式: H i = ( H ( K ) ± i 2 ) m o d   16 ( i < 7 ) H_i=(H(K)\pm i^2)mod\ 16(i<7) Hi=(H(K)±i2)mod 16(i<7)

然后根据计算可以得出哈希表如下:

0123456789101112131415
271401685584192010231179
次数312123113115

其中次数的计算方式如下:
按照序列的次序哈希,如果遇到地址冲突就二次探测,然后看二次探测的位置有没有冲突,如果没冲突就ok了,否则就再二次探测,直到找到空位置。
然后你计算了几次hash函数,次数就是几。这里的次数主要用来计算ASL。
另外注意,二次探测中 d i d_i di的取值为:1,-1,4,-4,9,-9…

成功查找时的平均查找长度为:
A S L 成 功 = ( 1 ∗ 6 + 2 ∗ 2 + 3 ∗ 3 + 5 ∗ 1 ) 12 = 2 ​ ASL_{成功}=\frac{(1*6+2*2+3*3+5*1)}{12}=2​ ASL=12(16+22+33+51)=2

例2:使用散列函数h(x)=x mod 11(哈希表地址空间0…10),把10个关键字9, 25, 15, 20, 7, 1, 36, 48, 12, 31存入哈希表中。(1) 若使用链地址法解决冲突,试画出构造好的哈希表。(2) 计算各关键字等概率查找条件下查找成功时的平均查找长度

​ 根据定义得到哈希表如下:
在这里插入图片描述

查找成功时的平均查找长度:
A S L 成 功 = 1 10 ( 1 ∗ 5 + 2 ∗ 4 + 3 ∗ 1 ) = 8 5 ASL_{成功}=\frac{1}{10}(1*5+2*4+3*1)=\frac{8}{5} ASL=101(15+24+31)=58

21.希尔排序给定增量序列,写出各趟增量排序结果

​ 希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

​ 具体步骤如下图所示(图源@chengxiao)

希尔排序

例1:设递减增量序列为5, 3, 1, 对以下10元序列进行由小到大希尔排序,写出各趟增量排序结果。49 38 65 97 76 13 27 49 55 04。

例21-1结果

22.快速排序各趟排序的结果,支点元素的最终位置

​ 快速排序(摘自《啊哈算法》):

​ 假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数(不要被这个名词吓到了,就是一个用来参照的数,待会你就知道它用来做啥的了)。为了方便,就让最左边的6作为基准数吧。接下来,需要将这个序列中所有比基准数大的数放在6的右边,比基准数小的数放在6的左边,类似下面这种排列:

3 1 2 5 4 6 9 7 10 8

​ 在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。想一想,你有办法可以做到这点吗?

​ 方法其实很简单:分别从初始序列“6 1 2 7 9 3 4 5 10 8”两端开始“探测”。先从找一个小于6的数,再从找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即=10),指向数字。

​ 首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要(请自己想一想为什么)。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。哨兵j停在了数字5面前,哨兵i停在了数字7面前。

​ 现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:

6 1 2 5 9 3 4 7 10 8

​ 到此,一次交换结束。接下来开始哨兵j继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。此时再次进行交换,交换之后的序列如下:

6 1 2 5 4 3 9 7 10 8

​ 第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下:

3 1 2 5 4 6 9 7 10 8

​ 到此此轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i的使命就是要找大于基准数的数,直到i和j碰头为止。

​ 到现在为止,基准数6已经归位,此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。

例1:8个待排序关键字为49, 38, 65, 97, 76, 13, 27, 49, 排序下标范围0~7,以0号元素为支点进行非递减的快速排序,求一趟快排的结果以及支点元素的下标。

快排结果1
快排结果2

23.堆排序初始建堆的结果,输出堆顶元素并进行调整后的结果

堆其实是一棵完全二叉树,每个结点的值都大于等于其左右儿子的值(大根堆)或都小于等于其左右儿子的值(小根堆)。

(1)初始建堆的过程

step1: 先将原序列建立成完全二叉树。

step2: 每次取编号最大的非叶子结点,将这个结点不断下放(以大根堆为例,如果该结点的左右儿子有比他大的,那么将这个结点和他左右儿子中更大的那个交换),直到无法下放(处于叶子结点或左右儿子均比他小)。

例1:将序列{ 49, 38, 65, 97, 76, 13, 27, 49}构建为大根堆。

堆排序1
堆排序2

24.基数排序各趟分配和收集的结果

​ 基数排序的LSD排序准则:各关键字位按由低位到高位的次序排序。这需要 通过“分配”和“收集”两种操作来实现。对于d元关键字组,需要进行d趟分配和收集操作。

​ 这部分其实和小学生数数差不多,真是有手就行了。

例1:对以下8个3位4进制数采用基数排序进行由小到大排序,写出对最低位、中间位以及最高位分别进行分配和收集的结果。101, 002, 332, 220, 123, 301, 021, 231

​ 具体步骤看下面的过程体会一下应该就可以get了
基数排序1

25.归并排序各趟结果

​ 归并排序:假设初始序列含有n个记录,则可以看成n个有序的子序列,每个子序列的长度为1,然后进行两两归并操作,得到 ⌈ n 2 ⌉ \left\lceil \frac{n}{2}\right\rceil 2n个长度为2或1的有序子序列,再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止。

​ 同样也是很好理解的排序方式,只需要不断合并就可以了。

例1:写出对如下序列归并排序的各趟结果:{20}{-8}{9}{26}{7}{-9}{-26}{34}{15}{-1}

​ 每次将相邻的两个子序列合并然后在子序列内部排序即可,过程如下:
归并排序


更新日志
2020.1.9 更新了对于哈希二次探测的次数计算方法解释

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Keller Wang

请Keller喝杯咖啡!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值