1、什么是递归
函数调用本身就叫递归,这没啥好说的,真正要理解的是‘递 和 ‘归’ 这字的含义,在理解这两个字的概念前,我们首先要清楚的是函数调用中会存在一个调用栈
的玩意,它就跟数组一样是个容器
,遵循着进栈
与 出栈
,这个进与出的消除循序是按照先进后出
,递归对应着就是这种关系,递
就是进
,归
就是出
,栗如 f1 函数里头调用其它函数时,f1 函数先进,等到最后才出,看例子
function f3() {}
function f2() { f3() }
function f1() { f2() }
f1()
进 [f1,f2,f3] 出 f3、f2、f1
如果函数里头没有调用其它函数,则进栈后立即出栈
function f1(){}
function f2() {}
f1()
f2()
进 [f1] 出 f1 进 [f2] 出 f2
现在思考下面的题,它的进栈是怎样的?
function f1() {
f1()
}
f1()
很明显一直是进 [f1,f1,f1,f1,f1,f1,f1 …] 没有出,由于调用栈
的容器
是有限的,超过一定的调用次数(进栈)它就会报 栈溢出
,上面案例就是一个死递归,我们应该给它一个终止条件变成一个活递归。
function f1(n) {
if (n == 1) return 1
fn(n - 1)
}
f1(4)
进栈 f1(4),f1(3),f1(2),f1(1)] 出栈 f1(1)、f1(2)、f1(3)、f1(4)
2、递归的作用
递归可以用来 深拷贝
、渲染树形图
、计算阶乘
、排序
、移动位置
、中间件调用
等各种场景,其本质就是复用,节省代码和循环,使代码看起来更加简洁、优雅。
3、递归案例分析
接下来我将使用 JS 作为案例演示
3.1 JavaScript 深拷贝
function copyDeep(obj) {
let o = {}
for (let key in obj) {
if (typeof key === 'Object') {
o[key] = copyDeep(key)
} else {
o[key] = obj[key]
}
}
return o
}
let father = {
name: 'Jack',
children: [
{
name: 'Tony',
age: 15,
children: [{name: 'Baby', age: 20}, {name: 'Anna', age: 2}]
},
{
name: 'Candy',
age: 20
}
]
}
let mother = copyDeep(father)
我们来分析一下上面的执行过程,首先我们只需关心里面的 o[key] = copyDeep(key)
递归调用,假设不使用递归而是把 copyDeep 换成 copy 函数
if (typeof key === 'Object') {
o[key] = copy(key)
} else {
o[key] = obj[key]
}
copy
函数的代码如下
function copy(obj) {
let o = {}
for (let key in obj) {
o[key] = obj(key)
}
return o
}
这样做没问题,但是我们知道,Tony 还有一层 children,所以里面的 copy 还要在新增一个 copy2 函数,代码如下
function copy2(obj) {
let o = {}
for (let key in obj) {
o[key] = key
}
return o
}
function copy(obj) {
let o = {}
for (let key in obj) {
if (key === 'Object') {
o[key] = copy2(key)
} else {
o[key] = key
}
}
return o
}
按照这种情况每一层对象都要新增一个方法,而且这些方法是一模一样,现在我们再来理解递归调用就能明白其作用了。
if (typeof key === 'Object') {
o[key] = copyDeep(key)
} else {
o[key] = obj[key]
}
3.2 快速排序
快速排序就是冒泡排序的升级版,想要详细了解快速排序可以阅读我前面写过的 理解快速排序
function swap(arr, i, j) {
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
function QuickSort(arr, low, high) {
if (low >= high) return
// 左右各自一个指针
// pivot 作为左右指针时比较的基准值
let i = low, j = high, pivot = arr[low]
while(i < j) { // 当出现 i < j 时说明左右边指针已经碰到一起了,可以结束掉
// 右边大于基准值的忽略掉继续从右往左走
while (arr[j] >= pivot && i < j ) {
j --
}
// 左边大于基准值的忽略掉继续从左往右走
while (arr[i] <= pivot && i < j ) {
i ++
}
// 指针走完后会出现 arr[j] < pivot && arr[i] > pivot 的局面,
// 这时候就可以调换位置了
swap(arr, i, j)
}
// 第一次排序完后变成这样:[10, 8, 2, 9, 4, 20, 30, 12]
// 这时候我们再将基准值放在它们两者的中间,最后就会变成
// [8, 2, 9, 4, 10, 20, 30, 12] 左边都是小于10,右边都是大于10
swap(arr, low, j)
// 按照上面这种关系,我们使用递归继续复用,传递左边的集合对应的索引即:[8, 2, 9, 4]
QuickSort(arr, low, j - 1)
// 按照上面这种关系,我们使用递归继续复用,传递左边的集合对应的索引即:[20, 30, 12]
QuickSort(arr, j + 1, high)
// 最终就能排好顺序了
return arr
}
let arr = [10, 2, 9, 12, 8, 20, 30, 4]
QuickSort(arr, 0, arr.length - 1)
// 排序后
// arr: [2, 4, 8, 9, 10, 12, 20, 30]
快速排序的实现版本有很多,但解题思路是一致的,它主要用到了分而治之思想,对快速排序不理解的同学建议别光看代码,自己先动动小手写一遍,然后使用相关 debugger
调试工具试图理解它们之间的传递逻辑。
4. 理解两层递归的执行顺序
如果你能理解两层递归的执行顺序,那我相信你对递归会有一个更深刻的理解,看下面的分析前,思考下面的代码最终会打印什么。
function f(n):
console.log('f::', n)
if (n == 1) {
return 1
}
return f(n-1) + f(n-1)
f(4)
分析:
调用 f(4) 打印 f:: 4,if 条件不符合继续往下走
调用f(n-1),打印 f::3,if 条件不符合继续往下走
调用f(n-1),打印 f::2,if 条件不符合继续往下走
调用f(n-1),打印 f::1,if 条件符合,结束当前函数,回栈
到 f::2 函数
f::2 后面调用 + f(n-1),打印 f::1,if 条件符合,结束当前函数,回栈
到 f::3 函数
f::3 后面调用 + f(n-1),打印 f::2,if 条件不符合继续往下走
调用f(n-1),打印 f::1,if 条件符合,结束当前函数,回到就 f::2 函数
f::2 后面调用 + f(n-1) ,打印 f::1,结束当前函数,回栈
到 f::4 函数
f::4 后面调用 + f(n-1),打印 f::3,if 条件不符合继续往下走
调用f(n-1),打印 f::2,if 条件不符合继续往下走
调用f(n-1),打印 f::1,if 条件符合,结束当前函数,回到 f::2 函数
f::2 后面调用 + f(n-1),打印 f::1,if 条件符合,结束当前函数,回到 f::3 函数
f::3 后面调用 + f(n-1),打印 f::2,if 条件不符合继续往下走
调用f(n-1),打印 f::1,if 条件符合,结束当前函数,回到 f::2 函数
f::2 后面调用 +f(n-1),打印 f::1,结束当前函数,全部栈已消除完毕
至此递归就全部调用完了,最终打印:
f:: 4
f:: 3
f:: 2
f:: 1
f:: 1
f:: 2
f:: 1
f:: 1
f:: 3
f:: 2
f:: 1
f:: 1
f:: 2
f:: 1
f:: 1
完!