目的
笔者认为,递归是不可控的,业务数据难免存在错误数据,一旦没有很好的把控,递归层数激增,StackOverflowError
就出现了,所以,在没有其他替代算法时,至少应该将递归转成循环逻辑
准备
所谓递归,即函数自己调用自己,最后调用的先结束,可见,它的调用顺序就是个栈—先入后出。通过手动维护一个栈结构,加上循环逻辑,那么,理论上递归就可以被取代了,事实也确实如此,所有的递归,都可以转为【栈+循环】的结构,这些可以找相应的文章查阅
示例
来个爬楼梯的例子:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶,并且每次你只可以爬 1 或 2 个台阶,那么有多少种不同的方法可以爬到楼顶呢?
例如:楼梯有3阶
则有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
用递归的方式,可以很好得出答案,且简单易懂
fun climbStairs(n: Int): Int {
if (n == 0) return 1
if (n < 0) return 0
val r1 = climbStairs(n - 1)
val r2 = climbStairs(n - 2)
return r1 + r2
}
但是,当n的数字很大时,递归是hold
不住的。
那么,怎么改成循环呢?
首先我们要有个栈,用来取代方法的先入后出式调用
fun climbStairs(n: Int): Int { val stack: Stack<Int> = Stack() ... }
至于栈的存放内容,应该是我们的台阶数为 n,即 剩余 n 个台阶时,还会有几种方式到达
fun climbStairs(n: Int): Int { val stack: Stack<Int> = Stack() stack.push(n) ... }
栈有了,那还得来个循环,而循环首先要确定终止条件,这个倒是不难,由递归改造而来的循环,自然是递归调用结束,也就是栈里面没内容时
fun climbStairs(n: Int): Int { val stack: Stack<Int> = Stack() stack.push(n) while(!stack.isEmpty()) { ... } ... }
至于循环的内容,就要根据具体业务来确定了。
本例中,首先我们需要返回值,所以要有一个循环作用域外的变量用来接收,而每次计算返回值的条件就是n=0
,即表示刚好登完台阶,多说无益,直接看代码就能理解了fun climbStairs(n: Int): Int { val stack: Stack<Int> = Stack() stack.push(n) var count = 0 //保存结果 while(!stack.isEmpty()) { val num = stack.pop() //每次出栈一个数据 var n1 = num - 1 //登1级台阶时 var n2 = num - 2 //登2级台阶时 if (n1 == 0 || n2 == 0) count++ //登完台阶时,表示这是一种方案,结果值加1 if (n1 > 0) stack.push(n1) //如果登完1阶后还有剩余台阶,则继续 if (n2 > 0) stack.push(n2) //如果登完2阶后还有剩余台阶,也一样继续 } return count }
优化
可以看出,将递归转为循环后,如果n
足够大,那么入栈的数据也是极大的,相对直接递归而言,无非就是将方法栈的开销转为了手工维护的栈的开销,空间复杂度或许少了些,但是时间复杂度依旧,所以,最好是能进一步优化
本例中,若n=5
,则必然存在这两种方案
- 先走两阶,剩余三阶
- 走一阶,再走一阶,剩余三阶
对于这剩下的三阶,就是重复计算了,而它们的计算结果显然是可以共用的,这就引出了一种通用的优化方案:利用哈希表
来存储中间值,避免重复计算
当然,本例只是具体样例,它的最优解,应是换做【动态规划】算法