[數據結構基礎] 掌握樹的四種遍歷方式,以及BFS, DFS

背景

上一篇文章, 咱們熟悉了樹, 二叉樹, 二叉搜索樹的基本概念, 以及作了對應的實戰題目:html

樹, 二叉樹, 二叉搜索樹 && 實戰練習前端

今天咱們繼續樹這個話題。node

本文的主要內容包括:面試

  • 理論:樹的前中後遍歷
  • 理論:廣度優先搜索
  • 理論:深度優先搜索
  • 理論:樹的層次遍歷
  • 實戰:Leetcode題目演練

樹是一種比較常見的數據結構, 面試中也比較常見。算法

熟悉樹的前中後序遍歷,只是讓你們明白樹的遍歷能夠有不一樣的順序, 實際的應用也比較少, 意義並不大,可是做爲基礎, 咱們仍是要學一下這部分。segmentfault

基本上,真正的遍歷仍是要看深度優先廣度優先遍歷。數據結構

廢話很少說, 咱們進入正文。post

正文

樹的前中後序遍歷

這三種遍歷的順序是十分好記的:學習

  • 前序: 根左右
  • 中序: 左根右
  • 後序: 左右根

前序遍歷

image.png

如圖所示, 這樣的一棵二叉樹的前序遍歷,this

先訪問根結點, 而後是左子樹, 再而後是右子樹。

遍歷的結果就是:

A, B, D, E, C, F, G

中序遍歷

image.png

先訪問的是左子樹, 而後是根, 再而後是右子樹。

遍歷的結果就是:

D, B, E, A, F, C, G

後序遍歷

image.png

先訪問的是左子樹, 而後是右子樹, 再而後是根。

遍歷的結果就是:

D, E, B, F, G, C, A

前中後序遍歷的代碼實現 - medium

若是你對這三種遍歷很是熟悉, 在面對驗證二叉搜索樹這類問題的時候, 就知道能夠用中序遍歷的特性來驗證。

下面咱們就大概看一下這三種遍歷的邏輯實現。

這裏借用來自社區大佬的Python實現, 很是的優雅:
image.png

leetcode 上也有這三種遍歷的題目, 由於不是本文重點,因此就用遞歸簡單實現一下

144 前序遍歷的簡單實現 - medium

給定一個二叉樹,返回它的 _前序 _遍歷。
輸入: [1,null,2,3]  
   1
    \
     2
    /
   3 

輸出: [1,2,3]

代碼實現:

var preorderTraversal = function (root) {
    var stack = []

    function helper(root) {
        if (!root) return
        stack.push(root.val)
        root.left && helper(root.left)
        root.right && helper(root.right)
    }

    helper(root)

    return stack
}

image.png

94: 中序遍歷的簡單實現

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

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

輸出: [1,3,2]

代碼實現:

var inorderTraversal = function (root) {
    var stack = []

    function helper(root) {
        if (!root) return

        root.left && helper(root.left)
        stack.push(root.val)
        root.right && helper(root.right)
    }

    helper(root)

    return stack
};

image.png

145: 後序遍歷的簡單實現 - hard

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

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

輸出: [3,2,1]

代碼實現:

var postorderTraversal = function (root) {
    var stack = []

    function helper(root) {
        if (!root) return

        root.left && helper(root.left)
        root.right && helper(root.right)
        stack.push(root.val)
    }

    helper(root)

    return stack
}

image.png

第一部分小結

上面這部分, 咱們熟悉了二叉樹的三種遍歷方式, 並熟悉了三道實戰題目, 下面咱們就正式接觸今天的主角: BFS & DFS

廣度優先搜索

廣度優先搜索(Breadth-First-Search), 簡稱BFS,是一種比較常見的二叉樹搜索方式。

先說一下, 爲何會出現這種搜索方式吧。

好比, 咱們在生活中, 須要在一個大的集合中, 找到某個特定的元素,這個集合多是一個狀態集,也多是一些樹,或者圖。

image.png

好比, 咱們要找到箭頭所指的這個點, 該怎麼找呢?

咱們最直觀的反應就是,層層遞進, 一層一層往下搜索

這種最符合咱們思惟方式的搜索方式就是廣度優先搜索

下面咱們看一下這種方式具體是怎麼搜索的。

image.png

首先, 訪問的是根結點1。

接下來, 依次訪問1的孩子,就是2, 3, 4結點, 依次類推。

就像水波同樣, 一層一層往前推, 比較符合人類的思惟習慣。

算法基礎:BFS和DFS的直觀解釋

BFS的實現思路也比較直觀:

從1開始, 依次把兒子結點放到隊列中去, 遍歷的結點依次放入隊列之中,隊列是先入先出的,這樣就達到了層次遍歷的效果。

BFS的實現

BFS僞代碼實現:

即刻時間課程截圖.png

爲了不重複搜索, 引入了判重的set, 來記錄已經搜索過的結點。

下面咱們看一個具體的例子:

有以下html結構,要求分層打印出每一個節點

<div id='root'>
    <span>
        <a></a>
        <div></div>
    </span>
    <span>
        <p></p>
    </span>
</div>

image.png

function BFS(node) {  
    var nodes = [];  
    if (node != null) {  
        var queue = [];  
        queue.unshift(node);  
        while (queue.length !== 0) {  
            var item = queue.shift(); // 取出第一個元素
            nodes.push(item);  
            var children = item.children;  
            for (var i = 0; i < children.length; i++) {
              queue.push(children[i]);  
            }     
        }  
    }  
    return nodes;  
}
var root = document.getElementById('root');
console.log(BFS(root));

image.png

下面咱們繼續看深度優先搜索。

深度優先搜索

深度優先搜索 - Depth First Search, 簡稱DFS。

BFS,使用的是隊列, 先入先出。
DFS,使用的是棧, 先入後出。

DFS, 這種方式, 比較耿直, 一根筋,一插到底, 到頭位置。

即刻時間課程截圖.png

DFS的直觀解釋


BDS, DFS的簡單的對比:

image.png

DFS的實現

DFS遞歸僞代碼(推薦):

image.png

DFS非遞歸僞代碼:

image.png

瞭解完思路, 咱們再回到開頭遍歷DOM結點那道題。

如今要求用DFS的方式來打印結點。

<div id='root'>
    <span>
        <a></a>
        <div></div>
    </span>
    <span>
        <p></p>
    </span>
</div>

image.png

咱們用遞歸和非遞歸兩種方式實現。

1. 遞歸

function DFS(node, nodeList) {
      if (node) {
          nodeList.push(node);
          var children = node.children;
          for (var i = 0; i < children.length; i++) {
              DFS(children[i], nodeList);
          }
      }
      return nodeList;
  }
  var root = document.getElementById('root')
  console.log(DFS(root, []))

image.png

2. 非遞歸

function DFS(node) {
    var nodeList = [];
    if (node) {
        var stack = [];
        stack.push(node);
        while (stack.length !== 0) {
            var childrenItem = stack.pop();
            nodeList.push(childrenItem);
            var childrenList = childrenItem.children;
            for (var i = childrenList.length - 1; i >= 0; i--) {
                stack.push(childrenList[i]);
            }
        }
    }
    return nodeList;
}
var root = document.getElementById('root')
console.log(DFS(root))

image.png

推薦第一種遞歸的寫法, 更容易理解, 也不須要額外的維護數據結構, 非遞歸的方式理解便可。

簡單的小結

對於這BFS, DFS兩個搜索方法,其實咱們是能夠輕鬆的看出來,他們有許多差別與許多相同點的。

1.數據結構上的運用

BFS, 選取狀態用隊列的形式,先進先出。
DFS, 用遞歸的形式,用到了結構,先進後出。

2.複雜度

DFS的複雜度與BFS的複雜度大致一致,不一樣之處在於遍歷的方式與對於問題的解決出發點不一樣,DFS適合目標明確,而BFS適合大範圍的尋找

3.思想

思想上來講這兩種方法都是窮竭列舉全部的狀況。

樹的層次遍歷

層次遍歷, 也叫 Level Order Search。

故名思意, 就是按層來遍歷, 和BFS 十分相似。

image.png

好比這樣一棵二叉樹:

3
   / \
  9  20
    /  \
   15   7

層次遍歷的結果就是: 3, 9, 20, 15, 7

leetcode 上就有這麼一道題目, 二叉樹的層次遍歷, 咱們就一塊兒來作一下, 進入實戰環節。

實戰題目

Leetcode-102: 二叉樹的層次遍歷

給定一個二叉樹,返回其按層次遍歷的節點值。 (即逐層地,從左到右訪問全部節點)。
例如:
給定二叉樹: [3,9,20,null,null,15,7],

    3
   / \
  9  20
    /  \
   15   7

返回其層次遍歷結果:

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

實現的方式有不少, 好比BFS。

一種BFS的代碼實現:

/**
 * Definition for a binary tree node.
 * function TreeNode(val) {
 *     this.val = val;
 *     this.left = this.right = null;
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number[][]}
 */
var levelOrder = function (root) {
    if (!root) return []
    let result = [], queue = [root]

    while (queue.length) {
        let currentLevel = []
        let levelSize = queue.length
        while (levelSize !== 0) {
            let node = queue.shift()
            currentLevel.push(node.val)
            if (node.left) queue.push(node.left)
            if (node.right) queue.push(node.right)
            levelSize--
        }
        result.push(currentLevel)
    }

    return result
};

image.png

這道題, 也能夠用 DFS 來實現,這裏給你一種Java 的實現, 你能夠理解一下思路, 而後本身實現一遍。
image.png

Leetcode 104, 二叉樹的最大深度

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

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

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

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

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 3 。

解法1. 遞歸

遞歸-動態圖.gif

var maxDepth = function(root) {
    if(!root) return 0
    
    var left = maxDepth(root.left) + 1
    var right = maxDepth(root.right) + 1
    // +1 是算上根結點的高度
    
    return left > right ? left : right
}

解法2: 用隊列實現-BFS

遍歷每一層的節點高度,而後求得最深的一個節點的高度,就是整個樹的高度了。

queue_1.jpg

var maxDepth = function (root) {
    if (!root) return 0

    let queue = []
    let depth = 0

    queue.push(root)

    while (queue.length) {
        depth++
        let size = queue.length

        while (size > 0) {
            let p = queue.shift()
            if (p.left) queue.push(p.left)
            if (p.right) queue.push(p.right)
            size--
        }
    }

    return depth
};

image.png

解法3: 用隊列實現-DFS

基本思路:

首先訪問根結點而後遍歷左子樹,最後遍歷右子樹。

從包含根結點且相應深度爲 1 的棧開始。

而後將當前結點彈出棧並推入子結點, 每一步都會更新深度。

時間複雜度:O(N)
空間複雜度:O(N)

代碼實現:

var maxDepth = function (root) {
    if (!root) return 0

    let stack = []
    let depthStack = []
    let depth = 1

    stack.push(root)
    depthStack.push(depth)

    while (stack.length > 0) {
        let node = stack.pop()
        let temp = depthStack.pop()

        if (depth < temp) depth = temp

        if (node.right) {
            stack.push(node.right)
            depthStack.push(temp + 1)
        }

        if (node.left) {
            stack.push(node.left)
            depthStack.push(temp + 1)
        }
    }

    return depth
}

image.png

還有 第101題,二叉樹的最大深度, 思路都是相似的, 這裏就不解了, 留給你練習。

結語

做文本年度的最後一篇文章,寫了一天多, 終於寫完了....

樹的深搜和廣搜, 是很是重要的兩種搜索方式, 也是面試中的重點

但願本文能對你有所幫助。

最後

以爲內容有幫助能夠關注下個人公衆號 「 前端e進階 」,一塊兒學習。

clipboard.png

相關文章
相關標籤/搜索