如何用循环取代递归
如何用循环取代递归
1. 引子
在实际开发中,我们经常会用到一种写法,那就是递归。只要是遍历一个有层级的结构,毫无疑问,你第一方法就是递归去处理。但是我在开发中,常常不想问了一个小功能,就去写一个方法处理递归,毕竟给方法命名是极其痛苦的,原谅的词汇量的稀少。以前大学时,听老师说过:凡是递归,必定可以用循环解决。所以就花了点时间思考了下如何用循环取代递归。
2. 递归和循环比较
先说下递归和循环各自的优缺点:
递归:
优点:简单,易于理解,不用关心嵌套了多少层
缺点:需要把递归的业务单独提取,开一个新的方法;如果递归层数较深,容易发生栈溢出;调试极其不友好;效率不太好,需要频繁进入方法
循环:
优点:效率高,不需要担心栈溢出问题
缺点:逻辑复杂,难理解,难维护(尤其是你写的又长又臭的时候)
3. 递归与循环的转换
一下所有讲解的代码为了方便我都使用JavaScript,请自己转换成自己用的语言
对于递归,最好的例子就是遍历树,所以我们先构建一棵树:
{
"id": 1,
"name": "根节点",
"children": [
{
"id": 2,
"name": "节点2",
"children": [
{
"id": 4,
"name": "节点4",
"children": [
{
"id": 6,
"name": "节点6",
"children": []
}
]
},
{
"id": 5,
"name": "节点5",
"children": []
}
]
},
{
"id": 3,
"name": "节点3",
"children": []
}
]
}
递归实现:
let json = JSON.parse(`{"id":1,"name":"根节点","children":[
{"id":2,"name":"节点2","children":
[{"id":4,"name":"节点4","children":[{"id":6,"name":"节点6","children":[]}
]}
,{"id":5,"name":"节点5","children":[]}]},{"id":3,"name":"节点3","children":[]}
]}`);
recursion(json);
function recursion( tree ){
// 如果不存在,直接返回,作为递归结束
if(!tree){
return;
}
// 业务处理
console.info(`${tree.id} -- ${tree.name}`);
// 递归处理子节点
if( tree.children ){
for (const item of tree.children) {
recursion(item);
}
}
}
循环实现(广度优先):
先给个提示:队列
let json = JSON.parse(`{"id":1,"name":"根节点","children":[
{"id":2,"name":"节点2","children":
[{"id":4,"name":"节点4","children":[{"id":6,"name":"节点6","children":[]}
]}
,{"id":5,"name":"节点5","children":[]}]},{"id":3,"name":"节点3","children":[]}
]}`);
// 创建一个数组作为队列
let queue = [];
// 顶层节点入队
queue.push(json);
while( queue.length > 0 ){
// 出队一个元素
let item = queue.shift();
// 业务处理
console.info(`${item.id} -- ${item.name}`);
// 子节点入队
if( item.children ){
for (const childItem of item.children) {
queue.push(childItem);
}
}
}
循环实现(深度优先):
先给个提示:堆栈
let json = JSON.parse(`{"id":1,"name":"根节点","children":[
{"id":2,"name":"节点2","children":
[{"id":4,"name":"节点4","children":[{"id":6,"name":"节点6","children":[]}
]}
,{"id":5,"name":"节点5","children":[]}]},{"id":3,"name":"节点3","children":[]}
]}`);
// 创建一个数组作为栈
let stack = [];
// 顶层节点入栈
stack.push(json);
while( stack.length > 0 ){
// 出栈一个元素
let item = stack.pop();
// 业务处理
console.info(`${item.id} -- ${item.name}`);
// 子节点入栈
if( item.children ){
for (const childItem of item.children.reverse()) {
stack.push(childItem);
}
}
}
4. 总结
用循环实现递归其实不难,借助队列和栈这两种数据结构就可以很简单地实现。但是我们需要将原本递归处理的数据封装成一个新的数据结构,作为元素传入队列/栈中。例如我们上个例子中,每个节点对象就是一个递归处理数据,因为节点对象本身就是一个对象,所以我们才没必要在封装了。但如果相对应的递归函数:recursion( x , y ),这样子,那我们就需要封装一下了:{ x: "", y: "" },封装成一个对象
如果可以,其实还是递归更简单,也推荐用递归,除非你像我一样,不喜欢创建多一个函数,或者栈溢出。