找往期文章包括但不限于本期文章中不懂的知识点:
个人主页: 我要学编程(ಥ_ಥ)-CSDN博客
所属专栏: 学校课程突击
下面均是为了应付学校考试所用,如果有涉及部分知识点下面未说明,可以去我的数据结构专栏看看或者自行在网上查阅资料。
以下所有知识均是阅读大话数据结构所得。如果有小伙伴对这本书也感兴趣,可以去看看。
通过百度网盘分享的文件:大话数据结构-目录版.rar
链接:https://pan.baidu.com/s/1ge7HSe00tvj1U1S2zWnA8w?pwd=2580
提取码:2580
目录
树与二叉树章节相关
我们前面学习的树都是特殊的二叉树,且都是采用孩子双亲表示法,而下面要学习的二叉树采用的是 孩子兄弟表示法,第一个节点表示孩子,第二个节点表示当前节点的兄弟节点。
cpp
typedef struct Node {
int val; // 节点的值
struct Node *child; // 指向第一个子节点的指针
struct Node *brother; // 指向下一个兄弟节点的指针
} TreeNode;
树与二叉树的转换
一般的树都不是只有两个分叉的,而是存在多个分叉,但我们要想办法将多个分叉的树转换为二叉树就需要用到下面的方法:
**1、加线。**在同一层的相邻兄弟节点之间加上一根线,将其相连。
**2、去线。**若某个节点存在多个子节点, 将除第一个子节点之外的所有子节点之间的线全部去掉。
3、调整层次。
上述就是树转换为二叉树的过程。
二叉树转换为树,就是树转换为二叉树的逆过程。
**1、加线。**若某个节点存在左子节点,且该子节点的右子节点不为空,则将父节点与该子节点的右子节点相连。
**2、去线。**将所有与右子节点的相连的线全部去掉。
3、调整层次。
森林与二叉树的转换
森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作。
1、先将森林中的每棵树都转为二叉树;
2、再以第一棵树作为基准,将后续的树全部使用。
上图转换为二叉树的过程。
二叉树转换为森林也是逆着来。但首先得判断这二叉树是转换成一棵树,还是森林。如果根结点存在右子树,那么这棵二叉树就可以转为森林;反之,则不能转为森林,就是直接转为树了。
1、先将森林分裂成多棵二叉树。将根结点与右子树之间的连线断开,继续从右子树为起点,重复上述步骤,直至右子树为空。
2、将分裂出的每棵子树,全部使用二叉树转换为树的方法去转换每棵树。
树与森林的遍历
树的遍历方式分为两种,一种是先根遍历树,即先访问树的根结点,然后依次先根遍历根的每棵子树。另一种是后根遍历,即先依次后根遍历每棵子树,然后再访问根结点。
树的先根遍历,就是二叉树的先序遍历,而树的后根遍历,就是二叉树的后序遍历。
如果直接看不出来的话,还有一种方法,将树先转为二叉树,然后再对二叉树进行相应的遍历即可。 将图中的树转为二叉树之后,再去遍历结果还是一样的。
森林的遍历也分为两种方式:一种是前序遍历:先访问森林中第一棵树的根结点,然后再依次先根遍历根的每棵子树,再依次用同样方式遍历除去第一棵树的剩余树构成的森林。
后序遍历:是先访问森林中第一棵树,后根遍历的方式遍历每棵子树,然后再访问根结点,再依次同样方式遍历除去第一棵树的剩余树构成的森林。
森林的前序遍历就是对每个树都进行先根遍历,同理后序遍历就是对每棵树都进行后序遍历。
同样如果直接看不出来的话,也可以先将森林转换为二叉树,然后再对二叉树进行先序遍历 与 中序遍历。这样得出的结果是一样的。 注意这里森林的后序遍历不是二叉树的后序遍历,而是中序遍历了。
注意:我们上面所说的树均不是二叉树,而是普通的树。只有当说是二叉树时,才表明这是二叉树。同样森林也不是由二叉树构成,而是由普通的树构成的。
赫夫曼树(哈夫曼树)
从树中一个结点到另一个结点之间的分支构成两个结点之间的路径,路径上的分支数目称做路径长度。树的路径长度就是从树根(根结点)到每一结点(叶子结点)的路径长度之和。针对带权路径来说,树的带权路径长度为树中所有叶子结点的带权路径长度之和。
假设有n个权值{W1,W2,...,Wn},构造一棵有n个叶子结点的二叉树,每个叶子结点带权Wk,每个叶子的路径长度为Ik,我们通常记作,则其中带权路径长度WPL最小的二叉树称做赫夫曼树。也被称为 哈夫曼树 与 最优二叉树。
考试中使用最多的就是去计算一棵树的带权路径长度。
那如何去构造一棵赫夫曼树呢?
1、将结点与权值绑定在一起。
2、找到结点权值最小的两个,连接在一起构造成一个新的结点(权值为两者之和)。
3、重复上述步骤,直至构造成一个完整的二叉树即可。
如下图所示:
注意:
1、将两个结点结合成一个新的结点时,下次再去找权值最小的两个结点就只能从结合的新结点与原来剩下的结点中去寻找。如果使用队列进行预处理的话,就是将队列中的两个权值最小的结点出队,然后再将结合的结点入队。
2、为了更好的处理,我们一般都会先将需要构造赫夫曼树的结点重新排序。上述图示只是为了更好的展示效果,实际的处理过程中,我们是使用优先级队列来处理的。
3、由于结点放置的左右顺序没有要求,因此最终所形成的赫夫曼树也是有不同的形状的,但是带权路径长度一定是最小的。
赫夫曼树构造出来之后,就可以求对应的加权路径长度,以及每个结点字符对应的赫夫曼编码了。
我们先来看赫夫曼树的加权路径长度(WPL)。其计算方法是将每个根结点到叶子结点的长度求出,然后乘上叶子结点的权值,最后将所有的结果相加就得到了WPL。
注意:为了做到不重不漏的计算出正确答案,我们最好是按照叶子结点从左到右的顺序来计算。
在学习赫夫曼编码之前,我们先来了解一下什么是编码?以及为什么需要编码?
编码是将数据从一种形式转换为另一种形式。例如,二进制编码就可以将不属于二进制的数据转换为属于二进制的数据。编码的作用主要是为了更好的传输与存储。这里的哈夫曼编码就是为了更好的传输。
在现实生活中,数据的传输都是先转换为二进制数据,然后通过光信号或者电信号发生出去,最后接收方再通过同样的方式将接受的光信号或者电信号转换为对应的二进制,最终在转换为相应的原始数据。
在上述情况下,便诞生了赫夫曼编码 与 赫夫曼树。赫夫曼编码既不会产生前缀不唯一的情况,也不会出现冗余。 赫夫曼编码是通过赫夫曼树得到的。这里的权值是每次字符出现的次数。我们还是拿最上面的那个赫夫曼树来举例。
其实现在我们再去看赫夫曼树的构造过程,使用权值最小的结点开始构造这样最终形成的编码也是最长的,但是权值小意味着出现的频率小,也就占用的编码长度短,因此采用赫夫曼树构造出来的树的带权路径长度一定是最短的。
注意:由于赫夫曼树的构造不止一种,因此对应字符的赫夫曼编码也是唯一的,但是带权路径长度一定是唯一且最短的。
除了上述构造赫夫曼树与对应的赫夫曼编码以及WPL之外,还常考的就是计算存储一段正文所需的二进制字节数(或者说存储这段正文所需的最小内存是多少)。
这种题目的思路是将对应字符出现的次数构造成一棵赫夫曼树,然后将对应字符的编码求出来,最后将编码长度乘以对应字符出现的次数整体求和就可以得到存储这段正文所需的内存,这里得到的是比特位的个数。
图章节相关
深度优先遍历 与 广度优先遍历的区别:
深度优先遍历的思想体现在"深",当遍历到一个新的顶点时,不会继续往当前路径去遍历,而是直接沿着新顶点的路径去遍历剩下的顶点了。
广度优先遍历的思想体现在"广",当遍历到一个新的顶点时,会将该顶点相邻的所有顶点都尝试去遍历一遍。
普利姆算法 与 克鲁斯卡尔算法的区别:
普利姆算法是需要确定一个顶点,从该顶点出发,寻找局部权值最小的边(与已知顶点相连的边),直至构成最小生成树。
克鲁斯卡尔算法是在图中所有的边中,选取一个权值最小的边,因此并不需要确定顶点出发。
顶点的度:
在有向图中,顶点的度包括了出度与入度,而在无向图中顶点的图只需要看出度或者入度即可。
关键路径:
**概念:**在项目网络图中,关键路径是从项目开始到结束的最长序列(边权值和最大)。这个路径上的任务总持续时间最长。
求关键路径一般就是求其的序列,而求关键路径长度时,才会去求具体的边权和。
关键活动是指关键路径上的顶点。
若一个有向无环图的关键路径有几条时,当要求加快进度缩短工期时,应如何才能做到?必须同时提高几条关键路径上的活动。
关键活动速度的提高在什么情况下才有效?在不改变关键路径路径的情况下才有效。
注意:关键路径只存在于有向无环图中。对于无向图或者有向成环图不存在这样的概念。
拓扑排序:
每次选入度为0的点,然后删除这个点和它的出边。接着继续在图中选择一个入度为0的点,并删除其出边,直至图中不存在点。
注意:拓扑排序只针对有向无环图才能生效,如果图中存在环,就不能使用拓扑排序或者说拓扑排序的结果不存在。
对上图进行拓扑排序:
1、选择入度为0的点: a;
2、删除a 与 其的出边(这里也可以认为是全部的边,因为其不存在入边);
3、从 b 与 c 中选择一个点即可,继续重复上述步骤。
最终拓扑排序的结果为(其中一种):a b c e d。
我们也可以看一下对于存在环的图是啥样的:
对上图进行拓扑排序:
1、选择入度为0的点: a;
2、删除a 与 其的出边(这里也可以认为是全部的边,因为其不存在入边);
3、c d e 都不满足入度为0的点,这个要求,因此没法继续拓扑排序了。
拓扑排序也常用来判断图中是否存在环。
查找章节相关
二叉搜索树:
1、求在等概率情况下的查找成功的平均查找长度:
查找成功的情况只能出现在树中有节点存在的情况才行,而查找本质上就是遍历整棵树。对于第一层的结点来说,查找一次即可,对于第二层的结点来说查找两次即可,依次类推,对于第n层的结点查找成功的次数是n。对于查找成功的平均查找长度就是将这棵树的结点全部查找所需的次数,然后除以节点的个数,就是最终的结果。
上面这棵树查找成功的平均查找长度:ASL = (1*1 + 2*2 + 3*4 + 4*2 + 5*1) / 10 = 3
2、求在等概率情况下的查找不成功的平均查找长度:
查找不成功就是指不是树中存在,那怎么知道不在树中存在呢?当查找到空节点时,就证明查找失败了。查找不成功的平均长度就是空节点的查找次数之和除以空节点的个数。注意的是,对于处于第n层的空节点来说,查找的次数是 n-1,而不是n。ASL = (6*3 + 3*4 + 2*5) / 11 = 3.64(约为)
二分查找:
对于有序表(12 ,18 , 24 , 35 , 47, 50, 62, 83, 90 ,115,134),在等概率的情况下:
1)请画出折半查找树(判定树);
2)求采用折半查找法时成功和不成功的平均查找长度。
3)当用折半查找法查找90时需要进行多少次查找可以确定成功?
4)查找100时需要进行多少次查找才能确定不成功?
1、对于 折半查找树画法,是根据二分查找(折半查找)的顺序来的。将二分查找取的数,作为当前子树的根结点。左区间就作为左子树,右区间就作为右子树。
2、查找成功与不成功的平均查找长度和前面的二叉搜索树是一样的做法,都是计算树中节点的查找的次数之和 除以 节点的个数。
ASL不成功 =(4*3 + 8*4)/ 12 = 3.67 ASL成功 =(1*1 + 2*2 + 4*3 + 4*4)/ 11 = 3
3、4:当我们画出折半查找树之后,就可以直接对照树来进行判断了。
90 是属于第二层,那么查找的次数就是2次。
100 是属于 第四层的空节点,查找的次数为3次。
哈希表:
设哈希(Hash)表的地址范围为0~17,哈希函数为:H(K)=K MOD 16。K为关键字,用线性探测法再散列法处理冲突,输入关键字序列:(10,24,32,17,31,30,46,47,40,63,49)造出Hash表,试回答下列问题:
- 画出哈希表的示意图;
- 若查找关键字63,需要依次与哪些关键字进行比较?
- 若查找关键字60,需要依次与哪些关键字比较?
- 假定每个关键字的查找概率相等,求查找成功时的平均查找长度。
1、画出哈希表:根据映射函数 + 线性探测法,将哈希表的图给画出来即可:
2、查找63,就是先通过哈希映射函数将对应的下标求出来,key = 15,先查找到31,不对;继续往后找:46 47 32 17 63,一共找了6次。
3、查找60,同样是通过哈希映射函数计算出下标,key = 12,查找到12的位置为空,就说明不存在这个元素,只需要比较一次。
4、计算平均查找长度就是将所有元素查找成功的次数之和 除以 元素的个数。哈希表中没有冲突的元素只需要查找一次,冲突的元素加上线性探测的次数即为最终的查找次数。
ASL= (6×1+2+3+3+3+6=6+2+3+3+3+6)/ 11 = 23 / 11 ≈ 2.09
数据结构的相关理解(记忆)
1、简述线性结构与非线性结构的不同点
答:线性结构反映结点间的逻辑关系是 一对一的,非线性结构反映结点间的逻辑关系是多对多的。
2、数据结构中讲述的结构主要有哪四种?这四种又分别属于哪二个大类?请分别给出这二类结构中你学了哪些结构?
答:
1)数据结构中的结构主要有以下四种:集合、线性表、树与图。
2)这四种又分别属于线性与非线二个大类,其中线性表属于线性结构,树与图属于非线性结构。
3)线性结构学了:线性表、栈、队列、串、数组
非线性结构学了:树、图
3、一般来说,一个问题往往有多种不同的算法,我们如何评价这些算法的好坏并能从中得到较好的算法呢?
答:评价一个算法的好坏主要从二个方面进行:一个方面是时间,这个主要用时间复杂度来度量;另一个方面是空间,这个主要用空间复杂度不度量;考虑一个算法的优劣主要是从时间复杂度方面来考虑。
4、数据结构中的存储结构主要有哪二种?它们分别通过什么来实现?他们各有什么特点与优缺点?
答:
1)数据结构中的存储结构主要有静态结构与动态结构
2)静态结构主要通过数组来实现,动态结构主要通过链表来实现;
3)静态结构的特点:逻辑上相邻的元素在存储上一定相邻;动态结构的特点:逻辑上相邻的元素在存储上不一定相邻
4)静态结构的优点:能随机访问,缺点是:插入删除要移动大量的元素;动态结构的优点:插入删除不需要移动元素,缺点是:不能随机访问元素
算法
cpp
// 一、使用C语言对链表进行排序
// 1、结构体的定义
typedef struct ListNode {
int data;
struct ListNode *next;
} ListNode, *List;
// 2、排序的主逻辑
void sort(List L) {
// 如果链表为空,或者只有一个结点则无需排序
if (L == NULL || L->next == NULL) {
return;
}
ListNode *p, *q;
// 外层循环
for (p = L->next; p != NULL; p = p->next) {
// 内层循环
for (q = p->next; q != NULL; q = q->next) {
// 如果数据较大,交换数据
if (p->data > q->data) {
int tmp = p->data;
p->data = q->data;
q->data = tmp;
}
}
}
}
// 2、将两个集合合并
typedef struct list {
ElemType *elem; // 集合元素
int length; // 有效元素个数
int listsize; // 集合的大小
}sqList
// 将 la 和 lb 线性表归并到 la
void mergelist_sq(sqlist &la, sqlist lb) {
ElemType *pa, *pb, *pc, *pa_last, *pb_last;
pa = la.elem;
pb = lb.elem;
pa_last = la.elem + la.length - 1;
pb_last = lb.elem + lb.length - 1;
// 合并后的表从 la 的末尾开始插入
pc = pa_last + 1;
// 归并 lb 到 la
while (pb <= pb_last) {
pa = la.elem;
// 在 la 中查找是否有与 pb 相等的元素
while (pa <= pa_last) {
if (*pa == *pb) {
break;
}
pa++;
}
// 如果 la 中没有相等的元素,则将 pb 插入 la
if (pa > pa_last) {
*pc++ = *pb;
la.length++;
}
pb++;
}
}
注意:使用 Hoare法、 挖坑法 对同一组序列进行快速排序时,最终排序的结果是一样的,但是中间排序的结果可能会不一样。
若一组记录的排序码为(46, 79, 56, 38, 40, 84),则利用快速排序的方法,以第一个记录为基准得到的一次划分结果为:()
A、 38, 40, 46, 56, 79, 84
B、 40, 38, 46, 79, 56, 84
C、 40, 38, 46, 56, 79, 84
D、 40, 38, 46, 84, 56, 79
1、Hoare法版:
2、挖坑法版:
上图题目的答案是C,采取的挖坑法。
好啦!本期 数据结构理论篇(期末突击) 的学习之旅 就到此结束啦!我们下一期再一起学习吧!