淺談樹形結構的特性和應用(上):多叉樹,紅黑樹,堆,Trie樹,B樹,B+樹...

monica2333發表於2020-08-01

上篇文章我們主要介紹了線性資料結構,本篇233醬帶大家康康 無所不在的非線性資料結構之一:樹形結構的特點和應用。

樹形結構,是指:資料元素之間的關係像一顆樹的資料結構。我們看圖說話:

它具有以下特點:

  1. 每個節點都只有有限個子節點或無子節點;
  2. 沒有父節點的節點稱為根節點;
  3. 每一個非根節點有且只有一個父節點;
  4. 除了根節點外,每個子節點可以分為多個不相交的子樹;
  5. 樹裡面沒有環路(cycle)

維基百科中列舉了電腦科學中樹形結構的種類

233醬當然不會一個個講,我們只挑一些熟悉的面孔:多叉樹,二叉樹,二叉查詢樹,紅黑樹,堆,Trie樹,B樹,B+樹,LSM Tree,瞭解他們在對不同規模的資料 增,刪,改,查 時所起到的作用就夠了。

限於篇幅,本文主要介紹非LSM Tree的內容。

多叉樹

樹體現了一種 繼承 的關係,節點之間為父子關係。多叉樹 是指一個父節點可以有多個子節點。也就是:爸爸可以有多個兒子,兒子只能有一個爸爸。

當需要這種分層,繼承的場景下均可以考慮用樹結構來實現,可以簡化我們對資料關係的描述。

此外,節點的每個分支也代表著一種選擇,可以用來做決策。

應用場景:
1.Linux檔案系統
2.組織關係表示,如公司的組織架構,角色許可權系統等。
3.XML/HTML資料。
4.類的繼承關係
5.決策,如遊戲中怪物使用的技能選擇,機器學習...

二叉樹

二叉樹(Binary tree)是每個節點最多隻有兩個分支(即不存在分支度大於2的節點)的樹結構,也就是說 爸爸 最多隻能有 兩個兒子。

因為計算機應用中存在很多“非黑即白”的場景,同樣我們可以利用 不是走左分支,就是走右分支 這種結構選擇來做一些決策。
另外,利用每個節點下參與方最多為兩個,也可以做一些事情。

應用場景:
1.編譯器的語法樹構造。
2.表示式求值和判斷:非葉子節點為操作符,葉子節點為數值。
3.Boolean求值,如(a and b)or (c or d)
4.霍夫曼編碼
5.IPv4路由表的儲存...

在我們的日常開發中,經常需要對資料進行增刪改查,每一個步驟的時間空間效率決定了應用最終的效能。

時間複雜度 體現在 能否快速定位到要操作的資料元素,核心在於 索引的好壞

空間複雜度 體現在 能否佔用 更小的記憶體空間,能否利用計算機各層介質的快取效能提高訪問速度(訪問速度:CPU>> 記憶體>>磁碟)。

在以下樹形結構的討論中,希望小夥伴能多從 索引,所佔用記憶體空間,操作的介質 這些方面考慮資料的增刪改查效能。

二叉查詢樹

二叉查詢樹(Binary Search Tree)首先是二叉樹,同時滿足一定的有序性:節點的左子節點比自己小,節點的右子節點比自己大。

這樣當我們定位一個元素的位置時,從根節點開始查詢,小於節點左拐,大於節點右拐。等於節點排序值就剛好找到。二分的索引思想就體現在其中。

應用場景:
二叉查詢樹的有序性是它能夠廣泛應用的原因。但是能否高效二分體現在樹的高度合理性上。下面要講的 紅黑樹/堆結構才是其廣泛應用。

紅黑樹

二叉查詢樹的缺點在於:只限制了節點的有序性,但有序樹的構造有好壞。一顆“壞”的有序樹直接會被拉成 “有序連結串列”。所以需要通過一定的條件保證樹的平衡性。

樹的平衡性是指整棵樹的最高子樹和最矮子樹相差不大,這樣整棵樹的高度相對來說低一些,相應的增,刪,改,查操作的效率較高較穩定(與樹高有關)。

紅黑樹(Red–black tree)是應用很廣泛的一種平衡樹,是面試官的裝X利器。我們來看一下它保證平衡性的一些特性:

  1. 節點是紅色或黑色。
  2. 根是黑色。
  3. 所有葉子都是黑色(葉子是NIL節點)。
  4. 每個紅色節點必須有兩個黑色的子節點。(從每個葉子到根的所有路徑上不能有兩個連續的紅色節點。)
  5. 從任一節點到其每個葉子的所有簡單路徑都包含相同數目的黑色節點。

這些約束確保了紅黑樹的關鍵特性:從根到葉子的最長路徑不多於最短路徑的兩倍長(根據性質4和性質5)。從而整棵樹的高度比較穩定,相應的增、刪、改、查操作的效率較高較穩定,而不同於普通的二叉查詢樹。

此外相比其他的平衡樹:如高度平衡樹AVL樹,紅黑樹的增刪改效率較高,同時查詢效能沒有下降很多也比較穩定。所以工業級應用更為廣泛。

應用場景:適合排序,查詢的場景。
1.容器的基本組成,如Java中的HashMap/TreeMap.
2.Linux核心的完全公平排程器
3.Linux中epoll機制的實現...

堆是一種特殊的資料結構,它滿足兩個特性:

  1. 堆是一個完全二叉樹;
  2. 堆中每一個節點的排序值都必須大於等於(或小於等於)其左右子節點的排序值。

這裡解釋以下完全二叉樹。它是指:除了最後一層,其他層的節點個數都是滿的,最後一層的節點都靠左排列。

這樣我們就可以用陣列來儲存。

假設根節點在i=0的位置:如果父節點的陣列下標為i,則左子節點的陣列下標為2 * i+1,右子節點的陣列下標為 2 * i + 2。陣列比連結串列的儲存好處上篇文章233醬提過了,這裡就不贅述了。

針對第2點,如果每個節點的值都大於等於子樹中每個節點值的堆,叫做“大頂堆”。反之叫做“小頂堆”。

堆這種結構相對有序,且堆頂元素要麼最小,要麼最大。且利用了 陣列和自身特性 增刪改查的時間複雜度較低。

應用場景:
1.堆排序
2.TopK,求中位數,求
3.合併多個有序小檔案或集合
4.優先佇列,如定時任務的儲存等...

Trie樹

Trie樹 又稱字首樹或字典樹,它是一種專門處理字串匹配的資料結構,用來解決在一組字串集合中快速查詢某個字串的問題。

它的特性為:

  1. 根節點不包含字元,除根節點外的每一個子節點都包含一個字元。
  2. 從根節點到某一節點,路徑上經過的字元連線起來,就是該節點對應的字串。
  3. 每個字串的公共字首作為一個字元節點儲存。

如果我們有and,as,at,cn,com這些關鍵詞,那麼構建的trie樹如下:

Trie 樹的本質,就是利用字串之間的公共字首,將重複的字首合並在一起。這樣當我們查詢某個字串時,只需要在Trie樹上匹配字串的每個字元,比較次數僅和 字串的長度 有關。

但是Trie 樹的缺點就是構造Trie樹需要很大的記憶體空間。因為父子字元節點之間用 指標關聯。如果用陣列儲存這些指標,這意味著子節點的陣列需要窮舉出每一種可能。如子節點字元為a-z,就需要分配長度為26的陣列來儲存這些可能的子節點。

改進記憶體分配的措施有:

1.子節點指標改為用:有序陣列、跳錶、雜湊表、紅黑樹來儲存。
2.子節點合併,如原來 hello儲存為:h->e->l->l->o ,現在可儲存為:h->e->llo

Trie 樹畢竟比較浪費空間,所以它在字串的查詢中,適合用於:1.字符集不能太大 2.字串的公共字首較多 的場景。

應用場景:
1.關鍵詞匹配,提示,糾錯。
2.最長公共字首匹配。
3.命令自動補全,如zsh.
4.網址瀏覽歷史記錄。
5.手機號碼簿查詢...

B樹、B+樹、LSM Tree是資料庫中經常出現的資料結構。對於資料庫的增刪改查效率,需要考慮的首要因素是:磁碟的IO訪問次數

如何減少IO訪問次數?

前文我們已經提到了索引,但是IO一次不容易,從磁碟中獲取資料通常是以塊為單位的。如果對於上千萬條資料,我們只建立一層索引結構的話,那索引的資料量也是巨大的。

如何降低索引量?

假設我們到全世界找一個人,我們是按照 國家/省/市/區/街道/小區的順序來定位。同理,隨著資料量的增大,我們需要一層層的建立 多級索引 。越上層的索引每個塊中表示的資料範圍越大。

如何保證我們建立的多級索引 是“合適”的?

這個合適主要指:儲存的資料量大並且樹高小。同紅黑樹一樣,我們需要一些 限制條件 來保證樹高。這也就是以下資料結構的限制條件原因了。

B樹

一個m階(該樹每個節點最多有 M 個子節點)的B樹具有以下特徵:

1.根節點至少有兩個子女。
2.每個中間節點都包含k-1個元素和k個孩子,其中 m/2 <= k <= m
3.每一個葉子節點都包含k-1個元素,其中 m/2 <= k <= m
4.所有的葉子節點都位於同一層。
5.每個節點中的元素從小到大排列,節點當中k-1個元素正好是k個孩子包含的元素的值域分劃。

一個3階的B樹插入示意圖如下:

應用場景:MongoDB

B+樹

一個m階的B+樹具有以下特徵:

1.有k個子樹的中間節點包含有k個元素(B樹中是k-1個元素),每個元素不儲存資料,只用來索引,所有資料都儲存在葉子節點。
2.所有的葉子節點中包含了全部元素的資訊,及指向含這些元素記錄的指標,且葉子節點本身依關鍵字的大小自小而大順序連結。
3.所有的中間節點元素都同時存在於子節點,在子節點元素中是最大(或最小)元素。

看不懂沒關係,我們只需要知道這些限制條件是為了讓B+樹資料“矮而胖”就好。

這裡我直接放張掘金小冊《從根兒上理解MYSQL》B+樹主鍵索引的示意圖:

應用場景
1.Mysql InnoDB儲存引擎。

看到這裡常考面試題來了:B樹和B+樹有什麼區別?為什麼Mongo用B樹?為什麼Mysql用B+樹?

B樹 vs B+樹

看圖說話,B樹 和 B+樹顯著不同的地方是:
1.B樹非葉子節點即是索引,也是資料;B+樹非葉子節點僅是索引,資料全部儲存在葉子節點。
2.B+樹葉子節點的資料之間是用連結串列連結的。
這會導致:

B+樹相比B樹:
1.資料的連續性:

  • B+樹葉子節點上一頁儲存的資料是連續的,當需要一個結點上的資料時,B+樹可以增大快取的命中率。

2.葉子結點之間的連線性:

  • 當作範圍或全文掃描時,B+樹可以依賴葉子結點做線性順序掃描,而B樹只能在每一層的結點上做掃描。B+樹同樣可以增大快取的命中率。

B樹相比B+樹:
當作單一資料查詢時,B樹的結點平均離根結點更近,平均查詢效率比B+樹快。

總結一下:B+樹相比B樹,前者更適合範圍查詢,後者更適合單一資料查詢。

Mongo是非關係型資料庫,資料之間的關係用巢狀解決。它的主要使用場景是: 追求 單個讀寫記錄的效能。

Mysql是關係型資料庫,資料之間的關係用共同的索引鍵,Join解決。它的使用場景:不僅存在大量的單一資料查詢,也存在大量的範圍查詢。

所以上面的問題可以簡單扯一扯了吧。

下篇文章233醬準備介紹近幾年資料庫中經常出現的角色:LSM Tree。覺得本文有收穫 or 期待下篇 請四連【關注,點贊,在看,轉發】支援下233吧~

參考資料:
[1].維基百科
[2].https://www.youtube.com/watch?v=OJ5NYq1Eii8
[3].https://time.geekbang.org/column/article/72414
[4].https://towardsdatascience.com/8-useful-tree-data-structures-worth-knowing-8532c7231e8c
[5].https://draveness.me/whys-the-design-mongodb-b-tree/

相關文章