在這一章節的學習中,咱們將要學習一個數據結構——二叉樹
(Binary Tree),和基於二叉樹上的搜索算法。html
在二叉樹的搜索中,咱們主要使用了分治法(Divide Conquer)來解決大部分的問題。之因此大部分二叉樹的問題可使用分治法,是由於二叉樹這種數據結構,是一個自然就幫你作好了分治法中「分」這個步驟的結構。java
本章節的先修內容有:node
本章節的補充內容有:面試
遍歷(Traversal),顧名思義,就是經過某種順序,一個一個訪問一個數據結構中的元素
。好比咱們若是須要遍歷一個數組,無非就是要麼從前日後,要麼從後往前遍歷。可是對於一棵二叉樹來講,他就有不少種方式進行遍歷:算法
咱們在以前的課程中,已經學習過了二叉樹的層序遍歷,也就是使用 BFS 算法來得到二叉樹的分層信息。經過 BFS 得到的順序咱們也能夠稱之爲 BFS Order。而剩下的三種遍歷,都須要經過深度優先搜索的方式來得到。而這一小節中,咱們將講一下經過深度優先搜索(DFS)來得到的節點順序,數據庫
首先訪問根結點,而後遍歷左子樹,最後遍歷右子樹。遍歷左、右子樹時,仍按先序遍歷。若二叉樹爲空則返回。api
該過程可簡記爲根左右,注意該過程是遞歸的。如圖先序遍歷結果是:ABDECF。數組
1 // 將根做爲root,空ArrayList做爲result傳入,便可獲得整棵樹的遍歷結果 2 private void traverse(TreeNode root, ArrayList<Integer> result) { 3 if (root == null) { 4 return; 5 } 6 result.add(root.val); 7 traverse(root.left, result); 8 traverse(root.right, result); 9 }
首先遍歷左子樹,而後訪問根結點,最後遍歷右子樹。遍歷左、右子樹時,仍按中序遍歷。若二叉樹爲空則返回。簡記爲左根右。
上圖中序遍歷結果是:DBEAFC。
核心代碼:
Java:markdown
1 private void traverse(TreeNode root, ArrayList<Integer> result) { 2 if (root == null) { 3 return; 4 } 5 traverse(root.left, result); 6 result.add(root.val); // 注意訪問根節點放到了遍歷左子樹的後面 7 traverse(root.right, result); 8 }
首先遍歷左子樹,而後遍歷右子樹,最後訪問根結點。遍歷左、右子樹時,仍按後序遍歷。若二叉樹爲空則返回。簡記爲左右根。
上圖後序遍歷結果是:DEBFCA。網絡
1 private void traverse(TreeNode root, ArrayList<Integer> result) { 2 if (root == null) { 3 return; 4 } 5 traverse(root.left, result); 6 traverse(root.right, result); 7 result.add(root.val); // 注意訪問根節點放到了最後 8 }
http://www.lintcode.com/problem/construct-binary-tree-from-inorder-and-postorder-traversal/
http://www.lintcode.com/problem/construct-binary-tree-from-preorder-and-inorder-traversal/
分治法(Divide & Conquer Algorithm)是說將一個大問題,拆分爲2個或者多個小問題,當小問題獲得結果以後,合併他們的結果來獲得大問題的結果。
舉一個例子,好比中國要進行人口統計。那麼若是使用遍歷(Traversal)的辦法,作法以下:
人口普查員小張本身一我的帶着一個本子,跑遍全中國挨家挨戶的敲門查戶口
而若是使用分治法,作法以下:
在這裏,把全國的任務拆分爲省級的任務的過程,就是分治法中分
的這個步驟。把各個小任務派發給別人去完成的過程,就是分治法中治
的這個步驟。可是事實上咱們還有第三個步驟,就是將小任務的結果合併到一塊兒的過程,合
這個步驟。所以若是我來取名字的話,我會叫這個算法:分治合算法
。
在一棵二叉樹(Binary Tree)中,若是將整棵二叉樹看作一個大問題的話,那麼根節點(Root)的左子樹(Left subtree)就是一個小問題,右子樹(Right subtree)是另一個小問題。這是一個自然就幫你完成了「分」這個步驟的數據結構。
分治法(Divide & Conquer)與遍歷法(Traverse)是兩種常見的遞歸(Recursion)方法。
先讓左右子樹去解決一樣的問題,而後獲得結果以後,再整合爲整棵樹的結果。
經過前序/中序/後序的某種遍歷,遊走整棵樹,經過一個全局變量或者傳遞的參數來記錄這個過程當中所遇到的點和須要計算的結果。
從程序實現角度分治法的遞歸函數,一般有一個返回值
,遍歷法一般沒有。
不少書上會把遞歸(Recursion)看成一種算法。事實上,遞歸是包含兩個層面的意思的:
與之對應的,有非遞歸(Non-Recursion)和迭代法(Iteration),你能夠認爲這兩個概念是同樣的概念(番茄和西紅柿的區別)。不須要作區分。
搜索分爲深度優先搜索(Depth First Search)和寬度優先搜索(Breadth First Search),一般分別簡寫爲 DFS 和 BFS。搜索是一種相似於枚舉(Enumerate)的算法。好比咱們須要找到一個數組裏的最大值,咱們能夠採用枚舉法,由於咱們知道數組的範圍和大小,好比經典的打擂臺算法:
int max = nums[0]; for (int i = 1; i < nums.length; i++) { max = Math.max(max, nums[i]); }
枚舉法一般是你知道循環的範圍,而後能夠用幾重循環就搞定的算法。好比我須要找到 全部 x^2 + y^2 = K 的整數組合,能夠用兩重循環的枚舉法:
// 不要在乎這個算法的時間複雜度 for (int x = 1; x <= k; x++) { for (int y = 1; y <= k; y++) { if (x * x + y * y == k) { // print x and y } } }
而有的問題,好比求 N 個數的全排列,你可能須要用 N 重循環才能解決。這個時候,咱們就傾向於採用遞歸的方式去實現這個變化的 N 重循環。這個時候,咱們就把算法稱之爲搜索
。由於你已經不能明確的寫出一個不依賴於輸入數據的多重循環了。
一般來講 DFS 咱們會採用遞歸的方式實現(固然你強行寫一個非遞歸的版本也是能夠的),而 BFS 則無需遞歸(使用隊列 Queue + 哈希表 HashMap就能夠)。因此咱們在面試中,若是一個問題既可使用 DFS,又可使用 BFS 的狀況下,必定要優先使用 BFS。
由於他是非遞歸的,並且更容易實現。
有的時候,深度優先搜索算法(DFS),又被稱之爲回溯法,因此你能夠徹底認爲回溯法,就是深度優先搜索算法。在個人理解中,回溯其實是深度優先搜索過程當中的一個步驟。好比咱們在進行全子集問題的搜索時,假如當前的集合是 {1,2} 表明我正在尋找以 {1,2}開頭的全部集合。那麼他的下一步,會去尋找 {1,2,3}開頭的全部集合,而後當咱們找完全部以 {1,2,3} 開頭的集合時,咱們須要把 3 從集合中刪掉,回到 {1,2}。而後再把 4 放進去,尋找以 {1,2,4} 開頭的全部集合。這個把 3 刪掉回到 {1,2} 的過程,就是回溯。
subset.add(nums[i]); subsetsHelper(result, subset, nums, i + 1); subset.remove(list.size() - 1) // 這一步就是回溯
詳情請參考:
http://www.jiuzhang.com/solutions/subsets/
咱們以《二叉樹的最大深度》和《二叉樹的前序遍歷》兩個題目爲例子,來分析一下遞歸的三要素
。
相關題目連接:
http://www.lintcode.com/problem/maximum-depth-of-binary-tree/
http://www.lintcode.com/problem/binary-tree-preorder-traversal/
每個遞歸函數,都須要有明確的定義,有了正確的定義之後,纔可以對遞歸進行拆解。
例子:
Java:
int maxDepth(TreeNode root)
Python:
def maxDepth(root):
表明 以 root 開頭的子樹的最大深度是多少
。
Java:
void preorder(TreeNode root, List<TreeNode> result)
Python:
def preorder(root, result):
表明 將 root 開頭的子樹的前序遍歷放到 result 裏面
一個
大問題
如何拆解爲若干個小問題
去解決。
例子:
Java:
int leftDepth = maxDepth(root.left); int rightDepth = maxDepth(root.right); return Math.max(leftDepth, rightDepth) + 1;
Python:
leftDepth = maxDepth(root.left)
rightDepth = maxDepth(root.right)
return max(leftDepth, rightDepth) + 1
整棵樹的最大深度,能夠拆解爲先計算左右子樹深度,而後在左右子樹深度中找到最大值+1來解決。
Java:
result.add(root); preorder(root.left, result); preorder(root.right, result);
Python:
result.append(root) preorder(root.left, result) perorder(root.right, result)
一棵樹的前序遍歷能夠拆解爲3個部分:
因此對應的,咱們把這個遞歸問題也拆分爲三個部分來解決:
何時能夠直接知道答案,不用再拆解,直接 return
例子:
Java:
// 二叉樹的最大深度 if (root == null) { return 0; }
Python:
# 二叉樹的最大深度 if not root: return 0
一棵空的二叉樹,能夠認爲是一個高度爲0
的二叉樹。
Java:
// 二叉樹的前序遍歷 if (root == null) { return; }
Python:
if not root: return
一棵空的二叉樹,天然不用往 result 裏聽任何的東西。
每個遞歸函數,都須要有明確的定義,有了正確的定義之後,纔可以對遞歸進行拆解。
例子:
Java:
int maxDepth(TreeNode root)
Python:
def maxDepth(root):
表明 以 root 開頭的子樹的最大深度是多少
。
Java:
void preorder(TreeNode root, List<TreeNode> result)
Python:
def preorder(root, result):
表明 將 root 開頭的子樹的前序遍歷放到 result 裏面
一個
大問題
如何拆解爲若干個小問題
去解決。
例子:
Java:
int leftDepth = maxDepth(root.left); int rightDepth = maxDepth(root.right); return Math.max(leftDepth, rightDepth) + 1;
Python:
leftDepth = maxDepth(root.left)
rightDepth = maxDepth(root.right)
return max(leftDepth, rightDepth) + 1
整棵樹的最大深度,能夠拆解爲先計算左右子樹深度,而後在左右子樹深度中找到最大值+1來解決。
Java:
result.add(root); preorder(root.left, result); preorder(root.right, result);
Python:
result.append(root) preorder(root.left, result) preorder(root.right, result)
一棵樹的前序遍歷能夠拆解爲3個部分:
因此對應的,咱們把這個遞歸問題也拆分爲三個部分來解決:
何時能夠直接知道答案,不用再拆解,直接 return
例子:
Java:
// 二叉樹的最大深度 if (root == null) { return 0; }
Python:
# 二叉樹的最大深度 if not root: return 0
一棵空的二叉樹,能夠認爲是一個高度爲0
的二叉樹。
Java:
// 二叉樹的前序遍歷 if (root == null) { return; }
Python:
if not root: return
一棵空的二叉樹,天然不用往 result 裏聽任何的東西。
一般是咱們定義在某個文件內部使用的一個類。好比:
Java:
class ResultType { int maxValue, minValue; public ResultType(int maxValue, int minValue) { this.maxValue = maxValue; this.minValue = minValue; } }
當咱們定義的函數須要返回多個值供調用者計算時,就須要使用 ResultType了。
因此若是你只是返回一個值就夠用的話,就不須要。
不是全部的語言都須要自定義 ResultType。
像 Python 這樣的語言,天生支持你返回多個值做爲函數的 return value,因此是不須要的。
二叉搜索樹(Binary Search Tree,又名排序二叉樹,二叉查找樹,一般簡寫爲BST)定義以下:
空樹或是具備下列性質的二叉樹:
(1)若左子樹不空,則左子樹上全部節點值均小於或等於它的根節點值;
(2)若右子樹不空,則右子樹上全部節點值均大於根節點值;
(3)左、右子樹也爲二叉搜索樹;
如圖即爲BST:
http://www.lintcode.com/en/tag/binary-search-tree/
BST是一種重要且基本的結構,其相關題目也十分經典,並延伸出不少算法。
在BST之上,有許多高級且有趣的變種,以解決各式各樣的問題,例如:
平衡二叉搜索樹(Balanced Binary Search Tree,又稱爲AVL樹,有別於AVL算法)是二叉樹中的一種特殊的形態。二叉樹當且僅當知足以下兩個條件之一,是平衡二叉樹:
如圖(圖片來自網絡),節點旁邊的數字表示左右兩子樹高度差。(a)是AVL樹,(b)不是,(b)中5節點不知足AVL樹,故4節點,3節點都再也不是AVL樹。
當AVL樹有N個節點時,高度爲O(logN)O(logN)O(logN)。爲什麼?
試想一棵滿二叉樹,每一個節點左右子樹高度相同,隨着樹高的增長,葉子容量指數暴增,故樹高必定是O(logN)O(logN)O(logN)。而相比於滿二叉樹,AVL樹僅放寬一個條件,容許左右兩子樹高度差1,當樹高足夠大時,能夠把1忽略。如圖是高度爲9的最小AVL樹,若節點更少,樹高毫不會超過8,也即爲什麼AVL樹高會被限制到O(logN)O(logN)O(logN),由於樹不可能太稀疏。嚴格的數學證實複雜,略去。
爲什麼普通二叉樹不是O(logN)O(logN)O(logN)?這裏給出最壞的單枝樹,若單枝擴展,則樹高爲O(N)O(N)O(N):
最大做用是保證查找的最壞時間複雜度爲O(logN)。並且較淺的樹對插入和刪除等操做也更快。
判斷一棵樹是否爲平衡樹
http://www.lintcode.com/problem/balanced-binary-tree/
提示:能夠自下而上遞歸判斷每一個節點是否平衡。若平衡將當前節點高度返回,供父節點判斷;不然該樹必定不平衡。
用 Morris 算法實現 O(1) 額外空間遍歷二叉樹
https://www.jiuzhang.com/tutorial/algorithm/402
遍歷順序爲根、左、右
http://www.lintcode.com/problem/binary-tree-preorder-traversal/
public class Solution { public List<Integer> preorderTraversal(TreeNode root) { Stack<TreeNode> stack = new Stack<TreeNode>(); List<Integer> preorder = new ArrayList<Integer>(); if (root == null) { return preorder; } stack.push(root); while (!stack.empty()) { TreeNode node = stack.pop(); preorder.add(node.val); if (node.right != null) { stack.push(node.right); } if (node.left != null) { stack.push(node.left); } } return preorder; } }
遍歷順序爲左、根、右
http://www.lintcode.com/problem/binary-tree-inorder-traversal/
public class Solution { /** * @param root: The root of binary tree. * @return: Inorder in ArrayList which contains node values. */ public ArrayList<Integer> inorderTraversal(TreeNode root) { Stack<TreeNode> stack = new Stack<>(); ArrayList<Integer> result = new ArrayList<>(); while (root != null) { stack.push(root); root = root.left; } while (!stack.isEmpty()) { TreeNode node = stack.peek(); result.add(node.val); if (node.right == null) { node = stack.pop(); while (!stack.isEmpty() && stack.peek().right == node) { node = stack.pop(); } } else { node = node.right; while (node != null) { stack.push(node); node = node.left; } } } return result; } }
遍歷順序爲左、右、根
http://www.lintcode.com/problem/binary-tree-postorder-traversal/
public ArrayList<Integer> postorderTraversal(TreeNode root) { ArrayList<Integer> result = new ArrayList<Integer>(); Stack<TreeNode> stack = new Stack<TreeNode>(); TreeNode prev = null; // previously traversed node TreeNode curr = root; if (root == null) { return result; } stack.push(root); while (!stack.empty()) { curr = stack.peek(); if (prev == null || prev.left == curr || prev.right == curr) { // traverse down the tree if (curr.left != null) { stack.push(curr.left); } else if (curr.right != null) { stack.push(curr.right); } } else if (curr.left == prev) { // traverse up the tree from the left if (curr.right != null) { stack.push(curr.right); } } else { // traverse up the tree from the right result.add(curr.val); stack.pop(); } prev = curr; } return result; }
二叉搜索樹能夠是一棵空樹或者是一棵知足下列條件的二叉樹:
均小於
它的根節點值。均大於
它的根節點值。https://www.jiuzhang.com/tutorial/algorithm/401
平衡二叉搜索樹又被稱爲AVL樹(有別於AVL算法),且具備如下性質:
也許由於輸入值不夠隨機,也許由於輸入順序的緣由,還或許一些插入、刪除操做,會使得二叉搜索樹失去平衡,形成搜索效率低落的狀況。
好比上面兩個樹,在平衡樹上尋找15就只要2次查找,在非平衡樹上卻要5次查找方能找到,效率明顯降低。
Java:
class TreeNode{ int val; TreeNode left; TreeNode right; pubic TreeNode(int val) { this.val = val; this.left = this.right = null; } }
Python:
class TreeNode: def __init__(self, val): self.val = val self.left, self.right = None, None
TreeSet / TreeMap 是底層運用了紅黑樹的數據結構
PriorityQueue是基於Heap實現的,它能夠保證隊頭元素是優先級最高的元素,但其他元素是不保證有序的。
好比滑動窗口須要保證有序,那麼這時能夠用到TreeSet,由於TreeSet是有序的,而且不須要每次移動窗口都從新排序,只須要插入和刪除(O(logn))就能夠了。
注:在 C++ 中相似的結構爲 set / map。在Python中沒有內置的TreeSet、TreeMap,須要使用第三方庫或者本身實現。
http://www.lintcode.com/problem/consistent-hashing-ii/
練習:鏈表轉平衡排序二叉樹
https://www.jiuzhang.com/tutorial/algorithm/33