今天是小浩算法 「365刷題計劃」 二叉樹入門 - 整合篇。本篇做爲入門整合篇,已經砍去難度較大的知識點,全部列出的內容,均爲必須掌握。由於很長,寫下目錄:java
二叉樹是啥node
二叉樹的最大深度(DFS)面試
二叉樹的層次遍歷(BFS)算法
二叉搜索樹驗證數組
二叉搜索樹查找數據結構
二叉搜索樹刪除app
平衡二叉樹機器學習
徹底二叉樹ide
二叉樹有多重要?單就面試而言,在 leetcode 中二叉樹相關的題目佔據了300多道,近三分之一。同時,二叉樹在整個算法板塊中還起到承上啓下的做用:不可是數組和鏈表的延伸,又能夠做爲圖的基礎。總之,很是重要!函數
什麼是二叉樹?官方是這樣定義的:在計算機科學中,二叉樹是每一個結點最多有兩個子樹的樹結構。一般子樹被稱做「左子樹」(left subtree)和「右子樹」(right subtree)。
上面那是個玩笑,二叉樹長這樣:
二叉樹常被用於實現二叉查找樹和二叉堆。樹比鏈表稍微複雜,由於鏈表是線性數據結構,而樹不是。樹的問題不少均可以由廣度優先搜索或深度優先搜索解決。
通常而言,咱們會看到下面這些與樹相關的術語:
小浩概念
與樹相關的術語
樹的結點(node):包含一個數據元素及若干指向子樹的分支;
孩子結點(child node):結點的子樹的根稱爲該結點的孩子;
雙親結點:B 結點是A 結點的孩子,則A結點是B 結點的雙親;
兄弟結點:同一雙親的孩子結點;堂兄結點:同一層上結點;
祖先結點: 從根到該結點的所經分支上的全部結點
子孫結點:以某結點爲根的子樹中任一結點都稱爲該結點的子孫
結點層:根結點的層定義爲1;根的孩子爲第二層結點,依此類推;
樹的深度:樹中最大的結點層
結點的度:結點子樹的個數
樹的度:樹中最大的結點度。
葉子結點:也叫終端結點,是度爲 0 的結點;
分枝結點:度不爲0的結點;
有序樹:子樹有序的樹,好比家族樹;
無序樹:不考慮子樹的順序;
瞭解了上面的基本概念以後。咱們將經過幾道例題,爲你們引入樹的經典操做。
複習上面的概念:樹的深度指的是樹中最大的結點層。
第104題:給定一個二叉樹,找出其最大深度。二叉樹的深度爲根節點到最遠葉子節點的最長路徑上的節點數。
說明: 葉子節點是指沒有子節點的節點。
示例:
給定二叉樹 [3,9,20,null,null,15,7],
3 / \ 9 20 / \ 15 7
基本概念掌握:每一個節點的深度與它左右子樹的深度有關,且等於其左右子樹最大深度值加上 1。即:
maxDepth(root) =
max(maxDepth(root.left),maxDepth(root.right)) + 1
以 [3,9,20,null,null,15,7] 爲例:
maxDepth(root-3) =max(maxDepth(sub-4),maxDepth(sub-20))+1 =max(1,max(maxDepth(sub-15),maxDepth(sub-7))+1)+1 =max(1,max(1,1)+1)+1 =max(1,2)+1 =3
根據分析,咱們經過遞歸進行求解:
1//Go 2func maxDepth(root *TreeNode) int { 3 if root == nil { 4 return 0 5 } 6 return max(maxDepth(root.Left), maxDepth(root.Right)) + 1 7} 8 9func max(a int, b int) int { 10 if a > b { 11 return a 12 } 13 return b 14}
其實咱們上面用的遞歸方式,本質上是使用了DFS的思想。因此這裏就能夠引出什麼是DFS:深度優先搜索算法(Depth First Search),對於二叉樹而言,它沿着樹的深度遍歷樹的節點,儘量深的搜索樹的分支,這一過程一直進行到已發現從源節點可達的全部節點爲止。( 注意,這裏的前提是對二叉樹而言。DFS自己做爲圖算法的一種,在後續我會單獨拉出來和回溯放一塊兒講。)
如上圖二叉樹,它的訪問順序爲:
A-B-D-E-C-F-G
到這裏,咱們思考一個問題?雖然咱們用遞歸的方式根據DFS的思想順利完成了題目。可是這種方式的缺點卻顯而易見。由於在遞歸中,若是層級過深,咱們極可能保存過多的臨時變量,致使棧溢出。這也是爲何咱們通常不在後臺代碼中使用遞歸的緣由。若是不理解,下面咱們詳細說明:
事實上,函數調用的參數是經過棧空間來傳遞的,在調用過程當中會佔用線程的棧資源。而遞歸調用,只有走到最後的結束點後函數才能依次退出,而未到達最後的結束點以前,佔用的棧空間一直沒有釋放,若是遞歸調用次數過多,就可能致使佔用的棧資源超過線程的最大值,從而致使棧溢出,致使程序的異常退出。
因此,咱們引出下面的話題:如何將遞歸的代碼轉化成非遞歸的形式。這裏請記住,基本全部的遞歸轉非遞歸,均可以經過棧來進行實現。非遞歸的DFS,代碼以下:
1//java 2private List<TreeNode> traversal(TreeNode root) { 3 List<TreeNode> res = new ArrayList<>(); 4 Stack<TreeNode> stack = new Stack<>(); 5 stack.add(root); 6 while (!stack.empty()) { 7 TreeNode node = stack.peek(); 8 res.add(node); 9 stack.pop(); 10 if (node.right != null) { 11 stack.push(node.right); 12 } 13 if (node.left != null) { 14 stack.push(node.left); 15 } 16 } 17 return res; 18}
上面的代碼,惟一須要強調的是,爲何須要先右後左壓入數據?是由於咱們須要將先訪問的數據,後壓入棧(請思考棧的特色)。
若是不理解代碼,請看下圖:
說明:
1:首先將a壓入棧
2:a彈棧,將c、b壓入棧(注意順序)
3:b彈棧,將e、d壓入棧
4,5:d、e、c彈棧,將g、f壓入棧
至此,非遞歸的 DFS 就講解完畢了。那如何經過非遞歸DFS的方式,來對本題求解呢?相信已經很簡單了,這個下去本身試試就ok了了。
在上文中,咱們經過例題學習了二叉樹的DFS(深度優先搜索),其實就是沿着一個方向一直向下遍歷。那咱們可不能夠按照高度一層一層的訪問樹中的數據呢?固然能夠,就是本節中咱們要講的BFS(寬度優先搜索),同時也被稱爲廣度優先搜索。
第102題:給定一個二叉樹,返回其按層次遍歷的節點值。(即逐層地,從左到右訪問全部節點)。
例如:
給定二叉樹: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其層次遍歷結果:[[3],[9,20],[15,7]]
BFS,廣度/寬度優先。說白了就是從上到下,先把每一層遍歷完以後再遍歷一下一層。假如咱們的樹以下:
按照BFS,訪問順序以下:
a->b->c->d->e->f->g
瞭解了BFS,咱們開始對本題進行分析。一樣,咱們先考慮本題的遞歸解法。想到遞歸,咱們通常先想到DFS。咱們能夠對該二叉樹進行先序遍歷(根左右的順序),同時,記錄節點所在的層次level,而且對每一層都定義一個數組,而後將訪問到的節點值放入對應層的數組中。
假設給定二叉樹爲[3,9,20,null,null,15,7],圖解以下:
根據分析,代碼以下:
1//Go 2func levelOrder(root *TreeNode) [][]int { 3 return dfs(root, 0, [][]int{}) 4} 5 6func dfs(root *TreeNode, level int, res [][]int) [][]int { 7 if root == nil { 8 return res 9 } 10 if len(res) == level { 11 res = append(res, []int{root.Val}) 12 } else { 13 res[level] = append(res[level], root.Val) 14 } 15 res = dfs(root.Left, level+1, res) 16 res = dfs(root.Right, level+1, res) 17 return res 18}
上面的解法,其實至關因而用DFS的方法實現了二叉樹的BFS。那咱們能不能直接使用BFS的方式進行解題呢?固然能夠。咱們使用Queue的數據結構。咱們將root節點初始化進隊列,經過消耗尾部,插入頭部的方式來完成BFS。
具體步驟以下圖:
根據分析,完成代碼:
1//Go 2func levelOrder(root *TreeNode) [][]int { 3 var result [][]int 4 if root == nil { 5 return result 6 } 7 // 定義一個雙向隊列 8 queue := list.New() 9 // 頭部插入根節點 10 queue.PushFront(root) 11 // 進行廣度搜索 12 for queue.Len() > 0 { 13 var currentLevel []int 14 listLength := queue.Len() 15 for i := 0; i < listLength; i++ { 16 // queue.Back():返回隊列中最後一個元素 17 // queue.Remove(queue.Back()).(*TreeNode) : 移除隊列中最後一個元素並將其轉化爲TreeNode類型 18 node := queue.Remove(queue.Back()).(*TreeNode) 19 currentLevel = append(currentLevel, node.Val) 20 if node.Left != nil { 21 queue.PushFront(node.Left) 22 } 23 if node.Right != nil { 24 queue.PushFront(node.Right) 25 } 26 } 27 result = append(result, currentLevel) 28 } 29 return result 30}
BST是二叉搜索樹,很重要。BST是二叉搜索樹,很重要。BST是二叉搜索樹,很重要。重要的事情說三遍。
第98題:給定一個二叉樹,判斷其是不是一個有效的二叉搜索樹。
示例 1:
輸入:
5
/ \
1 4
/ \ 3 6
輸出: false
解釋: 輸入爲: [5,1,4,null,null,3,6]。
根節點的值爲 5 ,可是其右子節點值爲 4 。
要驗證二叉搜索樹,首先得知道啥是二叉搜索樹。二叉搜索樹(Binary Search Tree),(又:二叉查找樹,二叉排序樹)它或者是一棵空樹,或者是具備下列性質的二叉樹:若它的左子樹不空,則左子樹上全部結點的值均小於它的根結點的值;若它的右子樹不空,則右子樹上全部結點的值均大於它的根結點的值;它的左、右子樹也分別爲二叉搜索樹。
這裏強調一會兒樹的概念:設T是有根樹,a是T中的一個頂點,由a以及a的全部後裔(後代)導出的子圖稱爲有向樹T的子樹。具體來講,子樹就是樹的其中一個節點以及其下面的全部的節點所構成的樹。好比下面這就是一顆二叉搜索樹:
下面這兩個都不是:
圖中4節點位置的數值應該大於根節點
回到題目,那咱們如何來驗證一顆二叉搜索樹?首先看完題目,咱們很容易想到 遍歷整棵樹,比較全部節點,經過 左節點值<節點值,右節點值>節點值 的方式來進行求解。可是這種解法是錯誤的,由於對於任意一個節點,咱們不光須要左節點值小於該節點,而且左子樹上的全部節點值都須要小於該節點。(右節點一致)因此咱們在此引入上界與下界,用以保存以前的節點中出現的最大值與最小值。
代碼其實很簡單:
1//GO 2func isValidBST(root *TreeNode) bool { 3 if root == nil{ 4 return true 5 } 6 return isBST(root,math.MinInt64,math.MaxInt64) 7} 8 9func isBST(root *TreeNode,min, max int) bool{ 10 if root == nil{ 11 return true 12 } 13 if min >= root.Val || max <= root.Val{ 14 return false 15 } 16 return isBST(root.Left,min,root.Val) && isBST(root.Right,root.Val,max) 17}
難就難在,可能你們看不懂這個遞歸!沒事,祭出大殺器:
這裏須要強調的是,在每次遞歸中,咱們除了進行左右節點的校驗,還須要與上下界進行判斷。其他的就很簡單了。
在上文中,咱們學習了二叉搜索樹。那咱們如何在二叉搜索樹中查找一個元素呢?
第700題:給定二叉搜索樹(BST)的根節點和一個值。你須要在BST中找到節點值等於給定值的節點。返回以該節點爲根的子樹。若是節點不存在,則返回 NULL。
例如,給定二叉搜索樹:
4 / \ 2 7 / \ 1 3
搜索: 2
你應該返回以下子樹:
2 / \ 1 3
在上述示例中,若是要找的值是 5,但由於沒有節點值爲 5,咱們應該返回 NULL。
先複習一下,二叉搜索樹(BST)的特性:
1.若它的左子樹不爲空,則全部左子樹上的值均小於其根節點的值
2.若它的右子樹不爲空,則全部右子樹上的值均大於其根節點得值
3.它的左右子樹也分別爲二叉搜索樹
以下圖就是一棵典型的BST:
如今咱們來看題,假設目標值爲 val。根據BST的特性,咱們能夠很容易想到查找過程(上面的驗證比查找稍難一點):
若是val小於當前結點的值,轉向其左子樹繼續搜索;
若是val大於當前結點的值,轉向其右子樹繼續搜索;
很簡單,不是嗎?而後咱們能夠給出迭代和遞歸兩種解法(給個Java的吧!):
1//java 2 3//遞歸 4public TreeNode searchBST(TreeNode root, int val) { 5 if (root == null) 6 return null; 7 if (root.val > val) { 8 return searchBST(root.left, val); 9 } else if (root.val < val) { 10 return searchBST(root.right, val); 11 } else { 12 return root; 13 } 14} 15 16//迭代 17public TreeNode searchBST(TreeNode root, int val) { 18 while (root != null) { 19 if (root.val == val) { 20 return root; 21 } else if (root.val > val) { 22 root = root.left; 23 } else { 24 root = root.right; 25 } 26 } 27 return null; 28}
查找有了,下面天然就要講刪除。(爲啥說我要着重墨在BST上面,由於BST這兩年在面試時很是高頻。面試官不可能說問你一個普通二叉樹的題目,要麼就是問堆,要麼就是問BST,或者就直接DFS考察回溯。)
第450題:給定一個二叉搜索樹的根節點 root 和一個值 key,刪除二叉搜索樹中的 key 對應的節點,並保證二叉搜索樹的性質不變。返回二叉搜索樹(有可能被更新)的根節點的引用。
通常來講,刪除節點可分爲兩個步驟:
首先找到須要刪除的節點;
若是找到了,刪除它。
說明:要求算法時間複雜度爲 O(h),h 爲樹的高度。
示例:
root = [5,3,6,2,4,null,7]
key = 3
5
/ \
3 6
/ \ \
2 4 7
給定須要刪除的節點值是 3,因此咱們首先找到 3 這個節點,而後刪除它。
一個正確的答案是 [5,4,6,2,null,null,7], 以下圖所示。
5
/ \
4 6
/ \
2 7
另外一個正確答案是 [5,2,6,null,4,null,7]。
5
/ \
2 6
\ \
4 7
若是你看到了這裏,相信確定知道BST是個啥了。因此直接分析題目。咱們要刪除BST的一個節點,首先須要找到該節點。而找到以後,會出現三種狀況。
或者比當前節點大的最小節點(後繼),來替換本身。
分析完畢,直接上代碼。這裏咱們給出經過後繼節點來替代本身的方案(能夠自行實現另外一種方案):
1//go 2func deleteNode(root *TreeNode, key int) *TreeNode { 3 if root == nil { 4 return nil 5 } 6 if key < root.Val { 7 root.Left = deleteNode( root.Left, key ) 8 return root 9 } 10 if key > root.Val { 11 root.Right = deleteNode( root.Right, key ) 12 return root 13 } 14 //到這裏意味已經查找到目標 15 if root.Right == nil { 16 //右子樹爲空 17 return root.Left 18 } 19 if root.Left == nil { 20 //左子樹爲空 21 return root.Right 22 } 23 minNode := root.Right 24 for minNode.Left != nil { 25 //查找後繼 26 minNode = minNode.Left 27 } 28 root.Val = minNode.Val 29 root.Right = deleteMinNode( root.Right ) 30 return root 31} 32 33 34func deleteMinNode( root *TreeNode ) *TreeNode { 35 if root.Left == nil { 36 pRight := root.Right 37 root.Right = nil 38 return pRight 39 } 40 root.Left = deleteMinNode( root.Left ) 41 return root 42}
BST講解完了。上面也說了,別人考察咱們確定是考察特殊的。那二叉樹裏還有啥特殊的東東嘞?平衡二叉樹算是一個。
**第110題:給定一個二叉樹,判斷它是不是高度平衡的二叉樹。
本題中,一棵高度平衡二叉樹定義爲:**
一個二叉樹每一個節點 的左右兩個子樹的高度差的絕對值不超過1。
示例 1:
給定二叉樹 [3,9,20,null,null,15,7]
3
/ \
9 20
/ \
15 7
返回 true 。
示例 2:
給定二叉樹 [1,2,2,3,3,null,null,4,4]
1 / \ 2 2 / \
3 3
/ \
4 4
返回 false 。
題實際上是一道很簡單的題,主要是拿來複習一下高度。咱們想判斷一棵樹是否知足平衡二叉樹,無非就是判斷當前結點的兩個孩子是否知足平衡,同時兩個孩子的高度差是否超過1。那隻要咱們能夠獲得高度,再基於高度進行判斷便可。
這裏惟一要注意的是,當咱們斷定其中任意一個節點若是不知足平衡二叉樹時,那說明整棵樹已經不是一顆平衡二叉樹,咱們能夠對其進行阻斷,不須要繼續遞歸下去。
而後還有一個初學者容易懵逼的:
這玩意,並非平衡二叉樹。上代碼:
1//GO 2func isBalanced(root *TreeNode) bool { 3 if root == nil { 4 return true 5 } 6 l := maxDepth(root.Left) 7 r := maxDepth(root.Right) 8 if abs(l-r)>1 { 9 return false 10 } 11 if isBalanced(root.Left){ 12 return true 13 } 14 return isBalanced(root.Right) 15} 16 17func maxDepth(root *TreeNode) int { 18 if root == nil { 19 return 0 20 } 21 return max(maxDepth(root.Left),maxDepth(root.Right)) + 1 22} 23 24func max(a,b int) int { 25 if a > b { 26 return a 27 } 28 return b 29} 30 31func abs(a int) int { 32 if a < 0 { 33 return -a 34 } 35 return a 36}
還有啥特殊的,要撈出來說一講的?
第222題:給出一個徹底二叉樹,求出該樹的節點個數。
說明:
徹底二叉樹的定義以下:在徹底二叉樹中,除了最底層節點可能沒填滿外,其他每層節點數都達到最大值,而且最下面一層的節點都集中在該層最左邊的若干位置。若最底層爲第 h 層,則該層包含 1~ 2h 個節點。
示例:
輸入:
1
/ \
2 3
/ \ /
4 5 6
輸出: 6
老樣子,咱們得說說啥是徹底二叉樹。徹底二叉樹由滿二叉樹引出,先來了解一下什麼是滿二叉樹。若是二叉樹中除了葉子結點,每一個結點的度都爲 2,則此二叉樹稱爲滿二叉樹。(二叉樹的度表明某個結點的孩子或者說直接後繼的個數,這個在上面已經說過了。對於二叉樹而言,1度是隻有一個孩子或者說單子樹,2度是有兩個孩子或者說左右子樹都有。)
那什麼又是徹底二叉樹呢:若是二叉樹中除去最後一層節點爲滿二叉樹,且最後一層的結點依次從左到右分佈,則此二叉樹被稱爲徹底二叉樹。好比下面這顆:
這個就不是:
上面作了這麼多題了,你應該能想到我要說啥 --- 遞歸。二叉樹的題目基本上均可以遞歸求解。
1func countNodes(root *TreeNode) int { 2 if root != nil { 3 return 0 4 5 } 6 return 1 + countNodes(root.Right) + countNodes(root.Left) 7}
可是很明顯,出題者確定不是要這種答案。由於這種答案和徹底二叉樹一毛錢關係都沒有。因此咱們繼續思考。
因爲題中已經告訴咱們這是一顆徹底二叉樹,咱們又已知了徹底二叉樹除了最後一層,其餘層都是滿的,而且最後一層的節點所有靠向了左邊。那咱們能夠想到,能夠將該徹底二叉樹能夠分割成若幹滿二叉樹和徹底二叉樹,滿二叉樹直接根據層高h計算出節點爲2^h-1,而後繼續計算子樹中徹底二叉樹節點。那如何分割成若干滿二叉樹和徹底二叉樹呢?對任意一個子樹,遍歷其左子樹層高left,右子樹層高right,相等左子樹則是滿二叉樹,不然右子樹是滿二叉樹。這裏可能不容易理解,咱們看圖。
假如咱們有樹以下:
咱們看到根節點的左右子樹高度都爲3,那麼說明左子樹是一顆滿二叉樹。由於節點已經填充到右子樹了,左子樹一定已經填滿了。因此左子樹的節點總數咱們能夠直接獲得,是2^left - 1,加上當前這個root節點,則正好是2^3,即 8。而後只須要再對右子樹進行遞歸統計便可。
那假如咱們的樹是這樣:
咱們看到左子樹高度爲3,右子樹高度爲2。說明此時最後一層不滿,但倒數第二層已經滿了,能夠直接獲得右子樹的節點個數。同理,右子樹節點+root節點,總數爲2^right,即2^2。再對左子樹進行遞歸查找。
根據分析,得出代碼:
/java lass Solution { public int countNodes(TreeNode root) { if (root == null) { return 0; } int left = countLevel(root.left); int right = countLevel(root.right); if (left == right) { return countNodes(root.right) + (1 << left); } else { return countNodes(root.left) + (1 << right); } } private int countLevel(TreeNode root) { int level = 0; while (root != null) { level++; root = root.left; } return level; }
該講的都講了,忽然想到忘了一個經典操做 - 剪枝。迅速補上!很是重要!這裏額外說一點,就本人而言,對這個操做以及其衍化形式的使用會比較頻繁。由於我是作反欺詐的,機器學習裏有一個概念叫作決策樹,那若是一顆決策樹徹底生長,就會帶來比較大的過擬合問題。由於徹底生長的決策樹,每一個節點只會包含一個樣本。因此咱們就須要對決策樹進行剪枝操做,來提高整個決策模型的泛化能力... 聽不懂也不要緊,簡單點講,就是我以爲這個很重要,或者每道算法題都很重要。若是你在工做中沒有用到,不是說明算法不重要,而多是你還不夠重要。
第814題:給定二叉樹根結點 root ,此外樹的每一個結點的值要麼是 0,要麼是 1。返回移除了全部不包含 1 的子樹的原二叉樹。
( 節點 X 的子樹爲 X 自己,以及全部 X 的後代。)
示例1:
輸入: [1,null,0,0,1]
輸出: [1,null,0,null,1]
解釋:
只有紅色節點知足條件「全部不包含 1 的子樹」。
右圖爲返回的答案。
示例2:
輸入: [1,0,1,0,0,0,1]
輸出: [1,null,1,null,1]
示例3:
輸入: [1,1,0,1,1,0,1,0]
輸出: [1,1,0,1,1,null,1]
說明:
給定的二叉樹最多有 100 個節點。
每一個節點的值只會爲 0 或 1 。
仍是先解釋一下,啥是剪枝:假設有一棵樹,最上層的是root節點,而父節點會依賴子節點。若是如今有一些節點已經標記爲無效,咱們要刪除這些無效節點。若是無效節點的依賴的節點還有效,那麼不該該刪除,若是無效節點和它的子節點都無效,則能夠刪除。剪掉這些節點的過程,稱爲剪枝,目的是用來處理二叉樹模型中的依賴問題。
說了好多遍了,二叉樹的問題,大多均可以經過遞歸進行求解。直接分析。假設咱們有二叉樹以下:[0,1,0,1,0,0,0,0,1,1,0,1,0]:
長這樣:
剪枝以後是這樣:
剪什麼你們應該都能理解。那關鍵是怎麼剪?過程也很簡單,在遞歸的過程當中,若是當前結點的左右節點皆爲空,且當前結點爲0,咱們就將當前節點剪掉便可。
其實很簡單,直接看代碼:
1func pruneTree(root *TreeNode) *TreeNode { 2 return deal(root) 3} 4 5func deal(node *TreeNode) *TreeNode { 6 if node == nil { 7 return nil 8 } 9 node.Left = deal(node.Left) 10 node.Right = deal(node.Right) 11 if node.Left == nil && node.Right == nil && node.Val == 0 { 12 return nil 13 } 14 return node 15}
二叉樹入門整合系列篇到這裏就完事了,相信你們若是能夠完整看完,必定會有所收穫。可是呢,其實你們能夠看到,上面的系列還有不少內容沒有講。好比很核心的一塊DFS和回溯。這些都會在後面出單獨的系列進行講解,但願你們多多支持!
今天的整合篇去除了以前的一些冗餘內容,對部分圖解也進行了重構,熬夜整合,猝死邊緣。