Leetcode Lect4 二叉樹中的分治法與遍歷法

在這一章節的學習中,咱們將要學習一個數據結構——二叉樹(Binary Tree),和基於二叉樹上的搜索算法。html

在二叉樹的搜索中,咱們主要使用了分治法(Divide Conquer)來解決大部分的問題。之因此大部分二叉樹的問題可使用分治法,是由於二叉樹這種數據結構,是一個自然就幫你作好了分治法中「分」這個步驟的結構。java

本章節的先修內容有:node

  • 什麼是遞歸(Recursion)—— 請回到第二章節中複習
  • 遞歸(Recursion)、回溯(Backtracking)和搜索(Search)的聯繫和區別
  • 分治法(Divide and Conquer)和遍歷法(Traverse)的聯繫和區別
  • 什麼是結果類 ResultType,何時使用 ResultType
  • 什麼是二叉查找樹(Binary Search Tree)
  • 什麼是平衡二叉樹(Balanced Binary Tree)

本章節的補充內容有:面試

  • Morris 算法:使用 O(1) 的額外空間複雜度對二叉樹進行先序遍歷(Preorder Traversal)
  • 用非遞歸的方法實現先序遍歷,中序遍歷和後序遍歷
  • 二叉查找樹(Binary Search Tree)的增刪查改
  • Java 自帶的平衡排序二叉樹 TreeMap / TreeSet 的介紹和麪試中的應用

 

二叉樹上的遍歷法

定義

遍歷(Traversal),顧名思義,就是經過某種順序,一個一個訪問一個數據結構中的元素。好比咱們若是須要遍歷一個數組,無非就是要麼從前日後,要麼從後往前遍歷。可是對於一棵二叉樹來講,他就有不少種方式進行遍歷:算法

  1. 層序遍歷(Level order)
  2. 先序遍歷(Pre order)
  3. 中序遍歷(In order)
  4. 後序遍歷(Post order)

咱們在以前的課程中,已經學習過了二叉樹的層序遍歷,也就是使用 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 }
View Code

中序遍歷(又叫中根遍歷)

首先遍歷左子樹,而後訪問根結點,最後遍歷右子樹。遍歷左、右子樹時,仍按中序遍歷。若二叉樹爲空則返回。簡記爲左根右
上圖中序遍歷結果是: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 }
View Code

後序遍歷(又叫後根遍歷)

首先遍歷左子樹,而後遍歷右子樹,最後訪問根結點。遍歷左、右子樹時,仍按後序遍歷。若二叉樹爲空則返回。簡記爲左右根
上圖後序遍歷結果是: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 }
View Code

一些有趣的題目:

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)的辦法,作法以下:

人口普查員小張本身一我的帶着一個本子,跑遍全中國挨家挨戶的敲門查戶口

而若是使用分治法,作法以下:

  1. 國家統計局的老闆小李想要知道全國人口的總數,因而他找來全國各個省的統計局領導,下派人口普查任務給他們,讓他們各自去統計本身省的人口總數。在小李這兒,他只須要最後將各個省彙報的人口總數結果累加起來,就獲得了全國人口的數目。
  2. 而後每一個省的領導,又找來省裏各個市的領導,讓各個市去作人口統計。
  3. 市找縣,縣找鎮,鎮找鄉。最後鄉里的幹部小王挨家挨戶敲門去查戶口。

在這裏,把全國的任務拆分爲省級的任務的過程,就是分治法中的這個步驟。把各個小任務派發給別人去完成的過程,就是分治法中的這個步驟。可是事實上咱們還有第三個步驟,就是將小任務的結果合併到一塊兒的過程,這個步驟。所以若是我來取名字的話,我會叫這個算法:分治合算法

爲何二叉樹的問題適合使用分治法?

在一棵二叉樹(Binary Tree)中,若是將整棵二叉樹看作一個大問題的話,那麼根節點(Root)的左子樹(Left subtree)就是一個小問題,右子樹(Right subtree)是另一個小問題。這是一個自然就幫你完成了「分」這個步驟的數據結構。

二叉樹的最大深度

 

 

判斷平衡二叉樹

 

判斷二叉搜索樹

 

 

 


遞歸,分治法,遍歷法的聯繫與區別

聯繫

分治法(Divide & Conquer)與遍歷法(Traverse)是兩種常見的遞歸(Recursion)方法。

分治法解決問題的思路

先讓左右子樹去解決一樣的問題,而後獲得結果以後,再整合爲整棵樹的結果。

遍歷法解決問題的思路

經過前序/中序/後序的某種遍歷,遊走整棵樹,經過一個全局變量或者傳遞的參數來記錄這個過程當中所遇到的點和須要計算的結果。

兩種方法的區別

從程序實現角度分治法的遞歸函數,一般有一個返回值,遍歷法一般沒有。

 

遞歸、回溯和搜索

什麼是遞歸 (Recursion) ?

不少書上會把遞歸(Recursion)看成一種算法。事實上,遞歸是包含兩個層面的意思的:

  1. 一種由大化小,由小化無的解決問題的算法。相似的算法還有動態規劃(Dynamic Programming)。
  2. 一種程序的實現方式。這種方式就是一個函數(Function / Method / Procedure)本身調用本身。

與之對應的,有非遞歸(Non-Recursion)和迭代法(Iteration),你能夠認爲這兩個概念是同樣的概念(番茄和西紅柿的區別)。不須要作區分。

什麼是搜索 (Search)?

搜索分爲深度優先搜索(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。由於他是非遞歸的,並且更容易實現。

什麼是回溯(Backtracking)?

有的時候,深度優先搜索算法(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/

1. 遞歸的定義

每個遞歸函數,都須要有明確的定義,有了正確的定義之後,纔可以對遞歸進行拆解。

例子:
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 裏面

2. 遞歸的拆解

一個大問題如何拆解爲若干個小問題去解決。

例子:
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個部分:

  1. 根節點本身(root)
  2. 左子樹的前序遍歷
  3. 右子樹的前序遍歷

因此對應的,咱們把這個遞歸問題也拆分爲三個部分來解決:

  1. 先把 root 放到 result 裏 --> result.add(root);
  2. 再把左子樹的前序遍歷放到 result 裏 --> preorder(root.left, result)。回想一下遞歸的定義,是否是正是如此?
  3. 再把右子樹的前序遍歷放到 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:

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個部分:

  1. 根節點本身(root)
  2. 左子樹的前序遍歷
  3. 右子樹的前序遍歷

因此對應的,咱們把這個遞歸問題也拆分爲三個部分來解決:

  1. 先把 root 放到 result 裏 --> result.add(root);
  2. 再把左子樹的前序遍歷放到 result 裏 --> preorder(root.left, result)。回想一下遞歸的定義,是否是正是如此?
  3. 再把右子樹的前序遍歷放到 result 裏 --> preorder(root.right, result)。

遞歸的出口

何時能夠直接知道答案,不用再拆解,直接 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 裏聽任何的東西。


使用 ResultType 返回多個值

什麼是 ResultType

一般是咱們定義在某個文件內部使用的一個類。好比:
Java:

class ResultType { int maxValue, minValue; public ResultType(int maxValue, int minValue) { this.maxValue = maxValue; this.minValue = minValue; } } 

何時須要 ResultType

當咱們定義的函數須要返回多個值供調用者計算時,就須要使用 ResultType了。
因此若是你只是返回一個值就夠用的話,就不須要。

其餘語言須要 ResulType 麼?

不是全部的語言都須要自定義 ResultType。
像 Python 這樣的語言,天生支持你返回多個值做爲函數的 return value,因此是不須要的。


什麼是二叉搜索樹

定義

二叉搜索樹(Binary Search Tree,又名排序二叉樹,二叉查找樹,一般簡寫爲BST)定義以下:
空樹或是具備下列性質的二叉樹
(1)若左子樹不空,則左子樹上全部節點值均小於或等於它的根節點值;
(2)若右子樹不空,則右子樹上全部節點值均大於根節點值;
(3)左、右子樹也爲二叉搜索樹;
如圖即爲BST:
圖片

BST 的特性

  • 按照中序遍歷(inorder traversal)打印各節點,會獲得由小到大的順序。
  • 在BST中搜索某值的平均狀況下複雜度爲O(logN)O(logN)O(logN),最壞狀況下複雜度爲O(N)O(N)O(N),其中N爲節點個數。將待尋值與節點值比較,若不相等,則經過是小於仍是大於,可判定該值只可能在左子樹仍是右子樹,繼續向該子樹搜索
  • 在balanced BST中查找某值的時間複雜度爲O(logN)O(logN)O(logN)。

BST 的做用

  • 經過中序遍歷,可快速獲得升序節點列表。
  • 在BST中查找元素,平均狀況下時間複雜度是O(logN)O(logN)O(logN);插入新節點,保持BST特性平均狀況下要耗時O(logN)。(參考連接)。
  • 和有序數組的對比:有序數組查找某元素能夠用二分法,時間複雜度是O(logN);可是插入新元素,維護數組有序性要耗時O(N)。

常見的BST面試題

http://www.lintcode.com/en/tag/binary-search-tree/

BST是一種重要基本的結構,其相關題目也十分經典,並延伸出不少算法。
在BST之上,有許多高級且有趣的變種,以解決各式各樣的問題,例如:

  • 用於數據庫或各語言標準庫中索引的紅黑樹
  • 提高二叉樹性能底線的伸展樹
  • 優化紅黑樹的AA樹
  • 隨機插入的樹堆
  • 機器學習kNN算法的高維快速搜索k-d樹

什麼是平衡二叉搜索樹

定義

平衡二叉搜索樹(Balanced Binary Search Tree,又稱爲AVL樹,有別於AVL算法)是二叉樹中的一種特殊的形態。二叉樹當且僅當知足以下兩個條件之一,是平衡二叉樹:

  • 空樹。
  • 左右子樹高度差絕對值不超過1左右子樹都是平衡二叉樹

圖片

如圖(圖片來自網絡),節點旁邊的數字表示左右兩子樹高度差。(a)是AVL樹,(b)不是,(b)中5節點不知足AVL樹,故4節點,3節點都再也不是AVL樹。

AVL樹的高度爲 O(logN)O(logN)O(logN)

當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):
圖片

AVL樹有什麼用?

最大做用是保證查找的最壞時間複雜度爲O(logN)。並且較淺的樹對插入和刪除等操做也更快。

AVL樹的相關練習題

判斷一棵樹是否爲平衡樹
http://www.lintcode.com/problem/balanced-binary-tree/
提示:能夠自下而上遞歸判斷每一個節點是否平衡。若平衡將當前節點高度返回,供父節點判斷;不然該樹必定不平衡。


補充內容:

用 Morris 算法實現 O(1) 額外空間遍歷二叉樹

https://www.jiuzhang.com/tutorial/algorithm/402

 

非遞歸的方式實現二叉樹遍歷

先序遍歷

思路

遍歷順序爲

  1. 若是根節點非空,將根節點加入到棧中。
  2. 若是棧不空,彈出出棧頂節點,將其值加加入到數組中。
    1. 若是該節點的右子樹不爲空,將右子節點加入棧中。
    2. 若是左子節點不爲空,將左子節點加入棧中。
  3. 重複第二步,直到棧空。

    練習

    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;
    }
}

 

中序遍歷

思路

遍歷順序爲

  1. 若是根節點非空,將根節點加入到棧中。
  2. 若是棧不空,取棧頂元素(暫時不彈出),
    1. 若是左子樹已訪問過,或者左子樹爲空,則彈出棧頂節點,將其值加入數組,若有右子樹,將右子節點加入棧中。
    2. 若是左子樹不爲空,則將左子節點加入棧中。
  3. 重複第二步,直到棧空。

練習

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;
    }
}

 

後序遍歷

思路

遍歷順序爲

  1. 若是根節點非空,將根節點加入到棧中。
  2. 若是棧不空,取棧頂元素(暫時不彈出),
    1. 若是(左子樹已訪問過或者左子樹爲空),且(右子樹已訪問過或右子樹爲空),則彈出棧頂節點,將其值加入數組,
    2. 若是左子樹不爲空,切未訪問過,則將左子節點加入棧中,並標左子樹已訪問過。
    3. 若是右子樹不爲空,切未訪問過,則將右子節點加入棧中,並標右子樹已訪問過。
  3. 重複第二步,直到棧空。

練習

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;
}

 

 

什麼是二叉搜索樹(Binary Search Tree)

二叉搜索樹能夠是一棵空樹或者是一棵知足下列條件的二叉樹:

  • 若是它的左子樹不空,則左子樹上全部節點值均小於它的根節點值。
  • 若是它的右子樹不空,則右子樹上全部節點值均大於它的根節點值。
  • 它的左右子樹均爲二叉搜索樹(BST)。
  • 嚴格定義下BST中是沒有值相等的節點的(No duplicate nodes)。
    根據上述特性,咱們能夠獲得一個結論:BST中序遍歷獲得的序列是升序的。以下述BST的中序序列爲:[1,3,4,6,7,8,10,13,14]

BST

BST基本操做——增刪改查(CRUD)

https://www.jiuzhang.com/tutorial/algorithm/401

 

 

平衡排序二叉樹(Self-balancing Binary Search Tree)

定義

平衡二叉搜索樹又被稱爲AVL樹(有別於AVL算法),且具備如下性質:

  • 它是一棵空樹或它的左右兩個子樹的高度差的絕對值不超過1
  • 左右兩棵子樹都是一棵平衡二叉搜索樹
  • 平衡二叉搜索樹一定是二叉搜索樹,反之則不必定。

平衡排序二叉樹 與 二叉搜索樹 對比

也許由於輸入值不夠隨機,也許由於輸入順序的緣由,還或許一些插入、刪除操做,會使得二叉搜索樹失去平衡,形成搜索效率低落的狀況。
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 

經常使用的實現辦法

Java中的 TreeSet / TreeMap

TreeSet / TreeMap 是底層運用了紅黑樹的數據結構

對比 HashSet / HashMap

  • HashSet / HashMap 存取的時間複雜度爲O(1),而 TreeSet / TreeMap 存取的時間複雜度爲 O(logn) 因此在存取上並不佔優。
  • HashSet / HashMap 內元素是無序的,而TreeSet / TreeMap 內部是有序的(能夠是按天然順序排列也能夠自定義排序)。
  • TreeSet / TreeMap 還提供了相似 lowerBoundupperBound 這兩個其餘數據結構沒有的方法
    • 對於 TreeSet, 實現上述兩個方法的方法爲:
      • lowerBound
        • public E lower(E e) --> 返回set中嚴格小於給出元素的最大元素,若是沒有知足條件的元素則返回 null
        • public E floor(E e) --> 返回set中不大於給出元素的最大元素,若是沒有知足條件的元素則返回 null
      • upperBound
        • public E higher(E e) --> 返回set中嚴格大於給出元素的最小元素,若是沒有知足條件的元素則返回 null
        • public E ceiling(E e) --> 返回set中不小於給出元素的最小元素,若是沒有知足條件的元素則返回 null
    • 對於 TreeMap, 實現上述兩個方法的方法爲:
      • lowerBound
        • public Map.Entry<K,V> lowerEntry(K key) --> 返回map中嚴格小於給出的key值的最大key對應的key-value對,若是沒有知足條件的key則返回 null
        • public K lowerKey(K key) --> 返回map中嚴格小於給出的key值的最大key,若是沒有知足條件的key則返回 null
        • public Map.Entry<K,V> floorEntry(K key) --> 返回map中不大於給出的key值的最大key對應的key-value對,若是沒有知足條件的key則返回 null
        • public K floorKey(K key) --> 返回map中不大於給出的key值的最大key,若是沒有知足條件的key則返回 null
      • upperBound
        • public Map.Entry<K,V> higherEntry(K key) --> 返回map中嚴格大於給出的key值的最小key對應的key-value對,若是沒有知足條件的key則返回 null
        • public K higherKey(K key) --> 返回map中嚴格大於給出的key值的最小key,若是沒有知足條件的key則返回 null
        • public Map.Entry<K,V> ceilingEntry(K key) --> 返回map中不小於給出的key值的最小key對應的key-value對,若是沒有知足條件的key則返回 null
        • public K ceilingKey(K key) --> 返回map中不小於給出的key值的最小key,若是沒有知足條件的key則返回 null
    • lowerBound 與 upperBound 均爲二分查找(所以要求有序),時間複雜度爲O(logn).

對比 PriorityQueue(Heap)

PriorityQueue是基於Heap實現的,它能夠保證隊頭元素是優先級最高的元素,但其他元素是不保證有序的。

  • 方法時間複雜度對比:
    • 添加元素 add() / offer()
      • TreeSet: O(logn)
      • PriorityQueue: O(logn)
    • 刪除元素 poll() / remove()
      • TreeSet: O(logn)
      • PriorityQueue: O(n)
    • 查找 contains()
      • TreeSet: O(logn)
      • PriorityQueue: O(n)
    • 取最小值 first() / peek()
      • TreeSet: O(logn)
      • PriorityQueue: O(1)

常見用法

好比滑動窗口須要保證有序,那麼這時能夠用到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

相關文章
相關標籤/搜索