递归算法是《数据结构与算法》中最简洁的算法之一,它可以非常简明地描述“减而治之”(decrease and conquer)和“分而治之”(divide and conquer)这两种算法思想。递归算法虽然从代码角度来看非常简单,但对于新手理解起来却不那么简单。本文我将结合《数据结构与算法》的专业描述和《程序员的数学》的通俗描述,并以“汉诺塔”为例来讲解我对“递归”算法的理解,并给出 Python 代码描述。
“汉诺塔”是由数学家 Edouard Lucas 于1883 年发明的游戏,具体内容可以看 wiki - 汉诺塔 ,下面给出 3 层汉诺塔的移动示意图:
从上图可以观察到,过程 1、2、3 和过程 5、6、7是非常相似的,前者将 2 个圆盘从 A 移到 C ;后者将 2 个圆盘从 C 移到 B 。总结如下:
从上面对“汉诺塔”迷题的观察和分析可以清楚的反映“递归”的思维方式:当我们碰到“简单问题易解(一层汉诺塔),复杂问题难解(多层汉诺塔)”时,尝试问自己“能将复杂的问题转化为较为简单的同类问题吗?”
这就是递归的思维方式,对于汉诺塔来说,就是将 n 层汉诺塔转换为 n-1 层汉诺塔问题,即在问题中找出递归结构。
如果找到了递归结构,接下来就是根据递归结构建立递归公式
换成专业术语来描述:递归算法的关键就是找到递归基(base case)和递推公式(recursion relation, recurrence)。其中,递归基一般比较容易找到,而递推公式往往隐藏在大量的细节中,需要我们去分析提取。一般来说,分析提出递归公式,先要归纳出递归实例(同类问题,规模不同),比如 n 层汉诺塔问题和 n-1 层汉诺塔问题。有了递归实例的概念,然后需要逆向思维(倒推),即观察构成更大规模问题的最后几步。比如:6 层汉诺塔 A –> B 移动完成前,最后一步(一大步)必然是将一个 5 层汉诺塔 C –> B 移动,而倒数第二步必然是最大的圆盘 A –> B 移动。很明显,这两步都可以概括为一个递归实例。倒推出了这两步,你也就明白了前面的所有步骤无非就是将一个 5 层汉诺塔 A –> B 移动。至此,就找到了 6 层汉诺塔问题的递归结构,然后再推广到 n 层汉诺塔问题。
下面通过代码来体会汉诺塔问题的递归算法
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
r'''
tower_of_hanoi.py
Usage:
python3 tower_of_hanoi.py
'''
def move(n, source, target, auxiliary):
# base case
if n == 1:
print(source, '-->', target)
return
# first step: move n - 1 disks from source to auxiliary, so they are out of the way
move(n-1, source, auxiliary, target)
# second step: move the nth disk from source to target
move(1, source, target, auxiliary)
# last step: move the n - 1 disks that we left on auxiliary onto target
return move(n-1, auxiliary, target, source)
if __name__ == '__main__':
# initiate call from source A to target C with auxiliary B
move(3, 'A', 'C', 'B')
从上面的代码可以看出:递归算法的确很简洁,真个算法,除了 base-case 外,就剩三次递归调用,这三次递归调用也清晰的描述了该问题的递归结构。