码农面试中的Coding Questions

我多年来一直被一个问题困扰,那就是在面试其他码农时,应该如何评估他们的写码能力。 通常,大公司会使用类似LeetCode的算法题来测试面试者,我自己在跳槽或找工作时也曾刷过一段时间的LeetCode。 但从面试官的角度看,LeetCode题给出的关于面试者的信息非常有限。 很多时候,能否解决一个问题完全取决于面试者是否知道某个算法或者某个trick。 知道就能做出来;不知道,就做不出来。 比如有个经典的LeetCode题目,给一堆整数,已知其中每个数都出现了两次,只有一个数只出现一次,如何使用常数空间找到这个数。 标准解法是使用异或,但如果你事先不知道这个trick,那么在现场怎么都想不出来。 最终面试官得到的信息只是面试者是否知道这个具体的trick。 类似的,很多LeetCode的题目是关于动态规划的。 诚然,动态规划是一个非常复杂的算法,通常会动态规划确实是算法学得比较好的标志。 但问题是动态规划在实际工作中几乎完全用不上,所以即使知道面试者很擅长动态规划,这对评估面试者在实际工作中的能力也没有什么帮助。 因此,我对如何在编程方面进行面试一直感到困惑。随着经验的积累,也逐渐有了一些自己的体会和思考,因此想在这里分享一下。 请注意,由于我的个人背景,这篇文章中的例子可能会偏向于机器学习,但整篇文章的原则适用于所有程序员的面试。

我认为,写码面试并不应该针对数据结构和算法。 原因是,有许多其他的方法可以更高效地测试面试者在数据结构和算法方面的熟练程度,比如快速问答、写伪码等。 然而,有很多重要的信号只有观察写码的过程才能得到,其中的核心是软件工程。 我曾经提过,能够独立编写出demo级别的代码和能够编写出适合多人协作、易于维护的代码是完全不同的。 这种在时间和跨团队协作方面的可扩展性,就是软件工程的能力。 在大的决策方面,软件工程的考察主要看系统设计面试。 然而,这毕竟偏宏观偏嘴炮,要真正做出一个产品,另一个不可或缺的微观能力就是写码的能力,或者说有没有吃过见过。 只有在这些方面得到更多的信号后,才能判断面试者是一个能够带领团队提高代码质量的bar raiser,还是一个bug不断需要别人擦屁股的小可爱。 具体来说,我建议用一个相对简单的问题,不考算法,主考实现。 比如,实现k近邻搜索,实现一个决策树或logistic regression。然后在此过程中我们要特别关注以下方面的信号:

  1. 正确性。
    • 差: 面试者不能按时完成程序的编写,或者他们写出的程序对于toy example也不能正确输出。
    • 良:他们的程序能够对toy example正确输出。
    • 优:最好的情况是,他们的程序能够正确处理一些corner case。
  2. Defensive code writing
    • 差: 面试者在做假设的时候没有意识到他在做一个假设,因此没有做任何检查。例如,在取数组a[1]的时候,他们没有检查数组的长度是否大于1。
    • 良: 面试者意识到了这一点并且做了一些检查。
    • 优: 面试者不仅意识到了自己代码里的假设,做了检查,而且把检查放到了单元测试里面。这样,如果别人未来改动了他的代码,引入的错误也会被单元测试发现。
  3. 可维护性:尤其是单元测试的撰写。
    • 差: 面试者没有意识到需要主动加入单元测试,在完成实现以后就结束了。
    • 良: 面试者主动写了单元测试,并且使用了业界流行的单元测试框架,如pytest来进行测试。
    • 优: 面试者先写测试,然后再写实现,这就是测试驱动开发(test driven development)。
  4. 可读性: 面试者应当使用合理的变量命名,简明合理的变量命名,并在适当的位置撰写注释和文档。
  5. 与具体题目相关的信号,例如决策树等机器学习算法的实现涉及到许多参数,如何通过合理的配置处理来避免调用者在参数管理上出错,也是可以提取的信号。
    • 差: 面试者直接将参数写成magic number,那么未来修改起来会很困难。同时,调用者也无法影响函数的具体行为。
    • 良: 面试者利用函数参数来管理配置。
    • 优: 面试者利用编程语言的特性来系统性地、可扩展地管理这些参数,例如在Python中使用kwargs,利用这个字典来系统化管理。

除了这些信号,还有其他一些可以考察的信号。这些信号不是hiring blockers,而是加分项。

  1. 对算法的熟悉程度。例如,对于决策树,面试者提到可以通过剪枝来优化。对于KNN,面试者提到可以通过某些近似算法来大大加速。这部分讨论更像是系统设计,不一定要写代码,可以通过讨论的方式进行。数据结构和算法的考察也可以在其中进行。
  2. 面试者应该熟悉自己使用的编程语言的特性和设计思路。例如,在使用Python时,不应该像使用C++那样大量使用循环,因为Python中的循环效率非常低。相反,应该使用list comprehension,这样既提高了可读性,也提高了效率。对于机器学习特定的情况,进行矩阵运算时,应尽可能使用NumPy,而不是用Python进行逐行处理。这样往往可以实现既易读又高效的代码。
  3. debugability。虽然在正确性方面对debugability已经有了一定程度的考察,但还有一些其他的信号可以参考。例如,面试者在类似coderpad这样的平台上编写代码时,是否能够使用pdb等python debugger。如果能使用这样的debugger,就说明他们有丰富的debug经验。当然,使用print等传统方式也是可以的。在这种情况下,我们可以观察面试者是否能够策略性地在适当的位置放置print语句,以便在最短的时间内快速定位bug的位置。此外,对错误信息的解读也是一个很好的信号。这种debugability的考察不仅针对面试者解题本身,同时还要考虑到这个函数的调用者。因此,如果面试者有经验,他们可能会考虑在函数的适当位置放置logging语句,并设置适当的verbose level,以帮助函数的调用者来进行debug。

总的来说,我们在观察面试者写代码的过程中,核心的观察点不仅是这个题目本身是否做得好,而是在写这道题的过程中所展现出的各种习惯,能否让他在入职后写出高质量的大规模软件。我们观察的不仅是写出的代码本身,而更多的是在这个过程中所展现出的习惯是否符合best practice。核心的观察点和思考点是预测这个人入职后的表现。

那么这个面试系列到这里就告一段落了。感兴趣的同学可以看一下:

Comments