1. 程式人生 > 其它 >如何用迴圈取代遞迴

如何用迴圈取代遞迴

如何用迴圈取代遞迴

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: "" },封裝成一個物件

​ 如果可以,其實還是遞迴更簡單,也推薦用遞迴,除非你像我一樣,不喜歡建立多一個函式,或者棧溢位。