JavaScript 數據結構與算法之美 - 遞歸

![JavaScript 數據結構與算法之美](https://upload-images.jianshu.io/upload_images/12890819-9f08a1abed2d7caf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

前言

  1. 算法爲王。
  2. 排序算法博大精深,前輩們用了數年甚至一生的心血研究出來的算法,更值得咱們學習與推敲。

由於以後要講有內容和算法,其代碼的實現都要用到遞歸,因此,搞懂遞歸很是重要。前端

1. 定義

  • 方法或函數調用自身的方式稱爲遞歸調用,調用稱爲遞,返回稱爲歸。

簡單來講就是:本身調用本身git

現實例子:週末你帶着女友去電影院看電影,女友問你,我們如今坐在第幾排啊 ?電影院裏面太黑了,看不清,無法數,如今你怎麼辦 ?github

因而你就問前面一排的人他是第幾排,你想只要在他的數字上加一,就知道本身在哪一排了。
可是,前面的人也看不清啊,因此他也問他前面的人。
就這樣一排一排往前問,直到問到第一排的人,說我在第一排,而後再這樣一排一排再把數字傳回來。
直到你前面的人告訴你他在哪一排,因而你就知道答案了。算法

基本上,全部的遞歸問題均可以用遞推公式來表示,好比:json

f(n) = f(n-1) + 1; 
// 其中,f(1) = 1

f(n) 表示你想知道本身在哪一排,f(n-1) 表示前面一排所在的排數,f(1) = 1 表示第一排的人知道本身在第一排。數組

有了這個遞推公式,咱們就能夠很輕鬆地將它改成遞歸代碼,以下:瀏覽器

function f(n) {
  if (n == 1) return 1;
  return f(n-1) + 1;
}

2. 爲何使用遞歸 ?遞歸的優缺點 ?

  • 優勢:代碼的表達力很強,寫起來簡潔。
  • 缺點:空間複雜度高、有堆棧溢出風險、存在重複計算、過多的函數調用會耗時較多等問題。

3. 什麼樣的問題能夠用遞歸解決呢 ?

一個問題只要同時知足如下 3 個條件,就能夠用遞歸來解決。數據結構

    1. 問題的解能夠分解爲幾個子問題的解。何爲子問題 ?就是數據規模更小的問題。

好比,前面講的電影院的例子,你要知道,本身在哪一排的問題,能夠分解爲前一排的人在哪一排這樣一個子問題。函數

    1. 問題與子問題,除了數據規模不一樣,求解思路徹底同樣

好比電影院那個例子,你求解本身在哪一排的思路,和前面一排人求解本身在哪一排的思路,是如出一轍的。學習

    1. 存在遞歸終止條件

好比電影院的例子,第一排的人不須要再繼續詢問任何人,就知道本身在哪一排,也就是 f(1) = 1,這就是遞歸的終止條件。

4. 遞歸常見問題及解決方案

    1. 警戒堆棧溢出:能夠聲明一個全局變量來控制遞歸的深度,從而避免堆棧溢出。
    1. 警戒重複計算:經過某種數據結構來保存已經求解過的值,從而避免重複計算。

5. 如何實現遞歸 ?

1. 遞歸代碼編寫

寫遞歸代碼的關鍵就是找到如何將大問題分解爲小問題的規律,而且基於此寫出遞推公式,而後再推敲終止條件,最後將遞推公式和終止條件翻譯成代碼。

2. 遞歸代碼理解

對於遞歸代碼,若試圖想清楚整個遞和歸的過程,其實是進入了一個思惟誤區。

那該如何理解遞歸代碼呢 ?

  • 若是一個問題 A 能夠分解爲若干個子問題 B、C、D,你能夠假設子問題 B、C、D 已經解決。
  • 並且,你只須要思考問題 A 與子問題 B、C、D 兩層之間的關係便可,不須要一層層往下思考子問題與子子問題,子子問題與子子子問題之間的關係。
  • 屏蔽掉遞歸細節,這樣子理解起來就簡單多了。

所以,理解遞歸代碼,就把它抽象成一個遞推公式,不用想一層層的調用關係,不要試圖用人腦去分解遞歸的每一個步驟。

6. 例子

1. 一個階乘的例子:

function fact(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * fact(num - 1);
    }
}
fact(3) // 結果爲 6

如下代碼可致使出錯:

var anotherFact = fact; 
fact = null; 
alert(antherFact(4)); //出錯

因爲 fact 已經不是函數了,因此出錯。

使用 arguments.callee

arguments.callee 是一個指向正在執行的函數的指針,arguments.callee 返回正在被執行的對現象。
新的函數爲:

function fact(num){ 
    if (num <= 1){ 
        return 1; 
    }else{ 
        return num * arguments.callee(num - 1); //此處更改了。 
    } 
} 
var anotherFact = fact; 
fact = null; 
alert(antherFact(4)); // 結果爲 24

2. 再看一個多叉樹的例子

先看圖

clipboard.png

葉子結點:就是深度爲 0 的結點,也就是沒有孩子結點的結點,簡單的說就是一個二叉樹任意一個分支上的終端節點。

數據結構格式,參考以下代碼:

const json = {
  name: 'A',
  children: [
    {
      name: 'B',
      children: [
        {
          name: 'E',
        },
        {
          name: 'F',
        },
        {
          name: 'G',
        }
      ]
    },
    {
      name: 'C',
      children: [
        {
          name: 'H'
        }
      ]
    },
    {
      name: 'D',
      children: [
        {
          name: 'I',
        },
        {
          name: 'J',
        }
      ]
    }
  ]
}

咱們如何獲取根節點的全部葉子節點個數呢 ?

遞歸代碼以下:

/**
 * 獲取根節點的全部 葉子節點 個數
 * @param {Object} json Object 對象
 */
function getLeafCountTree(json) {
  if(!json.children){
      return 1;
  } else {
      let leafCount = 0;
      for(let i = 0 ; i < json.children.length ; i++){
          // leafCount = leafCount + getLeafCountTree(json.children[i]);
          leafCount = leafCount + arguments.callee(json.children[i]);
      }
      return leafCount;
  }
}

遞歸遍歷是比較經常使用的方法,好比:省市區遍歷成樹、多叉樹、階乘等。

7. 文章輸出計劃

JavaScript 數據結構與算法之美 的系列文章,堅持 3 - 7 天左右更新一篇,暫定計劃以下表。

| 標題 | 連接 |
| :------ | :------ |
| 時間和空間複雜度 | https://github.com/biaochenxu... |
| 線性表(數組、鏈表、棧、隊列) | https://github.com/biaochenxu... |
| 實現一個前端路由,如何實現瀏覽器的前進與後退 ?| https://github.com/biaochenxu... |
| 棧內存與堆內存 、淺拷貝與深拷貝 | https://github.com/biaochenxu... |
| 遞歸 | https://github.com/biaochenxu... |
| 非線性表(樹、堆) | 精彩待續 |
| 冒泡排序 | 精彩待續 |
| 插入排序 | 精彩待續 |
| 選擇排序 | 精彩待續 |
| 歸併排序 | 精彩待續 |
| 快速排序 | 精彩待續 |
| 計數排序 | 精彩待續 |
| 基數排序 | 精彩待續 |
| 桶排序 | 精彩待續 |
| 希爾排序 | 精彩待續 |
| 堆排序 | 精彩待續 |
| 十大經典排序彙總 | 精彩待續 |

若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。

8. 最後

文章能夠轉載,但須註明做者及出處,須要轉載到公衆號的,喊我加下白名單就好了。

參考文章:

遞歸:如何用三行代碼找到「最終推薦人」?

筆芯

全棧修煉

相關文章
相關標籤/搜索