算法实习生面试
2018/5/21
前记: 现在大三,想找一份暑期实习生工作。陆陆续续面了几家公司,基本定位都在算法方向。由于前期没有准备,所以基本结果都很惨。第一次去面试时,直接被面试官现教。现在实习基本确定,所以想要整理一下这段时间的面试经过,特别是针对一些被问到的面试题统一规整,希望为之后的找工作留下经验,也分享出来供大家参考。
一. 面试题目
挑选我认为比较具有代表性的题目来分享,也是重新整理,给自己加深印象。
1.【头条】给出一个计算 x \sqrt x x 的算法,并手写代码。
只要学过计算机语言的同学应该都有做过这道题。这是一道非常基础且典型的迭代算法题。基本思想是用牛顿迭代法。
至于什么是牛顿迭代法,可以参考这里
牛顿迭代法常被用来作方程的寻根,这里最重要的就是把计算开根号的运算转化为方程寻根问题。公式如下:
x 2 − a = 0 x^2-a=0 x2−a=0
现在问题就变成了:已知一个非负数a,求出方程 f ( x ) = 0 f(x)=0 f(x)=0的根,其中 f ( x ) = x 2 − a f(x)=x^2-a f(x)=x2−a。
牛顿法的迭代公式如下: x n + 1 = x n − f ′ ( x n ) f ( x n ) x_{n+1}=x_n- \frac {f'(x_n)} {f(x_n)} xn+1=xn−f(xn)f′(xn)
具体迭代步骤如下:
1) n = 0 , x 0 n=0,x_0 n=0,x0为初始值,可以选择 x 0 = 1 x_0=1 x0=1;
2)计算 f ( x n ) f(x_n) f(xn),如果 ∣ f ( x ) ∣ < ϵ |f(x)|<\epsilon ∣f(x)∣<ϵ,则停止迭代, ϵ \epsilon ϵ为给定的误差项,否则转到3);
3)计算: x n + 1 = x n − f ′ ( x n ) f ( x n ) x_{n+1}=x_n- \frac {f'(x_n)} {f(x_n)} xn+1=xn−f(xn)f′(xn),回到2)。
到此牛顿迭代法就完成了。
我面试的时候,知道要将这个问题转换,因为不熟练愣是没有想起怎么转为方程求根。当时我索性用了二分的办法来计算,不过面试官似乎觉得也还能接受。这个经验告诉我学习时一定不要眼高手低,对于这种小细节也要同等关注,细节决定成败不是没有道理的。
2.【头条】题目:有一个根据日期排列,内容为当天天气温度的列表A。请你根据该列表A,返回一个同样大小的列表B,满足列表B中每一个位置的值为A中该天之后距离其最近且比其温度高的日期,若没有则该位置为-1。例如,用索引代表日期,列表 A = [ 23 , 22 , 15 , 14 , 17 , 23 ] A=[23,22,15,14,17,23] A=[23,22,15,14,17,23],则需要返回的列表为 B = [ − 1 , 5 , 4 , 4 , 5 , − 1 ] B=[-1,5,4,4,5,-1] B=[−1,5,4,4,5,−1]
最容易想到的办法,就是对A中的每一个元素按从前往后的顺序去遍历其后的温度,只要找到比它大的值,就可以停止。时间复杂度为 O ( n 2 ) O(n^2) O(n2),显然这不是最好的办法。
下面给出一种时间复杂度可以达到 O ( n ) O(n) O(n)的算法。
假设A共有n个元素,对列表A按照从后往前的顺序每一个进行遍历:
显然 B n − 1 = − 1 B_{n-1}=-1 Bn−1=−1;
对于 A n − 2 A_{n-2} An−2,将其与 A n − 1 A_{n-1} An−1比较:
- 如果 A n − 1 > A n − 2 A_{n-1}>A_{n-2} An−1>An−2说明对于 B n − 2 = n − 1 B_{n-2}=n-1 Bn−2=n−1;
- 如果 A n − 1 ≤ A n − 2 A_{n-1}\leq A_{n-2} An−1≤An−2,则 B n − 2 = − 1 , B_{n-2}=-1, Bn−2=−1,更为重要的是,对于索引小于n-2的元素 A i , ( i = n − 3 , n − 4… , 0 ) A_i,(i=n-3,n-4…,0) Ai,(i=n−3,n−4...,0), A n − 1 A_{n-1} An−1已经没有比较的意义,因为 A n − 2 A_{n-2} An−2比 A n − 1 A_{n-1} An−1离这些元素更近,且 A n − 1 不 会 比 A n − 2 大 A_{n-1}不会比A_{n-2}大 An−1不会比An−2大,此时我们可以在列表A中删去 A n − 1 A_{n-1} An−1, 这样可以节省前面的元素遍历时的时间复杂度。
对于 i = n − 3 , n − 4 , . . . , 0 i=n-3,n-4,…,0 i=n−3,n−4,...,0, 都是逐个与后面的值进行比较,直到找到比 A i A_i Ai大的值,但由于每次遍历都存在删除上的策略,所以可以大大降低时间复杂度,对于每一个 A i A_i Ai,遍历所消耗的时间复杂度为 O ( 1 ) O(1) O(1),整体复杂度为 O ( n ) O(n) O(n)。
在面试官的提示下,我开始手写代码。当时直接用了数组来做,但其实会遇到一个增加时间复杂度的问题,就是删除节点时会产生 O ( n ) O(n) O(n)的时间复杂度。后面发现其实更好的策略是用c++中的链表来做,这样在结构体中同时包含当前天气的日期以及温度,并且删除节点的时间复杂度为 O ( 1 ) O(1) O(1),大大提高了效率,并且时刻都能保证日期与温度的一一对应。
补上代码【Python 3】
def weather(A):
n = len(A)
B = A.copy()
C = [i for i in range(n)]
D = [-1 for i in range(n)]
for i in range(n-1,-1,-1):
k = i+1
tmp = len(B)
for j in range(i+1,tmp):
if(B[k]>B[i]):
D[i] = C[k]
break
else:
del B[k]
del C[k]
return D
if __name__=="__main__":
A = [23,22,15,14,17,23]
B = weather(A)
print(B);
3.【头条】题目:有一个列表,每个元素都是一个大于等于0的整数,请计算出符合一下条件的最大的 h h h: 列表中至少有h个元素的值大于等于 h 。 h。 h。例如,列表为 [ 1 , 2 , 6 , 3 , 7 ] [1,2,6,3,7] [1,2,6,3,7],最大的 h h h应为3。
这个问题比较绕,需要先看懂题意,尤其要注意条件中的“至少”,“大于等于”。
直接给出算法,主要是三部分:
- 令新列表C长度为 n + 1 n+1 n+1, C i C_i Ci表示B中值为i的元素的个数,其中 C n C_n Cn表示B中大于等于n的元素的个数。(因为 h h h的最大值为 n n n,所以对于B中大于 n n n的值不必再在C中为其设立对应的位置,这样可以节省不必要的内存开销。)建立列表C的时间复杂度为 O ( n ) O(n) O(n);
- 在列表C中从后往前判断 h h h的值,直到找到满足条件的 h h h,即为最大的 h h h。
整个算法的时间复杂度为 O ( n ) O(n) O(n)。
补上代码【Python3】
def largest_h(data):
n = len(data)
index = [0 for i in range(n+1)]
for i in range(n):
index[min(n,data[i])] += 1
for i in range(n,-1,-1):
if(i==n and index[i]>=n):
return i
if(i<n):
index[i]+=index[i+1]
if(index[i]>=i):
return i
if __name__=="__main__":
data = [1,2,6,3,7,5,4]
#data = [5,5,5,5,5]
print(largest_h(data))
4.【百度】利用堆排序找到一个列表中第 k k k大的数。
看网上已有的算法面经,似乎很多面试官都喜欢考堆排序算法。所以以后再想要去面算法工程师,一定要记得复习堆排序算法。其实不仅仅是堆排序,作为算法中经典的一类问题,排序算法始终都会成为考查的热点。比如快速排序,归并排序,冒泡排序,都是作为一个算法开发人员必备的基本技能点。
下面简单介绍堆排序算法。
堆排序是一种不稳定的选择排序算法,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
堆排序是依托于堆结构而存在的,根据节点的关系分为大顶堆和小顶堆。
大顶堆:每个节点的值大于等于其左右子节点的值。
小顶堆:每个节点的值小于等于其左右子节点的值。
堆在逻辑上是一种完全二叉树结构,但是我们在内存中用数组来存放(根据树从上层到下层,从左到右进行排放)。
用一个数组来定义堆:
堆中最后一个非叶节点的索引: ( n − 1 ) / 2 (n-1)/2 (n−1)/2;
大顶堆: a r r [ i ] ≥ a r r [ 2 i + 1 ] arr[i] \geq arr[2i+1] arr[i]≥arr[2i+1] && a r r [ i ] ≥ a r r [ 2 i + 2 ] arr[i]\geq arr[2i+2] arr[i]≥arr[2i+2];
小顶堆: a r r [ i ] ≤ a r r [ 2 i + 1 ] arr[i] \leq arr[2i+1] arr[i]≤arr[2i+1] && a r r [ i ] ≤ a r r [ 2 i + 2 ] arr[i]\leq arr[2i+2] arr[i]≤arr[2i+2];
堆排序基本步骤:
将无序数列构成一个堆,按升、降序选择大顶堆、小顶堆;
数组中最后一个非叶节点的索引: [ a r r . l e n g t h / 2 ] − 1 [arr.length/2]-1 [arr.length/2]−1,从右向左,从下至上调整。堆顶元素为最大(最小)元素,将堆顶与最后一个元素交换,将堆顶元素沉入数组末端;
重新调整数组,使其成为堆,再重复步骤2,直至排序结束。
补上代码【Python3, 升序排序】
def lowfilter(data, i, n):
while(i<n):
lindex = i*2+1
rindex = i*2+2
if (rindex < n):
if(data[rindex]>data[i] and data[rindex]>=data[lindex]):
data[i],data[rindex] = data[rindex],data[i]
i = rindex
elif(data[lindex]>data[i] and data[lindex]>=data[rindex]):
data[i],data[lindex] = data[lindex],data[i]
i = lindex
else:
i = n
elif(lindex<n):
if(data[lindex]>data[i]):
data[lindex],data[i] = data[i],data[lindex]
i = lindex
else:
i = n
else:
i = n
return data
def crestack(data):
n= len(data)
for i in range((n>>1)-1, -1, -1):
data = lowfilter(data, i, n)
return data
def stacksort(data):
n = len(data)
data = crestack(data)
for i in range(n):
data[n-i-1],data[0] = data[0],data[n-i-1]
data = lowfilter(data,0,n-i-1)
return data
if __name__=="__main__":
data = [6,4,1,7,0,-1]
sortdata = stacksort(data.copy())
print(data)
print(sortdata)
5.【阿里】有一个文本文件,共包含n行,每一行为一个浮点数,设计算法计算该文本中所有浮点数的均值和方差。
乍一看,这个题是不是很简单。我面试的时候看到题目也觉得很不可思议,不过在应试教育中挣扎多年的我,知道这个问题不简单,里面可能有坑。事实证明的确如此。
1)算法一:我一开始想到的算法是,遍历一次文本,并将其内容存入一个数组,在遍历的同时计算均值这并不困难。在完成一次遍历后即可得到均值,再根据已经存下来的数组,利用方差公式 σ 2 = 1 n ∑ i = 1 n ( x i − x ‾ ) 2 \sigma ^2=\frac 1 n\sum_{i=1}^n(x_i-\overline x)^2 σ2=n1∑i=1n(xi−x)2计算方差即可。所以时间复杂度为 O ( n ) O(n) O(n),空间复杂度也为 O ( n ) O(n) O(n)。
2)算法二:简化算法,使得空间复杂度为 O ( 1 ) O(1) O(1)。在算法一中,我们一次遍历,由于将内容村委了一个数组用来计算方差,所以消耗了 O ( n ) O(n) O(n)的空间复杂度。现在我们想要空间复杂度降低,其实很简单, 我们在第一次遍历的时候先计算出平均值,但是不保存数组。然后进行第二次遍历,依旧去读文本内容,根据第一次遍历得出的均值计算方差。这样时间复杂度仍然为 O ( n ) O(n) O(n),但空间复杂度降低为 O ( 1 ) O(1) O(1),弊端在于我们需要遍历两次文本。
3)算法三:算法二虽然可以达到 O ( 1 ) O(1) O(1)的空间复杂度,但是需要遍历两次文本。进一步优化算法,希望只需要遍历一次文本,并且空间复杂度仍为 O ( 1 ) O(1) O(1),时间复杂度仍为 O ( n ) O(n) O(n)。将方差公式展开:
σ 2 = 1 n ∑ i = 1 n ( x i − x ‾ ) 2 = 1 n ∑ i = 1 n ( x i 2 + x ‾ 2 − 2 x i x ‾ ) = 1 n ∑ i = 1 n x i 2 + x ‾ 2 − 2 n x ‾ ∑ i = 1 n x i \sigma ^2=\frac 1 n \sum_{i=1}^n(x_i-\overline x)^2 \\ \qquad \qquad \quad =\frac 1 n \sum_{i=1}^n(x_i^2+\overline x^2-2x_i\overline x)\\ \qquad \qquad \qquad =\frac 1 n \sum_{i=1}^nx_i^2+\overline x^2-\frac 2 n \overline x \sum_{i=1}^nx_i σ2=n1i=1∑n(xi−x)2=n1i=1∑n(xi2+x2−2xix)=n1i=1∑nxi2+x2−n2xi=1∑nxi
公式展开到这里,就可以看明白了。一次遍历中,累积 x i 2 , x i x_i^2, x_i xi2,xi 这样一次遍历后就可以一次得到均值 x ‾ \overline x x和方差 σ 2 \sigma^2 σ2。
这样就可以在时间复杂度为 O ( n ) O(n) O(n),空间复杂度为 O ( 1 ) O(1) O(1),且遍历一次文本就可以得到均值和方差。
至此,这个问题基本可以达到很好的解决了。
二. 一点经验
一些大厂,虽然他们的招聘要求都写得玄而又玄,看起来很厉害的样子。实际上,对于实习生,特别是年级较低的实习生,他们更看重的是你的基础能力。所谓的基础能力,包括编程能力(程序员基本功),编程再不济c++要会吧,而且要熟练的会吧;另外就是数学能力,特别是做算法岗,数学能力也是有必要的,如果有志向做机器学习、大数据分析等方向,那数学肯定是要过关的,最基本的函数求偏导,极限肯定是要会的。前面说的的基本能力,作为一个想要找实习的学生,想要达到并不难。一个岗位往往有很多人去竞争,一群能力差不多的人,凭什么选你呢。这时你需要有一个闪光的地方,让人家觉得你能给人家带来更亮眼的东西,比如你的编程能力很厉害,所谓厉害不是说对于面试官给你提出的问题你都能轻松的写出可执行代码。你需要有更多的大项目编程经历,比如一个很厉害的竞赛,面试官很看重这一点。或者说你的数学能力很厉害,你有着强悍的数学功底,对于一些AI方向的职位,可能会更受欢迎。
写在最后: 第一次找实习,也比较看重这次实习。从来没有走出过校门的学生党,走出去发现外面的世界还是很不一样的。企业里并不会在乎你有多大的发展空间,他们在乎的是你能否给他们创造利益。特别的,一些大厂,比如京东,他们大规模的招聘实习生,其中一个很重要的目的是为下一年的校招做准备,所以同等能力下,像我这种只是纯粹为了去实习的升学党就处于相对的弱势,因为招我们进去,更像是培养我们个人,对企业来讲得不到太长远的好处。 另外一点,就是比较后悔前两年没有出来实习。现在找实习发现编程、数据结构、算法什么的都是大一就学过的东西,那时候觉得自己学的都是些什么啊,拿到外面肯定都没用。现在才发现,其实大一大二完全有能力出来实习了,只不过那时候意识不到。所以趁着低年级假期没事干出来实习真的很有必要。
最后推荐几个个人认为比较好的IT刷题网站:牛客网(算法),Leetcode(算法,数据竞赛),Kuggle(数据竞赛)。