「種樹專業戶」“樹”業有專攻

童歐巴 發表於 2020-06-30

圖怪獸_2bd02daed73e352d21a854313ff206a5_23114.png
觀感度:🌟🌟🌟🌟🌟

口味:螞蟻上樹

烹飪時間:10min

本文已收錄在Github github.com/Geekhyt,感謝Star。

周樹人先生曾經說過:學好樹,資料結構與演算法你就掌握了一半!

食堂老闆(童歐巴):就算我們作為網際網路浪潮中的葉子結點,也需要有蚍蜉撼樹的精神,就算蚍蜉撼樹是自不量力。因為就算終其一生只是個普通人,但你總不能為了成為一個普通人而終其一生吧。

今日菜譜,螞蟻上樹,下面介紹一下演員。

樹的相關名詞科普

  • 根節點
  • 葉子節點
  • 父節點
  • 子節點
  • 兄弟節點
  • 高度
  • 深度

「種樹專業戶」“樹”業有專攻

A 是 根節點。C、D、F、G 是 葉子節點。A 是 B 和 E 的 父節點。B 和 E 是 A 的 子節點。B、E 之間是 兄弟節點高度、深度、層 如上圖所示。為了方便理解記憶,高度 就是抬頭看,深度 就是低頭看。與 高度、深度 不同, 類比盜夢空間裡的樓,樓都是從 1 層開始計算,盜夢空間中的樓顛倒過來,從上往下。

二叉樹 Binary Tree

每個節點最多有兩個子節點,也就是 左子節點右子節點

滿二叉樹 Full Binary Tree

葉子節點全都在最底層,除了葉子節點之外,每個節點都有左右兩個子節點。上文中的圖就是滿二叉樹。

完全二叉樹 Complete Binary Tree

葉子節點都在最底下兩層,最後一層的葉子節點都靠左排列,並且除了最後一層,其他層的節點個數都要達到最大。

堆其實就是一種完全二叉樹,一般採用的儲存方式是陣列。

採用陣列儲存完全二叉樹,無須像鏈式儲存一樣額外儲存左右子節點的指標,可以節省記憶體。

「種樹專業戶」“樹”業有專攻

二叉樹的遍歷

「種樹專業戶」“樹”業有專攻

  • 前序遍歷:先列印當前節點,再列印當前節點的左子樹,最後列印當前節點的右子樹 (ABCDEFG)
  • 中序遍歷:先列印當前節點的左子樹,再列印當前節點,最後列印當前節點的右子樹 (CBDAFEG)
  • 後序遍歷:先列印當前節點的左子樹,再列印當前節點的右子樹,最後列印當前節點 (CDBFGEA)
// 前序遍歷
const preorderTraversal = function(root) {
    const result = [];
    function pushRoot(node) {
        if (node != null) {
            result.push(node.val);
            if (node.left != null) {
                pushRoot(node.left);
            }
            if (node.right != null){ 
                pushRoot(node.right);
            } 
        }
    }
    pushRoot(root);
    return result;
};
// 中序遍歷
const inorderTraversal = function(root) {
    const result = [];
    function pushRoot(node) {
        if (node != null) {
            if (node.left != null) {
                pushRoot(node.left);
            }
            result.push(node.val);
            if (node.right != null){ 
                pushRoot(node.right);
            } 
        }
    }
    pushRoot(root);
    return result;
};
// 後序遍歷
const postorderTraversal = function(root) {
    const result = [];
    function pushRoot(node) {
        if (node != null) {
            if (node.left != null) {
                pushRoot(node.left);
            }
            if (node.right != null){ 
                pushRoot(node.right);
            }
            result.push(node.val);
        }
    }
    pushRoot(root);
    return result;
};

複雜度分析

  • 時間複雜度: O(n)
  • 空間複雜度:最壞情況為 O(n),平均情況為 O(logn)

二叉查詢樹 Binary Search Tree

又稱二叉排序樹、二叉搜尋樹。

中序遍歷二叉查詢樹,可以輸出有序的資料序列,時間複雜度是 O(n),非常高效。

二叉查詢樹中的任意一個節點,左子樹中的每個節點的值,都小於這個節點的值,而右子樹節點的值都大於這個節點的值。

「種樹專業戶」“樹”業有專攻

在二叉查詢樹中,查詢、插入、刪除等很多操作的時間複雜度都跟樹的高度成正比。兩個極端情況的時間複雜度分別是 O(n)O(logn),分別對應二叉樹退化成連結串列的情況和完全二叉樹的情況。

極端情況下複雜度退化並不是我們想要的,所以我們需要設計一種平衡二叉查詢樹。平衡二叉查詢樹的高度接近 logn,所以查詢、插入、刪除操作的時間複雜度也比較穩定,是 O(logn)

AVL 樹 Adelson-Velsky-Landis Tree

AVL 樹 是最先被發明的平衡二叉查詢樹(以發明者的名字來進行的命名),它定義任何節點的左右子樹高度相差不超過 1,並且左右兩個子樹都是一棵平衡二叉樹。

AVL 樹是一種高度平衡的二叉查詢樹。雖然查詢效率高,但是為了維持高度平衡,插入、刪除操作時都需要進行調整(左旋,右旋),需要付出很大的維持成本。

「種樹專業戶」“樹”業有專攻

紅黑樹 Red-balck Tree

紅黑樹可以被稱為“網紅樹”了,它的出場率相比其他的樹要高出一個天際線。與 AVL 樹不同,紅黑樹只是做到了近似平衡,並不是嚴格意義上的平衡。所以在維護平衡付出的成本上比 AVL 樹要低,查詢、插入、刪除等操作的效能都比較穩定,時間複雜度都是 O(logn)

一棵合格的紅黑樹應該滿足:

  • 每個結點或紅或黑
  • 根節點是黑色
  • 每個葉子節點都是黑色的空節點(葉子節點不儲存資料)
  • 相鄰的節點不能同時為紅色,紅黑相隔
  • 每個節點,從該節點到達其可達葉子節點的所有路徑,都包含相同數目的黑色節點

紅黑樹在工程上有著大量的應用,因為工程上對效能的穩定性要求是很高的。正因為紅黑樹的效能比較穩定,它扛起了工程應用上的大旗。

「種樹專業戶」“樹”業有專攻

Trie 樹

Google、百度一類的搜尋引擎強大的關鍵詞提示功能的背後,最基本的原理就 Trie 樹,通過空間換時間,利用字串的公共字首,降低查詢的時間以提高效率。除此之外,還有很多應用,比如:IP 路由中使用了 Trie 樹的最長字首匹配演算法,利用轉發表選擇路徑以及 IDE 中的智慧提示等。

「種樹專業戶」“樹”業有專攻

Trie 樹 是一棵非典型的多叉樹模型,它和一般的多叉樹不同,我們可以對比一下它們結點的資料結構設計。一般的多叉樹結點中包含結點值和指向子結點的指標。而 Trie 樹的結點中包含的是該結點是否是一個串的結束,以及字母對映表。通過字母對映表我們可以通過一個父結點來獲取它所有子結點的值。

在 Trie 樹 中查詢字串的時間複雜度是 O(k),k 是要查詢的字串長度。

「種樹專業戶」“樹”業有專攻

上圖中的 Trie 樹由 5 個單詞構成,分別是 color、coat、city、hi、hot。根結點的值為空。

LeetCode 208.實現 Trie 樹

class Trie {
  constructor() {
    this.root = {};
  }
  insert(word) {
    let curr = this.root;
    word.split('').forEach(ch => (curr = curr[ch] = curr[ch] || {}));
    curr.isWord = true;
  }
  traverse(word) {
    let curr = this.root;
    for (let i = 0; i < word.length; i++) {
      if (!curr) return null;
      curr = curr[word[i]];
    }
    return curr;
  }
  search(word) {
    let node = this.traverse(word);
    return !!node && !!node.isWord;
  }
  startsWith(word) {
    return !!this.traverse(word);
  }
}

B+ 樹

我們知道,將索引儲存在內容中,查詢速度是比儲存在磁碟中快的。但是當資料量很大的情況下,索引也隨之變大。記憶體是有限的,我們不得不將索引儲存在磁碟中。那麼,如何提升從磁碟中讀取的效率就成了工程上的關鍵之一。

大部分關係型資料庫的索引,比如 MySQL、Oracle,都是用 B+ 樹來實現的。B+ 樹比起紅黑樹更適合構建儲存在磁碟中的索引。B+ 樹是一個多叉樹,在相同個數的資料構建索引時,其高度要低於紅黑樹。當藉助索引查詢資料的時,讀取 B+ 樹索引,需要更少的磁碟 IO 次數。

一個 m 階的 B 樹滿足如下特徵:

  • 每個節點中子節點的個數 k 滿足 m > k > m/2,根節點的子節點個數可以不超過 m/2
  • 通過雙向連結串列將葉子節點串聯在一起,方便按區間查詢
  • m 叉樹只儲存索引,並不真正儲存資料
  • 一般情況,根節點被儲存在記憶體中,其他節點儲存在磁碟中

參考

  • 《資料結構與演算法之美》 王爭