许多同学经常把一个程序直接贴给我说:
老师,我的程序出错了,帮我看看吧。
对于这些同学的请求,我往往只能委婉的拒绝:
调试要靠自己哦~ 我帮你指出程序的错误,对于你自己一点帮助都没有。你今天先继续努力debug,明天要是还不能解决,我就帮你看看。
虽然大部分的人最终都能靠自己找到错误,但是学会好的调试技巧,确实能够起到事半功倍的刷题效果。
于是我总结了四种我自己常用的在刷题过程中的调试技巧,希望可以帮助到大家:
技巧1: 打印中间结果
依赖于IDE的断点调试,是十分浪费时间的一种调试方法。而且在面试中,你是基本没有断点调试的机会的(因为很多公司是白板写题,不会提供给你IDE)。
事实上在实际的工作中,你也很少能够有机会去进行断点调试,比如你进行的是 Web 开发,你只能想方设法的在代码中打印一些 Log,然后根据 Log 去分析出错的原因。你平时用 IDE 写代码,就十分容易养成这种断点调试的“坏习惯”。一个更好的方式,是使用打印中间结果的方式。以 LintCode 上的 Clone Graph 为例子:
LintCode 支持自己出测试数据进行测试的模式,点到“Testcase” 这个 tab之后,输入测试数据,点 Run Code。你的程序中的输出也会被显示在 Output 中。我们看到 代码中 第22行到27行,是在打印一个中间结果,这个中间结果就是在使用了 bfs 算法之后,从一个点出发,找到的所有的图中其他节点。这样我们就可以迅速验证,代码出错到底是因为 BFS 写错了,还是因为其他的部分写错了。
使用这种调试方法的另外一个好处是,你很自然的希望你的程序可以“分阶段”输出一些中间结果,这种分阶段处理的方式,也是我们在课上经常强调的,把大问题变成小问题,然后逐个击破的编程思想。养成这种编程习惯之后,可以非常有效的帮助你在写程序的时候,就避免掉错误。
技巧2: 一行一行改成参考程序
在 九章的官网 中有 LC 完整的参考程序库,其中 Java 程序的一部分代码是我自己亲自编写的,具备较高的质量和参考价值。很多题目也给了多种不同的解法。比如 Binary Tree Level Order Traversal 这一题,给出了三种 BFS 的参考程序和 1个 DFS的参考程序。通常来说很多题目,你不仅仅需要掌握其中一种方法,而是需要掌握这个题所有的解决办法。
有不少同学经常会来问我:
老师,我的代码和参考程序一样啊,为什么还是不对?
这种时候,我通常建议他们,将自己的代码,一行一行的改成参考程序。这样,当他们发现哪一行代码不一样的时候,就自然定位到了错误。
举一个例子,就以 Binary Tree Level Order Traversal 为例,宽度优先搜索算法(BFS)是面试必须掌握的一种算法。来看看下面这个有错误的代码(请花3分钟阅读一下,并找出错误所在):
从程序思想上来看,好像并没有什么错。很难直接发现有什么问题。与参考程序逐行对比之后会发现,错误发生在第14行。
参考程序中,写法是:
和上面的程序写法是:
这两种写法存在巨大区别,原因是在循环体内部,我们会不断往 queue 中放东西,这导致了queue.size() 的值是不断变化的。而没有起到我们只想循环当前这一层所有点的功能。
通过对这个程序逐行对比,我们能够马上学会如下的知识点:
- for循环中间的判断语句,在循环体的每一次循环中都会被重复执行,而不是在一开始就确定好执行多少次。
- 为了做到BFS的分层遍历,需要先把 size 取出来,再 for循环这个size,这是BFS分层遍历中最关键的一句话。
技巧3: 给小熊讲程序
这个技巧,是我曾经参加高中生算法竞赛的时候,一位算法竞赛国家队的前辈教我的。
当时我的算法水平可能已经还不错了,但是代码质量很差,很容易写出有bug的代码。这位前辈告诉我,可以放一只 “小熊” 在你的电脑旁边,一旦程序出错了。就对着这只小熊讲你的整个代码是怎么解决问题的。
因为是给小熊讲,所以你可以把它当作什么都不懂,于是需要更加仔细的去讲述你代码中每个细节,所以你需要一行一行的讲,甚至连为什么你要用 ArrayList 而不用 int[] 都要讲得清清楚楚。讲着讲着,你就有可能突然发现你的错误所在了。
这个技巧的意义在于,你在写代码的时候,脑子里可能想着一个事情,但是打出来的代码,可能是另外一回事儿。或者你脑子里想着有3个条件需要判断,但是打的时候,漏掉了一个。当你把代码重新讲一遍的时候,事实上是在重新整理你的逻辑,查漏补缺。这样很容易就能够发现你的错误。
技巧4: 必杀,重写一遍
大部分的bug,其实都是 typo。比如:
- 大于符号和小于符号打反了
- <= 写成了 <
- 本来要写一句话干什么来着但是后来写着写着忘记了
- if 后面忘了加 else
- for 循环后面忘了加括号,导致代码跑到了循环体之外
- 变量名打错:for(int j=0;i
诸如此类,不胜繁举。重新写一遍代码, 往往都能 fix 这些问题。
尾声
调试是在你出现 BUG 的时候,才需要做的事情。大家平时应该尽量去练习的是不要写出有BUG的代码。
这的确很难,但是你应该以此为训练目标,在你在 LintCode 上点击 “Submit” 之前,尽量的做到已经检查过自己的代码,不要过分依赖于 Online Judge帮你判断你的代码是否有错,更加不要依赖于本地 IDE 的断点调试功能。在 LintCode 这个界面(登陆后可见),可以看到自己提交的指标:
这个指标非常的重要,他是判断你的代码是否 Bug Free 的重要参考数据。如果你最近一个月内的这个数据在 3 以内,那么说明你的代码具备非常高的质量,不太容易出bug。如果在3-4之间,说明,还可以有所提高。如果在4以上,说明,你的代码质量比较糟糕了。
九章算法 ,国内&硅谷一线工程师在线直播授课,已经帮助30000+人成功拿到心仪offer。