人理解迭代,神则体会递归,从电影艺术到Python代码实现神的逆向思维模式

人理解迭代,神则体会递归,从电影艺术到Python代码实现神的逆向思维模式

    “从来如此,便对么?”,鲁迅先生在《狂人日记》中借狂人之口在月光下发出的质疑与呐喊,是的,从来如此,一般人的思维模式就是从来如此,以高数为例子,我们大抵都是先从数分、线代、解几去学泛函、抽代、拓扑等,其实就是按照标准路子来,这样做理论上可以增加对已学知识的理解程度,并对某些数分、线代中的问题看清其本质有所帮助。数学归纳法其实就是一种迭代(iteration),从一个简单的起点,推广到一般情况。而递归(recursion),则是一种反人类的逆向思维模式,作为研发人员,掌握这种反常识的思维逻辑是非常必要的,这里我们以一个推理故事为开端:

    在一个秋意绵绵的雨夜,日本警视厅搜查课警司古畑任三郎和其助手今泉驾着小汽车行驶在山路上,突然,汽车抛锚了,无奈之下,古畑只好下车徒步寻求帮助,不远处一所别墅的灯光吸引了他,古畑敲响了别墅的大门,打开门的是美女漫画家千奈美,这里正是她的私人别墅,千奈美得知来的是一位刑警,便告诉了古畑一个可怕的事实,在她私人别墅的仓库里,有一具尸体,这具尸体不是谁,正是漫画出版社的编辑畑野先生,死者面朝下趴在地面上,尸体的额头上有碰伤的痕迹,周围散落着一些稿纸。他把其中的一张稿纸紧紧捏在手里,而纸上却没留下任何遗言之类的东西,仅仅是一张白纸,而恰巧,旁边就有一只打开笔帽的钢笔,在简单勘察现场之后,古畑问起千奈美和畑野的关系,千奈美说他们不过是工作上的关系。但因为经常到这儿来商讨出版事宜,所以她给了畑野一把钥匙。

    随后,千奈美微笑解释到,畑野先生可能死于意外,仓库的门一旦关上,从里面是打不开的。畑野的死可能就是因为不小心把自己关在了里面,死因是由于缺氧窒息,她自己已经一个多月没有来到这所别墅了,也正是刚刚,她才发现了畑野的尸体。古畑则不这么认为,他怀疑这是一起谋杀。因为死者头部有伤痕,流了血。千奈美认为不可能,因为门是关上的,没有钥匙打不开,只有她和畑野先生有别墅的钥匙,别人不可能进来打破畑野的头,而自己又已经一个多月没有来过了,古畑此时起了疑心,他认为千奈美的说法不合常理。好比有一罐饼干,我们打开后发现有一块被人咬了一口,通常情况下,我们都会推测是有人先咬了饼干再盖上盖子的。聪明的古畑发现千奈美言行有疑点,于是又折回仓库,在死者的口袋里发现几张三天前的购物发票,据此,古畑分析道:死者肯定知道凶手的名字,因为三天前死者和凶手一同来到别墅,而死者身边肯定藏着揭穿凶手真相的线索,为什么死者会在这么多的稿纸中,紧紧抓住这一张空白的稿纸呢?明明有纸有笔,却什么都没有写。他到底想表达什么呢?古畑费解地思考着。
    此时,雷电交加,屋内突然停电了。千奈美忍不住叹息道:“今晚,真是糟糕透了!”,雷雨交加的黑夜、幽深的别墅、绝美的少女、漆黑的仓库以及恐怖的尸体,这一切显得那么奇特和诡秘,千奈美拿出蜡烛,发现古畑在看书,古畑赞叹千奈美的书真是一部杰作啊。夜深了,两个人都饿了,他们来到储藏室,女主不假思索拿出鸡蛋想做蛋羹汤。细心的古畑怀疑到:你一个多月没来过了,那么这里的鸡蛋真的还能吃吗?
    回到别墅,古畑对千奈美说道:“请原谅我得说出事实的真相,三天前,是千奈美小姐和畑野先生一起来到的别墅吧?”,“我上次来这里是一个月前的事情了,”,千奈美面无表情,“你没有说实话。请问你是怎么知道冰箱里的鸡蛋还是新鲜的?你最近一次来这里不是一个月前,也就是说你是三天前把鸡蛋放进冰箱的。在我的想象中,不知世间冷暖的美女漫画家和花花公子般的出版社编辑,你们之间发生了什么我不清楚,但其中一人终于发现对方只是逢场作戏,于是进行了残酷的报复。”,“古畑先生,这真是一个俗套的故事呢”,千奈美笑了笑,“凶手为什么会打破死者的头呢?如果让别人以为这是一起意外的话,凶手是绝不会这么做的。对吗?可是被害人却被打伤了,这一点很矛盾。于是我仔细考虑了一下,把谋杀伪装成意外的人和故意留下他杀证据的人会不会不是同一人呢?”,
“难道凶手有两个人吗?”,“凶手只有一个,我们其实忽略了非常关键的一点,就是这张白纸,死者确实有留下了线索,只是我们还没有发现罢了。线索就留存在这张纸上。我们换位思考一下,试想畑野先生当时的感受,他无论如何都想留下凶手的名字,但不管他如何想办法写下凶手的名字,一旦要是被凶手先发现的话,就会被销毁,他非常明确这一点,最先发现尸体的一定是凶手,因为凶手把他关在仓库里的,于是呢,他冥思苦想,先拿起一张纸,再把笔帽摘下,而就是什么都没有写,你懂了吗?在能够写下凶手名字的情况下,他什么都没有写,这就是他所留下的线索。他想传达的意思就是‘无论我写什么都是没用的’”。

    “那他头上的伤呢?怎么解释?“

    “这也是畑野先生留下的线索之一。因为他不管怎么想办法留下线索,如果万一案子被当作意外处理了,那么一切都没有意义了。所以他无论如何都要留下这是他杀的证据,因此,他自己给自己的脑袋来了一下。”
    是的,一张什么都没有写的白纸就是亡者留下的线索。这张白纸在默然无声地诉说:杀死我的凶手,就是最先发现我尸体的人。
    这个故事本身并不复杂,由爱生恨的狗血谋杀案,但是故事的核心却是一张白纸,亡者留下的唯一线索,这个线索被古畑发现了,亡者不是不想说出凶手的名字,而是不能说,这里古畑运用的实际上就是基于逆向思维的递归逻辑。

    那么,从代码层面上来看,递归可以帮我们解决什么问题呢?我们以高斯求和为例子,所谓高斯求和,即在一个阈值范围内,将所有的整数相加求和的算术题,如果使用迭代逻辑:

def sum_number(n):
total = 0
for i in range(1, n+1):
total += i
return total

    调用方法:

print(sum_number(5))

liuyue:mytornado liuyue$ python3 "/Users/liuyue/wodfan/work/mytornado/excel_test.py"
1
3
6
10
15
15
liuyue:mytornado liuyue$

    可以看到,迭代思想的本质是递增遍历,按照顺序将元素一个一个的累加,并不难理解,接着我们来试试递归的解法:

def sum_number(n):
if n <= 0:
return 0
return n+sum_number(n-1)

    可以看到,当我们使用递归设计程序的时候,我们从最终结果入手,即要想求得sum_number(5),电脑会把这个计算拆解为求得sum_number(4)的运算,以及sum_number(4)加上5的运算。以此类推,直到拆解为sum_number(1)的运算,就触发终止条件,也就是if结构中n<=0时,返回一个具体的数0。尽管整个递归过程很复杂,但在编写程序时,我们只需关注初始条件、终止条件及衔接,而无须关注具体的每一步。

    递归思维是自顶而下的,我们做事的时候可以先从整体上考虑。先明确需要达到的大目标,而不是一开始就在细节上较真,这其实也是系统论的思想。很多初入职的程序员,在没有清楚项目整体功能架构的情况下,就急于写代码,最终往往导致多次返工,事倍功半,不过使用Python设计递归程序需要注意栈溢出的问题,如果递归深度超出1000层就会报错,所以需要单独设置递归深度:

import sys
#更改递归深度为1百万
sys.setrecursionlimit(1000000)

    搞清楚递归的简单思路,让我们来试一试进阶的操作:尾递归。尾递归相对传统递归,其是一种特例。在尾递归中,先执行某部分的计算,然后开始调用递归,所以你可以得到当前的计算结果,而这个结果也将作为参数传入下一次递归。这也就是说函数调用出现在调用者函数的尾部,因为是尾部,所以其有一个优越于传统递归之处在于无需去保存任何局部变量,从内存消耗上,实现节约特性:

def tail_sum(n,result=0):
if n==0:
return result
else:
return tail_sum(n-1, result+n)

    传统递归的解题步骤:

5+sum_number(4)
5+(4+sum_number(3))
5+(4+(3+sum_number(2)))
5+(4+(3+(2+sum_number(1))))
5+(4+(3+(2+1)))
15

    每一次递归,程序会将计算结果存储在内存中,在递归过程中做累加,直到走向递归出口,尾递归则是通过传参将累加结果进行递归传递:

tail_sum(5,0)
tail_sum(4,5)
tail_sum(3,9)
tail_sum(2,12)
tail_sum(1,14)
tail_sum(0,15)

    如果递归深度非常大的情况下,就可以大量节约内存成本。

    相关视频攻略,请移步:

    

    综上,熟练运用递归需要注意以下三个特点:

    1.问题本身可以拆分成更为简单的子问题,而子问题可以通过相同的方法解决。

    2.解题需要提前考虑程序出口,否则会掉进递归死循环的陷阱。

    3.递归并不是非常高效的算法,大数量级的问题需要尾递归的参与。

    结语:掌握递归逆向思维的人,在解决一些棘手的问题时,往往能够另辟蹊径,善于运用资源,找到解决问题的巧妙办法,就像文章开篇的古畑刑警一样。当我们面对生活中、社会上的种种问题,是否能够想象出鲁迅先生那句“从来如此,便对么”的呐喊。“从来如此”或许对,或许不对。但是作为当代人,自省吾身,与君共勉。