赞
踩
本笔记是对《王道数据结构》中各章节涉及的基础知识进行整理。本笔记主要用以应对夏令营面试中可能会问到的数据结构方面的问题,比较泛泛而谈,如果您对这些内容感兴趣,建议参考原书。大佬可自行绕路
更多章节内容请参见:保研复习——数据结构篇-CSDN博客
目录
6.2 解释一下连通图、连通分量、强连通图、有(无)向完全图
7.1 如何从海量数据里找到最大的10000个数(top K问题)?
8.6 请给出插入排序、交换排序、选择排序、归并排序、基数排序的时间复杂、稳定性
有穷性、确定性、可行性、输入、输出
循环和递归两者是可以互换的,不能决定性的说循环的效率比递归高。 递归的优点是:代码简洁清晰,容易检查正确性;缺点是:当递归调用的次数较多时,要增加额外的堆栈处理,有可能产生堆栈溢出的情况,对执行效率有一定的影响。 循环的优点是:结构简单,速度快;缺点是:它并不能解决全部问题,有的问题适合于用递归来解决不适合用循环。
- 栈溢出概念: 栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致栈中与其相邻的变量的值被改变。
- 栈溢出的原因:
- 局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。局部变量是存储在栈中的,因此这个很好理解。解决这类问题的办法有两个,一是增大栈空间,二是改用动态分配,使用堆(heap)而不是栈(stack)。
- 递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。
- 指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。
- 堆和栈的区别: 1、堆是由低地址向高地址扩展;栈是由高地址向低地址扩展 2、堆中的内存需要手动申请和手动释放;栈中内存是由OS自动申请和自动释放,存放着参数、局部变量等内存 3、堆中频繁调用malloc和free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会产生内存碎片 4、堆的分配效率较低,而栈的分配效率较高
- 栈的效率高的原因: 栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。
要在数据流中实时计算中位数,可以使用对顶堆(也称为双堆)的方法。这个方法利用了最大堆和最小堆来高效地维护数据流的中位数。具体实现步骤如下:
维护两个堆:
- 最大堆:存储数据流中较小的一半元素,堆顶是这些元素中的最大值。
- 最小堆:存储数据流中较大的一半元素,堆顶是这些元素中的最小值。
插入元素:
- 每当一个新元素进入数据流,首先根据元素值与最大堆堆顶元素的比较结果决定将其插入最大堆还是最小堆。
- 插入后,可能需要调整两个堆以保持平衡,即两个堆的大小差不能超过1。
平衡堆:
- 如果最大堆的大小超过最小堆的大小超过1,将最大堆的堆顶元素移动到最小堆。
- 如果最小堆的大小超过最大堆的大小,将最小堆的堆顶元素移动到最大堆。
计算中位数:
- 如果两个堆的大小相等,中位数是两个堆顶元素的平均值。
- 如果两个堆的大小不等,中位数是较大堆的堆顶元素。
• 逻辑上相邻的元素在物理存储上也相邻(插入删除需要移动元素)
• 逻辑上相邻的元素物理上不一定相邻(插入删除不需要移动元素)
区别: 不管是否带头结点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一结点,结点中通常不存储信息。
注: 头结点的指针域指向线性表的第一个元素结点。
引入头结点的优点:
① 由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作保持一致,无需进行特殊处理。
② 无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。
穷举遍历:设一个检测指针 k 和一个遍历指针 q,count 记录遍历指针 q 走的步数。遍历指针每走一步,检测指针 k 就走遍历指针 q 之前走过的节点,若发现相同的节点便说明有环,直到遍历节点 q 遍历完整个链表,即q = NULL 为止。时间复杂度O(n^2),空间复杂度O(1)
标记法:在结点中设置一个标记变量,每走一个结点,就判断一次。若 visit == true,则说明有环。反之,说明无环。时间复杂度O(n),空间复杂度O(n)
快慢指针:设置两个指针,一个慢指针 low 和一个快指针 fast。初始值 慢指针 low 指向第一个结点,快指针 fast 指向第二个结点。只要快指针不为空,则慢指针slow就先向后走一个。然后两轮处理快指针。只要未遍历完链表,则将快指针向后移动一个,并判断快慢指针是否相遇。一旦快慢指针相遇,则说明有环。反之,则说明无环。时间复杂度O(n),空间复杂度O(1)
set 集合大小变化:根据集合的去重特性来判断。每遍历一个结点,就将该结点加入集合。若加入该新结点后,集合大小不再发生变化,则说明集合中的该元素已存在,即说明存在环。倘若,遍历完所有结点,集合大小都能正常判断,则无环。时间复杂度O(n),空间复杂度O(n)
表达式求值和语法解析
- 中缀表达式转换为后缀表达式
- 后缀表达式求值
括号匹配
树的遍历
函数调用管理
深度优先搜索(DFS)
撤销操作
浏览器的前进和后退功能
递归算法的非递归实现
内存管理
操作系统中多作业、多任务的排队,进程调度
广度优先搜索(BFS)
缓冲区(Buffer)管理
- 打印队列
- IO操作缓冲区
- 树的层次遍历
资源管理:
- 打印机
- 进程间通信
缓存(Cache):如FIFO缓存策略。
- 中 → 后:两个栈。
- 计算后缀表达式:一个栈。
- 计算前缀表达式:从右到左(反向)扫描,数字压栈,运算符 弹栈*2 计算。
KMP算法是在简单模式匹配的基础上对串的模式匹配进行优化。
主要的思路是每趟比较过程中让子串先滑动到一个合适的位置。
当发生不匹配时,不同于简单模式匹配的右移一位,而是移动到适合的位置。
这里所移动的位置依靠与NEXT[]数组,求next[]数组的方法是比较前后缀相同元素。
叶子结点树为n0,度为2的结点数为n2,则n0=n2+1
根据遍历的次序,将二叉链表中空的指针域修改为指向前驱或后继
双亲表示法、孩子链表表示法、孩子兄弟表示法
不能,因为无法区分左右子树。
除此之外,先序、后序、层序的两两组合也无法唯一确定一棵二叉树。
(1)度为2的树要求树中最少有一个结点的孩子数为2,二叉树则只要求度不超过2即可
(2)二叉树子树区分左右,度为2的树则不区分
哈夫曼树带权路径最短(带权路径:树中结点的值乘于结点到根的距离)
从集合中选取根结点权值最小的两棵树(集合中的树一开始都是结点)组成一颗新树,新树的权值为左右子树权值之和,删除集合选取的两个结点,增加新树的结点,重复上述操作;
• 从一个顶点到另一个顶点经过的顶点序列且中间经过的顶点不重复
• 首尾相同的路径
• 简单路径+回路
• 任意两点直接都是连通的,例如vi和vj之间至少存在一条路径,无论是谁指向谁。
• 非连通图中,连通的子图的数量(无向图G的极大连通子图),一个非连通图有许多连通分量
• 在连通图的基础上,任意两点之间都存在路径,注意要互相存在路径,互相指向。
• 无向(有向)任意两点都有一条弧边(弧)
• 连通且无回路
• 有n-1条边的连通图
• (补充):有向图中,有一个顶点入度为0,其余结点入度为1
AOV网是以顶点表示活动的有向无环图(DAG),也称为顶点表示活动的网络。在AOV网中,顶点代表项目中的活动或任务,边表示活动之间的依赖关系。
AOE网是以边表示活动的有向图。在AOE网中,边代表项目中的活动或任务,顶点表示事件或活动的开始和结束。
• 左右子树高度差绝对值不大于1
• 左右子树都是平衡树
• B+树只能到达叶子结点才能命中(B树可以在非叶子结点命中)
• B+树更加适合文件系统
• B树n个结点有n+1个分支,B+树n个结点有n个分支
• B树的每个结点包含信息,B+树非叶结点起索引作用,只有叶子结点包含信息(且包含了全部关键字)
• B+树有一个指针指向关键字最小的叶子结点,所有叶子结点链接成一个链表,B树没有
• B树和B+树每个结点的关键字个数取值范围不同;
Dijkstra算法是一种典型的贪心算法,适用于所有边权重为非负数的情况。如果将其用于负权图的最短路径求解,可能会得到错误答案。
- 一个联通子图,包含所有结点,边数 = n-1,边权之和最小。存在多个。
- prim(点 稠密图):每次选择 与连通块之间的边权值最小的顶点(堆),直到已选 n 个顶点。维护 {边权, 边顶点1, 顶点2} 的结构体,用堆来存,每次 logV 选最小顶点,选完更新其他顶点(旧信息出堆,新信息入堆,Σ边数×logV,常数为最大度),O((E+V)logV)。
- kruskal(边 稀疏图):每次选择 两顶点未连通(并查集)权值最小的边,直到已选 n-1 条边。边按权值排序 然后依次选边,O(V+ElogE) 排序 。
- 割点 / 关节点:
- 若删除某个节点 u 和它带有的边,连通分量数目增加。
- 若生成树中某个非叶子顶点V,其某棵子树的根和子树中的其他节点,均没有指向V的祖先的回边,则V为关节点。因为删去v,则其子树和图的其它部分被分割开来。
- 用DFS实现,同时维护一个dfn和low数组
从源点到汇点的路径中的最长路径为关键路径,完成整个工期所需要的最短时间就是关键路径长度所代表的时间。
关键路径上的活动又称为关键活动
方法一:建最小堆,先拿10000个数建最小堆,然后依次添加剩余元素,如果大于堆顶的数(10000中最小的),将这个数替换堆顶,并调整结构使之仍然是一个最小堆,这样,遍历完后,堆中的10000个数就是所需的最大的10000个。建堆时间复杂度是O(mlogm),算法的时间复杂度为O(nmlogm)(n为10亿,m为10000)。
方法二:用分块查找(找相对大的数,而非一定是最大的10000个数),可以把所有10亿个数据分组存放,比如分别放在1000个文件中。这样处理就可以分别在每个文件的10^6个数据中找出最大的10000个数,合并到一起在再找出最终的结果。
构造方法:
- 直接定址法
- 除留余数法
- 数字分析法
- 平方取中法
解决冲突:
1)开放定址法:
1.1 线性探测法
1.2 平方探测法
1.3 双散列法
1.4 伪随机序列法
2)拉链法:
(1)选用的散列函数
(2)选用的冲突处理方法
(3)散列表的饱和程度
装填因子α:散列表中的元素个数与散列表大小的比值
特点:α越小,填入表中的元素较少,产生冲突的可能性就越小。
直接插入排序:排序区看成左右两部分,左边有序、右边无序,对于未排序的数据,在已排序序列中从后向前扫描,找到相应位置并插入
希尔排序:通过将原序列分割成若干个子序列分别进行插入排序。随着排序过程的推进,子序列的规模逐渐减小,直到最后进行一次常规的插入排序(增量为1),此时整个序列已经接近有序
折半插入排序:相比于直接插入排序,在插入未排序元素时,通过二分查找法找到插入位置,从而减少比较操作的次数。尽管比较次数减少了,但移动元素的次数并没有改变。所以时间复杂度和直接插入排序一致。
冒泡排序:多次遍历待排序序列,每次遍历时比较相邻的元素,如果顺序不对就交换它们的位置,直到整个序列排序完成。
快速排序:每次排序前先找到一个枢纽值,然后通过一次排序将待排序序列分成两个子序列,其中一个子序列的所有元素都小于(或等于)另一个子序列的所有元素,然后递归地对这两个子序列进行快速排序。
简单选择排序:每次从未排序部分中选择最小(或最大)的元素,将其放在已排序部分的末尾,直到整个序列排序完成。
堆排序:将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根结点,将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成构造成一个堆,这样就会得到n个元素中的次大值。如此反复执行,便能得到一个有序序列了。
分解:
- 归并排序开始于将待排序的数组不断地“一分为二”,直到每个子数组只包含一个元素。这个过程是递归进行的,即每个子数组也会继续被分解成更小的子数组,直到每个子数组只包含一个元素。
递归排序与合并:
- 在分解过程完成后,递归地开始合并这些子数组。合并时,会取出两个相邻的子数组,并将它们合并成一个有序的新数组。
- 合并过程中,会比较两个子数组中的元素,并按照大小顺序依次放入新数组中,直到两个子数组中的所有元素都被考虑完毕。
- 这个合并过程是递归进行的,每次合并两个子数组,生成的新有序数组又会被视为新的子数组,继续参与后续的合并过程。
- 基数排序的思想是把位数相同的一组数组依次从后往前比较其每一位上的大小,经过几轮比较使得数据达到有序的做法。比较的次数跟数据的位数有关系。比如要比较一组手机号码从小到大排列,可以比较手机号每一位大小,然后比较11次,手机号达到有序。
- 注意:基数排序每次位的比较可以使用线性排序的方式,比如桶排序或者计数排序,因为它们的时间复杂度为O(n),而且每轮的比较需要保证每次比较数据的稳定性,不然基数排序就无法完成。
注意:快排平均和最优的时间复杂度均是O(nlogn),最差的时间复杂度为O(n^2)
假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
快速排序时间复杂度为O(n×log(n))的证明_试证明:当输入序列已经呈现有序状态时,快速排序的时间复杂度为o(n2) 。-CSDN博客
- 堆排序
- 优先队列
- 快速找最值
内部排序是在内存中进行排序
外部排序是由于信息庞大,无法将整个文件放在内存中进行排序,需要将待排序的记录存储在外存上,排序时候再把数据一部分一部分调用内存进行排序。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。