如何用迴圈取代遞迴
阿新 • • 發佈:2021-07-07
如何用迴圈取代遞迴
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: "" },封裝成一個物件
如果可以,其實還是遞迴更簡單,也推薦用遞迴,除非你像我一樣,不喜歡建立多一個函式,或者棧溢位。