【算法題】遞歸求二叉樹深度

二叉樹的深度算法,是二叉樹中比較基礎的算法了。對應 LeetCode 第104題html

而後你會發現 LeetCode 後面有些算法題須要用到這個算法的變形,好比第110題、543題。這兩道題,若是你知道二叉樹深度算法的遞歸過程,就很容易作出來。node

關於二叉樹的相關知識,能夠看個人這篇文章:數據結構】樹的簡單分析總結(附js實現)算法

題目描述

給定一個二叉樹,找出其最大深度。編程

二叉樹的深度爲根節點到最遠葉子節點的最長路徑上的節點數。數組

說明: 葉子節點是指沒有子節點的節點。緩存

示例: 給定二叉樹 [3,9,20,null,null,15,7],bash

3
   / \
  9  20
    /  \
   15   7
返回它的最大深度 3 。
複製代碼

注意這裏的二叉樹是經過 鏈式存儲法 存儲的,而不是數組。網絡

1. 遞歸是什麼

在解題以前,咱們先了解下什麼是遞歸(若是你已經掌握,請直接跳過這節)。數據結構

那麼就開始朗(wang)誦(ba)課本(nian)內容(jing)。函數

遞歸分爲 「遞」 和 「歸」。「遞」 就是傳進去,「歸」就是一個函數執行完解決了一個子問題。遞歸的實現經過不停地將問題分解爲子問題,並經過解決子問題,最終解決原問題。

遞歸的核心在於遞歸公式,當咱們分析出遞歸公式後,遞歸問題其實也就解決了。遞歸是一種應用普遍的編程技巧,不少地方都要用到它,好比深度優先遍歷(本題就用到這個)、二叉樹的前中後序遍歷。

遞歸須要知足三個條件:

  1. 能夠分解爲多個子問題;
  2. 子問題除了數據規模不一樣,求解思路不變;
  3. 存在遞歸終止條件。

遞歸的特色是代碼比較簡潔,雖然大多數狀況下你都比較難理解遞歸的每一個過程,由於它不符合人類的思惟習慣,但其實你也沒必要去真正瞭解,你只要知道B和 C 被解決後,能夠推導出 A 就行,無需考慮 B 和 C 是如何經過子問題解決的(由於都和前面同樣的!)。

其次遞歸若是太深,可能會致使內存用盡。由於遞歸的時候要保存許多調用記錄,就會維護一個調用棧,當棧太大而超過了可用內存空間,就會發生內存溢出的狀況,咱們稱之爲 堆棧溢出。解決方案有下面 4 種:

  1. 遞歸調用超過必定深度以後,直接報錯,再也不遞歸下去。 深度到底到多少會發生溢出,並不能經過計算得出,另外報錯也致使程序沒法繼續運行下去,因此這個方案雖然確實能夠防止內存溢出,並好像沒有什麼用。
  2. 緩存重複計算。 遞歸可能會重複調用已經求解過的 f(k) 的結果,對於這種狀況,就要對 f(k) 進行緩存,通常用哈希表來緩存(js 中能夠經過對象實現)。當咱們第二次執行 f(k) 時,直接從緩存中獲取便可。
  3. 改成非遞歸代碼。 其實就是改成循環的寫法。修改後的循環寫法本質上也是遞歸,只是咱們手動地實現了遞歸棧而已。循環寫法代碼實現會比遞歸複雜,並且也不夠優雅。
  4. 尾遞歸。 使用的是一種 尾調用優化 的技術,須要看運行環境是否提供這種優化。在支持尾調用優化的狀況下,若是函數 A 的 最後一步 調用另外一個函數 B,那進入 B 時,就不會保留 A 的調用記錄(好比一些 A 的內部變量),這樣就不會產生很長的調用棧,而致使堆棧溢出了。

說到遞歸,那就不得不提遞歸的一道經典題目了,那就是「爬樓梯問題」,對應 LeetCode 第70題

爬樓梯的問題描述是:假設你正在爬樓梯。須要 n (正整數)階你才能到達樓頂。每次你能夠爬 1 或 2 個臺階。你有多少種不一樣的方法能夠爬到樓頂呢?

首先你能夠列出 n = 1,n = 2... 的走法,試着找出規律。

走法 走法總數
1 1 1
2 1 + 1,2 2
3 1 + 2, 1 + 1 + 1, 2 + 1 3

到這裏咱們就能夠發現一些規律了。那就是 走到第 3 階的走法爲 2 階 和 1 階的和。爲何會這樣呢?咱們就要透過現象發現本質,本質就是,要走到第 n 階,首先就要先走到 第 n-1 階,而後再爬一個臺階,或者是先走到 n - 2 階,而後爬兩個臺階。

因此咱們獲得這麼一個遞歸公式:f(n) = f(n-1) + f(n - 2)

遞歸寫法:

var climbStairs = function(n) {
    let map = {};
    function f(n) {
        if (n < 3)  return n;
        if (map[n]) return map[n];
        
        let r =  f(n-1) + f(n - 2);
        map[n] = r;
        return r;
    }
    return f(n)
複製代碼

由於 f(n) = f(n-1) + f(n-2)。這裏的f(n-1),又由 f(n-2)+f(n-3) 得出。這裏的 f(n-2) 被執行了兩次,因此就須要緩存 f(n-2) 的結果到 map 對象中,來減小運算時間。

循環寫法:

var climbStairs = function(n) {
    if (n < 3) return n;

    let step1 = 1,  // 上上一步
        step2 = 2;  // 上一步

    let tmp;
    for (let i = 3; i <= n; i++) {
        tmp = step2;
        step2 = step1 + step2;
        step1 = tmp;
    }
    return step2;

};
複製代碼

2. 問題分析

說完遞歸後,咱們就來分析題目吧。

首先咱們試着找出遞歸規律。首先咱們知道,除了葉子節點,二叉樹的全部節點都有會有左右子樹。那麼若是咱們知道左右子樹的深度,找出兩者之間的最大值,而後再加一,不就是這個二叉樹的深度嗎?其次以 葉子節點 爲根節點的二叉樹的高度是 1,咱們就能夠根據經過這個做爲遞歸的結束條件。

3. 代碼實現

/** * Definition for a binary tree node. * function TreeNode(val) { * this.val = val; * this.left = this.right = null; * } */
/** * @param {TreeNode} root * @return {number} */
var maxDepth = function(root) {
    function f(node) {
        if (!node) return 0;
        return Math.max(f(node.left), f(node.right)) + 1;
    }
    return f(root);
};
複製代碼

這裏用到了深度優先遍歷,會沿着二叉樹從根節點往葉子節點走。另外,由於沒有重複計算,因此不須要對結果進行緩存。還有就是,由於沒有多餘的變量要保存,能夠直接把 maxDepth 函數寫成遞歸函數。

4. 擴展:數組存儲的二叉樹如何求深度?

關於如何用數組存儲(順序存儲法)的二叉樹,這裏就不提了,請看我前面提到的相關文章。

求一個數組表示的二叉樹的深度,能夠看做求 對應的徹底二叉樹的深度

在此以前,咱們先看看如何求出一個節點個數爲 n 的 滿二叉樹 的深度 k。

深度 k 個數 n
1 1
2 3 (=1+2)
3 7 (=1+2+4)
4 15 (=1+2+4+8)

規律很明顯,經過等比數列求和公式化簡,咱們獲得 k = Math.log2(n+1),其中 k 爲深度,n 爲滿二叉樹的節點個數。那麼對於一個徹底二叉樹來講,將 k 向上取整便可:k = Math.ceil( Math.log2(n+1) )

因此對於一個順序存儲法存儲的長度爲 n 的二叉樹,其高度 k 爲:

k = Math.ceil( Math.log2(n+1) )
複製代碼

(須要注意的是,這裏的數組是從 0 開始存儲節點的。)

參考

  1. 阮一峯的網絡日誌——尾調用優化
  2. 數據結構與算法之美:10 | 遞歸:如何用三行代碼找到「最終推薦人」?
相關文章
相關標籤/搜索