觀感度:?????
口味:螞蟻上樹
烹飪時間: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 叉樹只儲存索引,並不真正儲存資料
- 一般情況,根節點被儲存在記憶體中,其他節點儲存在磁碟中
參考
- 《資料結構與演算法之美》 王爭