定义
递归是一种广泛应用的算法,或者说,一种编程技巧。
当一个问题满足以下三个条件时,即可通过递归解决
- 一个问题的解可以拆分为几个子问题的解
- 除了数据规模,子问题的求解思路完全一致
- 存在递归终止条件
如何编写递归代码
- 发现规律,推到出递归公式
- 找到终止条件
- 翻译成代码
优点和缺点
优点
- 代码简洁高效
缺点
- 栈溢出风险
- 重复计算
实战
斐波那契数列
- 递归公式
f(n) = f(n-1) + f(n-2)
- 终止条件
f(0) = 0
f(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
}
二叉树的前序遍历
- 递归公式
preOrder(node) = node.Val + preOrder(node.Left) + preOrder(node.Right)
- 终止条件
node = null
- 翻译成代码
/**
* 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
}
分析:对于二叉树的遍历来说,每个节点都是应该被遍历的,不存在重复的计算。
同理:二叉树的中序和后续遍历。
拓展思考
如何控制栈溢出
- 每种编程语言,对于递归调用,每个线程都会有一个最大的调用栈阈值,超出这个阈值,就会报栈溢出异常,终止程序。比如Java可以通过
-Xss来控制线程的堆栈大小。可以理解为这是一个全局配置,可以来控制递归栈的深度。 - 在全局配置范围内,在程序中手动控制栈深度.
- 将递归代码转化为循环迭代代码。通过循环迭代,手动模拟函数调用入栈出栈,自然不受全局配置的限制。
如何避免重复计算
使用数据结构Map(即散列表),用空间换时间。
如何将递归代码转化为循环迭代代码
笼统的讲,所有的递归代码都可以转化为循环迭代代码,不过本质没变,只是手动模拟函数调用入栈出栈,陡增实现复杂度,且可读性不好,理解难度高。
- 斐波那契数列
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
}
- 二叉树的前序遍历
/**
* 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来缓存子问题的解,避免重复计算,用空间换取时间,提高递归效率。