数据结构-考研复试面试题-汇总大合集
_写在前面的话:第二次写文章,本篇文章涉及内容主要包括数据结构与算法,包含市面上最热门的面试题,加以总结,用于本人的专业课面试复习,包括一些个人理解和总结,
如果能帮到你,欢迎点赞,如有写的不妥当的欢迎指出
参考主要书目:《数据结构》严蔚敏,以及辅导教材书 王道《数据结构》,天勤《数据结构高分笔记》
a、数组:顺序存储,随机访问 链表:链表存储,顺序访问
b、栈,分为栈顶和栈底,遵循先进后出原则
c、队列 ,一个线性表,像排队一样,受约束控制,遵循先进先出原则
d、树:二叉树、平衡二叉树、大顶堆,小顶堆等
e、图:最短路径,关键路径
从逻辑结构来看:
a) 数组必须事先定义固定的长度(元素个数),不能适应数据动态地增减的情况。当数据增加时,可能超出原先定义的元素个数;当数据减少时,造成内存浪费;数组可以根据下标直接存取。
b) 链表动态地进行存储分配,可以适应数据动态地增减的情况,且可以方便地插入、删除数据项。(数组中插入、删除数据项时,需要移动其它数据项,非常繁琐)链表必须根据next指针找到下一个元素
从内存存储来看:
a) (静态)数组从栈中分配空间, 对于程序员方便快速,但是自由度小
b) 链表从堆中分配空间, 自由度大但是申请管理比较麻烦
从上面的比较可以看出,如果需要快速访问数据,很少或不插入和删除元素,就应该用数组;相反, 如果需要经常插入和删除元素就需要用链表数据结构了。
排序算法有很多,每种算法有不同的时间和空间复杂度,效率也有差别,那么针对使用上也有不同的场合。
下面列一些常见的算法:插入排序,冒泡排序,选择排序,快速排序,堆排序,归并排序,基数排序,希尔排序等。
摘自百度:散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。
也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
给定表M,存在函数f(key),对任意给定的关键字值key,代入函数后若能得到包含该关键字的记录在表中的地址,则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数
只对长度大于k的子序列递归调用快速排序,让原序列基本有序,然后再对整个基本有序序列用插入排序算法排序。实践证明,改进后的算法时间复杂度有所降低,且当k取值为 8 左右时,改进算法的性能最佳。
选择基准元的方式
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。也就是说,基准的选择是很重要的。选择基准的方式决定了两个分割后两个子序列的长度,进而对整个算法的效率产生决定性影响。最理想的方法是,选择的基准恰好能把待排序序列分成两个等长的子序列。
方法1 固定基准元
如果输入序列是随机的,处理时间是可以接受的。如果数组已经有序时,此时的分割就是一个非常不好的分割。
方法2 随机基准元
这是一种相对安全的策略。由于基准元的位置是随机的,那么产生的分割也不会总是会出现劣质的分割。在整个数组数字全相等时,仍然是最坏情况,时间复杂度是O(n^2)
实际上,随机化快速排序得到理论最坏情况的可能性仅为1/(2^n)。所以随机化快速排序可以对于绝大多数输入数据达到O(nlogn)的期望时间复杂度。
方法3 三数取中
引入的原因:虽然随机选取基准时,减少出现不好分割的几率,但是还是最坏情况下还是O(n^2),要缓解这种情况,就引入了三数取中选取基准。
分析:最佳的划分是将待排序的序列分成等长的子序列,最佳的状态我们可以使用序列的中间的值,也就是第N/2个数。可是,这很难算出来,并且会明显减慢快速排序的速度。这样的中值的估计可以通过随机选取三个元素并用它们的中值作为基准元而得到。事实上,随机性并没有多大的帮助,因此一般的做法是使用左端、右端和中心位置上的三个元素的中值作为基准元。
递归和循环两者完全可以互换。不能完全决定性地说循环地效率比递归的效率高。
递归算法
优点:代码简洁、清晰,并且容易验证正确性。
缺点:它的运行需要较多次数的函数调用,如果调用层数比较深,需要增加额外的堆栈处理(还有可能出现堆栈溢出的情况),比如参数传递需要压栈等操作,会对执行效率有一定影响。
但是,对于某些问题,如果不使用递归,那将是极端难看的代码。在编译器优化后,对于多次调用的函数处理会有非常好的效率优化,效率未必低于循环。
循环算法
优点:速度快,结构简单。
缺点:并不能解决所有的问题。有的问题适合使用递归而不是循环。如果使用循环并不困难的话,最好使用循环。
哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。
1) 线性探测法
2) 平方探测法
3) 伪随机序列法
4) 拉链法
在一个字符串中查找是否包含目标的匹配字符串。
其主要思想是每趟比较过程让子串先后滑动一个合适的位置。
当发生不匹配的情况时,不是右移一位,而是移动(当前匹配的长度– 当前匹配子串的部分匹配值)位。
关键字的数量不同:B+树中分支结点有m个关键字,其叶子结点也有m个,其关键字只是起到了一个索引的作用,但是B树虽然也有m个子结点,但是其只拥有m-1个关键字。
存储的位置不同:B+树中的数据都存储在叶子结点上,也就是其所有叶子结点的数据组合起来就是完整的数据,但是B树的数据存储在每一个结点中,并不仅仅存储在叶子结点上。
分支结点的构造不同:B+树的分支结点仅仅存储着关键字信息和儿子的指针(这里的指针指的是磁盘块的偏移量),也就是说内部结点仅仅包含着索引信息。
查询不同:B树在找到具体的数值以后,则结束,而B+树则需要通过索引找到叶子结点中的数据才结束,也就是说B+树的搜索过程中走了一条从根结点到叶子结点的路径。
最小生成树:
最小生成树来自于无向网。
无向图在边上加上权值就成了无向网。
一个无向图可以有多种不同姿态连接的生成树。
最小生成树就是–各边上权值之和最小的生成树。
普里姆算法(Prim)和克鲁斯卡尔(Kruskal)算法
普里姆算法的基本思想:(简单的说就是一直加点)
取图中任意一个顶点 v 作为生成树的根,之后往生成树上添加新的顶点 w。添加顶点w的条件为:w 和已在生成树上的顶点v 之间必定存在一条边,并且该边的权值在所有连通顶点 v 和 w 之间的边中取值最小。之后继续往生成树上添加顶点,直至生成树上含有 n-1 个顶点为止。
克鲁斯卡尔算法的基本思想:(简单的说就是找不围成圈的最小的边)
考虑问题的出发点: 为使生成树上边的权值之和达到最小,则应使生成树中每一条边的权值尽可能地小。
具体做法: 先构造一个只含 n 个顶点的子图 SG,然后从权值最小的边开始,若它的添加不使SG 中产生回路,则在 SG 上加上这条边,如此重复,直至加上 n-1 条边为止。
当带权连通图的任意一个环中所包含的权值均不相同
1.prim算法过程:
prim算法是最小生成树算法,它运用的是贪心原理,设置两个点集合,一个集合为要求的生成树的点集合A,另一个集合为未加入生成树的点B。
它的具体实现过程是:
(1):所有的点都在集合B中,A集合为空。(memset(visited,0,sizeof(visited))
(2):任意以一个点为开始,把这个初始点加入集合A中,从集合B中减去这个点(visited[1]=1)。寻找与它相邻的点中路径最短的点,如后把这个点也加入集合A中,从集合B中减去这个点(visited[pos]=1)。
(3):更新未被访问的节点的dist[]值。
(4):重复上述过程。一直到所有的点都在A集合中结束。
2.dijkstra算法过程:
(1)初始时,S只包含源点v,即S=v。U包含除v外的其他顶点,U中顶点u距离为边上的权(若v与u有边)或(若u不是v的出边邻接点)。
(2)从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。
(3)以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u(u U)的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。
(4)重复步骤(2)和(3)直到所有顶点都包含在S中。
3.小总结
1:Prim是计算最小生成树的算法,Dijkstra是计算最短路径的算法,
2、都是使用贪婪和线性规划,每一步都是选择权值/花费最小的边。
贪婪:一个局部最有解也是全局最优解;
线性规划:主问题包含n个子问题,而且其中有重叠的子问题。
深度优先搜索(回溯法)
算法思路
深度优先搜索(DFS,Depth-First Search)是搜索的手段之一
它从某个状态开始,不断地转移状态直到无法转移,然后回退到前一步状态,继续转移到其他状态,如此不断重复,直到找到最终的解.根据深度优先搜索的特点,采用递归函数(隐式地利用了栈进行计算)实现比较简单.
算法效率
深度优先搜索从最开始的状态出发,遍历所有可以到达的状态.由此可以对所有的状态进行操作,或列举出所有的状态.作为搜索算法的一种,DFS对于寻找一个解的NP(包括NPC)问题作用很大.
但是,搜索算法毕竟是时间复杂度是O(n!)的阶乘级算法,它的效率比较低,在数据规模变大时,这种算法就显得力不从心了.关于深度优先搜索的效率问题,有多种解决方法.最具有通用性的是**剪枝(prunning),**也就是去除没有用的搜索分支.有可行性剪枝和最优性剪枝两种.此外,对于很多问题,可以把搜索与动态规划(DP,dynamic programming)、完备匹配(匈牙利算法)等高效算法结合.
2.宽度优先搜索(分支限界法)
算法思路
宽度优先搜索(BFS,Breadth-First Search)也是搜索的手段之一.他与深度优先搜索类似,从某个状态出发搜索所有可以到达的状态.根据宽度优先搜索的特点,采用队列实现比较简单.
算法效率
与深度优先不同之处在与搜索的顺序,宽度优先搜索总是先搜索距离初始状态近的状态.也就是说,它是按照开始状态->只需1次转移就可以到达的所有状态->只需2次转移就可以到达的所有状态->…这样的顺序进行搜索.对于同一个状态,宽度优先搜索只经过一次,因此复杂度为
O(状态数*转移的方式).很容易地用来求最短路径、最少操作之类问题的答案.
广度搜索的判断重复如果直接判断十分耗时,我们一般借助哈希表来优化时间复杂度.
3.Death-Breadth总结
宽度优先搜索与深度优先搜索一样,都会生成所有能够遍历到的状态,因此需要对所有状态进行处理时使用宽度优先也是可以的.但是递归函数可以很简短地编写,而且状态的管理也更简单,所以大多数情况下还是用深度优先搜索实现.反之,在求取最短路时深度优先搜索需要反复经过同样的状态,所以还是使用宽度优先搜索比较好.
宽度优先搜索会把状态逐个加入队列,因此通常需要与状态数成正比的内存空间.反之,深度优先搜索是与最大的递归深度成正比的.一般与状态数相比,递归的深度并不会太大,所以可以认为深度优先搜索更加节省内存.
贪心算法顾名思义就是做出在当前看来是最好的结果,它不从整体上加以考虑,也就是局部最优解。贪心算法从上往下,从顶部一步一步最优,得到最后的结果,它不能保证全局最优解,与贪心策略的选择有关。
动态规划是把问题分解成子问题,这些子问题可能有重复,可以记录下前面子问题的结果防止重复计算。动态规划解决子问题,前一个子问题的解对后一个子问题产生一定的影响。在求解子问题的过程中保留哪些有可能得到最优的局部解,丢弃其他局部解,直到解决最后一个问题时也就是初始问题的解。动态规划是从下到上,一步一步找到全局最优解。(各子问题重叠)
分治法(divide-and-conquer):将原问题划分成n个规模较小而结构与原问题相似的子问题;递归地解决这些子问题,然后再合并其结果,就得到原问题的解。(各子问题独立)
分治模式在每一层递归上都有三个步骤:
分解(Divide):将原问题分解成一系列子问题;
解决(conquer):递归地解各个子问题。若子问题足够小,则直接求解;
合并(Combine):将子问题的结果合并成原问题的解。
例如归并排序。