五、紅黑樹
2-3查找樹能保證在插入元素之後,樹依然保持平衡狀態,它的最壞情況下所有子結點都是2-結點,樹的高度爲lgN,相比普通的二叉查找樹保證了最壞情況下的時間複雜度。紅黑樹是2-3樹思想的簡單實現
紅黑樹主要是對2-3樹進行編碼,紅黑樹背後的基本思想是(標準的二叉查找樹)和一些額外的信息替換3-結點來表示2-3樹。所以將樹中的鏈接分爲兩種類型:
紅鏈接:將兩個2-結點鏈接起來構成一個3-結點(紅鏈接只能是左斜鏈接)
黑鏈接:是2-3樹中的普通鏈接
紅黑樹定義:
- 紅鏈接均爲左鏈接
- 沒有任何一個結點同時和兩條紅鏈接相連
- 紅黑樹是黑色平衡的,即任意空鏈接到根結點的路徑上黑鏈接數量相同
- 將紅黑樹水平繪製就變成你熟悉的2-3樹了
*
1、紅黑樹的平衡化
對紅黑樹進行一些增刪改操作後,很有可能會出現紅色的右鏈接或者兩條連着的紅鏈接。這些不滿足紅黑樹的定義,所以我們需要通過旋轉進行修復,讓紅黑樹保持平衡。
左旋:
左旋時機:當某個結點的左子結點爲黑色,右子結點爲紅色,此時需要左旋
左旋過程:
- E.right = S.left
- S.left = E
- E.color = S.color
- S.color = false (變爲黑色)
右旋:
當某個結點的左子結點爲紅色,且左子結點的左子結點也是紅色。需要右旋
(tips:右旋相當於把原來的臨時4-結點變爲三個2-結點,提升中間結點熟悉吧)
優選過程:
- S.left = E.right
- E.right = S
- S.color = E.color
- E.color = false
這仍是有問題的,因爲右側不允許出現紅鏈接。所以後續會處理
2、紅黑樹插入算法
向單個2-結點中插入新鍵
一棵只含有一個鍵的紅黑樹只含有一個2-結點,插入另一個鍵後,我們需要馬上旋轉
- 如果新插入的結點小於當前結點,則新增一個紅色結點即可
- 如果新城結點大於當前結點,會出現右紅鏈接,則需要將樹左旋,保證紅黑樹的特性
向底部的2-結點插入新鍵
用二叉查找樹的方式向紅黑樹中插入一個新鍵,會在樹的底部增加一個結點(保證有序性),區別在於:我們用紅鏈接將其與其父結點相連
- 插入C,小於E,進入左子樹
- C>A,放入A的右子結點
- 加上紅鏈接(在右側,需要左旋)
- 左旋
顏色反轉
當一個結點的左子結點和右子結點的color都爲紅色時,也就是出現了臨時4-結點,此時需要把左子結點和右子結點的color變爲black,同時讓當前結點的顏色變爲紅色
(tips:相當於將4-結點的中間元素提升與其父結點組成3-結點)
向一棵雙鍵樹(即一個3-結點)中插入新鍵
可以有三種情況:
-
新鍵>原樹中的兩個鍵
-
插入c,c>b && c>a 所以c放入b的右子結點
-
與父結點建立紅鏈接,形成臨時4-結點
-
顏色反轉(如果b爲整個樹根結點則將b.color = false)
-
新鍵>原樹中一個鍵
- 插入b,假如a<b<c,進入a的右子結點
- 與a建立紅鏈接(在右側,需要左旋)
- 左旋後形成左邊連續兩個紅鏈接,需要右旋
- 提升b,下沉c
- 顏色反轉
-
新鍵不大於原樹中的兩個鍵
- 插入a,a<b<c,所以a放在b的左子結點,建立紅鏈接
- 左邊連續兩個紅鏈接,需要右旋
- 提升b,顏色反轉
向樹底部的3-結點插入新鍵
分析那麼多步驟:跟着也分析一波唄
-
出現兩個連續左紅鏈接,需要右旋,提升R,下沉S
-
顏色反轉,E-R之間出現紅鏈接
-
右側出現紅鏈接,需要左旋
大功告成,看到這你會發現也不難其實。
六、手搓紅黑樹
又到手搓環節,激動嘛,跟着寫一遍試試唄,吹爆面試官
package TreeTest;
public class RedBlackTree<Key extends Comparable<Key>,Value> {
//定義樹的根結點
private Node root;
//記錄樹中元素個數
private int N;
//紅色鏈接
private static final boolean RED = true;
private static final boolean BLACK = false;
//左旋轉
private Node rotateLeft(Node h){
//找出當前結點的右子結點
Node x = h.right;
//將x的左子結點放到h的右子結點處
h.right = x.left;
//將x提升,把h作爲x的左子結點
x.left = h;
h.color = x.color;
x.color = BLACK;
return x;
}
//判斷當前結點的color是否爲RED
private boolean isRed(Node h){
if(h==null){
return false;
}
return h.color==RED;
}
//右旋轉
private Node rotateRight(Node h){
//找到當前結點的左子結點
Node x = h.left;
//將x的右子結點掛到h的左子結點
h.left = x.right;
x.right = h;
return x;
}
//顏色反轉
private void flipColors(Node h){
h.color = RED;
h.left.color = BLACK;
h.right.color = BLACK;
}
//在整棵樹中插入結點
public void put(Key key,Value value){
root = put(root,key,value);
//根結點顏色置爲BLACK
root.color = BLACK;
}
private Node put(Node h,Key key,Value value){
//如果知道不存在結點的位置,就可以將新增結點放入位置
if(h == null){
N++;
return new Node(key,value,null,null,RED);
}
//比較要插入結點與當前結點大小
//要插入的結點大於當前結點的key
int cmp = key.compareTo(h.key);
if (cmp>0){
h.right = put(h.right,key,value);
}else if (cmp<0){
//要插入的結點小於當前結點的key
h.left = put(h.left,key,value);
}else{
//要插入結點的key與當前結點的key相同,替換當前位置的value
h.value = value;
}
//判斷是否需要左旋
//左旋條件:右側出現紅鏈接
if(isRed(h.right)){
//參數爲h的原因是:要根據當前結點尋找不合規則的左右結點
h = rotateLeft(h);
}
//判斷是否需要右旋
//右旋條件:左側連續兩個紅鏈接
if(isRed(h.left) && isRed(h.left.left)){
h = rotateRight(h);
}
//顏色反轉
if(isRed(h.left) && isRed(h.right)){
flipColors(h);
}
return h;
}
//根據key,從樹中找到對應的value
public Value get(Key key){
return get(root,key);
}
private Value get(Node h,Key key){
if(h == null){
//沒有要找的結點
return null;
}
//判斷該節點的key值該去左子樹還是右子樹
int cmp = key.compareTo(h.key);
if(cmp > 0){
return get(h.right,key);
}else if (cmp < 0){
return get(h.left,key);
}else{
return h.value;
}
}
//獲取樹中元素個數
public int size(){
return N;
}
//結點結構
private class Node {
private Key key;
private Value value;
//該結點指向其父結點的鏈接顏色
public boolean color;
//記錄左右子結點
private Node left;
private Node right;
public Node(Key key,Value value,Node left,Node right,boolean color){
this.key = key;
this.value = value;
this.left = left;
this.right = right;
this.color = color;
}
}
public static void main(String[] args) {
RedBlackTree<Integer, String> bt = new RedBlackTree<>();
bt.put(4, "二哈");
bt.put(1, "張三");
bt.put(3, "李四");
bt.put(5, "王五");
System.out.println(bt.size());
System.out.println(bt.get(1));
bt.put(1,"老三");
System.out.println(bt.get(1));
System.out.println(bt.size());
}
}
七、B樹與B+樹
B樹:
B樹中允許一個結點中包含多個Key,可以是3個、4個甚至更多。我們可以選取一個參數M,來構造一個B樹(其實就是一個n-結點的樹)
- 每個結點最多有M-1個Key,並且以升序排序
- 每個結點最多有M個子結點
- 根結點至少要有兩個子結點
B樹的應用:
B樹在磁盤IO中使用
因爲磁盤的訪問速度較慢,所以訪問時一般都會讀取一段空間內的所有數據保存到內存中,利用局部性原理提高數據的查詢效率。現在計算機一般採用頁作爲管理存儲器的邏輯塊,文件系統的設計者利用磁盤預讀原理,將一個結點的大小設置爲等於一個頁,這樣每個結點只需要一次I/O就可以完全載入。這樣三層的B樹就可以容納102410141024差不多10億個數據。大大提高了IO效率
B+樹:
B+樹顧名思義就是B樹的變形,與B樹的差異在於
- 非葉結點僅具有索引作用,也就是說,非葉結點只存儲Key,不存儲Value
- 樹的所有葉子結點構成一個有序鏈表,可以按照Key排序的次序遍歷全部數據
可以看出上面的樹中非葉結點都是索引,僅用於查找數據,並不存儲真實的數據
真實的數據全部放在葉子結點並且橫向連接成爲一個有序的鏈表
B樹與B+樹的對比
B樹優點:
由於B樹中的每一個結點都包含Key和Value。因此我們根據Key查找Value時,只需要找到Key所在的位置,就能夠找到數據。但是B+樹需要根據索引一直找到樹的最大深度處,纔可以找到Value
B+樹優點:
由於B+樹在非葉結點不存儲數據,只作爲索引,因此在內存相同的情況下,可以存放更多的Key。因爲整個樹的葉子結點都是相連的,所以對整棵樹的遍歷只需要一次線性遍歷葉子結點即可,並且數據有序,方便區間查找和搜索。而bB樹則需要不斷遞歸
B+樹的應用:
B+樹在數據庫中的索引上使用。
因爲數據庫較爲重視查詢效率。那麼我們可以基於某張表的某個字段建立索引,提高查詢效率。
如果未建立索引,那麼查詢一條數據的話就需要從上往下一條一條查找,查找的時間複雜度就爲O(n),在數據多時將會嚴重拖慢效率
如果建立主鍵索引,我們就可以很方便的去左右子樹尋找到數據
並且由於B+樹葉子結點形成有序鏈表,在進行區間查詢時只要找到索引的區間就可以很容易的進行區間查詢