js遞迴遍歷講解

伊澤瑞爾發表於2018-12-19

JavaScript的遞迴遍歷會經常遇到,適當的運用遞迴遍歷,可以提高程式碼性質量。

1.某些時候遞迴能替換for迴圈

我們先看一下下面2個例子。

var arrList = [1,2,3,5,100,500,10000,10000,1000,10000002]
 //for迴圈測試
 function forTest(){
     console.time("for迴圈")
     for(let i in arrList){
         console.log(((arrList[i] + arrList[i]) * 5 - arrList[i])/arrList[i])
     }
     console.timeEnd("for迴圈")
 }
 //遞迴遍歷測試
 function recursive() {
     console.time("遞迴遍歷")
     const testFun = function (i) {
         console.log(((arrList[i] + arrList[i]) * 5 - arrList[i])/arrList[i])
         if(i == arrList.length - 1){
             return
         }
         i++
         testFun(i)
     }
     testFun(0)
     console.timeEnd("遞迴遍歷")
 }
 forTest()
 recursive()
複製程式碼

執行結果:

在這裡插入圖片描述

可以看到,for迴圈去遍歷一個陣列和用遞迴遍歷去遍歷同一個陣列得到的結果一樣,耗時也幾乎相同。但是寫法上有很大區別。

遞迴特點

每個遞迴都有一個結束遞迴的條件,上例中的:if(i == arrList.length - 1){ return }。每一個遞迴都會在函式內部把函式本身呼叫一次,但是函式在每次執行的時候,都會發生一些變化,用來觸發遞迴的結束,上例中,testFun函式在內部呼叫了自己,並且每次呼叫i的值會+1,最終觸發了結束條件,讓遞迴結束。

2.使用遞迴,可以輕鬆實現多級遍歷

我們先看下面的程式碼:

var data = [
 {
     name: "所有物品",
     children: [
         {
             name: "水果",
             children: [{name: "蘋果", children: [{name: '青蘋果'}, {name: '紅蘋果'}]}]
         },
         {
             name: '主食',
             children: [
                 {name: "米飯", children: [{name: '北方米飯'}, {name: '南方米飯'}]}
             ]
         },
         {
             name: '生活用品',
             children: [
                 {name: "電腦類", children: [{name: '聯想電腦'}, {name: '蘋果電腦'}]},
                 {name: "工具類", children: [{name: "鋤頭"}, {name: "錘子"}]},
                 {name: "生活用品", children: [{name: "洗髮水"}, {name: "沐浴露"}]}
             ]
         }
  ]
}]
複製程式碼

某些時候,坑逼後臺讓我們遍歷上面的一個陣列,最後在頁面上顯示沒一級的最後一個資料。就是下面的資料:

青蘋果;紅蘋果;北方米飯;南方米飯;聯想電腦;蘋果電腦;鋤頭;錘子;洗髮水;沐浴露;

我們先用遍歷的方式來實現:

//普通遍歷實現
var forFunction = function () {
    var str = ""
    data.forEach(function(row){
        row.children.forEach(function(row){
            row.children.forEach(function(row){
                row.children.forEach(function(row){
                    str += (row.name + ";")
                })
            })
        })
    })
    console.log(str)
}
forFunction()
//輸出:青蘋果;紅蘋果;北方米飯;南方米飯;聯想電腦;蘋果電腦;鋤頭;錘子;洗髮水;沐浴露;
複製程式碼

可以看到,前端累死累死寫了4個forEach實現了產品要的效果,這時候如果再加點別的什麼邏輯,就很難弄了。這時候我們嘗試用遞迴的思想實現:

//遞迴遍歷實現
var recursiveFunction = function(){
    var str = ''
    const getStr = function(list){
        list.forEach(function(row){
            if(row.children){
                getStr(row.children)
            }else {
                str += row.name + ";"
            }
        })
    }
    getStr(data)
    console.log(str)
}
recursiveFunction()
//輸出:青蘋果;紅蘋果;北方米飯;南方米飯;聯想電腦;蘋果電腦;鋤頭;錘子;洗髮水;沐浴露;
複製程式碼

可以看到,遞迴的方式來實現的時候,我們只需要一個for迴圈,每次遍歷接受到的資料,通過判斷是否還有children,沒有就代表是最後一級了,有就繼續把children這個list傳給函式繼續遍歷,最後就得到了我們想要的資料。

很明顯,forEach的遍歷的方式能實現多級的遍歷,並且資料格式可以靈活一些,但是遍歷的層級有限,而且對於未知層級的情況下,就無從下手了。 遞迴遍歷,理論上,只要記憶體夠用,你能實現任意層級的遍歷,但缺點也很明顯,沒一個層級裡面需要有固定的資料格式,否則無法遍歷。

3.遞迴遍歷輕鬆實現多個非同步結果的依次輸出

我們先看一下下面的需求,需要依次輸出1,2,3,4,5,每個輸出中間間隔1s。 我們先嚐試下面的方式:

//常規實現
var forTest = function () {
   console.time("for時間")
    for (let i = 0; i < 5; i++) {
        setTimeout(function () {
            console.log('for迴圈輸出:' + (i + 1))
            if(i+ 1 === 5){
                console.timeEnd("for時間")
            }
        }, 1000 * (i + 1))
    }
}
forTest()
//每隔一秒輸出了1,2,3,4,5
複製程式碼

上面我們用let當前作用域的特點實現了每隔1s輸出,其實可以看出,是一起執行的,但是由於間隔時間不一樣,導致結果是每隔一秒輸出的。 我們再用遞迴的方式實現:

//遞迴遍歷實現
var recursiveTest = function(){
   console.time("遞迴時間")
    const test = function (i) {
        setTimeout(function () {
            i = i + 1
            console.log('遞迴輸出:' + i)
            if(i < 5){
                test(i)
            }else {
                console.timeEnd("遞迴時間")
            }
        }, 1000)
    }
    test(0)
}
recursiveTest()
//每隔一秒輸出1,2,3,4,5
複製程式碼

這裡利用遞迴實現就很好理解了,在setTimeout裡面輸出了之後,再開始下一個1s的輸出。 最後我們看一下執行截圖:

輸出結果
通過截圖上的耗時來看:遞迴遍歷需要的時間更長,但是更好理解一下,而且不依賴es6的環境。

4.遞迴遍歷實現排序

var quickSort = function(arr) {
if (arr.length <= 1) {//如果陣列長度小於等於1無需判斷直接返回即可
    return arr;
}
var pivotIndex = Math.floor(arr.length / 2);//取基準點
var pivot = arr.splice(pivotIndex, 1)[0];//取基準點的值,splice(index,1)函式可以返回陣列中被刪除的那個數
var left = [];//存放比基準點小的陣列
var right = [];//存放比基準點大的陣列
for (var i = 0; i < arr.length; i++){ //遍歷陣列,進行判斷分配
    if (arr[i] < pivot) {
        left.push(arr[i]);//比基準點小的放在左邊陣列
    } else {
        right.push(arr[i]);//比基準點大的放在右邊陣列
    }
}
//遞迴執行以上操作,對左右兩個陣列進行操作,直到陣列長度為<=1;
return quickSort(left).concat([pivot], quickSort(right));
};

var arr = [14, 50, 80, 7, 2, 2, 11];
console.log(quickSort(arr));
複製程式碼

這是利用遞迴實現的快速排序,效率很高,有興趣的同學可以試試普通的遍歷實現,再進行比較。

5.總結

1.很多時候可以用遞迴代替迴圈,可以理解為遞迴是一種特殊的迴圈,但通常情況下不推薦這樣做。 2.遞迴一般是在函式裡面把函式自己給呼叫一遍,通過每次呼叫改變條件,來結束迴圈。 3.遞迴在資料格式一致,在資料層級未知的情況下,比普通的遍歷更有優勢。 4.遞迴在非同步的時候,更容易理解,且更容易實現,因為可以在非同步的回撥裡面,呼叫自己來實現每次都能拿到非同步的結果再進行其他操作。 5.遞迴實現的快速排序比普通遍歷實現的排序效率更好。

所有的程式碼都能在GitHub下載:下載。 想看更多文章,可以關注我的個人公眾號:

公眾號

相關文章