【数据结构&算法】14-递归&转非递归
目录
- 前言
- 概念定义
- 递归优缺点
- 递归条件
- 编写递归代码方法
- 递归思维建议
- 递归代码注意翻车点
- 堆栈溢出
- 重复计算
- 递归代码改为非递归代码
前言
递归是一种应用非常广泛的算法、编程技巧,如:
- DFS 深度优先搜索;
- 前中后序二叉树遍历等等。
所有的递归都可以转为非递归实现。
李柱明博客:
概念定义
递归的定义:
- 把一个直接调用自己或者通过一系列的调用语句间接地调用自己的函数,称为递归函数。
- 每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。(出口条件)
递归与栈结构:
- 递归进入时,压栈。
- 递归退出时,出栈。
递归优缺点
优点:递归代码的表达力很强,写起来非常简洁。
缺点:
- 空间复杂度高。
- 有堆栈溢出的风险。
- 存在重复计算。
- 过多的函数调用会耗时较多。
递归条件
递归需要满足的两个条件:
-
一个问题可以分解为 n 个子问题。且子问题与原问题有着相同的形式。
- 即是子问题和原问题相比,除了数据规模不同,其求解思路一样。
- 如:中序遍历二叉树。二叉树可以分为一个一个子树,其求解思路都是一样,左中右。
-
存在递归终止条件。
编写递归代码方法
编写递归代码的方法 1:
- 写出递推公式。
- 找到终止条件。
如中序遍历:
- 递推公式,左中右:
in_order(c) = in_order(c_left) -> print c -> in_order(c_right)
- 终止条件:要遍历的结点为空。即是传进来的参数为空。
例子:假如这里有 n 个台阶,每次你可以跨 1 个台阶或者 2 个台阶,请问走这 n 个台阶有多少种走法?
分析:
- 想象成树,下次只能走 1 步或者走 2 步,二叉树。
- 初步结束条件就是没有台阶了。
- 有 n 个台阶的结果是左右子树结果的和。就是
f(n) = f(n-1) + f(n-2)
。 - 结束条件为没有台阶,把 n 值拉到边界进行思考。f(2) = f(1) + f(0),剩下 2 台阶时,走 1 步还剩 1 台阶未结束。但是走 2 步就终止。所以固定终止条件未 f(2) = 2,f(1) = 1。
参考代码:
int f(int n)
{
if (n == 1) return 1;
if (n == 2) return 2;
return f(n-1) + f(n-2);
}
递归思维建议
编写递归代码的关键是,只要遇到递归,我们就把它抽象成一个递推公式,不用想一层层的调用关系,不要试图用人脑去分解递归的每个步骤。
递归代码注意翻车点
堆栈溢出
递归使用的是系统堆栈,如果系统堆栈溢出会造成系统性崩溃。
避免递归堆栈溢出方法:
- 限制递归深度。(采用全局或者静态变量记录递归深度)
重复计算
参考上面台阶例子,递归分解为二叉树:
上面 f(3)就重复计算了,其实没必要。
可以记录每个 f(x) 的结果。在递归过程时优先遍历这些结果,如果没有直接结果才计算。
保存 f(x) 的结果的数据结构有很多种,如散列表、链表、树等等。
递归代码改为非递归代码
所有的递归代码都可以改为迭代循环的非递归写法。
非递归方法思路:因为递归是借助栈来实现的,所以:
- 递归中发生变化的变量(递归返回值)-> 循环中每一次处理完相应变量存入 stack 中(push 进栈)。
- 递归中使用递归返回值 -> 循环中取出 stack 中的值进行处理(pop 出栈)。
既然入栈又出栈,那直接使用一个固定空间,如函数栈,即是使用变量来保存该值。
一般从尾递归开始反过来迭代循环。意思是迭代循环是循环递归中出栈的流程。
如台阶代码:
- 迭代循环递归中出栈的部分,就是从尾递归开始循环,剩余 3 步、剩余 4 步、剩余 5 步...的结果。
- 比如剩余 4 步的结果是前面循环剩余 3 步的结果和剩余 2 步的结果之和。
int f(int n)
{
if (n == 1) return 1;
if (n == 2) return 2;
int ret = 0;
int pre = 2;
int prepre = 1;
for (int i = 3; i <= n; ++i)
{
ret = pre + prepre;
prepre = pre;
pre = ret;
}
return ret;
}