华科考研834 2019(二)
华中科技大学计算机考研试卷总结 2019(二) 834
-
邻接表结合了顺序存储和链式存储,有效减少不必要的浪费。
有向图中有\(n\)个表头结点和\(m\) 个表结点,则表示该图有\(n\)个结点,有\(m\)条边。存储结点的顺序结构又叫顶点表,存储边的链式结构又叫边表。边表的头指针和结点信息,采用顺序结构存储到一个顺序表中。邻接表中存在两种表结点,顶点表结点 和边表结点
邻接表的特点是:每一个顶点引出与该顶点相关的所有边的集合(边的集合用链式存储链接);而顶点之间用顺序表链接
填空题
for(i=1;i<n;i++){
s=0;
t=1;
for(j=1;j<i;j++){
t = t*j;
s= s+t;
}
}
时间复杂度:\(O(n^2)\) 因为有两套for循环,第一套for循环的起始终止范围是\(1…n\) ,第二套for循环的起始终止范围是\(1…i\) ,但是 \(i\) 是受n约束,则\(O(n)*O(n)=O(n^2)\) ,若是\(i\) 不受n约束,答案就是\(O(n)*O(i)=O(ni)\) 一般看时间复杂度的题型只需要考虑循环,比如\(while \ for\) ,因为采用O标记考虑时间复杂度本来就是理论分析,只有在循环中才有可能n趋向于无穷大。
-
普通二叉树中,叶子节点\(n_0\)和度为2的\(n_2\)的节点一定有\(\ n_0=n_2+1\)
二叉链表与二叉线索树不同,二叉链表是最基本的二叉树链表存储结构;
作为一颗二叉树,一共有n个结点,由于每一个结点(除首结点)只有一个直接前驱,因此会有n-1 条边;这棵二叉树存储到二叉链表中的时候,每一个节点都会有两个孩子指针,因此总的链域值是\(2*n\) ** ,除首节点外每个结点都有一个前驱,因此用掉了n-1** 个指针,剩下的指针\(2n-(n-1)=n+1\)都是空指针
-
循环队列:逻辑上将队列视为一个环。其实对于队列,无论是线性队列,还是循环队列,队列收尾的两个指针\(front \ rear\) 都是朝着相同的方向移动
顺序队列:
对于顺序队列,初始时队首指针front和队尾指针rear都是指向\(Q.rear=Q.front=0\) ,
进队操作:先将元素填入到队列末尾,再对指针加一(顺序存储的队列,要是链式队列则使用next指针进行进出队列操作,另外这里的操作和栈的进栈操作执行顺序不一样,务必牢记!!!,栈是先移动指针,再填入元素)
出队操作:先取对头元素的值,再对指针进行加一 进出队列操作都在++之下,其实队列一直朝着队列序号增大的方向一直"移动" ,这样也就造成了顺序队列的假溢出现象,序号较小的数组元素可能已经被释放空闲
循环队列:
为了解决假溢出现象,引进了循环队列
初始时,仍然有\(Q.front=Q.rear=0\)
入队操作 \(Q.rear = (Q.rear+1)/Maxsize\)
出队操作 \(Q.front = (Q.front+1)/Maxsize\)
由于\(Q.front=Q.rear\) (加入添加元素速度大于删除元素速度,会有套圈的存在,此时会有会有$Q.rear=Q.front $ )
不能判断队空还是队满,处理循环队列队空、队满方法有更改数据类型使得能够表示数据元素个数、更改数据类型添加tag位,最常用的还是人为逻辑上牺牲一个数据单元,使得循环队列达不到"物理上真正的满队列",这种情况下,空队列判断标识还是\(Q.rear=Q.front\) ,满队列判断标识变成了\(Q.front=(Q.rear+1)/Maxsize\)
-
假设一个二维数组\(A[5][6]\) ,这个表达形式意味着这个数组是由5行6列构成,行数范围\(0,1,2,3,4\),列数范围\(0,1,2,3,4,5,\) 每一个元素占4个数据单元,可以将矩阵画出来。
\(起始数据块起点地址+数据块个数*每一个数据块数据单元个数=目标数块起点地址\)
-
广义表的元素可以是广义表,也可以是单个元素,还可以为空。 表头可以为表或者是单个元素值,表尾就是除去表头以外的子表,不可以是单个元素值,而是表,即使是空表,也必须是表,剩下单个元素也必须以表的形式形成子表,这是尾表的定义所形成
广义表 {(a,b),(c,d),(e),(f,()),(g,h)}的头表是(a,b) //表头是广义表的第一个元素,这里的元素可以直接是原子,也可以是广义表 尾表是{(c,d),(e),(f,()),(g,h)} //表尾是除去第一个元素之外的剩余元素所组成的子广义表 //表头和表尾的定义区别很大! 广义表的长度:广义表中最上层中,元素的个数 广义表的深度:表嵌套的最大层数
-
带权路径长度指的是:从树中的根节点到任意目标结点的路径长度(边数)与该结点(只有一个)的乘积,叫做当前节点的带权路径长度(带权路径长度只需要考虑当前待研究结点的权和从根节点到当前结点的路径长度[边数])。而树的带权路径长度就是所有叶节点的带权路径长度之和,记为\(WPL(weighted\ path\ length\ of\ tree )\) ,计算WPL仍然是只需要考虑叶节点的权值 和叶节点的路径长度
带权路径WPL最小树的叫做最优二叉树 ,也叫做哈弗曼树
构造方法:
- 先必须有\(n\)个带权值的节点,这\(n\)个带权值的结点就是最终最优二叉树的叶结点
- 选出其中权值最小的两个结点,用着两个结点向上捧出一个新的结点,新结点的权值是原来两个结点之和
- 不断重复,一直到最后形成一颗大树
特点:
- 每个初始结点最终都会变成最优二叉树的叶子节点,而且权值越小的叶子节点距离根节点越远
- n个结点的Haffman树的构造过程会形成\(n-1\) 个内部节点,从而总共有\(2n-1\)个节点
- Haffman 树的构造过程,每次会选两颗子树作为孩子节点捧成一颗新的子树,因此haffman 树不存在度为1的结点
哈夫曼编码:
普通编码往往是等长编码,等长编码往往对编码空间浪费严重,效率不高;于是人们有提出了不等长编码,不同字符的编码长度不同,而且使用频率高的字符支持使用短码编制,对使用频率低的字符支持使用长码编制,这种编码减少的数据位以及数据空间的浪费,做到了数据的压缩。哈夫曼编码就是一种典型的数据压缩算法,也是最典型的不等长编码。
哈弗曼编码就是哈弗曼树的一个具体应用:
叶结点表示字符,并且叶结点也附上了对应的频度表示权值,习惯上最优二叉树左孩子分支为0,右孩子分支是1(并不是规定),每一个字符的二进制编码就是从根节点到叶子节点的路径长度上的序列的连接。在哈弗曼编码中带权路径长度$WPL $又叫二进制编码长度
序列是(67,40,78,15,40,99,12)的序列,采用插入排序,第四趟后的结果是:
第一趟后(40,67, 78,15,40,99,12) 第二趟后(40,67,78, 15,40,99,12) 第三趟后(15,40,67,78, 40,99,12) 第四趟后(15,40,40,67,78, 99,12)
-
有向无环图常常表示事件(结点)之间的驱动依赖关系。有向无环图的拓扑排序,对有向图中的边而言\(<u,v>\) ,总是有\(u\)出现之后才会有\(v\)出现。拓扑排序的目的就是使得杂乱无章的有向图结点能够按事件发生顺序一字排开。
实现方式:V :入度表、E:DFS,时间复杂度就是\(O(V+E)\)
入度表:1.不断找开始结点,即入度为0的结点,找到后放到队列(根据输出情况或者可以用栈)
2.找到后,删除,再继续找,不断重复1,即完成拓扑排序。
上图就是一个从DAG图到拓扑排序的例子。这种排序方式的优点是可以根据事件发生的先后顺序进行排列。
- 对一组初始记录进行快排(70,71,69,21,91,14,3),以70为基准元素的一趟快排结果
既然是快排就要想到两个指针L,R,分别负责把大数运送到右边,把小数运送到左边。
-
判断
-
评价算法优劣不止有算法的时间复杂度
-
循环队列解决的是顺序溢出的"假溢出"问题,但是仍然会发生空间溢出即"真溢出"。这个没办法解决,凡是顺序存储结构必然会发生空间不足,进而引起溢出,循环队列也不例外。使用指针队列能够解决空间溢出
-
图的遍历算法可以判断图的连通性,但是这也不是必然的,得分情况讨论:
有向图,无向图,连通图,不连通图,连通分量,强连通分量都有着不同的情况
对于无向连通图,可以使用一次图遍历算法(无论是DFS,还是BFS)
对于无向非连通图,无法仅用一次就做到遍历整个图
对于强连通有向图,可以使用一次图遍历算法遍历整个图中节点
对于非强连通有向图,做不到仅用一次遍历整个图
-
静态链表和动态链表:动态链表即是我们传统意义上认识的链表,灵活程度非常高的一种数据结构;静态链表有点类似数组(顺序表)和链表的一种结合。
静态链表在设立之初空间大小就已经确定,并且存储结构是一块连续的顺序结构,这一点和数组非常像,但是在删除和插入元素的时候又不像数组那样移动大量元素,而是通过修改指针(静态链表中称作指针)插入和删除元素。 基本等同于给没有指针的顺序表添加上指针的功能
-
二叉排序树。BST(binary sort tree)
二叉排序树满足以下特征:要满足二叉排序树,这颗二叉树以及所有的子树都要满足\(左子树<中间节点<右子树\) 的模式
左子树上的所有节点均满足小于根节点;
右子树上的所有节点均满足大于根节点
所有的二叉排序树使用中序遍历这颗二叉树一定得到的是一个递增序列。\(即中序遍历递增\iff二叉排序树\)
二叉排序树的插入和查找过程几乎一样,都是从树的根节点开始操作比较。值得注意的是二叉排序树的插入一定是插在了叶子节点上,查找过程中不存在所找元素就会进行插入新元素,这是一个动态树。
二叉排序树的构造:从一颗空树进行构造,不断查找,不断对当前树插入
比如对一颗空树进行关键字序列查找(空树查找关键字,能查找才怪!),查找序列是
计算题
-
使用克鲁斯卡尔算法得到最小生成树
生成最小生成树有两种算法prim算法和克鲁斯卡尔算法
生成树是由连通图得到,包含连通图的所有顶点,使用最少的边,这种情况下生成树并不唯一。若图中的边带有权,得到的一系列生成树,其中这些生成树边上的权值之和是一个常数,将这些生成树放到一个集合中去,选择生成树权值最小的那颗生成树作为最小生成树。
最小生成树其实也是不唯一的(极端一点,假如所有边上的权都相等。所有生成树都是最小生成树);若生成树边上的权值两两互不相等,那么可以唯一确定一个生成树,特殊的图-树(顶点-1=边数 判定树的必要条件)本身就是一个最小生成树
- 虽然最小生成树不唯一,但是最小生成树的权值唯一(实际上还是相同权值和树形捣的鬼,一旦这两个条件满足,最小生成树就是唯一的)
生成最小生成树的算法有prim算法和克鲁斯卡尔算法,都是基于贪心算法所得 (每次添加一条边以及一个顶点)
Prim算法(类似dijkstra算法)——一定是有权图 从顶点扩张生成树
先构建一个空集T, 最开始的时候,从图中随机选一个结点A放到T集合中,然后开始循环,查找与A相邻的所有结点,选其中权值最小的边以及其连接的顶点,加入到T中(每次都会加入一条边和一个顶点);接着选一个距离当前T集合权值最小的边以及顶点,再次加入到T中,不断重复一直到结束。就能形成一个最小生成树
Kruskal算法——按照边权值递增次序扩张生成树
初始时,先保证各节点分别为独立的连通分量,然后按照权值从小到大添加边,添加边不光秉持着权值最小,还要保证添加边之后连通性发生变化,即边两端顶点原来分别属于不同的连通分量,添加边之后,合并成一个连通分量,如此反复,让最开始的\(n\)个连通分量变成最后一个连通分量
-
查找方法分类:
常用的查找方法分为以下几类:
- 线性结构:
- 顺序查找
- 折半查找
- 分块查找
- 树形查找
- 二叉排序树(也叫二叉查找树)
- 二叉平衡树
- B树、B+树
- 散列查找
衡量查找效率的指标有平均查找长度,且是衡量查找算法效率最主要的指标。平均查找长度(ASL,\(average\ search\ length\))指 在一次查找过程中一次比较关键字的次数 。值得注意的是查找成功和查找失败计算平均查找长度方法相同,只是计算过程稍有不同。线性查找、树形查找、散列查找都有ASL。
\[\begin{equation} ASL = \sum_{i=1}^n(P_iC_i) \end{equation} \]其中\(P_i\)是查找某个元素的概率,通常各种考试中都是等概率查找,概率就是\(1/n\),\(C_i\)是查找某个数据元素的路径上所比较的次数(查找成功与查找失败在选取查找路径上的比较次数是有区别的)
-
二分查找
首先需要明确二分查找只适用有序的顺序表 ,因为要做到随机定位查找位置,比如刚开始就要找到中间位置,这使得二分查找必须使用顺序表,不适合链式,有序性非常自然,因为要做到不断比较,减小查找范围。二分查找的思想是:每次选取序列最中间元素与我们待查找元素进行比较,并且不断缩小查找范围,直至查找成功或者失败返回结果。二分查找过程可以用一个平衡二叉树来表示。平衡二叉树表示:若待查找序列中有n个元素,那么二叉平衡树就有n个非叶结点,由二叉树的特性,所以叶子结点就有\(n+1\) 个。每一颗子树的左孩子结点都小于根节点,每一颗子树的右孩子结点大于根节点。
构造判定树遵循不断选择序列中间值原则,如果是奇数,中间值唯一;如果是偶数,中间值要么一致向前,要么一致向后,考试中往往选一致向前,即\(\lfloor (n_{i}+1)/2 \rfloor\) ,记得最后挂上矩形失败结点。比较时从耿洁典向下比较,直至命中或不中。
计算二分查找的ASL,首当其冲还是要画出该序列的判定树,本质是一个平衡二叉树。
计算成功查找:当前结点到根节点的路径上的所有结点 的累加和,再除以当前成功查找过程的所有真实结点数(其实就是判定树的内部结点);
计算失败查找:我们往往会给平衡二叉树添加一些方形结点作为叶子节点,这些节点就是查询失败的结点(结点内容往往是一些区间范围)。我们选取失败结点的父节点一直到根节点的个数作为查找失败的长度,再把每个失败结点的查找长度累加起来,再除以查找失败对应的失败结点数(判定树的叶结点)得到查找失败的ASL。
-
哈希查找
又叫散列查找。顺序查找时间复杂度$ O(n)\(、二分查找时间复杂度\) O(log_2N)$ 、二叉搜索树时间复杂度\(O(h)\) ,最好情况\(O(log_2n)\) ,最差是\(O(n)\) 、二叉平衡树时间复杂度\(O(log_2n )\) 。当数据量非常大的时候,十亿,百亿,从里面找数据,即使使用树形查找时间复杂度也只能提升到\(O(log_2n)\) 。而使用散列查找的时间复杂度是\(O(1)\) ,效率提升不所谓不大!
散列思想是:给出一个元素,将这个元素经过一个函数计算得到另一个值,把得到的这个值作为存储地址。这种方式和前面线性、树形查找完全不同,不基于比较,而是将关键字值的信息和存储地址直接联系起来。根据选择的函数,有些函数可能会把不同的值映射到相同的存储地址
散列函数构造(我们都以数字散列值为研究对象,字符散列不做研究)
散列函数构造原则:散列函数简单、映射空间应该分布均匀
-
直接定址法
-
除留余数法
最常用方法。
装填因子,衡量一个散列表满的程度。假设记录是n,装填因子是\(\alpha\) ,那么散列表表长就是\(size = n/\alpha\) 。选一个小于表长size的最大质数\(p\) ,然后用元素\(key\)值不断
\[\begin{equation} f = key \% p \end{equation} \] -
数字分析法
-
折叠法
解决冲突几乎无法避免。现有的解决冲突思想有两种
-
开放地址法 (换个地方放)
-
线性探测
顺序查看表中单元,直至查到空闲位置。散列地址上会有聚集现象
-
平方探测
可以避免聚集,但只能检测散列表上的一半元素
-
双散列(再散列) 使用两个散列函数。要是第一个散列函数发生冲突,使用第二个散列函数计算增量 当装填因子太大,查找效率会下降,一般装填因子在\(0.5\leq\alpha\leq0.85\) ,要是再大,可以让散列表加倍减小\(\alpha\)
-
-
链地址法(把同一位置的冲突对象组织在一起)
将相同冲突位置的元素存储到同一个单链表中。形式上有点像邻接表。一个顺序结构结合链式结构,链表部分存储和查找效率低
散列查找是以较小的\(\alpha\)为前提,用空间换时间,才会让时间复杂度非常优秀。
散列查找不便于顺序查找,不便于最大最小值查找
开放定址法的散列表是顺序结构;
链地址法的散列表是顺序结构和连式结构相结合。
太小的\(\alpha\)导致空间浪费,太大的\(\alpha\)又以时间为代价效率低
总结:解决哈希查找的一般方法:
考试中求解哈希平均查找长度解答步骤:
- 使用除留余数法+开放定址法或者使用除留余数法+链地址法 的组合构造散列表
- 每一个关键字数值形成比较次数的表格
- 根据比较次数算出成功平均查找长度以及失败平均查找长度
在计算失败平均查找长度的时候有些许变化,由于是失败,比较次数就是从hash(kay)得到的起始位置一直向后一边比较一遍遍历直至遇到第一个空位置 的次数;概率也变了,查找成功时的概率是依据在散列表中的有效记录数,而查找失败时的概率是依据整个散列表的长度。
散列函数的P值,一般并不和记录数、表长有直接关联。其只不过是在表长范围内选取的一个最大素数,并不一定和表长相等。有的题目中表长和哈希函数中的素数值相等,重属于巧合
-
- 线性结构:
-
树、森林与二叉树的转换
- 树转换成二叉树:遵循当前节点左指针仍然连接左孩子,右指针连接当前节点的相邻右兄弟。
- 森林转换成二叉树:与数转换成二叉树类似。左孩子右兄弟原则,只不过多了 把森林中的每颗子树连接到前边树的右子树环节(因为每颗树形成的二叉树都没有右子树,右子树位置是空着的)
- 二叉树转换成树:与上面过程相反。对一整颗大树取左子树,作为森林中的第一棵树,拆下来,拆下来的部分再转成树(右孩子结点上提升到右兄弟[本应该就是右兄弟]);再对剩下的部分继续取左子树,拆分,如此重复。