数据结构和算法-递归

132 阅读3分钟

定义

递归是一种广泛应用的算法,或者说,一种编程技巧。

当一个问题满足以下三个条件时,即可通过递归解决

  1. 一个问题的解可以拆分为几个子问题的解
  2. 除了数据规模,子问题的求解思路完全一致
  3. 存在递归终止条件

如何编写递归代码

  1. 发现规律,推到出递归公式
  2. 找到终止条件
  3. 翻译成代码

优点和缺点

优点

  1. 代码简洁高效

缺点

  1. 栈溢出风险
  2. 重复计算

实战

斐波那契数列

  1. 递归公式
f(n) = f(n-1) + f(n-2)
  1. 终止条件
f(0) = 0
f(1) = 1
  1. 翻译成代码
func fib(n int) int {
    if n == 0 {
        return 0
    }
    if n == 1 {
        return 1
    }
    return fib(n-1) + fib(n-2)
}

分析:这个问题的求解过程存在大量重复计算。
比如 f(n) = f(n-1) + f(n-2) 和 f(n-1) = f(n-2) + f(n-3),f(n) 和 f(n-1) 的解包含相同的计算 f(n-2),这种情况依此叠加,会导致递归过程存在大量重复计算。通过数据结构Map缓存子问题的解来避免这种情况,用空间换取时间,提高递归效率。

var cache = map[int]int{}

func fib(n int) int {
    if n == 0 {
        return 0
    }
    if n == 1 {
        return 1
    }
    if v, has := cache[n]; has {
        return v
    }
    val := fib(n-1) + fib(n-2)
    cache[n] = val
    return val
}

二叉树的前序遍历

  1. 递归公式
preOrder(node) = node.Val + preOrder(node.Left) + preOrder(node.Right)
  1. 终止条件
node = null
  1. 翻译成代码
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func preorderTraversal(root *TreeNode) []int {
    ret := []int{}
    var preOrder func(*TreeNode)
    preOrder = func(node *TreeNode) {
        if node == nil {
            return
        }
        ret = append(ret, node.Val)
        preOrder(node.Left)
        preOrder(node.Right)
    }
    preOrder(root)
    return ret
}

分析:对于二叉树的遍历来说,每个节点都是应该被遍历的,不存在重复的计算。
同理:二叉树的中序和后续遍历。

拓展思考

如何控制栈溢出

  1. 每种编程语言,对于递归调用,每个线程都会有一个最大的调用栈阈值,超出这个阈值,就会报栈溢出异常,终止程序。比如Java可以通过 -Xss 来控制线程的堆栈大小。可以理解为这是一个全局配置,可以来控制递归栈的深度。
  2. 在全局配置范围内,在程序中手动控制栈深度.
  3. 将递归代码转化为循环迭代代码。通过循环迭代,手动模拟函数调用入栈出栈,自然不受全局配置的限制。

如何避免重复计算

使用数据结构Map(即散列表),用空间换时间。

如何将递归代码转化为循环迭代代码

笼统的讲,所有的递归代码都可以转化为循环迭代代码,不过本质没变,只是手动模拟函数调用入栈出栈,陡增实现复杂度,且可读性不好,理解难度高。

  1. 斐波那契数列
func fib(n int) int {
    if n < 2 {
        return n
    }
    pre, cur := 0, 1
    for i:=2; i<=n; i++ {
        tmp := pre + cur
        pre = cur
        cur = tmp
    }
    return cur
}
  1. 二叉树的前序遍历
/**
 * Definition for a binary tree node.
 * type TreeNode struct {
 *     Val int
 *     Left *TreeNode
 *     Right *TreeNode
 * }
 */
func preorderTraversal(root *TreeNode) []int {
    ret := []int{}
    stack := []*TreeNode{}
    node := root 
    for node != nil || len(stack) > 0 {
        for node != nil {
            ret = append(ret, node.Val)
            stack = append(stack, node)
            node = node.Left
        }
        node = stack[len(stack)-1].Right
        stack = stack[:len(stack)-1]
    }
    return ret
}

总结

递归是一种编程技巧,满足递归三个条件的问题都可以翻译成递归代码来解决;递归代码简洁高效,可读性高,容易理解,但是容易出现栈溢出和重复计算的问题。对于栈溢出问题,可以通过将代码转化为循环迭代来解决,随之带来的弊端就是实现难度大,代码可读性差;对于重复计算问题,可以通过使用散列表Map来缓存子问题的解,避免重复计算,用空间换取时间,提高递归效率。