概念
- 在程序中函数直接或间接调用自己。
- 就递归而言最重要的就是跳出结构,因为跳出了才可以有结果。
递归要点
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
以二叉树的前序遍历为例:
- 函数的参数和返回值: 因为要打印前序遍历的节点值,所以要传入的参数就包含树的根节点,只涉及到打印,所以不要返回值和其他参数了,代码如下
const preOder = node => {
};
- 确定终止条件: 在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下:
// 递归的结束条件
if (!node) return;
- 确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下:
console.log(node.val);
//递归的基本结构
preorder(node.left);
preorder(node.right);
一般什么问题需要使用递归
数组深层扁平化
我们在没封装成函数之前,肯定是用一个全局变量来保存最终的结果的,然而当我们使用函数将功能封装起来的时候,最好是不使用全局变量,而是在函数上添加一个参数用来记录整个递归过程中我们需要关注的这个值。
代码实现:
function flattenDeep(arr, ret = []) {
arr.forEach((item) => {
// 先判断 item 如果是数组,则进行递归
if (Array.isArray(item)) {
const flatItem = flattenDeep(item); //递归
flatItem.forEach((n) => ret.push(n));
} else {
// 如果不是数组,则把当前项加入到返回结果中
ret.push(item);
}
});
return ret;
}
还有下面的这道题
同样我们有三个地方来存贮最终的结果: 全局变量
递归函数内部的引用值
递归函数的参数
,在这里说白了,后两者是没有什么差异的,但是我觉得后者更容易理解,因为这里最终要的结果是一个数组,而数组在函数中传值是引用传递的
我们直接来看一下代码吧。
第二种:这种我们就需要接受递归函数的返回值,再加入到最外层的ret
结果里面,显得有些繁琐。
function preorderTraversal( root ) {
if(!root) return []
let ret = []
ret.push(root.val)
if(root.left){
let leftVal = preorderTraversal(root.left)
ret = ret.concat(leftVal)
}
if(root.right){
let rightVal = preorderTraversal(root.right)
ret = ret.concat(rightVal)
}
return ret
}
第三种:
function preorderTraversal( root, ret = [] ) {
if(!root) return ret
// write code here
ret.push(root.val)
preorderTraversal(root.left, ret)
preorderTraversal(root.right, ret)
return ret
}
实现一个深拷贝
像深拷贝这种,又和前面的情况不一样了。因为它的返回结果不在是一个线性的了,貌似用参数的形式不太好处理了,
普通深拷贝
- 只考虑Object Array
- 无法转换Map 和 Set 和 循环引用
代码实现
function deepclone(obj:any){
if(typeof obj !=='object' || obj ==null) return obj
let result: any
if(obj instanceof Array){
result = []
}else{
result = {}
}
for(let key in obj){
if(obj.hasOwnProperty(key)){
result[key] = deepclone(obj[key]) //递归调用
}
}
return result
}
遍历DOM树
//遍历Dom tree
/**
* 访问节点
* @param n node
* */
function visitNode(n: Node){
if (n instanceof Comment){
//注释
console.info('Comment node ---',n.textContent)
}
if (n instanceof Text){
//文本
const t = n.textContent?.trim()
if (t){
console.info('Text node ----',t)
}
}
if (n instanceof HTMLElement){
//element
console.info('Element node ---',`<${n.tagName.toLowerCase()}>`)
}
}
/**
* 深度优先遍历——递归
* @param root dom node
* */
function depthFirstTraverse(root:Node){
visitNode(root)
const childNodes = root.childNodes //.childNodes(比children 多了一些文本 和 注释) 和 .children 不一样
if (childNodes.length){
childNodes.forEach(child => {
depthFirstTraverse(child)
})
}
}
/**
* 深度优先遍历——栈
* @param root dom node
* */
function depthFirstTraverse1(root:Node){
const stack:Node[] = []
//根节点压栈
stack.push(root)
while (stack.length > 0){
const curNode = stack.pop() // 出栈
if(curNode == null) break
visitNode(curNode)
//子节点压栈
const childNodes = curNode.childNodes
if (childNodes.length > 0){
//reverse 反顺序压栈
Array.from(childNodes).reverse().forEach(child => stack.push(child))
}
}
}
/**
* 广度优先遍历
* @param root dom node
* */
function breadthFirstTraverse(root:Node){
//定义一个队列
const queue:Node[] = [] //数组 vs 链表
//根节点入队列
queue.unshift(root)
while (queue.length > 0){
const curNode = queue.pop()
if (curNode == null) break
visitNode(curNode)
//子节点入队
const childNodes = curNode.childNodes
if (childNodes.length){
childNodes.forEach(child => queue.unshift(child))
}
}
}
const box = document.getElementById('box')
if(box == null) throw new Error('box is null')
//深度功能测试-递归、贪心,可以不用递归来实现,可以使用栈来实现
depthFirstTraverse(box)
//广度功能测试-使用队列(数组 vs 链表)
breadthFirstTraverse(box)
树形结构扁平化
function flattTree(data, res) {
data.forEach((el) => {
if (el.children) {
res.push(el);
flattTree(el.children, res); //递归
//要把孩子children删除掉
delete el["children"];
} else {
res.push(el);
}
});
}
小结
-
本来是想记录一下,自己写递归函数时候的一些技巧的,结果发现在写文章的时候,貌似没有什么技巧,所以还是记住三要素就行了。
-
还有就是平时会写写一些工具函数,比如,
let arr = [
{
},
'',
undefined,
{
name: ''
},
value: 100
]
大概意思就是删除数组或者对象中存在的空数组
,空字符串
,undefined
,这些可能是在业务中初始化form
给定的初始值,但是在传递给后端的时候又不能传递过去,所以我们就需要写一个通用的方法来处理请求中的参数。
参考文章
- 我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。