1. 程式人生 > 實用技巧 >JS 前序遍歷、中序遍歷、後序遍歷、層序遍歷詳解,深度優先與廣度優先區別,附leetcode例題題解答案

JS 前序遍歷、中序遍歷、後序遍歷、層序遍歷詳解,深度優先與廣度優先區別,附leetcode例題題解答案

壹 ❀ 引

按照一天一題的速度,不知不覺已經刷了快兩多月的leetcode了,因為本人較為笨拙,一道簡單的題有時候也會研究很久,看著提交了兩百多次,其實也才解決了70來道簡單題,對於二分法,雙指標等也只是有個初步概念,並非熟練。

若你有注意我以往題解文章,會發現我做過的大多題型均以陣列和字串為主。這是因為我在選擇題目的時候始終將自己限制在熟悉的知識體系裡,我非常害怕樹,害怕遞迴,害怕動態規劃。我深知害怕解決不了問題,自己始終得面對它們,所以今天我決定開始啃樹了(學習樹形結構不是啃樹皮),懦弱的心做出稍微勇敢的決定。不積跬步,無以至千里;不積小流,無以成江海,願你我共勉,那麼本文開始。

貳 ❀ 前序遍歷、中序遍歷、後序遍歷

在學習樹之前,我想大家或多或少會聯想到深度優先與廣度優先相關概念。我在學習樹的卡片時,結果又注意到前序遍歷,中序遍歷,後序遍歷以及層序遍歷等概念,這一下我就懵了。所以在學樹之前,我們先 瞭解這些常見的搜尋規則。

貳 ❀ 壹 前序遍歷

前序遍歷可以說最符合大家閱讀樹結構的查詢順序,它滿足根節點=>左子樹=>右子樹的順序,我們來看個例子:

如上圖,其中A為根節點,B為左子樹,C為右子樹,所以遍歷順序為ABC

我們再看看層級複雜點的二叉樹結構,如下:

還是一樣的套用上面的規律,首先是根節點A,之後遍歷左子樹B,而B又有自己的左子樹D和右子樹E,所以BDE又可以看成一個獨立二叉樹,因此遍歷完B之後,緊接著就是遍歷左子樹D與右子樹E。

E又有自己的左子樹和右子樹,因此遍歷完E緊接著就是GH,到這裡左子樹完整遍歷結束。於是來到右子樹C,由於C沒有左子樹,所以緊接著遍歷F,F遍歷完又遍歷了自己的左子樹I,到這裡遍歷結束。所以完整的順序為ABDEGHCFI

我們永遠先遍歷根節點,緊接著判斷有沒有左子樹,如果有接著遍歷,而左子樹也可以有自己的左子樹與右子樹,所以我們可以用遞迴來做到遍歷。

來看一道題,題目來源144. 二叉樹的前序遍歷,題目描述如下:

給定一個二叉樹,返回它的 前序 遍歷。

示例:

輸入: [1,null,2,3]

1
 \
 2
/
3 

輸出: [1,2,3]
進階: 遞迴演算法很簡單,你可以通過迭代演算法完成嗎?

讓我們嘗試實現它:

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var preorderTraversal = function(root) {
    let res = [];
    // 遍歷函式
    function traversal(root) {
        if (root !== null) {
            // 訪問根節點的值
            res.push(root.val);
            if (root.left) {
                // 遞迴遍歷左子樹
                traversal(root.left);
            };
            if (root.right) {
                // 遞迴遍歷右子樹
                traversal(root.right);
            };
        };
    };
    traversal(root);
    return res;
};

貳 ❀ 貳 中序遍歷

中序遍歷滿足左子樹=>根節點=>右子樹的順序進行查詢,我們還是以簡單二叉樹為例。

當跑到到根節點B時,先得看看有沒有左子樹,正好有,所以先遍歷了左子樹A之後才是B,最後遍歷右子樹C,所以完整順序順序為ABC

我們再來用中序遍歷分析稍微複雜的二叉樹,如下圖:

從F開始,先看有沒有左子樹,因為有於是成功跑到了B,注意此時並不是直接取到B,由於B也有左子樹,所以第一個遍歷到的節點其實是A,而A沒有其它分支了,所以緊接著遍歷A的根節點B,然後跑到B的右子樹D。而D也有自己的左子樹與右子樹,所以D不能被立刻遍歷,而是先遍歷C,然後才是D,最後是E。到這裡F節點的左子樹全部遍歷完成,此時順序為ABCDEF

我們再看F的右子樹,由於G沒有左子樹,所以直接遍歷G,然後跑到G的右子樹I,但I有左子樹,所以先取H,再取I。結合上面,此二叉樹的中序遍歷順序為ABCDEFGHI,我是以字母順序提前排好了遍歷順序,大家陌生就多自己走幾遍。

那麼如何實現一箇中序遍歷呢,很簡單,將前序遍歷程式碼換個順序就好了,我們始終先遍歷節點的左子樹,子又有左子樹那就一直往下找,找到底才是當前左子樹根節點,最後右子樹。

看一道題,題目來自leetcode94. 二叉樹的中序遍歷,描述如下:

給定一個二叉樹,返回它的中序遍歷。

示例:

輸入: [1,null,2,3]

1
 \
 2
/
3 

輸出: [1,2,3]
進階: 遞迴演算法很簡單,你可以通過迭代演算法完成嗎?

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function(root) {
    let res = [];
    // 遍歷函式
    function traversal(root) {
        if (root !== null) {
            if (root.left) {
                // 遞迴遍歷左子樹
                traversal(root.left);
            };
            // 訪問根節點的值
            res.push(root.val);
            if (root.right) {
                // 遞迴遍歷右子樹
                traversal(root.right);
            };
        };
    };
    traversal(root);
    return res;
};

貳 ❀ 叄 後序遍歷

後序遍歷滿足左子樹=>右子樹=>根節點的順序進行查詢,還是從簡單二叉樹開始:

從根節點C出發,先訪問左子樹A,緊接著右子樹B,最後根節點C,所以順序為ABC

我們再來看一個較為複雜的二叉樹,如下:

還是從根節點I出發,於是成功找到了左子樹E,而E也有自己的左子樹,所以第一個遍歷到了A,緊接著來到E的右子樹D,但D也有自己的左右子樹,所以第二個遍歷的是B,緊接著是C,最後是根節點D,根節點E。到這裡,左子樹遍歷完成,順序為ABCDE

我們再來看I的右子樹H,由於H有右子樹G,所以H不能被遍歷,G也有自己的左子樹F,於是遍歷到了F,G沒右子樹,於是F之後就是G,謹記著H,到這裡I的右子樹遍歷完成,最後遍歷根節點I。所以完整的順序為ABCDEFGHI,順序還是與字母順序一致,大家跟著思路多多感受。

看到題,題目來自leetcode145. 二叉樹的後序遍歷,題目描述如下:

給定一個二叉樹,返回它的後序遍歷。

示例:

輸入: [1,null,2,3]

1
 \
 2
/
3 

輸出: [1,2,3]
進階: 遞迴演算法很簡單,你可以通過迭代演算法完成嗎?

那麼如何實現它呢?還是將上面的實現換個順序即可。

/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var postorderTraversal = function(root) {
    let res = [];
    // 遍歷函式
    function traversal(root) {
        if (root !== null) {
            if (root.left) {
                // 遞迴遍歷左子樹
                traversal(root.left);
            };
            if (root.right) {
                // 遞迴遍歷右子樹
                traversal(root.right);
            };
            // 訪問根節點的值
            res.push(root.val);
        };
    };
    traversal(root);
    return res;
};

叄 ❀ 層序遍歷

層序遍歷滿足從上到下,從左到右一層一層遍歷的順序,以簡單的二叉樹為例:

首先從根節點A開始,這是第一層,遍歷完成往下一層推進,於是訪問到了B和C,完整順序為ABC

我們再來看一個較為複雜的二叉樹,由於層序遍歷理解上還是較為容易,下圖遍歷順序為ABCDEFGHI,確實是從上到下從左往右一層層往下推進的遍歷順序。

來看一道題,題目來自leetcode102. 二叉樹的層序遍歷,描述如下:

給你一個二叉樹,請你返回其按 層序遍歷 得到的節點值。 (即逐層地,從左到右訪問所有節點)。

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

   3
  / \
 9   20
    /  \
   15   7

返回其層次遍歷結果:

[
[3],
[9,20],
[15,7]
]

讓我們來實現它,我們每遍歷一層,都會將本層節點裝到一個數組裡,每往下推進一層,我們都得建立一個新的子陣列,實現如下:

/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function(root) {
    let res = [];

    function traversal(root, depth) {
        if (root !== null) {
            if (!res[depth]) {
                res[depth] = [];
            };
            res[depth].push(root.val)
            if (root.left) {
                traversal(root.left, depth + 1);
            };
            if (root.right) {
                traversal(root.right, depth + 1);
            };
        };
    };
    traversal(root, 0);
    return res;
};

關於層序遍歷我們來趁熱打鐵,再來補一道我昨天做的題,題目來自leetcode104. 二叉樹的最大深度,描述如下:

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

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

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

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

   3
  / \
 9   20
    /  \
   15   7

返回它的最大深度 3 。

注意,由於我們是要統計最大深度,所以第一層就應該是從1開始,這裡直接貼程式碼:

/**
 * @param {TreeNode} root
 * @return {number}
 */
var maxDepth = function(root) {
    let ans = 0;
    if(root===null){
        return ans;
    };
    function traversal(root,deepth){
        // 始終取最大數為最深層級
        ans = Math.max(ans,deepth)
        if(root.left){
            traversal(root.left,deepth+1);
        };
        if(root.right){
            traversal(root.right,deepth+1);
        };
    };
    // 從1開始
    traversal(root,1);
    return ans;
};

肆 ❀ 深度優先、廣度優先與前序、中序、後序、層序遍歷關係

那麼我們經常聽聞的深度優先DFS與廣度優先(寬度優先)BFS又和前面的前序、中序、後序、層序遍歷又有何關係呢?我想聰明的同學應該已經聯想到了,前序、中序、與後序遍歷均為深度優先。而層序遍歷也就是所謂的廣度優先了。

對比不難發現,深度優先更具鑽研精神,只要子樹還有子,就一直往下查詢,至於順序對應前中後的查詢順序。而廣度是將每一層節點遍歷完成為止,才會進入下一層。

伍 ❀ 總

需要說明的是,上述實現程式碼其實都不是最優做法,部分實現均借鑑了leetcode題解一套拳法