一、前话
互联网公司面试基本就是: 首先自我介绍,然后开始盘问你简历中做过的项目,最后都会出题现场写代码,考察一个程序员最基本的素质。因此对于数据结构的掌握尤为重要,很多人都会被这个环节给刷掉,要么是临场紧张写不出来,要么就是没有考虑复杂度问题:代码冗余,耗时耗内存。 本系列便分享一些大型互联网公司的数据结构面试题,提供分析思路并通过python实现!
二、数据结构面试题分析
1、输出序列逆序对儿,如输入数组[1,2,5,4,3],输出(5,4),(5,3),(4,3)
(1) 拿到此题一般的思路最简单的思路自然是对整个序列从前往后循环两边,元素两两之间相互比较,直接输出a[i] > a[j]序列对; 此种算法复杂度是O(N^2)
def inverse_pair(a, n):
'''
打印逆序对儿,如输入数组[1,2,5,4,3],输出(5,4),(5,3),(4,3)
'''
for i in range(n):
for j in range(i+1, n):
if a[i] > a[j]:
print('(%d, %d)'%(a[i], a[j]), end='')
a = [1,2,5,4,3]
inverse_pair(a, len(a))
#(5,4),(5,3),(4,3)
(2) 我想如果在面试过程中面试官出了这道题,你给出了上面的代码,我想你面试已经挂了,显然面试官出此题的意思是想让你用复杂度更低更巧妙的方式去实现,而是采用归并 分治的思想,时间复杂度O(N*log(N))
归并排序可否记得,先通过二分法不断拆分整个序列,然后再在合并过程中两两组合,实现排序,而此题就是采用归并排序的思想,只不过在合并的过程中是两两前后比较,输出逆序对
def output_merge(a, start_idx, middle_idx, end_idx):
'''merge合并过程输出逆序对'''
for i in range(start_idx, middle_idx+1):
for j in range(middle_idx+1, end_idx+1):
if a[i] > a[j]:
print((a[i], a[j]), end='')
def inverse_pair_merge(a, start_idx, end_idx):
'''递归实现对数组进行拆分'''
if start_idx < end_idx:
middle_idx = int((start_idx + end_idx)/2)
inverse_pair_merge(a, start_idx, middle_idx)
inverse_pair_merge(a, middle_idx+1, end_idx)
output_merge(a, start_idx, middle_idx, end_idx)
a = [5, 4, 3, 2, 1]
inverse_pair_merge(a, 0, len(a)-1)
#(5, 4)(5, 3)(4, 3)(2, 1)(5, 2)(5, 1)(4, 2)(4, 1)(3, 2)(3, 1)
2、实现稀疏矩阵乘法
(1) 假如现在有两个两个矩阵A(m, n)、B(n, n),普通矩阵三次for循环便实现矩阵乘法,但首先明白此题是系数矩阵;
(2) 系数矩阵:在矩阵中,若数值为0的元素数目远远多于非0元素的数目,并且非0元素分布没有规律,因此对于稀疏矩阵,里含有大量的零元素,如果按照正常的方式进行运算,则会耗费很多的无用功,矩阵元素相乘:c[i][j] = a[i][k] * b[k][j], 因此当判断矩阵A、B对应元素均非零时,才计算
def mat_mul(a, b):
c = np.asarray(np.zeros((a.shape[0], b.shape[1])))
for i in range(a.shape[0]):
for k in range(a.shape[1]):
if a[i][k] != 0:
for j in range(b.shape[1]):
c[i][j] += a[i][k] * b[k][j] if b[k][j] != 0 else 0
return c
a = np.asarray([[0, 0, 1, 0], [2, 0, 1, 0], [0, 0, 1, 2]])
b = np.asarray([[0, 1, 0], [1, 0, 0], [1, 1, 0], [1, 2, 0]])
c = mat_mul(a, b)
3、查找二叉树任意两个节点的最近公共祖先
审题: 二叉树,可能是完全二叉树,也可能是非完全二叉树
本题我们全面分析一下,尝试从两种情况去考虑
(1) 二叉树中每个节点都存储了父节点索引
思路: 先计算出两个节点在路上的高度, 例如为h1、h2(存在h1 >= h2),先将高度为h1的节点通过寻找父节点往上迭代 node1 = node1.parent,直到当前node1节点的高度now_h1 = h2,此时node1和node2节点高度相同,同时向上索引父节点,当二者的父节点相等时,则便是node1和node2的最近公共祖先
def height(node):
'''求节点的高度'''
node_height = 0
while node.parent != None:
node_height += 1
node = node.parent
return node_height
def common_parent(root, node1, node2):
'''查找二叉树任意两个节点的最近公共祖先'''
node1_height = height(node1)
node2_height = height(node2)
while node1_height < node2_height:
node2_height -= 1
node2 = node2.parent
while node1_height > node2_height:
node1_height -= 1
node1 = node1.parent
while node1 and node2 and node1 != node2:
node1 = node1.parent
node2 = node2.parent
if node1 == node2:
return node1
else:
return None
(2) 二叉树中每个节点可能没有存储父节点索引,提供两种解法
思路1:
A、首先判断这两个节点是否等于根节点root, 如果是则root便是最近公共父节点
B、否则递归左、右子树,如果一个节点在左子树,一个节点在右子树, 则根节点root就是最近公共父节点,否则如果两个节点都在左子树, 则最近公共父节点都在左子树,否则都在右子树
def last_common_parent2(root, node1, node2):
if root == None or node1 == None or node2 == None:
return None
#如果两个节点中有一个等于该子树的根节点,则直接返回子树的根节点
if node1 == root or node2 == root:
return root
#递归左子树和右子树
left_lca = last_common_parent2(root.lnode, node1, node2)
right_lca = last_common_parent2(root.rnode, node1, node2)
#如果左右子树返回值都不为None,则说明返回在node1 == root or node2 == root处返回的,则返回None
if left_lca and right_lca:
return root
#如果返回的是left_lca为None,则说明左子树找到地也没有找到,则两个节点都在右子树,则返回right_lca
#如果返回的是right_lca为None,则说明y右子树找到地也没有找到,则两个节点都在左子树,则返回left_lca
if left_lca == None:
return right_lca
else:
return left_lca
思路2:
A、找到从根root到node1的路径,然后存放在数组和链表中
B、找到从根root到node2的路径,然后存放在数组和链表中
C、从两个数组或链表的开始往下访问,当遇到不同的节点时,则上一个节点便是最近公共子节点
def find_path(pnode, node, queue):
'''
给出一棵树和一个节点,查找根节点到这个节点的路径 使用队列结构存储路径
'''
find = False
queue.put(pnode)
if pnode == node:
return True, queue #这里的return 返回是为了给find赋值的
if pnode.lnode != None:
find, _ = find_path(pnode.lnode, node, queue)
if not find and pnode.rnode != None:
find, _ = find_path(pnode.rnode, node, queue)
if not find:
queue.get()
return find, queue
def last_common_parent3(root, node1, node2):
'''
查找二叉树任意两个节点的最近公共祖先
'''
_, queue1 = find_path(root, node1, LifoQueue())
_, queue2 = find_path(root, node2, LifoQueue())
plist1 = []
plist2 = []
while not queue1.empty():
plist1.append(queue1.get())
while not queue2.empty():
plist2.append(queue2.get())
plist1, plist2 = plist1[::-1], plist2[::-1]
flag = False
pnode = None
for i in range(min(len(plist1), len(plist2))):
if plist1[i] == plist2[i]:
continue
else:
pnode = plist1[i-1]
flag = True
break #当找到不同点时,就跳出循环
if not flag:
pnode = plist1[-1] if len(plist1) < len(plist2) else plist2[-1]
return pnode
此处建立一棵二叉树供测试使用
class Tree:
def __init__(self, data, lnode=None, rnode=None, parent=None):
self.data = data
self.lnode = lnode
self.rnode = rnode
self.parent = parent
#手动建立二叉树,非完全二叉树
a = [5, 3, 4, 1, 6, 7, 8, 2, 9]
a = list(map(lambda x: Tree(x), a))
root = a[0]
a[0].lnode, a[0].rnode = a[1], a[2]
a[1].parent, a[1].lnode, a[1].rnode = a[0], a[3], a[4]
a[2].parent, a[2].lnode, a[2].rnode = a[0], a[5], a[6]
a[3].parent, a[3].lnode = a[1], a[7]
a[4].parent, a[4].rnode = a[1], a[8]
a[5].parent, a[6].parent, a[7].parent, a[8].parent = a[2], a[2], a[3], a[4]
4、一个元素可能正可能负的数组,输出连续的子数组的最大和
假设数组为a[0:n],最大和子数组为a[i:j],注意最大和子数组有这样的特点:
(1) 从a[i-1]开始往左(下标减小方向)累加和,和都为负。(因为一旦和为正,这段累加和对应的子数组便可以并入a[i:j]构成更大和)
(2) 从a[i]开始往右累加和, 在到达a[j]前,和都为正,且累加到a[j]时和最大
算法步骤:
A、 给两个变量,now_sum和max_sum,now_sum初始化为0,max_sum初始化为负无穷;
B、 从a[1]开始遍历,并把元素值累加给now_sum,一旦now_sum为负数,就清零;
C、 在now_sum变化时,max_sum始终保留最大的now_sum;
D、 如果最后now_sum和max_sum都是负数,输出最大的负值
def max_seq_of_array(a, n):
'''
一个数组,元素可能正可能负,求连续的子数组里,和最大的那个
Args:
a: 数组
n:数组长度
Return:
连续子数组最大和
'''
now_sum = 0
max_sum = -float('inf')
for i in range(n):
now_sum += a[i]
if now_sum > max_sum:
max_sum = now_sum
eidx = i
if now_sum <= 0:
now_sum = 0
return max_sum
a = [-1,-2,-3,1,2,3,-3,4]
max_seq_of_array(a, len(a))
5、由rand5()生成rand7()
注意:rand5():随机产生整数1-5,rand7()既是随机产生整数1-7
思路: 考虑问题是从数据映射的角度去考虑,肯定是需要两次调用rand5(), 先扩大数据产生更大的数然后缩小数据往1-7上进行映射
def rand5():
'''
1-5的随机生成器
'''
return random.randint(1, 5)
def rand7():
'''
现有一个rand5()的随机数生成器,等可能生成1~5之间的数,要求用它实现rand7(),等可能生成1~7之间的数
'''
n = float('inf')
while(n>=21):
a = rand5()
b = rand5()
n = (a-1)*5 + b
return int(n/3)+1
6、现场手写归并排序、快速排序、堆排序
注意:显然这是最基础的排序,但还别说,能干到一片猿类,不是说大家不会放在平时都会写,但现场那点时间如果不能随手就写出来,靠着慢慢想还真是容易出错,所以大家平时要记住这些基础算法的思路,别sort()函数调用习惯了,排序也都不会写了
网上关于归并排序、快速排序和堆排序解释很多,这里不重复,直接粘上代码
归并排序
def merge_array(a, start_index, mid_index, end_index):
'''
合并两个有序数组成一个有序数组
Args:
a = [] 待排序数组
start_index = 数组开始索引值
mid_index = 数组中间索引值,区分开两个有序数组
end_index = 数组结束索引值
Return:
a = [] 合并的有序数组
'''
i, j = start_index, mid_index + 1
m, n = mid_index, end_index
temp_list = []
while i <= m and j <= n:
if a[i] <= a[j]:
temp_list.append(a[i])
i += 1
else:
temp_list.append(a[j])
j += 1
while j <= n:
temp_list.append(a[j])
j += 1
while i <= m:
temp_list.append(a[i])
i += 1
for index in range(len(temp_list)):
a[start_index + index] = temp_list[index]
def merge_sort(a, start_index, end_index):
'''
归并排序核心函数
Args:
a = [] 待排序数组
start_index = 数组开始索引值
end_index = 数组结束索引值
Return:
a = [] 完成排序数组
'''
if start_index < end_index:
mid_index = (start_index + end_index)/2
merge_sort(a, start_index, mid_index)
merge_sort(a, mid_index + 1, end_index)
merge_array(a, start_index, mid_index, end_index)
a = [3, 4, 1, 2, 9, 6, 5, 7, 12, 11, 10]
merge_sort(a, 0, len(a) - 1)
快速排序
def quict_sort(a, start_index, end_index):
'''
快速排序核心函数
Args:
a = [] 待排序数组
start_index = 数组开始索引值
end_index = 数组结束索引值
Return:
a = [] 完成排序数组
'''
#if条件是递归结束条件
if start_index >= end_index:
return
boundary_index = quick_boundary(a, start_index, end_index)
quict_sort(a, start_index, boundary_index - 1)
quict_sort(a, boundary_index + 1, end_index)
return a
def quick_boundary(a, start_index, end_index):
'''
快速排序获取拆分边界索引值辅助函数
Args:
a = [] 待排序数组的拆分
start_index = 拆分数组开始索引值
end_index = 拆分数组结束索引值
Return:
boundary_index = Int 边界索引值
'''
boundary_index = start_index
standard = a[boundary_index]
left_index, right_index = start_index, end_index
while left_index < right_index:
while left_index < right_index and standard <= a[right_index]:
right_index -= 1
a[left_index] = a[right_index]
while left_index < right_index and standard >= a[left_index]:
left_index += 1
a[right_index] = a[left_index]
a[left_index] = standard
boundary_index = left_index
print(boundary_index)
print(a)
return boundary_index
a = [49, 38, 65, 97, 76, 13, 27, 49]
quict_sort(a, 0, len(a) - 1)
堆排序
def adjustheap(a, i, n):
'''调整堆'''
j = 2*i + 1
while j < n:
if j+1 < n and a[j] < a[j+1]:
j += 1
if a[j] < a[i]:
break
a[i], a[j] = a[j], a[i]
i = j
j = 2*i + 1
def makeheap(a, n):
'''建堆'''
for i in range(int(n/2)-1, -1, -1):
adjustheap(a, i, n)
def heapsort(a, n):
'''堆排序'''
makeheap(a, n)
for i in range(n-1, -1, -1):
a[i], a[0] = a[0], a[i]
adjustheap(a, 0, i)
a = [5, 2, 3, 1, 6, 8, 7, 4]
heapsort(a, len(a))
三、后话
后续将持续更新 BAT[阿里、百度、腾讯]等互联网公司数据结构面试题,python实现分析,供参考,欢迎大家关注讨论,有问题欢迎指正!