数据结构保研面试--h版

重点内容:

数据结构:二叉排序树、平衡二叉树、红黑树、B树、B+树、哈希表

算法:最短路径算法、快速排序、归并排序

1.介绍一下时间复杂度和空间复杂度。

时间复杂度 :时间复杂度衡量了算法运行所需的时间资源。时间复杂度分析关注的是算法在最坏情况下的时间消耗。

空间复杂度:空间复杂度衡量了算法运行所需的存储空间。空间复杂度分析关注的是算法在最坏情况下所使用的额外空间。

2. 请说出常见的存储结构,以及这些存储结构的优缺点。★★

(1)顺序存储

把++++逻辑上相邻的元素存储在物理位置上也相邻的存储单元中++++,元素之间的关系由存储单元的邻接关系来体现。

优点:随机存取,每个元素占用最少的存储空间(存储密度高);

缺点:只能使用相邻的一整块存储单元,因此可能产生较多的外部碎片。

(2)链式存储

不要求逻辑上相邻的元素在物理位置上也相邻,借助指示元素存储地址的++++指针++++来表示元素之间的逻辑关系。

优点:是不会出现碎片现象,能充分利用所有存储单元(利用率高);

缺点:是每个元素因存储指针而占用额外的存储空间(指针开销),且只能实现顺序存取。

(3)索引存储

在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)。

优点:是检索速度快;

缺点:是附加的索引表额外占用存储空间。另外,增加和删除数据时也要修改索引表,因而会花费较多的时间。

(4)散列存储

根据元素的关键字直接计算出该元素的存储地址,又称哈希存储。

优点:是检索、增加和删除结点的操作都很快;

缺点:是若散列函数不好,则可能出现元素存储单元的冲突,而解决冲突会增加时间和空间开销。

3. 循环比递归的效率高吗?★

循环和递归两者是可以互换的,不能决定性的说循环的效率比递归高。

(1)递归

优点:代码简洁清晰,容易检查正确性;

缺点:当递归调用的次数较多时,要增加额外的堆栈处理,有可能产生堆栈溢出的情况,对执行效率有一定的影响。

(2)循环

优点:结构简单,速度快;

缺点:它并不能解决全部问题,有的问题适合于用递归来解决不适合用循环。

4. 线性表包括了顺序表和链表,请比较它们的区别。★★

(1)存取(读写)方式

  1. 顺序表可以顺序存取,也可以随机存取。
  2. 链表只能顺序存取。

(2)逻辑结构与物理结构

  1. 顺序存储:逻辑上相邻的元素,物理存储位置也相邻。
  2. 链式存储:逻辑上相邻的元素,物理存储位置不一定相邻,对应的逻辑关系是通过指针链接来表示的。

(3)查找、插入和删除操作

  1. 查找:

对于**++++按值查找++++**,顺序表无序时,两者的时间复杂度均为O(n); 顺序表有序时,可采用折半查找,此时的时间复杂度为O(logn) 。

对于**++++按序号查找++++**,顺序表支持随机访问,时间复杂度仅为0(1), 而链表的平均时间复杂度为O(n)。

  1. 插入、删除:

顺序表的插入、删除操作,平均需要移动半个表长的元素。

链表的插入、删除操作,只需修改相关结点的指针域即可。由于链表的每个结点都带有指针域,故而存储密度不够大。

5. 说说栈和队列的区别。★★

栈和队列都是操作受限的线性表。

栈:后进先出 LIFO,入栈出栈都在栈顶

队列:先进先出 FIFO,队尾插入,队头删除

6. 简要说说共享栈。★

让两个顺序栈共享一个一维数组空间 ,将两个栈的栈底分别设置在共享空间的两端,两个栈顶向共享空间的中间延伸。这样能够更有效的利用存储空间,只有在整个存储空间被占满时才发生溢出。

优点:空间利用率极高,只有整个数组满了才会溢出。

7. 如何区分循环队列是队空还是队满?★★

普通情况下,循环队列队空和队满的判定条件是一样的,都是Q.front == Q.rear。

为了区分可采用两种方法:

  1. 牺牲一个位置

队空:front == rear

队满:(rear+1)% MaxSize == front

  1. 增加 size 计数器 表示元素个数
    队空:size==0
    队满:size==MaxSize

8. 说说栈在括号匹配中的算法思想。★★

  1. 出现的凡是左括号,则进栈;
  2. 出现的是右括号,如果栈不空而且栈顶元素是左括号,那么相匹配,否则不匹配。
  3. 表达式 检验结束时,如果栈空则表明表达式中匹配正确,否则表明"左括号"有余

9. 说说栈在后缀表达式求值的算法思想。★★

顺序扫描表达式的每一项,然后根据它的类型做如下相应操作:

  1. 若该项是操作数,则将其压入栈中;
  2. 若该项是操作符,则连续从栈中退出两个操作数y 和x,形成运算指令XY,并将计算结

果重新压入栈中。

  1. 当表达式的所有项都扫描并处理完后,栈顶存放的就是最后的计算结果。

10. 说说栈在计算机系统中的应用。★★

1 函数调用:栈在函数调用中扮演着重要角色。每当一个函数被调用时,函数的参数、局部变量和返回地址等信息都会被保存在栈中的帧中。函数执行完毕后,相应的帧会被从栈中弹出,恢复上一个函数的执行。

2 表达式求值:编译器和解释器通常使用栈来求解表达式。例如,中缀表达式转换为后缀表达式时,使用栈来调整操作符的顺序。

3 括号匹配:栈也常用于括号匹配的问题。通过遍历输入字符串并将左括号入栈,当遇到右括号时,检查栈顶元素是否与之匹配。可以利用栈的后进先出(LIFO)特性来快速判断括号是否匹配。

11. 说说队列在计算机系统中的应用。★★

1 任务调度 :操作系统中的任务调度器通常使用队列数据结构来管理和调度进程或线程。

2 缓冲区管理:网络通信、磁盘I/O等场景中常需要使用队列来处理数据的缓冲区。

3 消息传递:消息队列是实现异步通信和解耦的重要方式。多个组件之间通过将消息放入队列来进行通信,接收者可以从队列中取出并处理消息。

4 树的层次遍历&图的广度优先遍历

12. 介绍一下KMP算法。★★★

KMP算法是一种高效的字符串匹配算法,用于**++++在一个文本串中查找一个模式串的出现位置++++**。KMP算法通过利用模式串自身的信息,在匹配过程中避免不必要的回溯,从而提高匹配效率。

KMP算法的核心思想是使用一个部分匹配表,也称为next数组,来记录模式串中每个位置的 ++++最长公共前后缀的长度++++。这样,在匹配失败时,可以根据部分匹配表的信息,将模式串向右移动尽可能少的步数。

vnexti:第i位前的最长前后缀长度+1

KMP算法的时间复杂度O(n+m),朴素算法的时间复杂度O(n*m),n和m是两个串的长度。

KMP算法的具体步骤如下:

  1. 预处理next数组:对于模式串,遍历每个位置,计算该位置之前子串的最长公共前后缀的长度,并保存到next数组中。
  2. 匹配过程:从文本串的起始位置开始,用两个指针分别指向文本串和模式串的当前位置,逐个字符进行比较。
  1. 如果当前字符匹配成功,则两个指针同时向后移动一位。
  2. 如果当前字符匹配失败:

根据next数组中的信息,将模式串向右移动尽可能少的步数。根据当前失败位置的部分匹配值,向右移动模式串的指针。

同时,保持文本串的指针不动,继续与模式串的新位置进行比较。

如果模式串的指针移到末尾,则表示匹配成功,返回在文本串中的起始位置。如果文本串的指针移到末尾,则表示未找到匹配,返回-1。

13. 满二叉树和完全二叉树有什么区别?★★

满二叉树:对于一颗高为h的二叉树,结点个数为2^h-1,表现为除了最后一层叶子结点之外,根节点以及分支结点都有两个孩子,即每一层都是满的。

完全二叉树 :在满二叉树的基础上,在最后一层从右往左依次删除一定数量的叶子结点所形成的二叉树。++++完全二叉树的特点是叶子结点只出现在倒数第一和第二层,且如果有分支结点仅有一个孩子,那只能是左孩子。++++

满二叉树和完全二叉树可以用顺序存储结构来存储。

14. 如何由遍历序列构造一棵二叉树? 必须有中序遍历

(1)由二叉树的**++++先序序列和中序序列++++**可以唯一地确定一棵二叉树。

在先序遍历序列中,第一个结点一定是二叉树的根结点;

而在中序遍历中,根结点必然将中序序列分割成两个子序列,前一个子序列是根结点的左子树的中序序列,后一个子序列是根结点的右子树的中序序列。根据这两个子序列,在先序序列中找到对应的左子序列和右子序列。在先序序列中,左子序列的第一个结点是左子树的根结点,右子序列的第一个结点是右子树的根结点。如此递归地进行下去,便能唯一地确定这棵二叉树。

(2)由二叉树的**++++后序序列和中序序列++++**也可以唯一地确定一棵二叉树。

因为后序序列的最后一个结点就如同先序序列的第一个结点,可以将中序序列分割成两个子序列,然后采用类似的方法递归地进行划分,进而得到一棵二叉树。

(3)由二叉树的**++++层序序列和中序序列++++**也可以唯一地确定一棵二叉树。

需要注意的是,若只知道二叉树的先序序列和后序序列,则无法唯一确定一棵二叉树。

15. 简要说说线索二叉树。★

对于n个结点的二叉树,在二叉链存储结构中有n+1个空链域 (n+1是怎么来的:m=n-1,2n-m=n+1),利用**++++这些空链域存放在某种遍历次序下该结点的前驱结点和后继结点的指针++++**,这些指针称为线索,加上线索的二叉树称为线索二叉树。

根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。

线索二叉树++++解决了无法直接找到该结点在某种遍历序列中的前驱和后继结点的问题。++++

16. 简要说说树的存储结构。★

(1)双亲表示法

这种存储方式采用一组连续空间来存储每个结点,同时在每个结点中增设一个指针,指示其双亲结点在数组中的位置。

优点:该存储结构可以很快得到每个结点的双亲结点

缺点:但求结点的孩子时需要遍历整个结构。

(2)孩子表示法

孩子表示法是将每个结点的孩子结点都用单链表链接起来形成一个线性结构,此时n 个结点就有n个孩子链表(叶子结点的孩子链表为空表)

这种存储方式寻找孩子容易,而寻找双亲的操作麻烦。

(3)孩子兄弟表示法 左孩子右兄弟

孩子兄弟表示法以二叉链表作为树的存储结构。孩子兄弟表示法使每个结点包括三部分内容:结点值、指向结点第一个孩子结点的指针,及指向结点下一个兄弟结点的指针。

这种存储表示法比较灵活,其最大的优点是 ++++可以方便地实现树转换为二叉树的操作++++。这种存储方式寻找孩子容易,而寻找双亲的操作麻烦。若为每个结点增设一个parent域指向其父结点,则查找结点的父结点也很方便。

(4)二叉树的存储

①顺序存储:

按照++++二叉树层序遍历++++的顺序将结点存储于顺序表中,特别注意空节点也需要占有位置。若某结点下标为i,则其左孩子下标为2i,右孩子下标为2i+1,父节点下下标为i/2向下取整。该存储结构适合存储完全二叉树;

②链式存储:

每个结点通常一个数据域与两个指针域,分别指向自己的左孩子和右孩子。而为了充分利用左右孩子指针,可以将左孩子指向自己的前序、中序或后序遍历的直接前序,右孩子指向直接后继,从而形成二叉线索树,方便查找。

17. 简要说说二叉搜索树。★★ BTS

二叉搜索树性质:非空左子树的所有键值小于其根节点的键值;非空右子树的所有键值大于其根节点的键值;左右子树都是二叉搜索树。

对二叉排序树进行中序遍历,即可以得到从小到大有序的关键码序列。它利用了二分的思想,可以快速查找到关键码,查找效率为O(logn)。

缺点:如果按照从小到大插入构建BST,会导致查找效率退回O(n)级别。

改进:插入时要旋转保证平衡因子绝对值不大于1,于是产生了AVL树。

18. 简要说说平衡二叉树。★★★

二叉平衡树是一种特殊的二叉排序树,它满足对于树上的任意一个结点,其左子树的深度与右子树的深度之差的绝对值不超过1。

平衡因子可以用于描述二叉平衡树,平衡因子是某个结点的左子树深度与右子树深度之差,对于一棵二叉平衡树,其任意结点的平衡因子只能是-1,0或1。

缺点:如果插入操作比查询操作多,AVL就要花费大量开销做旋转来调整节点以保证树的平衡。

改进:为了减少旋转开销,引入了红黑树。

19. 简要说说二叉搜索树的查找、插入和删除过程。★★★

****查找:****从根节点开始,大于往右子树查找,小于往左子树查找

****插入:****基于查找操作进行,查找合适的位置进行插入。该合适的位置指的是按照查找步骤进行到的叶子节点处,若欲插入的关键码大于该叶子结点,则插入为右孩子,反之为左孩子。插入的结点必须是叶子结点。若开始树空,则直接成为根节点;若欲插入的关键码已存在,则插入失败。二叉树的构造过程也是不断插入的过程。

****删除:****同样是基于查找操作,首先查找到欲删除的结点。此时,删除结点通常包括三种情况

①若删除的结点是叶子结点,则可以直接将结点删去;

②若删除的结点只有左孩子或者右孩子,则用它的孩子代替它;

③若删除的结点有左右孩子,则可以寻找其中序遍历的直接前驱或者直接后继代替它,再删去该直接前驱或直接后继。 也就是左子树的最大值或者右子树的最小值替代

20. 简要说说红黑树。★★

红黑树中的每一个结点的颜色不是黑色就是红色。

根结点和所有外部结点(NULL节点、叶节点)的颜色是黑色。

从根结点到外部结点的途中没有连续两个结点的颜色是红色。

所有从根到外部结点的路径上都有相同数的黑色结点数量。

对于红黑树搜索的时间复杂度为O(logn)。插入删除频繁就选红黑树

21. 简要说说B树。★★★★

引入B树的原因:

若普通二叉树作为文件系统的索引,随着数据的插入,发现树的深度会变深。而文件系统的索引在磁盘上,磁盘的数据要加载到内存中才能处理,需要反复IO影响查询的效率,于是引入了B树可以作为文件系统的索引

B树是多叉树,一棵m阶B树的性质:

  1. 节点关键字数量

非根节点:关键字个数最多 m-1,最少 ⌈m/2⌉-1

根节点:关键字个数最多 m-1,最少1

  1. 子节点分叉数

非根节点:子节点最多 m 个,最少 ⌈m/2⌉ 个

根节点:子节点最多 m 个,最少 2 个

  1. 结构特性

所有失败节点(叶子节点)都在同一层

B 树的每个节点中可以同时存放关键字(键)与对应数据

  1. ++++简要说说B+树。★★★★++++

1 引入B+树的原因:

B 树的每个节点都会存放实际数据 ,而数据通常比关键字占用空间大得多,导致单个磁盘块能容纳的索引项变少。 在数据量很大时,树的深度依然偏大,磁盘 I/O 次数仍较多。

因此引入 B+ 树

  • 非叶子节点只存关键字和指针,不存数据
  • 单个磁盘块能容纳更多索引项
  • 树的深度进一步降低,磁盘 I/O 更少,查询更稳定、更快

2 m 阶 B+ 树的性质

  • 关键字与分叉数
  1. 非根节点:关键字最多 m 个,最少 ⌈m/2⌉ 个;
  2. 分叉数与关键字数相等
  3. 根节点:关键字最多 m 个,最少 1 个
  • 结构特点
  1. 分支节点仅保存其子节点中的最大(或最小)关键字与子节点指针,仅作索引使用
  2. 所有数据都存放在叶子节点中
  3. 叶子节点包含全部关键字及对应数据指针,并按关键字++++有序排列++++
  4. ++++相邻叶子节点通过链表指针相互链接++++,便于范围查询
  5. 所有叶子节点在同一层次
  1. B+树的应用:

数据库中,B+树常被用作索引结构,用于快速查找和排序大量数据。如主键索引、唯一索引、辅助索引等。

文件系统中,B+树通常用于管理磁盘上的文件块和索引节点。

23. 简要说说B树和B+树的区别。★★★★★

m阶B树和B+树:B树的根节点关键字个数取值1到m-1,B+树的根节点关键字取值1到m;B树非根节点关键字取值ceil(m/2)-1到m-1,B+树非根节点关键字取值ceil(m/2)到m。

B树分叉个数等于关键字个数+1,B+树分叉个数等于关键字个数。

++++B树的每个节点既有关键字,又有数据;B+树的数据只在叶子上,非叶子节点只有关键字。++++

++++B+树的叶子节点相互之间有一个链路,B树没有。++++

当查找数据时,从根出发,B树可能不需要查找到叶子节点就可以找到数据,而B+树要找到叶子节点才找到数据。

24. 什么是哈夫曼树?如何构造?哈夫曼树的应用★★★

哈夫曼树又称为最优二叉树 ,其特点是,给定一组带权的叶子结点,若构造所得到的++++二叉树拥有最小的带权路径长度WPL++++,则称该二叉树为一棵哈夫曼树。

构造:

将带权叶子结点并入一个集合,首先在集合中挑选出两个权值最小的叶子结点进行合并得到新的结点加入集合,再将两个被选中的结点剔除出集合。在树的构造上,将这两个结点作为叶子结点衔接到合并而成的新结点上。重复以上过程直到集合中只有一个元素,哈夫曼树则完成构造

应用:哈夫曼树的应用是哈夫曼编码, 其特点是消除了++++编码前缀相同的二义性++++(哈夫曼编码是一种前缀编码,前缀编码就是任何一个编码都不是另外一个编码的前缀)。在哈夫曼编码中,只有哈夫曼树的叶子结点可以进行编码。

25. 简单介绍一下并查集,说说如何改进?★★★

并查集是一种用于解决集合合并和查询问题 的数据结构。它能够高效地进行集合的合并和判断两个元素是否属于同一集合,并在实际应用中常用于++++解决图的连通性、最小生成树++++等问题。

在并查集中,每个元素被看作是一个节点,并以树的形式组织。每个树的根节点代表一个集合,而每个节点则指向其父节点,形成一棵树。

并查集的基本操作:

  1. Union:用于将两个集合合并为一个集合。通过找到两个元素所属集合的根节点,将其中一个根节点的父节点指向另一个根节点,实现合并操作。
  2. Find:查找根节点,用于确定元素所属的集合。通过迭代地或递归向上查找父节点,直到找到根节点,即可确定所属的集合。

改进措施:

  1. 路径压缩:是指在进行Find操作时,++++将节点指向根节点的路径上的所有节点直接连接到根节点,++++可以减小树的高度,加快查找。可以使用递归或迭代的方式进行路径压缩。

  2. 按秩合并:是指在进行Union操作时,++++将高度较低的树合并到高度较高的树上++++,从而保持树的平衡性。可以通过记录每个集合的秩(即树的高度或节点数量)来实现按秩合并。

  3. 简单介绍一下dfs和bfs,并说说它们的区别?★★★★

dfs:使用栈数据结构来辅助实现深度优先搜索。

dfs是按照一个路径一直访问到底,当前节点没有未访问的邻居节点时,然后回溯到上一个节点,不断的尝试,直到访问到目标节点或所有节点都已访问。

dfs不保证找到最短路径,因为它一直往深处搜索。

bfs:使用队列数据结构来辅助实现广度优先搜索。

bfs是按层次访问的,先访问源点,再访问它的所有相邻节点,并且标记结点已访问,根据每个邻居结点的访问顺序,依次访问它们的邻居结点,并且标记节点已访问,重复这个过程,一直访问到目标节点或无未访问的节点为止。

bfs能够保证找到的路径是最短路径,因为它按照距离起始节点的层级进行搜索。

使用场景:

dfs适用于找到一个可行解,不需要找最短路径的情况。它的空间复杂度较低,适合在深度方向上搜索,例如拓扑排序、连通性判断、回溯等问题。

bfs适用于找到最短路径。它的时间复杂度较低,适合在广度方向上搜索,例如寻找最短路径、连通性判断、社交网络中查找关系等问题。

27. 简单介绍一下最短路径算法(Dijkstra、Floyd等)?★★★★★

最短路径算法通常有Bfs算法、Dijkstra算法和Floyd算法。

Bfs只能处理无权图,Dijkstra算法可以进一步解决带权图问题,Floyd可以进一步解决带负权边图问题。

(1) Bfs :通过队列来实现,首先将单原点加入队列 。每次循环将队列中队头元素弹出,并且将与该元素所代表的结点相邻的结点加入队列,直到队列为空。每次循环代表距离加一,Bfs可以找出单源点到其他结点的路径长度,取最小即为最短路。

(2) Dijkstra :应用了贪心 的思想,通常解决单源点问题,时间复杂度为O(n²),堆优化后是O(nlogn)。

声明:vis\[\]表示这个节点是否访问,visu=1表示出圈,dis\[\]表示该点到原点的最短路径,u表示圈外距离圈内最小的点,s为源点。

①初始时,将所有点都在圈内,所有节点vis\[\]=0,dis\[\]=inf,diss=0。

②其次从圈内选择距离最小的点u,打标记出圈visu=1。

③对u的所有出边进行松弛操作,v是u所指向的节点,w是u->v的权值,若disu+w<disv,那么disv=disu+w。

④重复②③直到所有节点更新完成。

(3) Floyd: 应用了动态规划的思想,通常解决多源点问题,时间复杂度为O(n³)

首先初始化,声明dp数组,dpij表示从i到j的权值,一开始数组中值均为inf,而后更新dpii所有点到自己的权值是0。

输入每条边的结点和权值u,v,w,更新dpuv=w,表示从u到v的权值是w。

利用三层循环,然后逐步试探当前加入的点。

dpij=min(dpij,dpik+dpkj);如果i到k的权值+k到j的权值比i到j的权值小,那么进行松弛,否则就继承原来的dpij

直到循环结束

(4) 其他最短路径算法补充:

Bellman-ford算法,单源最短路径,能处理负权边,时间复杂度为O(VE)

SPFA算法,求单源最短路径,能处理负权边,是Bellman-ford的优化,平均时间复杂度低于Bellman-ford,最坏情况仍为O(VE)。

A*算法 ,适用于在有向图中找到从起始节点到目标节点的最短路径,通过++++启发式函数来估计每个节点的代价。++++该算法综合考虑当前节点的路径长度和启发式评估值,通过优先级队列来选择下一个扩展的节点。

28. 简单介绍一下求最小生成树的算法?★★★★

(1) Prim :Prim算法是一种贪心算法,从一个起始节点开始逐步扩展最小生成树的边

首先选择一个起始节点,然后将该节点标记为已访问。

通过不断选择与已访问节点相连且权重最小的边,并将相连节点标记为已访问,将这条边加入到最小生成树中。

重复上述步骤,直到所有节点都被访问过,即构建出最小生成树。

(2) Kruskal: Kruskal算法也是一种贪心算法,通过按照边的**++++权重从小到大++++**的顺序逐步构建最小生成树。

将图中的所有边按照权重从小到大进行排序。

依次遍历排序后的边,如果当前边的两个端点不在同一棵树中(即不会形成环,常常借助并查集实现),则将该边加入最小生成树中,并将两个端点所在的树合并为一棵树。

重复上述步骤,直到最小生成树包含图中的所有节点。

(3) 应用场合:

Prim算法适合在稠密图中进行求解,时间复杂度为O(v²),其中v为节点数。

Kruskal算法适合在边稀疏图中进行求解,时间复杂度为O(eloge),其中e为边数。

29. 介绍一下哈希表,如何构造哈希函数,如何解决哈希冲突?★★★★

哈希表又称为散列表,是根据**++++关键字的值直接进行访问的数据结构++++**,即它通过把关键码的值映射到表中的一个位置以加快查找速度,其中映射函数叫做哈希函数,存放记录的数组叫做哈希表。

(1)哈希函数的构造方法:

① 直接定址法:取关键字的某个线性函数值作为散列地址,H(key)=a*key+b。

② 除留余数法:取关键字对p取余的值作为散列地址,即H(key)=key%p,p尽量选择质数,为了使散列分布更均匀。

(2)哈希冲突的解决方法:

① 开放地址法:当发生冲突时,使用一定的探测序列方法,在哈希表中寻找下一个可用的空槽位,将冲突的元素插入到这个空槽位中。常见的探测序列方法有线性探测、二次探测等。

②链地址法:将哈希表的每个索引位置看作一个链表的头节点,当发生冲突时,将冲突的元素插入到链表中即可。这样,每个索引位置都可以存储多个元素。

③ 再哈希法:当发生冲突时,使用另一个哈希函数重新计算新的索引位置,直到找到一个空槽位来解决冲突。

30. 介绍一下各种排序算法的性能?★★★★★★

不稳定的排序算法有:希尔排序、选择排序、堆排序、快速排序。

  1. 介绍一下插入排序?★★★★

每次将一个待排序的记录插入到前面已经排好序的序列当中

|----------------|--------------------|---------------------------------------|
| 直接插入 | 折半插入 | 希尔排序 |
| 顺序找到插入的位置,移动元素 | 折半查找到插入的位置,在统一移动元素 | 先将排序表分为子表,对各个子表分别进行直接插入排序。缩小增量d,直到d=1 |
| 稳定 | 稳定 | 不稳定 |
| | 适用于顺序存储的线性表 | 适用于顺序存储的线性表 |

32. 介绍一下选择排序?★★★

将数组分为已排序区和未排序区。初始时,已排序区为空,而未排序区包含所有元素。

从未排序区中找到最小的元素,并记录其索引。

将最小元素与未排序区的第一个元素交换位置,将其放入已排序区的末尾。

重复步骤2和步骤3,直到未排序区的元素全部交换完毕,得到最终的有序数组。

选择排序的平均时间复杂度是O(n²),最坏时间复杂度是O(n²),空间复杂度是O(1),是一种不稳定排序。

33. 介绍一下冒泡排序 稳定 ?★★★

它重复地遍历待排序数组,依次比较相邻的元素,并将较大的元素交换到右侧,从而逐步将最大的元素沉到数组的末尾。

相邻元素比较,如果前面元素比后面更大,则交换位置。第一轮把最大的元素放到末尾,第二轮把第二大的元素放到倒数第2个的位置,直到所有都排好序。

冒泡排序的平均时间复杂度是O(n²),最坏时间复杂度是O(n²),空间复杂度是O(1),是一种稳定排序。

34. 介绍一下快速排序 不稳定 ?★★★★★★

快速排序采用了分治 的思想。快速排序的++++核心思想++++ ++++:++++选择一个基准元素,通过将数组中的元素按照基准元素进行划分,使得左侧的元素都小于基准元素,右侧的元素都大于基准元素。然后对左右两个子数组分别进行递归排序,直到整个数组有序。

具体来说,选一个基准元素。例如选取最左边的元素记作基准。定义i和j两个指针,一开始分别指向l和r,j用来寻找比基准小的元素,i用来寻找比基准大的元素,若i和j都找到而且i<j那么ai和aj交换,从而保证了左边的小于pivot,右边的大于pivot。若最后i==j,那么将pivot移动到该位置。

快速排序的平均时间复杂度是O(nlogn),最坏时间复杂度是O(n²),空间复杂度是O(1),是一种不稳定排序。

35. 为什么快速排序最坏情况会退化成O(n²)?★★★★★★

最坏情况发生在待排序的序列 ++++已经有序或近乎有序++++ 的情况下。在这种情况下,如果每次选择的基准元素都是当前子数组的最大或最小值,那么快速排序的分割过程将会非常不平衡,导致递归树的高度接近于n。

在这种情况下,每次划分只能将序列分成一个空的子数组和一个包含n-1个元素的子数组,而不是将序列均匀地分成两个大小相等的子数组。

36. 介绍一下归并排序?★★★★★★

归并排序采用了分治的思想。归并排序的核心思想是++++将待排序数组逐步分割成单个元素,然后将这些单个元素合并成有序的数组++++。它通过不断地将两个有序的子数组合并成一个更大的有序数组,最终得到整个数组有序。

归并排序的平均时间复杂度是O(nlogn),最坏时间复杂度是O(nlogn),空间复杂度是O(n),是一种稳定排序。

37. 简述一下快速排序和归并排序的优缺点(从平均最坏时间复杂度、空间复杂度、稳定性的角度)。★★★★★★

(1)快速排序

优点:

①平均时间复杂度较低:快速排序的平均时间复杂度为O(nlogn),在大多数情况下都能够达到较好的排序效果。

②空间复杂度较低:快速排序通常只需要使用很少的额外空间,只需对原数组进行原地操作。

缺点:

①最坏情况下的性能:在最坏情况下,即待排序序列已经有序或近乎有序时,快速排序的时间复杂度会退化到O(n²),导致性能下降。

②不稳定性:快速排序是一种不稳定的排序算法,在交换元素的过程中可能改变相同关键字元素的相对顺序。

(2)归并排序

优点:

①稳定性:归并排序是一种稳定的排序算法,它能够保持相同关键字元素的相对顺序不变。

②适用于外部排序:归并排序的特点使其非常适用于外部排序,即当排序的数据量太大无法完全加载到内存时,可以通过分阶段地读取和写入数据进行排序。

③性能稳定:归并排序的时间复杂度始终保持在O(nlogn),无论是最佳、最坏还是平均情况下。

缺点:需要额外的空间:归并排序需要额外的空间来存储临时数组,因此它的空间复杂度相对较高。

39. 为什么排序需要稳定?★★★★

排序算法的稳定性意味着对于具有相同关键字的元素,排序后它们的相对顺序保持不变。在很多实际应用中,我们需要保持数据中相等元素的顺序关系。

例如,在排序员工工资的数据时,如果有多名员工拥有相同的工资水平,我们可能希望按照他们的入职时间来排序,以维持他们在公司内部的先后顺序。如果使用不稳定排序,就可能打乱他们的相对顺序。

40. 归并排序的最坏时间复杂度优于快排,为什么我们还是选择快排?★★★★★★

快速排序通常比归并排序更快。尽管快速排序在最坏情况下的性能可能较差,但在大多数情况下,它的平均时间复杂度要比归并排序低。

快速排序是原地排序算法。原地排序算法是指排序过程中不需要额外的存储空间,只利用原始输入数组进行排序。

快速排序的实现相对简单。相比于归并排序,快速排序的实现更为简洁,代码量更少。

41. 介绍一下堆排序?★★★★★★

堆排序可以分为两个主要步骤:建堆和排序。

建堆的步骤如下:

(1)若数组有n个元素,从第n/2个元素(非叶节点)开始一直到第1个元素,进行堆的调整。

(2)对于每个非叶子节点,进行一次下沉操作,将当前节点与其子节点进行比较,如果不满足堆的性质,则交换位置。

(3)重复步骤2,直到整个数组被构建成一个堆,即满足父节点大于等于子节点(大顶堆)或父节点小于等于子节点(小顶堆)的性质。

排序的步骤如下:

(1)首先,将建好的堆中的根节点与最后一个元素交换位置。

(2)然后,将堆的大小减1,并对新的根节点进行一次下沉操作,以找到新的最大值。

(3)重复步骤1和步骤2,直到堆的大小为1,即所有元素都排好序。

堆的插入的步骤如下:

(1)把插入的元素放在数组的末尾,数组的长度+1。

(2)首先,该节点将其与其父节点进行比较,如果该节点的值大于父节点的值,则交换位置。

(3)继续将该节点与其新的父节点进行比较,重复上述步骤,直到节点上浮到正确的位置或者达到根节点。(实际上是堆的上浮)

堆的下沉(堆的调整):

(1)用于将一个节点下沉到合适的位置以满足堆的性质。

(2)从待调整节点开始,将其与其左右子节点中较大的节点进行比较,如果该节点的值小于某个子节点的值,则交换位置。

(3)继续将该节点与其新的子节点进行比较,重复上述步骤,直到节点下沉到正确的位置或者达到叶子节点。

42. 请你写出以上排序的代码。★★★★★★

冒泡排序:

选择排序:

插入排序:

快速排序:

归并排序:

堆排序:

43. 如何在100万个元素中快速找到前10个最大的数?(Top K问题)★★★

(1)最小堆法(堆中维护的是当前遍历序列中最大的10个元素)

构造一个长度为10的最小堆,遍历100万个元素。

① 堆元素个数小于10时,直接插入该元素。

② 堆元素个数等于10时,如果该元素比堆顶元素(堆中最小的元素)还大,那么移除堆顶元素,把这个元素放入堆然后进行调整。

遍历完毕后,最后堆中剩下的10个元素就是前10个最大的数。

最小堆法的时间复杂度为O(nlogK),空间复杂度是O(K),K=10。

(2)快速选择算法

选择枢轴,然后进行递归选择:

① 如果枢轴位置恰好是第10大的数的位置,那么前10个最大的数就在枢轴左侧。

② 如果枢轴位置大于10,则在左侧数组中递归查找。

③ 如果枢轴位置小于10,则在右侧数组中递归查找,但只关注前10个最大的数。

不断递归。直到找到前10个最大的数。平均时间复杂度为O(n),但最坏情况下会退化到O(n^2)。

44. 如何快速判断一个元素是否在1亿个元素中?★★

使用HashMap很难处理,因为非常消耗内存空间,很可能会导致内存溢出。因此可以采用位图法(布隆过滤器)。

首先对1亿个元素j进行遍历,对每一个元素使用一个函数或者多个哈希函数进行映射,把它映射到一个具有32比特位的位图中,那么这个位图理论上可以存储2^32个元素,且只占用512MB内存(2^32bit = 2^29B = 2^9MB = 512MB,事实上当2^27最接近1亿,也可以使用27位的位图)。等1亿个元素都存储到位图中时,再对想要判断的元素进行相同的哈希函数映射,如果一个元素实际不存在于位图,但是也有被判断为存在,这是因为有可能会产生哈希冲突。

如果构建好了位图,那么判断一个元素是否在1亿个元素的时间复杂度是O(1)。