如何考虑是否该用递归解决问题呢

40 阅读3分钟

通常,遇到一个问题时,要想判断他是否能进行递归,主要还是判断他能否被拆解,在判断他能否被拆解的过程中,可以寻找以下三个元素:

  1. 多次相同的大动作
  2. 具体执行的操作
  3. 停止拆解的条件

以汉诺塔的游戏为例,我们来判断下他能否用递归解决。
三个柱子(A B C)、一叠中间带洞的盘子串在A上(这些盘子从上往下,一个比一个大),要求是: 将所有盘子从A移动到C,一次只能移动一个盘子,且移动过程中不能出现大盘子在小盘子上面的场景

分析:
我们看下能否用递归解决这个问题。

寻找相同的大动作

假设有n个盘子,我们把上面n-1个盘子先放到B上,再把A上的n号盘子放到C上,再把B上的n-1个盘子放到C上,不就完成了?
在上面这步操作中我们可以看到把上面n-1个盘子先放到B上, 再把B上的n-1个盘子放到C上 这两个操作,可以进一步拆解,移动n-1个盘子到某个柱子,他跟把n个盘子放到C上,是一样的。
所以,把一摞盘子,移动到某一处,就是一个大动作。

寻找具体执行的操作,(不能都是大动作,没有具体执行吧?)

什么是具体执行的操作呢 还是看上面的一段话,·再把A上的n号盘子放到C上·,这一步,就是具体的执行,这是细化操作的关键,递归就是靠这步具体的执行一点点蚕食掉大的运算。

停止拆解的条件

从寻找大动作时做的拆解中可以看到,我们不断拆解的是将一摞盘子移动到某处,那什么时候停止呢,我们可以把n换成具体的数值来看。
n=1的时候,说明,我们要把上面的0个盘子移动到辅助柱子上,再把1这个盘子放到目标柱子上,再把0这个盘子放到目标柱子上, n=0的时候,就没啥需要执行的了 所以,拆解的中断条件,应该是n=0
通过以上描述我们可以得出以下算法

// 提供盘子数,柱子排列
hannuo(10, 'zhuA','zhuB','zhuC')
// 方法的作用是将一摞盘子移动到某处
// 参数为,盘子数,源柱子,辅助柱子,目标柱子
function hannuo(number,src, aux, des) {
    if(number === 0){
        // 不再移动
        return
    }
    // 移动大模块, 这里注意,我们每次移动大模块,源柱子与目标柱子是会变的
    hannuo(number-1, src, des, aux);
    // 具体操作
    console.log("移动第 "+number+" 个盘子from "+src+ " to " + des)
    // 移动大模块
    hannuo(number-1, aux, src, des);
}

再来拆解一个二叉树中序遍历问题。
假设每个节点的结构如下{id: 'xxx', name: 'xxx', leftChild: {},rightChild: {}}
这个能否用递归解决呢
中序读取,先读根节点,再读左子树,再读右子树。
很明显,读取根节点这个动作已经是具体操作了。
重复的大动作呢
左子树和右子树都是很大的结构,这两个都是重复的大子树读取动作。
什么时候结束呢?
这个节点本身,我们是要读的,但是他没有左子树,就结束了,那就开始读右子树,如果右子树也没有,就真正结束了。
所以,他有两个结束条件。 接下来可以写算法了

const tree={id: 1,name: 1, leftChild: {id:2,name: 2,leftChild: {id: 3,name:3},rightChild:{id: 4,name:4}},rightChild:{id: 5,name: 5}}
const readPath = []
readTree(tree)
function readTree(tree) {
    // 读取根节点
    readPath.push(tree.id)
    // 读取左子树
    tree.leftChild && readTree(tree.leftChild)
    // 读取右子树
    tree.right && readTree(tree.rightChild)
}

以上两种场景都是在已知可以用递归的场景下,进行总结的,至于能不能应用到未知的情况下尚未可知,欢迎大家也来帮我验证。