为什么算法这么难?
不过话说回来,面试中的面授算法,包括面授项目中几乎不可能用到的算法,也不能说不合理。算法往往是学习和理解能力的试金石,难的东西都能掌握,容易的东西往往就更不用说了。有志于上层的人将从中层获益。反之则不然。另一方面,虽然教科书上的算法大部分即使使用了也是直接被模块使用的,但遗憾的是,我们搬砖的人有时候作为发明者也要做点什么:要么我们要把算法改进成白盒,以满足手头的具体需求;或者干脆发明轮子。所以虽然面试算法本身不一定具备,但是熟悉各种算法的人通常更容易熟悉算法的思想,所以更容易具备这里所说的两种能力。
那么,算法为什么难呢?这个问题只有两种可能的原因:
算法本身就很难。换句话说,算法本身对于人脑来说就是一件很难的事情。
那是一个糟糕的演讲。
下面会解释,算法之所以被大多数人认为很难,是因为以上两个原因都是。
我们说算法难,有两种情况:一种是算法难学。第二是算法设计难。对于前者,大部分人(至少我当年是这样做的)学习算法几乎是背得滚瓜烂熟,就像背菜谱一样(《菜谱》是广大码农喜爱的一类书),但算法和菜谱的区别在于,算法所包含的细节的复杂程度是菜谱的无数倍,算法的问题描述千变万化,逻辑过程无穷无尽,往往让人感到悲哀。相比之下,任何菜谱都只涉及几个基本要素(所以程序员必须具备成为好厨师的潜质:d)注意,即使你看了算法的证明,某种程度上还是“背”的(为什么这么说,我们后面再细说)。我自己遇到新算法基本都会看到证明,但是找到后很快就忘了。这是死记硬背的典型症状。如果你也啃过算法书,我相信很有可能你会有同感:为什么当时明明懂了,没多久就忘了?为什么你当时很懂证明,但想自己证明没多久就发现在上交所补不上缺失的一环?
初中学几何证明的时候,你会傻到背一个定理证明吗?不行,你只能背诵结论。为什么?一方面是因为证明过程包含了很多细节。另一方面,证明的过程是环环相扣的,只要注意一两个关键步骤就可以自己推导出来。算法的逻辑描述就像定理,证明算法的过程就像证明定理的过程。遗憾的是,与数学中许多简洁的基本结论不同,算法的“结论”并不那么容易背诵。在很多情况下,算法本身的逻辑几乎包含了与其证明过程相同的信息量,甚至算法本身的逻辑就是证明过程(只要翻开一本经典的算法书,看几本经典的教科书算法,你就会发现算法的逻辑与算法的证明有多么紧密的联系)。所以我们回到刚才的问题:你会背数学证明吗?既然没人傻到背完整个证明,又何必生硬地背算法呢?
所以,不背就不背,理解算法的证明怎么样?理解算法的证明过程,就更容易记住算法的逻辑细节,理解记忆。然而遗憾的是,绝大多数算法书在这方面做得很差,证明很完整,逻辑也相当严谨。但是,似乎没有一个作者能够真正还原算法发明者本人是如何得到算法以及算法证明的思维过程。按理说,证明过程应该体现这种思维过程,但在下面这个关于霍夫曼编码的例子中,你会看到,事实上,备受好评的CLRS和算法不仅没有还原这一过程,反而掩盖了这一过程。
必须说明的是,没有一个作者是故意这样做的,但在解释自己已经明白的东西时,任何人都会不自觉地将自己的解释“线性化”,比如证明问题。如果你回忆一下高中证明平面几何问题的经历,你会发现证明的过程其实充满了“非线性”,比如试错、联想、逆向推导、特例、修改问题条件、穷举等等。一个混沌的过程,而不是像教科书上写的那样——引理1,引理2,定理1,定理2,一下子直到最后的结论。这个证明过程可能很容易理解,但绝对不容易记住。过几天,你会忘记一个或几个引理,一个或几个关键技术,然后当你想回去自己试着证明的时候,你会发现它卡在了一个关键的地方。为什么?因为证明没有告诉你为什么作者当时认为证明算法需要这样一个引理或者技巧,你看了证明就知道了算法结论的原因,却不知道算法证明过程的原因。在我们大脑的记忆系统中,新的知识必须与已有的知识联系起来,才容易回忆起来(如何有效地学习和记忆)。链接越多,越容易回忆。然而,一个飞行引理与我们现有的知识没有任何联系,没有母亲的孩子很容易被遗忘。为什么还原思维过程这么难?我曾经在《知其所以然(我)》中阐述过。
正是因为大部分算法书中悲剧的算法证明过程,很多人发现证明本身并不容易记住,所以更愿意直接记住结论。当时我在数学系,考试证明过程,但是好像计算机系的考试算法证明过程很荒谬?作为一个“工程化”的程序设计,似乎更注重使用和结果。但是如果项目中需要自己设计一个算法呢?这时候你最起码要做的就是证明算法的正确性。我们在面试的时候,经常会遇到一些算法设计的问题。我总是要求考生证明算法的正确性,因为即使是一个看起来“正确”的算法,往往也不是那么容易证明的。
所以绝大多数算法书从培养算法设计者的角度来说都是失败的,比数学教育还要失败。大部分人在初中学完平面几何后都会做证明题(数学书不要求你记住几何的所有定理),但是很多人看了一本算法书还是一塌糊涂,一些基本算法都不会证明。我们一个结论一个结论的背,不仅很多没用,连在一起用的也不会证明。为什么会有这样的差异?因为数学教育的理想目的是让你成为一个能发现新定理的科学家,但是代码农业系的算法教育的目的更现实,是为了让你成为一个能用算法做事的工程师。然而,真的有那么简单吗?如果是这样的话,连算法的结论都不用背,只要知道算法在做什么,时空复杂度是多少就行了。
如果说上面提到的算法的难度(解释和记忆的难度)属于偶然复杂性,那么算法的另一个难点就是本质复杂性:算法设计。或者拿数学证明做类比(如果你读过《算法导论:一种创造性的方法》,你就会知道算法和数学证明有多相似。),相比于仅仅证明,设计一个算法的难点在于定理和证明都需要你去探索,尤其是前者——需要你自己去发现关键的定理。比起证明已知结论(你知道结论是正确的,你只需要把结论和条件用逻辑联系起来),这件事的复杂程度往往要困难一个数量级。
一个有趣的事实是,算法探索的过程往往包含算法证明的过程。理想的算法书应该还原算法探索的过程,让读者不仅能自己推导证明的过程,还能有探索新算法的能力。我这么说是因为我是个懒人。懒人总是梦想着学点东西来达到以下两个目的:
一劳永逸:程序员都知道“写一次,到处跑”的好处,简单多了。学了就忘了,忘了又要重新学,一遍又一遍的浪费生命。为什么不能看一次就念念不忘呢?是教的不好还是学的不好?
事半功倍:其实程序员不仅讲究一次编写,到处运行,还讲究“一次编写,到处使用”(也称“复用”)。如果学习一个算法获得的经验可以随处使用,学一个当十个,推而广之,时间利用的效率会大大提高。怎样才能学会最大限度的发挥外推经验的效率?
想要做到这两点,就必须尽可能从知识树的“根节点”开始。虽然这是一个美好的梦,比如寻找数学中“根节点”的梦想由来已久(“用泡利亚解题的一点历史”),但哥德尔的证明让梦想化为乌有(“永恒的黄金对角线”);但是,这并不妨碍我们寻找更高层次的节点——更普遍的解决问题的原则和方法。所以,理想的算法书或算法讲解,应该从最一般的思维规律出发,对算法进行逻辑推导。这个过程应该尽量还原一个“普通人”的思维过程,而不是让人觉得“这怎么能想到?”
以本文第一部分提到的霍夫曼编码为例。第一次看到霍夫曼编码的时候,只看到了算法描述,挺直观的。过了两年,我忘了,因为我不知道为什么两个节点的频率加在一起是一个节点——如果我不知道“为什么”,我就记不好。知道了“为什么”就为这件事提供了必然性。如果你不知道“为什么”,你可以选择这个或那个。我们的大脑经常会把其他的东西搞混,比较容易记住那些有理有据的东西(从信息论的角度来说,一个必然的东西的概率是1,一个替代的东西的信息量是0,而一个替代的东西的信息量大于0)。第二次看是下班后,终于知道需要看证明了。我拿出著名的算法,边看边点头。感觉真的很好,一看就明白为什么要构造那样的最优编码树了。可是没过多久,我又忘记了!这次我忘了不是把两个节点的频率加起来算一个,而是忘了我为什么要这么做,因为我不明白霍夫曼为什么能想到我为什么要那样构造最优编码树。结果我只知道一件事,不知道另一件事。
必须注意的是,如果你只关心算法的结论(也就是算法逻辑),那么理解算法的证明就够了,光背算法的逻辑是很难记住的,理解了证明就容易记住很多。但要记住算法的证明,不仅要理解证明,还要理解背后的思维,也就是背后的为什么。后者一般很难在书籍和资料中找到,只有你自己去揣摩。为什么要烦这个神?只要不忘结论,不就结束了吗?看你想做什么,如果你想真正理解算法设计背后的想法,不去揣摩算法的原作者是怎么想出来的是不可能的。