数据结构 面试相关整理

2022/3/21 6:29:43

本文主要是介绍数据结构 面试相关整理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

其他面试整理在这里

排序

  1. 列举八大排序算法和他们的时间/空间复杂度
排序方式平均情况时间复杂度最坏情况时间复杂度最好情况时间复杂度空间复杂度稳定性复杂性
插入排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)稳定简单
希尔排序 O ( n 1.3 ) O(n^{1.3}) O(n1.3)-- O ( 1 ) O(1) O(1)不稳定较复杂
冒泡排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n ) O(n) O(n) O ( 1 ) O(1) O(1)稳定简单
快速排序 O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n 2 ) O(n^2) O(n2) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( log ⁡ n ) O(\log n) O(logn)不稳定较复杂
选择排序 O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( n 2 ) O(n^2) O(n2) O ( 1 ) O(1) O(1)不稳定简单
堆排序 O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( 1 ) O(1) O(1)不稳定较复杂
归并排序 O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n log ⁡ n ) O(n\log n) O(nlogn) O ( n ) O(n) O(n)稳定较复杂
基数排序 O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( d ( n + r ) ) O(d(n+r)) O(d(n+r)) O ( r ) O(r) O(r)稳定较复杂

1)插入排序:将数组分为无序区和有序区两个区,然后不断将无序区的第一个元素按大小顺序插入到有序区中去,最终将所有无序区元素都移动到有序区完成排序。
2)希尔排序:又称增量缩小排序。先将序列按增量(互不相邻的元素)划分为元素个数相同的若干组,使用插入排序法进行排序,然后不断缩小增量直至为1,最后使用插入排序完成排序。
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
3)冒泡排序:将序列划分为无序和有序区,不断通过交换较大元素至无序区尾完成排序。
4)快速排序:针对一个锚点privot,另他的左序列均小于privot,右序列均大于privot,递归对左右序列进行排序,直至所有序列全部排序完成
5)选择排序:将序列划分为无序和有序区,寻找无序区中的最小值和无序区的首元素交换,有序区扩大一个,循环最终完成全部排序。
6)堆排序:利用大根堆或小根堆思想,首先建立堆,然后将堆首与堆尾交换,堆尾之后为有序区。
7)归并排序:将原序列划分为有序的两个序列,然后利用归并算法进行合并,合并之后即为有序序列。
8)基数排序:将数字按位数划分出n个关键字,每次针对一个关键字进行排序,然后针对排序后的序列进行下一个关键字的排序,循环至所有关键字都使用过则排序完成。

  1. 基数排序和桶排序的区别
    1)基数排序:根据键值的每位数字来分配桶
    2)桶排序:每个桶存储一定范围的数值
  2. 桶排序的过程
  3. 选择排序为什么是不稳定的排序
    因为在无序区找到最小值之后,进行的是与无序区的首位数值的交换操作,因此是不稳定的排序。
  4. 快速排序的优化
    已知由于privot的选择是第一个元素,而快速排序最差情况的产生是输入序列为有序状态,因此通过随即选择privot可以优化快速排序。
  5. 堆排序的过程
    以大根堆为例,首先要对无序数组进行建堆操作,然后取root作为最大值,再删除root元素,从root开始调整堆,重新得到最大值root,以此类推,直到所有数组被删除完成就得到了排序好的数组(由小到大且不需要额外空间)
  6. 堆调整的过程
    堆调整的时间复杂度是 O ( log ⁡ n ) O(\log n) O(logn)
    通过数组来表示大根堆,数组的下标为1~n,即有如下描述,下面与堆相关的描述均相同
    root -> i = 1
    parent[i] = i / 2
    i.left_child = 2i
    i.right_child = 2i + 1
    
    max_heapify(A, i):		// x的左子树和右子树都是大根堆
    	compare A[i], A[2i], A[2i+1]
    	if A[i] >= A[2i] && A[i] >= A[2i+1]
    		调整完成
    	if A[2i] >= A[i] && A[2i] >= A[2i+1]
    		exchange A[2i], A[i]
    		max_heapify(A, 2i)
    	if A[2i+1] >= A[i] && A[2i+1] >= A[2i]
    		exchange A[2i+1], A[i]
    		max_heapify(A, 2i+1)
    
  7. 堆建立的过程
    build_heap(A):
    	for i = n / 2 downto 1		// 从n/2+1 ~ n 都是叶子节点,不用调整
    		do max_heapify(A, i)
    
  8. 堆插入的过程
    insert_heap(A, x):
    	A[n+1] = x
    	i = n+1
    	while A[i/2] < A[i]:				// 递归和父节点比较
    		exchange A[i/2], A[i]
    		i = i / 2
    
  9. 堆删除的过程
    一般只删除根节点,否则需要重新建堆,因为做的是交换操作,而且只(在数值上)更改了数组大小,所以排序完之后,会不占用额外内存,且从小到大排序
    delete(A):
    	exchange A[1], A[n]
    	n = n - 1
    	max_heapify(A, 1)
    

基本

  1. 树的前序遍历
  2. 树的中序遍历
  3. 树的后序遍历
  4. 树的dfs非递归写法
  5. 树的bfs非递归写法

二叉树

  1. 已知前序和后序能不能构建二叉树
    不能,“根左右”和“左右根”无法判断左右子树
  2. 什么是二叉搜索树
    1)二叉查找树可以是一棵空树
    2)或者是具有下列性质的二叉树
    • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
    • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
    • 它的左、右子树也分别为二叉排序树。
  3. 二叉搜索树的优点
    1) 有链表的快速插入与删除操作的特点
    2) 有数组快速查找的优势
  4. 如何对二叉搜索树进行节点删除
  5. 如何对二叉搜索树进行节点插入
  6. 如何对二叉搜索树进行节点的查找
  7. 什么是完全二叉树
  8. 什么是平衡二叉树
  9. 一个具有N个节点的完全二叉树深度是多少,叶子节点是多少

红黑树

  1. 什么是红黑树
  2. 红黑树的特点

其他

  1. 什么是前缀树

散列表(哈希)

  1. 什么是散列表
            就是哈希表,是能够通过给定的关键字的值直接访问到具体对应的值的一个数据结构。也就是说,把关键字映射到一个表中的位置来直接访问记录,以加快访问速度。我们将关键字称为 Key,把对应的记录称为 Value,通过 Key 访问一个映射表来得到 Value 的地址。而这个映射表,也叫作散列函数或者哈希函数,存放记录的数组叫作散列表。
            散列表有两种用法:一种是 Key 的值与 Value 的值一样,一般我们称这种情况的结构为 Set(集合);而如果 Key 和 Value 所对应的内容不一样时,那么我们称这种情况为 Map,也就是人们俗称的键值对集合。
  2. 散列表的存储
    以chaining解决碰撞为例
    在这里插入图片描述
  3. 散列表读取的时间复杂度
    O(1+α),α为装载因子, α = n m \alpha=\frac{n}{m} α=mn​, n n n表示key个数, m m m表示slot个数
  4. 常见的散列表哈希函数
    1)直接寻址法
    取关键字或关键字的某个线性函数值为散列地址。
    2)数字分析法
    通过对数据的分析,发现数据中冲突较少的部分,并构造散列地址。例如同学们的学号,通常同一届学生的学号,其中前面的部分差别不太大,所以用后面的部分来构造散列地址。
    3)平方取中法
    当无法确定关键字里哪几位的分布相对比较均匀时,可以先求出关键字的平方值,然后按需要取平方值的中间几位作为散列地址。这是因为:计算平方之后的中间几位和关键字中的每一位都相关,所以不同的关键字会以较高的概率产生不同的散列地址。
    4)取随机数法
    使用一个随机函数,取关键字的随机值作为散列地址,这种方式通常用于关键字长度不同的场合。
    5)除留取余法
    取关键字被某个不大于散列表的表长 n 的数 m 除后所得的余数 p 为散列地址。这种方式也可以在用过其他方法后再使用。该函数对 m 的选择很重要,一般取素数或者直接用 n。
    6)Multiplication Method
    h ( k ) = ( A ⋅ k m o d    2 w ) r s h ( w − r ) h(k)=(A\cdot k\mod 2^w) rsh (w-r) h(k)=(A⋅kmod2w)rsh(w−r)
    A A A 一般为一个不接近 2 w − 1 2^{w-1} 2w−1 和 2 w 2^w 2w,且位于他俩之间的奇数
    w w w 表示计算机中一个字的长度为 w w w 位,哈希表大小(槽位数) m = 2 r m=2^r m=2r, r s h rsh rsh 表示右移操作
  5. 散列表处理碰撞的方式
    1)开放地址法(也叫开放寻址法)
    对计算出来的地址进行一个探测再哈希,比如往后移动一个地址,如果没人占用,就用这个地址。如果超过最大长度,则可以对总长度取余。这里移动的地址是产生冲突时的增列序量。
    2)再哈希法
    在产生冲突之后,使用关键字的其他部分继续计算地址,如果还是有冲突,则继续使用其他部分再计算地址。
    3)链地址法
    对Key通过哈希之后落在同一个地址上的值,做一个链表。
    4)建立公共溢出区
    建立一个公共溢出区,当地址存在冲突时,把新的地址放在公共溢出区。
  6. 开放寻址法的问题
    在做删除操作的时候,需要特殊设计,比如将 k k k 通过哈希函数投影到 x x x,随后发现在 x x x 处有冲突,因此根据探测再哈希投影到 x ′ x' x′,后期删除操作时,删除了位于 x x x 位置的记录,那么再次搜索 k k k 时,会发现 x x x 处不存在记录,从而不进行探测,进而认为 k k k 记录不存在
  7. 散列表的特点
    1)访问速度很快
    由于散列表有散列函数,可以将指定的 Key 都映射到一个地址上,所以在访问一个 Key(键)对应的 Value(值)时,根本不需要一个一个地进行查找,可以直接跳到那个地址。所以我们在对散列表进行添加、删除、修改、查找等任何操作时,速度都很快。
    2)需要额外的空间
    首先,散列表实际上是存不满的,如果一个散列表刚好能够存满,那么肯定是个巧合。而且当散列表中元素的使用率越来越高时,性能会下降,所以一般会选择扩容来解决这个问题。另外,如果有冲突的话,则也是需要额外的空间去存储的,比如链地址法,不但需要额外的空间,甚至需要使用其他数据结构。
    这个特点有个很常用的词可以表达,叫作“空间换时间”,在大多数时候,对于算法的实现,为了能够有更好的性能,往往会考虑牺牲些空间,让算法能够更快些。
    3)无序
    为了能够更快地访问元素,散列表是根据散列函数直接找到存储地址的,这样我们的访问速度就能够更快,但是对于有序访问却没有办法应对。
    4)可能会产生碰撞
    没有完美的散列函数,无论如何总会产生冲突,这时就需要采用冲突解决方案,这也使散列表更加复杂。通常在不同的高级语言的实现中,对于冲突的解决方案不一定一样。
  8. 什么是哈希冲突
    就是散列表碰撞,指的是在对一个目标进行哈希映射之后,发现存储位置已有其他映射的情况

并查集

  1. 什么是并查集
  2. 并查集如何实现快速查找
  3. 并查集如何实现快速合并
  4. 并查集如何压缩路径

  1. bfs和dfs的使用场景
    1)bfs适用于求两个节点之间的最短通路
    2)dfs适用于求图中是否存在环/进行topology sort
  2. 图的BFS搜索
    # 用广度优先的方式寻找从s开始所有的reachable nodes
    def BFS(s, Adf):
    	level = {s: 0}		# 字典,存储所有节点都是s在几个move下reach得到的
    	parent = {s: None}	# 字典,存储每个节点的parent
    	i = 1
    	frontier = [s] 		# i-1 move 的情况下可以reach的node
    	while frontier:
    		next = []		# i move 的情况下可以reach的node
    		for u in frontier:
    			for v in Adj[u]:
    				if v not in level:
    					level[v] = i
    					parent[v] = u
    					next.append(v)
    		frontier = next
    		i += 1
    
  3. 图的DFS搜索
    # 用深度优先的方式寻找从s开始所有的reachable nodes
    parent = {s: None}
    def dfs_visit(V, Adf, s):
    	for v in Adj[s]:
    		if v not in parent:
    			parent[v] = s
    			dfs_visit(V, Adj, v)
    
    
    # 用深度优先的方式遍历全图
    def dfs(V, Adf):
    	parent = {}
    	for s in V:
    		if s not in parent:
    			parent[s] = None
    			dfs_visit(V, Adf, s)
    

其他

  1. 如何交换两个数字的值但不申请额外的空间
    用异或
    a = a ^ b
    b = a ^ b		// a = a^b^b
    a = a ^ b		// b = a^b^a
    
  2. 堆的应用有
    priorityQueue
  3. 递归的缺点
  4. 链表和线性表的区别
  5. array和hash set的区别
  6. 如何判断一个算法是线性的还是非线性的
  7. Top-K问题适合用什么


这篇关于数据结构 面试相关整理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程