如何用循環取代遞歸


如何用循環取代遞歸

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM