如何用循環取代遞歸
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: "" },封裝成一個對象
如果可以,其實還是遞歸更簡單,也推薦用遞歸,除非你像我一樣,不喜歡創建多一個函數,或者棧溢出。