算法 & 理解递归

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

完!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cookcyq

请作者喝杯暖暖的奶茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值