上一篇文章, 咱們熟悉了樹, 二叉樹, 二叉搜索樹的基本概念, 以及作了對應的實戰題目:html
今天咱們繼續樹這個話題。node
本文的主要內容包括:面試
樹的前中後遍歷
廣度優先搜索
深度優先搜索
樹的層次遍歷
Leetcode題目演練
樹是一種比較常見的數據結構, 面試中也比較常見。算法
熟悉樹的前中後序遍歷,只是讓你們明白樹的遍歷能夠有不一樣的順序, 實際的應用也比較少, 意義並不大,可是做爲基礎, 咱們仍是要學一下這部分。segmentfault
基本上,真正的遍歷仍是要看深度優先
和廣度優先
遍歷。數據結構
廢話很少說, 咱們進入正文。post
這三種遍歷的順序是十分好記的:學習
前序
: 根左右中序
: 左根右後序
: 左右根如圖所示, 這樣的一棵二叉樹的前序遍歷,this
先訪問根結點, 而後是左子樹, 再而後是右子樹。
遍歷的結果就是:
A, B, D, E, C, F, G
先訪問的是左子樹, 而後是根, 再而後是右子樹。
遍歷的結果就是:
D, B, E, A, F, C, G
先訪問的是左子樹, 而後是右子樹, 再而後是根。
遍歷的結果就是:
D, E, B, F, G, C, A
若是你對這三種遍歷很是熟悉, 在面對驗證二叉搜索樹
這類問題的時候, 就知道能夠用中序遍歷的特性來驗證。
下面咱們就大概看一下這三種遍歷的邏輯實現。
這裏借用來自社區大佬的Python實現, 很是的優雅:
leetcode 上也有這三種遍歷的題目, 由於不是本文重點
,因此就用遞歸簡單實現一下
:
給定一個二叉樹,返回它的 _前序 _遍歷。
輸入: [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 }
給定一個二叉樹,返回它的中序 遍歷。
示例: 輸入: [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 };
給定一個二叉樹,返回它的 後序 遍歷。
示例: 輸入: [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 }
上面這部分, 咱們熟悉了二叉樹的三種遍歷方式, 並熟悉了三道實戰題目, 下面咱們就正式接觸今天的主角: BFS & DFS
。
廣度優先搜索(Breadth-First-Search), 簡稱BFS,是一種比較常見的二叉樹搜索方式。
先說一下, 爲何會出現這種搜索方式吧。
好比, 咱們在生活中, 須要在一個大的集合中, 找到某個特定的元素
,這個集合多是一個狀態集,也多是一些樹,或者圖。
好比, 咱們要找到箭頭所指的這個點, 該怎麼找呢?
咱們最直觀的反應就是,層層遞進, 一層一層往下搜索
。
這種最符合咱們思惟方式的搜索方式就是廣度優先搜索
。
下面咱們看一下這種方式具體是怎麼搜索的。
首先, 訪問的是根結點1。
接下來, 依次訪問1的孩子,就是2, 3, 4結點, 依次類推。
就像水波同樣, 一層一層往前推, 比較符合人類的思惟習慣。
BFS的實現思路也比較直觀:
從1開始, 依次把兒子結點放到隊列中去, 遍歷的結點依次放入隊列
之中,隊列是先入先出
的,這樣就達到了層次遍歷的效果。
BFS僞代碼實現:
爲了不重複搜索, 引入了判重的set, 來記錄已經搜索過的結點。
下面咱們看一個具體的例子:
有以下html結構,要求分層打印出每一個節點
:
<div id='root'> <span> <a></a> <div></div> </span> <span> <p></p> </span> </div>
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));
下面咱們繼續看深度優先搜索。
深度優先搜索 - Depth First Search, 簡稱DFS。
BFS,使用的是隊列, 先入先出。
DFS,使用的是棧, 先入後出。
DFS, 這種方式, 比較耿直, 一根筋,一插到底, 到頭位置。
BDS, DFS的簡單的對比:
DFS遞歸僞代碼(推薦
):
DFS非遞歸僞代碼:
瞭解完思路, 咱們再回到開頭遍歷DOM結點那道題。
如今要求用DFS的方式來打印結點。
<div id='root'> <span> <a></a> <div></div> </span> <span> <p></p> </span> </div>
咱們用遞歸和非遞歸兩種方式實現。
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, []))
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))
推薦第一種遞歸的寫法, 更容易理解, 也不須要額外的維護數據結構, 非遞歸的方式理解便可。
對於這BFS, DFS兩個搜索方法,其實咱們是能夠輕鬆的看出來,他們有許多差別與許多相同點的。
1.數據結構上的運用
BFS, 選取狀態用隊列
的形式,先進先出。
DFS, 用遞歸的形式,用到了棧
結構,先進後出。
2.複雜度
DFS的複雜度與BFS的複雜度大致一致,不一樣之處在於遍歷的方式與對於問題的解決出發點不一樣,DFS適合目標明確
,而BFS適合大範圍的尋找
。
3.思想
思想上來講這兩種方法都是窮竭列舉
全部的狀況。
層次遍歷, 也叫 Level Order Search。
故名思意, 就是按層來遍歷, 和BFS 十分相似。
好比這樣一棵二叉樹:
3 / \ 9 20 / \ 15 7
層次遍歷的結果就是: 3, 9, 20, 15, 7
leetcode 上就有這麼一道題目, 二叉樹的層次遍歷, 咱們就一塊兒來作一下, 進入實戰環節。
給定一個二叉樹,返回其按層次遍歷的節點值。 (即逐層地,從左到右訪問全部節點)。
例如: 給定二叉樹: [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 };
這道題, 也能夠用 DFS 來實現,這裏給你一種Java 的實現, 你能夠理解一下思路, 而後本身實現一遍。
給定一個二叉樹,找出其最大深度。
二叉樹的深度爲根節點到最遠葉子節點的最長路徑上的節點數。
說明: 葉子節點是指沒有子節點的節點。
示例: 給定二叉樹 [3,9,20,null,null,15,7], 3 / \ 9 20 / \ 15 7 返回它的最大深度 3 。
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 }
遍歷每一層的節點高度,而後求得最深的一個節點的高度,就是整個樹的高度了。
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 };
基本思路:
首先訪問根結點而後遍歷左子樹,最後遍歷右子樹。
從包含根結點且相應深度爲 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 }
還有 第101題,二叉樹的最大深度, 思路都是相似的, 這裏就不解了, 留給你練習。
做文本年度的最後一篇文章,寫了一天多, 終於寫完了....
樹的深搜和廣搜
, 是很是重要
的兩種搜索方式, 也是面試中的重點
。
但願本文能對你有所幫助。
以爲內容有幫助能夠關注下個人公衆號 「 前端e進階 」,一塊兒學習。