1. 程式人生 > 其它 >有序表及其應用

有序表及其應用

當我們學習了各種各樣的資料結構之後,就會發現它們最終都只有一個目的:提高資料的查詢效率!

當我們以順序表或者連結串列組織資料的時候,查詢一個數據需要O(n)的時間複雜度。可當資料是海量的時候,O(n)的時間複雜度可吃不消。於是一些牛人就發現:如果將資料有序的組織起來,查詢一個數據的時候可以做到O(logn)的時間複雜度。沒錯,這就是基於有序表的二分查詢。

可是,如果資料用連結串列形式組織起來,查詢只能從頭到尾。於是基於鏈式的二分查詢的各種資料結構應運而生:二叉搜尋樹(BST)、平衡二叉樹(AVL)、B樹、B+樹、紅黑樹、跳錶等,這些都可以稱為有序表。

紅黑樹

由於二叉搜尋樹可能有退化成單鏈表的可能,所以出現了平衡二叉樹(AVL)。每當插入一個數據的時候,通過不斷調整樹的結構,使得樹的左右子樹層級不大於1,來保證這顆BST以平衡的姿態使得資料的查詢速度接近於二分法查詢的速度log(n),從而避免了BST退化成連結串列而降低查詢效率的可能。

可是正是因為這種完美的平衡反而使得它不完美,因為每次插入操作都可能會引起整顆樹通過不斷左旋、右旋操作使其達到完美平衡狀態,反而降低了效率。

所以紅黑樹就是一顆不完美的平衡二叉樹,通過降低一定的平衡,避免每次操作都帶來大量的調整,來提高效率。那麼它是怎麼實現的呢?

性質

一顆紅黑樹必須滿足:

  1. 節點要麼黑,要麼紅。
  2. 根結點是黑色的。
  3. 每個葉子節點nil是黑色的(個人覺得有沒有這個nil葉子節點不影響,只是為了滿足性質4而想象出來的)
  4. 每個紅色節點的兩個子節點一定是黑色的。
  5. 任意一個節點到每一個節點的路徑都包含數量相同的黑節點

紅黑樹的自平衡除了左旋和右旋之外,還有一個變色,即紅變黑,或黑變紅,來滿足性質5。所以完美平衡二叉樹的平衡依據是平衡因子,而紅黑樹平衡的依據是性質5,所以也稱紅黑樹為黑色完美平衡

查詢

紅黑樹的查詢操作和AVL樹一樣,時間複雜度也為O(logn),主要的區別就是插入操作,多了一步變色。

插入

首先待插入的節點初始時候都是紅色。理由很簡單,紅色在父結點(如果存在)為黑色結點時,紅黑樹的黑色平衡沒被破壞,不需要做自平衡操作。但如果插入結點是黑色,那麼插入位置所在的子樹黑色結點總是多1,必須做自平衡。

然後查詢插入位置插入,再根據不同的情景來自旋,變色來保持平衡。

  • 當紅黑樹為空樹,直接插入,節點設為黑色。

  • 當插入節點key已經存在,只需要把節點的值更新即可。

  • 插入節點的父節點為黑色時候,直接插入。

  • 插入節點的父節點為紅色時,就需要變色了,因為性質4。

    1. 當叔叔節點存在並且為紅節點時

      黑紅紅變為紅黑紅,之後把pp作為新的插入節點去不斷向上調整,如果pp剛好為根節點,那麼需要把它重新變為黑色,黑色節點變增加了。這也是唯一一種會增加紅黑樹黑色節點層數的插入情景。

    2. 叔叔節點不存在或為黑色節點,並且插入節點的父親節點是祖父節點的左子節點

      插入節點是父節點的左子節點

      插入節點是右子節點

    3. 叔叔節點不存在或為黑色節點,並且插入節點的父親節點是祖父節點的右子節點,這種和上面一樣,方向變了而已。

刪除

刪除是紅黑樹最複雜的操作,過程還是兩步:查詢目標節點,刪除後自平衡。

二叉搜尋樹的刪除一共分三步:

  • 若刪除結點無子結點,直接刪除
  • 若刪除結點只有一個子結點,用子結點替換刪除結點
  • 若刪除結點有兩個子結點,用後繼結點(大於刪除結點的最小結點)替換刪除結點,接著遞迴刪除後繼節點。(由於不斷用後繼節點替換當前節點,對於樹來說,真正發生刪除的操作總是發生在樹末,即前兩種情況)

平衡二叉樹和紅黑樹作為特殊的BST也遵循這三步,無非就是刪除後需要調整樹的結構來達到平衡狀態,這裡不再深入。

很多程式語言中的有序表底層結構都是紅黑樹,比如C++中的ordered_map,ordered_set等。

跳錶(Skip List)

跳錶插入、刪除、查詢元素的時間複雜度跟紅黑樹都是一樣量級的,時間複雜度都是O(logn)

我們知道單鏈表的查詢操作只能從頭到尾,而無法通過二分查詢來達到logn級別的查詢效率,而跳錶就是通過在單鏈表上加索引使得連結串列能夠實現二分查詢。

如果每兩個元素建立一個索引節點,(只儲存key和幾個指標,不需要儲存完整的物件)這樣的查詢過程就和二分查詢一樣了,時間複雜度為O(logn)。

所以跳錶就是通過建立索引來提高查詢效率的,典型的“空間換時間”。空間複雜度為O(n):n/2+n/4+...+2=n-2。

如果每三個節點抽建立一個索引節點,可以減少空間複雜度,但查詢效率也會有一定的下降,所以可以根據不同場景來調整這個閾值。

插入

通過查詢找到待插入資料在原始連結串列中的位置的時候,插入即可。但是如果一直往原始連結串列插入資料而不更新索引的話,極端情況下就會使跳錶退化為單鏈表,所以需要對索引進行維護。

首先需要明白,當資料量足夠大的時候,我們在原始連結串列中隨機的選 n/2 個元素做為一級索引是不是也能通過索引提高查詢的效率。雖然不是每隔一個元素抽取一個索引節點,但對於查詢效率來說,影響不大,尤其是資料量不夠大且抽取足夠隨機的時候。

所以我們維護這樣一個索引:隨機選 n/2 個元素做為一級索引、隨機選 n/4 個元素做為二級索引、隨機選 n/8 個元素做為三級索引,依次類推,一直到最頂層索引

實現過程:

可以在每次新插入元素的時候,儘量讓該元素有 1/2 的機率建立一級索引、1/4 的機率建立二級索引、1/8 的機率建立三級索引,以此類推,就能滿足我們上面的條件。

當每次有資料插入的時候,先通過概率演算法告訴我們這個元素需要插入到幾級索引中,然後開始維護索引並把資料插入到原始連結串列中。

randomLevel() 方法返回 1 表示當前插入的該元素不需要建索引,只需要儲存資料到原始連結串列即可(概率 1/2)

randomLevel() 方法返回 2 表示當前插入的該元素需要建一級索引(概率 1/4)

randomLevel() 方法返回 3 表示當前插入的該元素需要建二級索引(概率 1/8)

randomLevel() 方法返回 4 表示當前插入的該元素需要建三級索引(概率 1/16)

。。。以此類推

既然返回2的時候是建立一級索引,為什麼概率是1/4呢?不是應該為1/2嘛?

因為當建立二級索引的時候,同時也會建立一級索引;當建立三級索引時,同時也會建立一級、二級索引。

加入此時需要建立二級索引,那麼它的概率為1-1/2-1/4=1/4。

整個維護索引的操作無非是在查詢的過程新增一個索引節點而已,每層插入的時間複雜度為O(1),所以整個插入操作時間複雜度為O(logn)。

刪除

刪除操作無非是在查詢的過程中順便刪掉每層索引的節點,時間複雜度也是O(logn)。

總結

  • 跳錶是可以實現二分查詢的有序連結串列;
  • 每個元素插入時隨機生成它的level;
  • 最底層包含所有的元素;
  • 如果一個元素出現在level(x),那麼它肯定出現在x以下的level中;
  • 每個索引節點包含兩個指標,一個向下,一個向右

Redis中的有序集合zset底層就是跳錶,為什麼不使用紅黑樹呢?

因為zset支援範圍查詢,按照區間查詢資料時,跳錶可以做到 O(logn) 的時間複雜度定位區間的起點,然後在原始連結串列中順序往後遍歷就可以了,非常高效。而紅黑樹底層是一顆二叉搜尋樹,它的有序性必須通過中序遍歷來實現,效率沒那麼高。

B樹(B-tree)

B樹和平衡二叉樹稍有不同的是B樹屬於多叉樹又名平衡多路查詢樹(查詢路徑不只兩個,即每個節點可以擁有更多的子節點,每個節點也可以含有多個關鍵字。

B-樹有如下特點:

  1. 所有鍵值分佈在整顆樹中(索引值和具體data都在每個節點裡);
  2. 任何一個關鍵字出現且只出現在一個結點中;
  3. 搜尋有可能在非葉子結點結束(最好情況O(1)就能找到資料);
  4. 在關鍵字全集內做一次查詢,效能逼近二分查詢;

B-樹是專門為外部儲存器設計的,如磁碟,它對於讀取和寫入大塊資料有良好的效能,所以一般被用在檔案系統及資料庫中。

傳統用來搜尋的平衡二叉樹有很多,如 AVL 樹,紅黑樹等。這些樹一般應用於載入到記憶體中的資料的查詢。而當資料量非常大時,記憶體不夠用,大部分資料只能存放在磁碟上,只有需要的資料才載入到記憶體中。一般而言記憶體訪問的時間約為 50 ns,而磁碟在 10 ms 左右。速度相差了近 5 個數量級,磁碟讀取時間遠遠超過了資料在記憶體中比較的時間。所以B樹通過降低樹的高度,來減少磁碟IO的數量。

查詢

多叉的好處很明顯,就是為了降低樹的高度,因為一次向下查詢要讀去一次磁碟IO,一般一顆B-樹的高度在3層左右。

B樹的每個節點,都是存多個值的,不像二叉樹那樣,一個節點就一個值,B樹把每個節點都給了一點的範圍區間,區間更多的情況下,搜尋也就更快了,比如:有1-100個數,二叉樹一次只能分兩個範圍,0-50和51-100,而B樹,分成4個範圍 1-25, 25-50,51-75,76-100一次就能篩選走四分之三的資料。所以作為多叉樹的B樹是更快的。

一般根節點是被載入到記憶體中的,查詢時先在節點內部做二分查詢,然後定位下一次要查詢的節點,進行一次磁碟IO,接該節點讀入記憶體,接著在記憶體中進行二分查詢,直到找到key。

B+樹

B+樹是B-樹的變體,也是一種多路搜尋樹, 它與 B- 樹的不同之處在於:

  1. 所有關鍵字儲存在葉子節點出現,內部節點(非葉子節點)並不儲存真正的 data
  2. 為所有葉子結點增加了一個鏈指標,使得B+樹可以順序訪問

因為內節點並不儲存 data,所以一般B+樹的葉節點和內節點大小不同,而B-樹的每個節點大小一般是相同的,為一頁。

查詢

B+樹內節點不儲存資料,所以它的查詢時間複雜度固定為O(logn),而B-樹不固定,最高位O(1)。

由於B+樹葉節點兩兩相連,所以可以使用範圍查詢,大大增加區間訪問性,B-樹不支援範圍查詢。

B+樹比B-樹更適合外部儲存,因為內節點無data域,每個節點可以索引的範圍更大更精確。

磁碟儲存的最小資料單元是扇區——512位元組

檔案系統最小單元是塊——4k,一個檔案大小為1位元組,但也不得不佔用磁碟上4KB的空間

InnoDB儲存引擎最小儲存單元是頁——16k(也可以通過引數設定),假設一行資料1k,一頁就可以儲存16行這樣的資料。

資料庫如何通過B+樹來組織資料的?

首先所有資料分別存放在不同的頁中,除了存放資料的頁,還有存放key+指標的索引頁,也就是B+樹中的內節點,這種頁稱為索引組織表。

查詢過程就是B+樹的查詢過程,我們通過這棵B+樹來查詢,首先找到根頁,你怎麼知道user表的根頁在哪呢?

其實每張表的根頁位置在表空間檔案中是固定的,即page number=3的頁

通常一顆B+樹可以存放多少行資料呢?

上文我們已經說明單個葉子節點(頁)中的記錄數=16K/1K=16。(這裡假設一行記錄的資料大小為1k,實際上現在很多網際網路業務資料記錄大小通常就是1K左右)。

那麼現在我們需要計算出非葉子節點能存放多少指標?

其實這也很好算,我們假設主鍵ID為bigint型別,長度為8位元組,而指標大小在InnoDB原始碼中設定為6位元組,這樣一共14位元組

我們一個頁中能存放多少這樣的單元,其實就代表有多少指標,即16384/14=1170。

根據同樣的原理我們可以算出一個高度為3的B+樹可以存放:1170 * 1170 * 16=21902400條這樣的記錄。

所以在InnoDB中B+樹高度一般為1-3層,它就能滿足千萬級的資料儲存,一般一張表對應一顆B+樹。

在查詢資料時一次頁的查詢代表一次IO,所以通過主鍵索引查詢通常只需要1-3次IO操作即可查詢到資料。

Mysql索引為什麼使用B+樹而不是其他樹?

因為B樹不管葉子節點還是非葉子節點,都會儲存資料,這樣導致在非葉子節點中能儲存的指標數量變少

指標少的情況下要儲存大量資料,只能增加樹的高度,導致IO操作變多,查詢效能變低;

字典樹

字典樹是一種空間換時間的資料結構,又稱Trie樹,字首樹,典型用於統計、排序、和儲存大量字串。所以經常被搜尋引擎系統用於文字詞頻統計。它的優點是:利用字串的公共字首來減少查詢時間,最大限度地減少無謂的字串比較,查詢效率比雜湊樹高。

性質

1:根節點不包含字元,除了根節點每個節點都只包含一個字元。root節點不含字元這樣做的目的是為了能夠包括所有字串。

2:從根節點到某一個節點,路過字串起來就是該節點對應的字串。

3:每個節點的子節點字元不同,也就是找到對應單詞、字元是唯一的。

參考連結:
https://www.jianshu.com/p/e136ec79235c

https://www.jianshu.com/p/9d8296562806

https://www.jianshu.com/p/ace3cd6526c4

https://mp.weixin.qq.com/s/NGjJzYGT64uiuwtsR3QKyQ

轉載請註明出處:https://www.cnblogs.com/yrxing/