请勿在你的程序中使用 暴力递归 。
文末增加爬楼梯问题解法(Swift)。
真实经历: 在某项目中做 笔记 应用时, 有个需求需要递归遍历 文件夹 下的所有笔记, 文件夹 内包括 文件夹, 默认可以无限包含文件夹, 即 root文件夹 -> 一级文件夹1 | 一级文件夹2 | ... -> 二级文件夹1 | 二级文件2 | ... -> ... -> ... | n级文件夹n . 当时是直接暴力使用递归函数进行开发的, 还好用户量不大并且实际使用中并没有超过 5层 的嵌套, 所以项目在第一阶段还算运行良好, 当我们意识到这里存在重大漏洞后, 经过协商产品最终同意默认文件夹嵌套不超过5层. 操作系统采用的文件存储结构不是简单的数据库, 这里不作过多讨论.
本文将以 Fibonacci 进行讲解为何不应该直接使用暴力递归.
题目十分简单,计算斐波那数列第n项的值.
备注1: 本文代码使用了
&+运算符,不讨论结果,只看重实际运算耗时情况备注2: 本文使用的真机为
iPhone 6系统为iOS 12.4备注3: 为了使用尾递归优化,工程设置了relase模式
# 暴力递归写法
一般使用简单递归解法,代码如下
func fibonacciUseRecursion(_ count: Int) -> Int {
if count == 1 || count == 2 {
return 1
}
return fibonacciUseRecursion(count - 1) &+ fibonacciUseRecursion(count - 2)
}
其时间复杂度 O(2^N).
-
当
count = 30时其执行时间如下 -
当
count = 50时执行时间如下:3次之后我的
iPhone 6已经成为了暖手宝🙃, 平均耗时86s -
count > 50我没勇气试了
对于暴力求解的优化一般是缓存已经求得的结果, 如 [f(3): 2, f(4): 3, f(5): 5, f(6): 8, ...], 下次求解时直接从缓存中查找并返回,本文不作过多讨论,具体可见demo中的 fibonacciUseMemory 等, 其 count = 50 的执行结果如下:
# 使用 For 循环
在使用尾递归前先使用 for 循环求解,代码如下:
func fibonacciUseForLoop(_ count: Int) -> Int {
guard count > 2 else { return 1 }
var sum1 = 1
var sum2 = 1
for _ in 3...count {
(sum1, sum2) = (sum2, sum1 &+ sum2)
}
return sum2
}
期时间复杂度为 O(N).
count = 30, count = 50, count = 10000 等的执行时间,见使用尾递归对比
# 使用尾递归
为确保编译器对尾递归进行优化请在 scheme 设置中将 Run 的配置从 Debug 改为 Release,见文章 备注3.
尾递归写法如下
func fibonacciUseTailRecursion(_ count: Int) -> Int {
if count == 1 || count == 2 {
return 1
}
func fibonacciInternal(sum1: Int = 1, sum2: Int = 1, total: Int) -> Int {
guard total > 1 else { return sum1 &+ sum2 }
return fibonacciInternal(sum1: sum2, sum2: sum1 &+ sum2, total: total - 1)
}
return fibonacciInternal(total: count - 2)
}
其时间复杂度为 O(N).
与 for 循环对比如下
| count | for 循环 | 尾递归 |
|---|---|---|
| 30 | ||
| 50 | ||
| 10000 |
# 总结
当然这里列举 Fibonacci 的暴力递归本身就是 O(2^N) 的复杂度,你可能会列举一个累加1...n 的普通递归进行反驳,代码如下
func sum(_ n: UInt) -> UInt {
if n == 1 {
return 1
}
return n + sum(n - 1)
}
其时间复杂度为 O(N).
尾递归写法如下:
func sum(_ n: UInt) -> UInt {
func sumInternal(_ total: UInt, current: UInt = 0) -> UInt {
if n == 1 {
return current + 1
}
return sumInternal(total - 1, current: current + total)
}
return sumInternal(n)
}
时间复杂度同样为 O(N).
但是别忘了, 普通的递归会一直压栈,如果 n 过大, 例如 n = 10000000 时, 你的程序很可能会因为因为 堆栈爆炸 而崩溃.
因为编译器的优化尾递归并无此问题.
结论: 你应该在应用中尽量避免使用 暴力递归 , 实在需要时请替换为 尾递归 .
附 demo 传送门
# 爬楼梯问题
经典的爬楼梯问题也是一个斐波那数列问题,原版本为:
每次可以爬楼梯1层或者2层, 求第
n层时共有几种爬法;
递归解法为:
f(1) = 1
f(2) = 2
f(n) = f(n-1) + f(n-2)
这里给出变种问题:
每次最多爬
m层, 求爬到第n层有几种爬法;
解法如下:
// MARK: 爬楼梯问题
extension ViewController {
func countFor(totalStep s: Int, maxStep m: Int) -> Int {
if m == 0 || s == 0 {
return 0
}
if s == 1 || m == 1 {
return 1
}
var resultArray: [Int] = Array(repeating: 0, count: s + 1)
resultArray[1] = 1
if s <= m {
for idx in 2...s {
// f(idx-1) + f(idx-2) + ... + f(1)
let pre = (1..<idx).reduce(0, { res, index in
return res &+ resultArray[idx - index]
})
resultArray[idx] = pre &+ 1
}
} else {
// 计算前 m 项的 结果
for idx in 2...m {
// f(idx-1) + f(idx-2) + ... + f(1)
let suf = (1..<idx).reduce(0, { res, index in
return res &+ resultArray[idx - index]
})
resultArray[idx] = suf &+ 1
}
// 计算剩余的结果
for idx in m + 1...s {
resultArray[idx] = Array(1...m).reduce(0, { res, index in
return res &+ resultArray[idx - index]
})
}
}
print("最大步数为 \(m) 时, 走 \(s) 层, 共有 \(resultArray[s]) 种走法")
return resultArray[s]
}
// MARK: 按最大 3 步走时的验证函数
func countByThree(for s: Int) -> Int {
var num1 = 1 // maxStep is one for one step
var num2 = num1 + 1 // maxStep is two for two steps
var num3 = num2 + num1 + 1 // maxStep is three for three steps
// 对于 maxStep 为 m 则有公式 f(m) = f(m-1) + f(m-2) + ... + f(1) + 1
for _ in 4...s {
(num1, num2, num3) = (num2, num3, num1 + num2 + num3/* 2*num3 - num1 */)
}
print("最大步数为 3 时, 走 \(s) 层, 共有 \(num3) 种走法")
return num3
}
}
内含注释,如有疑问请留言。