這裏把各種樹做個總結,分別介紹各個樹是什麼,什麼原理,什麼特點,什麼情況下使用,另外很多時候它們很多地方是相似的,還要加以區別,之前我身邊一個很多年開發的經驗的老開發還以爲B樹、B-樹、B+樹是三種樹,實際沒有B-樹,它實際就是B樹,要是不區分清楚鬧出這樣的笑話就尷尬了。或者別人說“平衡樹”、“滿二叉樹”、“3階樹”等概念時你一臉懵逼,想吹牛逼但是沒詞兒,那也挺尷尬,怎麼辦,一點一點學吧,下面一 一介紹。
一、樹的基本術語
若一個結點有子樹,那麼該結點稱爲子樹根的"雙親",子樹的根是該結點的"孩子"。有相同雙親的結點互爲"兄弟"。一個結點的所有子樹上的任何結點都是該結點的後裔。從根結點到某個結點的路徑上的所有結點都是該結點的祖先。
結點的度:結點擁有的子樹的數目。
葉子:度爲零的結點(無子樹的結點)。
分支結點:度不爲零的結點。
樹的度:樹中結點的最大的度(下圖中樹的度即爲3)。
層次:根結點的層次爲1,其餘結點的層次等於該結點的雙親結點的層次加1。
樹的高度(樹的深度):樹中結點的最大層次。
無序樹:如果樹中結點的各子樹之間的次序是不重要的,可以交換位置。
有序樹:如果樹中結點的各子樹之間的次序是重要的, 不可以交換位置。
森林:0個或多個不相交的樹組成。對森林加上一個根,森林即成爲樹;刪去根,樹即成爲森林。
二、二叉樹
2.1 定義
二叉樹又叫二叉排序樹(Binary Sort Tree),“二叉”就是樹上的一根樹枝開兩個叉,而這棵樹上的節點是已經排好序的,具體的排序規則如下:
- 若左子樹不空,則左子樹上所有節點的值均小於它的根節點的值
- 若右子樹不空,則右字數上所有節點的值均大於它的根節點的值
- 它的左、右子樹也分別爲二叉排序數(遞歸定義)
上面的排序規則可以看出二叉樹的特點,如果我們要查找某個元素,它可以使我們具有和二分法等同的效率,每經過一個節點就可以減少一半的可能,可以使我們的查詢效率大幅提高。但是也會有比較極端的情況,那就是所有節點都位於同一側,直觀上看就是一條直線(這種樹也叫斜樹,如上右圖),這時查詢效率就和原來的順序查找一樣了,效率很低,於是就有了“平衡二叉樹”。
這裏“平衡”要重點解釋一下,說的是這棵樹的各個分支的高度是均勻的,它的左子樹和右子樹的高度之差絕對值小於1,這樣就不會出現一條支路特別長的情況。於是,在這樣的平衡樹中進行查找時,總共比較節點的次數不超過樹的高度,這就確保了查詢的效率(時間複雜度爲O(logn))。
二叉樹的應用:
- 哈夫曼編碼,來源於哈夫曼樹(給定n個權值作爲n個葉子結點,構造一棵二叉樹,若帶權路徑長度達到最小,稱這樣的二叉樹爲最優二叉樹,也稱爲赫夫曼樹(Huffman tree)。即帶權路徑長度最短的樹),在數據壓縮上有重要應用,提高了傳輸的有效性,詳見《信息論與編碼》。
- 海量數據併發查詢,二叉樹複雜度是O(K+LgN)。二叉排序樹就既有鏈表的好處,也有數組的好處, 在處理大批量的動態的數據是比較有用。
2.2 滿二叉樹
這裏還有幾個常見的概念:
“滿二叉樹”:在一棵二叉樹中如果所有分支結點都存在左子樹和右子樹,且所有葉子都在同一層上,這樣的二叉樹稱爲滿二叉樹。
滿二叉樹的特點有:
- 葉子只能出現在最下一層。出現在其它層就不可能達成平衡。
- 非葉子結點的度一定是2。
- 在同樣深度的二叉樹中,滿二叉樹的結點個數最多,葉子數最多。
2.3 完全二叉樹
“完全二叉樹”:一棵二叉樹中,只有最下面兩層結點的度可以小於2,並且最下一層的葉結點集中在靠左的若干位置上。這樣的二叉樹稱爲完全二叉樹。
葉子結點只能出現在最下層和次下層,且最下層的葉子結點集中在樹的左部。顯然,一棵滿二叉樹必定是一棵完全二叉樹,而完全二叉樹未必是滿二叉樹
2.4 二叉樹的存儲
二叉樹有兩種存儲方式:順序存儲和鏈式存儲
2.4.1 順序存儲
二叉樹的順序存儲結構就是使用一維數組存儲二叉樹中的結點,並且結點的存儲位置,就是數組的下標索引。
上圖所示的一棵完全二叉樹採用順序存儲方式,可以這樣表示:
同理,看下下面的右斜樹:
順序表示爲:
其中,∧表示數組中此位置沒有存儲結點。此時可以發現,順序存儲結構中會有空間浪費的情況。因此順序存儲結構一般適用於完全二叉樹。
2.4.2 鏈式存儲
既然順序存儲不能滿足二叉樹的存儲需求,那麼考慮採用鏈式存儲。節點表示成:
樹可以表示爲:
二叉鏈表結構靈活,操作方便,對於一般情況的二叉樹,甚至比順序存儲結構還節省空間。因此,二叉鏈表是最常用的二叉樹存儲方式。
2.5 二叉樹的遍歷
二叉樹有三種遍歷方式:前序/中序/後續遍歷
2.5.1 前(先)序遍歷
所謂的先序遍歷就是先訪問根節點,再訪問左節點,最後訪問右節點。若二叉樹爲空,則退出。
上面的完全二叉樹的前序遍歷順序就是:A、B、D、H、I、E、J、C、F、G
2.5.2 中序遍歷
所謂的中序遍歷就是先訪問左節點,再訪問根節點,最後訪問右節點。若二叉樹爲空,則退出。
同樣的完全二叉樹的中序遍歷順序就是:H、D、I、B、J、E、A、F、C、G
2.5.3 後序遍歷
所謂的後序遍歷就是先訪問左節點,再訪問右節點,最後訪問根節點。若二叉樹爲空,則退出。
同樣的完全二叉樹的中序遍歷順序就是:H、I、D、J、E、B、F、G、C、A
2.6 代碼實例
上面概念、原理說了一大通了,該是動手用代碼實現一下的時候了,Talk is cheap, show me the code。
package tree.binary;
import java.util.LinkedList;
import java.util.List;
/**
* @Author: GeFeng
* @Date: 2020年6月10日10:10:44
* @Description: 二叉樹節點
*/
public class BinaryTreeDemo {
public static void main(String[] args) {
BinaryTree bt = new BinaryTree();
bt.addNode(6);
bt.addNode(4);
bt.addNode(8);
bt.addNode(1);
bt.addNode(11);
bt.addNode(2);
bt.addNode(7);
System.out.println("【前序:】");
preOrder(bt.root);
System.out.println("【中序:】");
midOrder(bt.root);
System.out.println("【後序:】");
posOrder(bt.root);
}
/**
* 新建二叉樹
*/
public static class BinaryTree {
BinaryTreeNode root;
public void addNode(int value) {
root = addNode(root, value);
}
private BinaryTreeNode addNode(BinaryTreeNode current, int value) {
if (current == null) {
return new BinaryTreeNode(value);
}
if (value < current.data) {
current.leftChild = addNode(current.leftChild, value);
} else if (value > current.data) {
current.rightChild = addNode(current.rightChild, value);
} else {
return current;
}
return current;
}
}
/**
* 前序遍歷 根-> 左-> 右
* 遞歸
*/
public static void preOrder(BinaryTreeNode Node)
{
if (Node != null)
{
System.out.print(Node.getData() + " ");
preOrder(Node.getLeftChild());
preOrder(Node.getRightChild());
}
}
/**
* 中序遍歷 左-> 根-> 右
* 遞歸
*/
public static void midOrder(BinaryTreeNode Node)
{
if (Node != null)
{
midOrder(Node.getLeftChild());
System.out.print(Node.getData() + " ");
midOrder(Node.getRightChild());
}
}
/**
* 後序遍歷 左-> 右-> 根
* 遞歸
*/
public static void posOrder(BinaryTreeNode Node)
{
if (Node != null)
{
posOrder(Node.getLeftChild());
posOrder(Node.getRightChild());
System.out.print(Node.getData() + " ");
}
}
}
結果:
【前序: 根-> 左-> 右】
6 4 1 2 8 7 11
【中序: 左-> 根-> 右】
1 2 4 6 7 8 11
【後序: 左-> 右-> 根】
2 1 4 7 11 8 6
2.7 平衡二叉樹
爲什麼最後一小節來介紹平衡二叉樹呢?一是平衡二叉樹是前面普通二叉樹的升級版,放在最後做個昇華;二是下面要介紹的紅黑樹和平衡二叉樹有很多相似的地方,因此在這裏介紹下,希望能承上啓下。平衡二叉樹是啥樣的?見下圖:
可見平衡二叉樹的特點:
- 從任何一個節點出發,左右子樹深度之差的絕對值不超過1。
- 左右子樹仍然爲平衡二叉樹。
前面我們介紹了“左斜樹”、“右斜樹”,會造成查詢效率低下的問題,一顆二叉查找樹的優勢完全喪失了。怎麼辦呢?既然上面的二叉查找樹在插入的時候變成了“一條腿”,也就是喪失了平衡,那我們乾脆做出一點改進,讓它平衡一下。比如上面的平衡二叉樹中我們要再插入一個4,按照普通的二叉樹規則會出現下面的情況:
若按照平衡二叉樹的要求,則會調整數的結構,使得整體滿足平衡二叉樹的規則:
平衡二叉樹是高度平衡的,優勢就是能夠保持高效的查詢效率;但是在插入和刪除節點時因爲要動態維護平衡,也會影響性能。
三、紅黑樹 —— RBTree
3.1 定義
上面說了平衡二叉樹大量插入和刪除節點的場景下,平衡二叉樹爲了保持平衡需要調整的頻率會更高,性能會受到影響,這時紅黑樹成了首選。
紅黑樹其實就是一種數據結構,設計它的目的就是爲了高效地進行增刪改查,紅黑樹放棄了追求完全平衡,追求大致平衡,在與平衡二叉樹的時間複雜度相差不大的情況下,保證每次插入最多隻需要三次旋轉就能達到平衡,實現起來也更爲簡單,而平衡二叉樹追求絕對平衡,條件比較苛刻,實現起來比較麻煩,每次插入新節點之後需要旋轉的次數不能預知。
所以在大量查找的情況下,平衡二叉樹的效率更高,也是首要選擇。在大量增刪的情況下,紅黑樹是首選。
那到底啥是紅黑樹?看下圖:
特性:
- 每個節點只有兩種顏色:紅色和黑色;
- 根節點是黑色的;
- 從根節點到葉子節點,不會出現兩個連續的紅色節點;
- 葉子節點都爲黑色,且爲 null;
- 從任何一個節點出發,到葉子節點,這條路徑上都有相同數目的黑色節點。
因此不能就直接說紅黑樹不追求平衡,紅黑樹和平衡二叉樹(AVL樹)都是二叉查找樹的變體,但紅黑樹的統計性能要好於AVL樹。因爲,AVL樹是嚴格維持平衡的,紅黑樹是黑平衡的。維持平衡需要額外的操作,這就加大了數據結構的時間複雜度,所以紅黑樹可以看作是二叉搜索樹和AVL樹的一個折中,維持平衡的同時也不需要花太多時間維護數據結構的性質。
3.2 紅黑樹中的操作
紅黑樹的基本操作和其他樹形結構一樣,一般都包括查找、插入、刪除等操作,不同的是因爲要符合紅黑樹規則而多了旋轉操作。旋轉操作有分爲左旋和右旋。
3.2.1 左旋
(盜的動態圖,完美!)
3.2.2 右旋
(還是盜的動態圖,完美!)
3.2.3 插入
紅黑樹的插入過程和二叉查找樹插入過程基本類似,不同的地方在於,紅黑樹插入新節點後,需要進行調整,以滿足紅黑樹的性質。
3.2.4 刪除
紅黑樹的插入過程和二叉查找樹插入過程基本類似,不同的地方在於,紅黑樹插入新節點後,需要進行調整,以滿足紅黑樹的性質。相較於插入操作,紅黑樹的刪除操作則要更爲複雜一些。刪除操作首先要確定待刪除節點有幾個孩子,如果有兩個孩子,不能直接刪除該節點。而是要先找到該節點的前驅(該節點左子樹中最大的節點)或者後繼(該節點右子樹中最小的節點),然後將前驅或者後繼的值複製到要刪除的節點中,最後再將前驅或後繼刪除。
3.3 紅黑樹的應用
- linux進程調度Completely Fair Scheduler,用紅黑樹管理進程控制塊
- 廣泛用在C++的STL中,map和set都是用紅黑樹實現的
- epoll在內核中的實現,用紅黑樹管理事件塊
- nginx中,用紅黑樹管理timer等
- Java的TreeMap、HashMap實現
上面可能比較陌生,不容易接觸到,可以重點看下JDK裏怎麼應用的紅黑樹,這裏重點介紹了HashMap中對紅黑樹的應用及實現:https://blog.csdn.net/weixin_41231928/article/details/103413167。
3.4 紅黑樹代碼實現
@Data
public class RBTreeNode {
private final boolean RED = false;
private final boolean BLACK = true;
private int key;
private boolean color;
private RBTreeNode left;
private RBTreeNode right;
private RBTreeNode parent;
}
@Data
public class RBTree {
RBTreeNode root;
private final boolean RED = false;
private final boolean BLACK = true;
public RBTreeNode query(int key) {
RBTreeNode tmp = root;
while (tmp != null) {
if (tmp.getKey() == key)
return tmp;
else if (tmp.getKey() > key)
tmp = tmp.getLeft();
else
tmp = tmp.getRight();
}
return null;
}
public void insert(int key) {
RBTreeNode node = new RBTreeNode(key);
if (root == null) {
root = node;
node.setColor(BLACK);
return;
}
RBTreeNode parent = root;
RBTreeNode son = null;
if (key <= parent.getKey()) {
son = parent.getLeft();
} else {
son = parent.getRight();
}
//find the position
while (son != null) {
parent = son;
if (key <= parent.getKey()) {
son = parent.getLeft();
} else {
son = parent.getRight();
}
}
if (key <= parent.getKey()) {
parent.setLeft(node);
} else {
parent.setRight(node);
}
node.setParent(parent);
//fix up
insertFix(node);
}
private void insertFix(RBTreeNode node) {
RBTreeNode father, grandFather;
while ((father = node.getParent()) != null && father.getColor() == RED) {
grandFather = father.getParent();
if (grandFather.getLeft() == father) { //F爲G左兒子的情況,如之前的分析
RBTreeNode uncle = grandFather.getRight();
if (uncle != null && uncle.getColor() == RED) {
setBlack(father);
setBlack(uncle);
setRed(grandFather);
node = grandFather;
continue;
}
if (node == father.getRight()) {
leftRotate(father);
RBTreeNode tmp = node;
node = father;
father = tmp;
}
setBlack(father);
setRed(grandFather);
rightRotate(grandFather);
} else { //F爲G的右兒子的情況,對稱操作
RBTreeNode uncle = grandFather.getLeft();
if (uncle != null && uncle.getColor() == RED) {
setBlack(father);
setBlack(uncle);
setRed(grandFather);
node = grandFather;
continue;
}
if (node == father.getLeft()) {
rightRotate(father);
RBTreeNode tmp = node;
node = father;
father = tmp;
}
setBlack(father);
setRed(grandFather);
leftRotate(grandFather);
}
}
setBlack(root);
}
public void delete(int key) {
delete(query(key));
}
private void delete(RBTreeNode node) {
if (node == null)
return;
if (node.getLeft() != null && node.getRight() != null) {
RBTreeNode replaceNode = node;
RBTreeNode tmp = node.getRight();
while (tmp != null) {
replaceNode = tmp;
tmp = tmp.getLeft();
}
int t = replaceNode.getKey();
replaceNode.setKey(node.getKey());
node.setKey(t);
delete(replaceNode);
return;
}
RBTreeNode replaceNode = null;
if (node.getLeft() != null)
replaceNode = node.getLeft();
else
replaceNode = node.getRight();
RBTreeNode parent = node.getParent();
if (parent == null) {
root = replaceNode;
if (replaceNode != null)
replaceNode.setParent(null);
} else {
if (replaceNode != null)
replaceNode.setParent(parent);
if (parent.getLeft() == node)
parent.setLeft(replaceNode);
else {
parent.setRight(replaceNode);
}
}
if (node.getColor() == BLACK)
removeFix(parent, replaceNode);
}
//多餘的顏色在node裏
private void removeFix(RBTreeNode father, RBTreeNode node) {
while ((node == null || node.getColor() == BLACK) && node != root) {
if (father.getLeft() == node) { //S爲P的左兒子的情況,如之前的分析
RBTreeNode brother = father.getRight();
if (brother != null && brother.getColor() == RED) {
setRed(father);
setBlack(brother);
leftRotate(father);
brother = father.getRight();
}
if (brother == null || (isBlack(brother.getLeft()) && isBlack(brother.getRight()))) {
setRed(brother);
node = father;
father = node.getParent();
continue;
}
if (isRed(brother.getLeft())) {
setBlack(brother.getLeft());
setRed(brother);
rightRotate(brother);
brother = brother.getParent();
}
brother.setColor(father.getColor());
setBlack(father);
setBlack(brother.getRight());
leftRotate(father);
node = root;//跳出循環
} else { //S爲P的右兒子的情況,對稱操作
RBTreeNode brother = father.getLeft();
if (brother != null && brother.getColor() == RED) {
setRed(father);
setBlack(brother);
rightRotate(father);
brother = father.getLeft();
}
if (brother == null || (isBlack(brother.getLeft()) && isBlack(brother.getRight()))) {
setRed(brother);
node = father;
father = node.getParent();
continue;
}
if (isRed(brother.getRight())) {
setBlack(brother.getRight());
setRed(brother);
leftRotate(brother);
brother = brother.getParent();
}
brother.setColor(father.getColor());
setBlack(father);
setBlack(brother.getLeft());
rightRotate(father);
node = root;//跳出循環
}
}
if (node != null)
node.setColor(BLACK);
}
private boolean isBlack(RBTreeNode node) {
if (node == null)
return true;
return node.getColor() == BLACK;
}
private boolean isRed(RBTreeNode node) {
if (node == null)
return false;
return node.getColor() == RED;
}
private void leftRotate(RBTreeNode node) {
RBTreeNode right = node.getRight();
RBTreeNode parent = node.getParent();
if (parent == null) {
root = right;
right.setParent(null);
} else {
if (parent.getLeft() != null && parent.getLeft() == node) {
parent.setLeft(right);
} else {
parent.setRight(right);
}
right.setParent(parent);
}
node.setParent(right);
node.setRight(right.getLeft());
if (right.getLeft() != null) {
right.getLeft().setParent(node);
}
right.setLeft(node);
}
private void rightRotate(RBTreeNode node) {
RBTreeNode left = node.getLeft();
RBTreeNode parent = node.getParent();
if (parent == null) {
root = left;
left.setParent(null);
} else {
if (parent.getLeft() != null && parent.getLeft() == node) {
parent.setLeft(left);
} else {
parent.setRight(left);
}
left.setParent(parent);
}
node.setParent(left);
node.setLeft(left.getRight());
if (left.getRight() != null) {
left.getRight().setParent(node);
}
left.setRight(node);
}
private void setBlack(RBTreeNode node) {
node.setColor(BLACK);
}
private void setRed(RBTreeNode node) {
node.setColor(RED);
}
private void inOrder(RBTreeNode node) {
if (node == null)
return;
inOrder(node.getLeft());
System.out.println(node);
inOrder(node.getRight());
}
}
四、B樹
首先就要說明白,“B樹”和“B-樹”是一個哈,B-樹不是一種新的樹,不要多想。
4.1 爲什麼要有B樹?
學習前首先問下爲啥需要B樹?已經有紅黑樹、二叉樹等一堆多樹了,又來個B樹幹啥?可以從下面幾個方面考慮一下:
計算機有一個局部性原理,就是說,當一個數據被用到時,其附近的數據也通常會馬上被使用。所以當你用紅黑樹的時候,你一次只能得到一個鍵值的信息,而用B樹,可以得到最多M-1個鍵值的信息。這樣來說B樹當然更好了。另外一方面,同樣的數據,紅黑樹的階數更大,B樹更短,這樣查找的時候當然B樹更具有優勢了,效率也就越高。
4.2 爲什麼要有B樹?
B樹事實上是一種平衡的多叉查找樹,也就是說最多可以開m個叉(m>=2),我們稱之爲m階b樹。
先盜個圖:
一個m階B樹應該具備下面的特徵:
- 根結點只有一個,分支數量範圍爲[2,m];
- 分支結點,每個結點包含分支數範圍爲[ceil(m/2), m];
- 所有的葉結點都在同一層上;
- 有 k 棵子樹的分支結點則存在
k-1
個關鍵碼,關鍵碼按照遞增次序進行排列; - 每個結點關鍵字的數量範圍爲[ceil(m/2)-1, m-1]
(m階指的是分叉的個數最多爲m個,即一個非葉子節點最多可以有m個子節點。ceil表示向上取整,ceil(2.5)=3),下面是一個五階B樹:
這是一棵5階的B樹,每個節點的分支數在【3,5】之間,同時除根節點,一般節點所擁有的分支數也不得少於3;每個節點至多擁有4個關鍵碼,除根節點外每個節點至少擁有2個關鍵碼,結點內的關鍵字是有序的。
4.3 B樹的查詢規則
在B-樹中查找給定關鍵字的方法是,首先把根結點取來,在根結點所包含的關鍵字K1,…,Kn查找給定的關鍵字(可用順序查找或二分查找法),若找到等於給定值的關鍵字,則查找成功;否則,一定可以確定要查找的關鍵字在Ki與Ki+1之間,Pi爲指向子樹根節點的指針,此時取指針Pi所指的結點繼續查找,直至找到,或指針Pi爲空時查找失敗。
4.4 B樹代碼實例
因爲B樹的特徵比較多,所以它的代碼比較複雜,首先建立節點內的Entry類:
/**
* B樹節點中的鍵值對
*/
public class Entry<K, V> {
private K key;
private V value;
public Entry(K k, V v)
{
this.key = k;
this.value = v;
}
public K getKey()
{
return key;
}
public V getValue()
{
return value;
}
public void setValue(V value)
{
this.value = value;
}
@Override
public String toString()
{
return key + ":" + value;
}
}
在建立B樹中的節點類(這裏的Node類似HashMap的結構,新增/查詢等也類似,可以參考):
package com.wo.domain.Btree;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
/**
* B樹中的節點。
*/
public class BTreeNode<K, V>
{
/** 節點的項,按鍵非降序存放 */
private List<Entry<K,V>> entrys;
/** 內節點的子節點 */
private List<BTreeNode<K, V>> children;
/** 是否爲葉子節點 */
private boolean leaf;
/** 鍵的比較函數對象 */
private Comparator<K> kComparator;
BTreeNode()
{
entrys = new ArrayList<Entry<K, V>>();
children = new ArrayList<BTreeNode<K, V>>();
leaf = false;
}
public BTreeNode(Comparator<K> kComparator)
{
this();
this.kComparator = kComparator;
}
public boolean isLeaf()
{
return leaf;
}
public void setLeaf(boolean leaf)
{
this.leaf = leaf;
}
/**
* 返回項的個數。如果是非葉子節點,根據B樹的定義,
* 該節點的子節點個數爲({@link #size()} + 1)。
* @return 關鍵字的個數
*/
public int size()
{
return entrys.size();
}
@SuppressWarnings("unchecked")
int compare(K key1, K key2)
{
return kComparator == null ? ((Comparable<K>)key1).compareTo(key2) : kComparator.compare(key1, key2);
}
/**
* 在節點中查找給定的鍵。
* 如果節點中存在給定的鍵,則返回一個SearchResult,
* 標識此次查找成功,給定的鍵在節點中的索引和給定的鍵關聯的值;
* 這是一個二分查找算法,可以保證時間複雜度爲O(log(t))。
* @param key - 給定的鍵值
* @return - 查找結果
*/
public SearchResult<V> searchKey(K key)
{
int low = 0;
int high = entrys.size() - 1;
int mid = 0;
while(low <= high)
{
mid = (low + high) / 2; // 先這麼寫吧,BTree實現中,l+h不可能溢出
Entry<K, V> entry = entrys.get(mid);
if(compare(entry.getKey(), key) == 0) // entrys.get(mid).getKey() == key
break;
else if(compare(entry.getKey(), key) > 0) // entrys.get(mid).getKey() > key
high = mid - 1;
else // entry.get(mid).getKey() < key
low = mid + 1;
}
boolean result = false;
int index = 0;
V value = null;
if(low <= high) // 說明查找成功
{
result = true;
index = mid; // index表示元素所在的位置
value = entrys.get(index).getValue();
}
else
{
result = false;
index = low; // index表示元素應該插入的位置
}
return new SearchResult<V>(result, index, value);
}
/**
* 將給定的項追加到節點的末尾,
* 你需要自己確保調用該方法之後,節點中的項還是
* 按照關鍵字以非降序存放。
* @param entry - 給定的項
*/
public void addEntry(Entry<K, V> entry)
{
entrys.add(entry);
}
/**
* 刪除給定索引的entry
* 你需要自己保證給定的索引是合法的。
* @param index - 給定的索引
*/
public Entry<K, V> removeEntry(int index)
{
return entrys.remove(index);
}
/**
* 得到節點中給定索引的項。
* 你需要自己保證給定的索引是合法的。
* @param index - 給定的索引
* @return 節點中給定索引的項
*/
public Entry<K, V> entryAt(int index)
{
return entrys.get(index);
}
/**
* 如果節點中存在給定的鍵,則更新其關聯的值。
* 否則插入。
* @param entry - 給定的項
* @return null,如果節點之前不存在給定的鍵,否則返回給定鍵之前關聯的值
*/
public V putEntry(Entry<K, V> entry)
{
SearchResult<V> result = searchKey(entry.getKey());
if(result.isExist())
{
V oldValue = entrys.get(result.getIndex()).getValue();
entrys.get(result.getIndex()).setValue(entry.getValue());
return oldValue;
}
else
{
insertEntry(entry, result.getIndex());
return null;
}
}
/**
* 在該節點中插入給定的項,
* 該方法保證插入之後,其鍵值還是以非降序存放。
* 不過該方法的時間複雜度爲O(t)。
* 注意:B樹中不允許鍵值重複。
* @param entry - 給定的鍵值
* @return true,如果插入成功,false,如果插入失敗
*/
public boolean insertEntry(Entry<K, V> entry)
{
SearchResult<V> result = searchKey(entry.getKey());
if(result.isExist())
return false;
else
{
insertEntry(entry, result.getIndex());
return true;
}
}
/**
* 在該節點中給定索引的位置插入給定的項,
* 你需要自己保證項插入了正確的位置。
* @param index - 給定的索引
*/
public void insertEntry(Entry<K, V> entry, int index)
{
/*
* 通過新建一個ArrayList來實現插入
* 要是有類似C中的reallocate就好了。
*/
List<Entry<K, V>> newEntrys = new ArrayList<Entry<K, V>>();
int i = 0;
// index = 0或者index = keys.size()都沒有問題
for(; i < index; ++ i)
newEntrys.add(entrys.get(i));
newEntrys.add(entry);
for(; i < entrys.size(); ++ i)
newEntrys.add(entrys.get(i));
entrys.clear();
entrys = newEntrys;
}
/**
* 返回節點中給定索引的子節點。
* 你需要自己保證給定的索引是合法的。
* @param index - 給定的索引
* @return 給定索引對應的子節點
*/
public BTreeNode<K, V> childAt(int index)
{
if(isLeaf())
throw new UnsupportedOperationException("Leaf node doesn't have children.");
return children.get(index);
}
/**
* 將給定的子節點追加到該節點的末尾。
* @param child - 給定的子節點
*/
public void addChild(BTreeNode<K, V> child)
{
children.add(child);
}
/**
* 刪除該節點中給定索引位置的子節點。
* 你需要自己保證給定的索引是合法的。
* @param index - 給定的索引
*/
public void removeChild(int index)
{
children.remove(index);
}
/**
* 將給定的子節點插入到該節點中給定索引
* 的位置。
* @param child - 給定的子節點
* @param index - 子節點帶插入的位置
*/
public void insertChild(BTreeNode<K, V> child, int index)
{
List<BTreeNode<K, V>> newChildren = new ArrayList<BTreeNode<K, V>>();
int i = 0;
for(; i < index; ++ i)
newChildren.add(children.get(i));
newChildren.add(child);
for(; i < children.size(); ++ i)
newChildren.add(children.get(i));
children = newChildren;
}
}
把查詢結果單獨保存:
/**
* 在B樹節點中搜索給定鍵值的返回結果。
* 該結果有兩部分組成。第一部分表示此次查找是否成功,
* 如果查找成功,第二部分表示給定鍵值在B樹節點中的位置,
* 如果查找失敗,第二部分表示給定鍵值應該插入的位置。
*/
public class SearchResult<V>
{
private boolean exist;
private int index;
private V value;
public SearchResult(boolean exist, int index)
{
this.exist = exist;
this.index = index;
}
public SearchResult(boolean exist, int index, V value)
{
this(exist, index);
this.value = value;
}
public boolean isExist()
{
return exist;
}
public int getIndex()
{
return index;
}
public V getValue()
{
return value;
}
}
下面進入真正的BTree類:
public class BTree<K, V>
{
private static Log logger = LogFactory.getLog(BTree.class);
private static final int DEFAULT_T = 2;
/** B樹的根節點 */
private BTreeNode<K, V> root;
/** 根據B樹的定義,B樹的每個非根節點的關鍵字數n滿足(t - 1) <= n <= (2t - 1) */
private int t = DEFAULT_T;
/** 非根節點中最小的鍵值數 */
private int minKeySize = t - 1;
/** 非根節點中最大的鍵值數 */
private int maxKeySize = 2*t - 1;
/** 鍵的比較函數對象 */
private Comparator<K> kComparator;
/**
* 構造一顆B樹,鍵值採用採用自然排序方式
*/
public BTree()
{
root = new BTreeNode<K, V>();
root.setLeaf(true);
}
public BTree(int t)
{
this();
this.t = t;
minKeySize = t - 1;
maxKeySize = 2*t - 1;
}
/**
* 以給定的鍵值比較函數對象構造一顆B樹。
*
* @param kComparator - 鍵值的比較函數對象
*/
public BTree(Comparator<K> kComparator)
{
root = new BTreeNode<K, V>(kComparator);
root.setLeaf(true);
this.kComparator = kComparator;
}
public BTree(Comparator<K> kComparator, int t)
{
this(kComparator);
this.t = t;
minKeySize = t - 1;
maxKeySize = 2*t - 1;
}
@SuppressWarnings("unchecked")
int compare(K key1, K key2)
{
return kComparator == null ? ((Comparable<K>)key1).compareTo(key2) : kComparator.compare(key1, key2);
}
/**
* 搜索給定的鍵。
*
* @param key - 給定的鍵值
* @return 鍵關聯的值,如果存在,否則null
*/
public V search(K key)
{
return search(root, key);
}
/**
* 在以給定節點爲根的子樹中,遞歸搜索
* 給定的<code>key</code>
*
* @param node - 子樹的根節點
* @param key - 給定的鍵值
* @return 鍵關聯的值,如果存在,否則null
*/
private V search(BTreeNode<K, V> node, K key)
{
SearchResult<V> result = node.searchKey(key);
if(result.isExist())
return result.getValue();
else
{
if(node.isLeaf())
return null;
else
search(node.childAt(result.getIndex()), key);
}
return null;
}
/**
* 分裂一個滿子節點<code>childNode</code>。
* <p/>
* 你需要自己保證給定的子節點是滿節點。
*
* @param parentNode - 父節點
* @param childNode - 滿子節點
* @param index - 滿子節點在父節點中的索引
*/
private void splitNode(BTreeNode<K, V> parentNode, BTreeNode<K, V> childNode, int index)
{
assert childNode.size() == maxKeySize;
BTreeNode<K, V> siblingNode = new BTreeNode<K, V>(kComparator);
siblingNode.setLeaf(childNode.isLeaf());
// 將滿子節點中索引爲[t, 2t - 2]的(t - 1)個項插入新的節點中
for(int i = 0; i < minKeySize; ++ i)
siblingNode.addEntry(childNode.entryAt(t + i));
// 提取滿子節點中的中間項,其索引爲(t - 1)
Entry<K, V> entry = childNode.entryAt(t - 1);
// 刪除滿子節點中索引爲[t - 1, 2t - 2]的t個項
for(int i = maxKeySize - 1; i >= t - 1; -- i)
childNode.removeEntry(i);
if(!childNode.isLeaf()) // 如果滿子節點不是葉節點,則還需要處理其子節點
{
// 將滿子節點中索引爲[t, 2t - 1]的t個子節點插入新的節點中
for(int i = 0; i < minKeySize + 1; ++ i)
siblingNode.addChild(childNode.childAt(t + i));
// 刪除滿子節點中索引爲[t, 2t - 1]的t個子節點
for(int i = maxKeySize; i >= t; -- i)
childNode.removeChild(i);
}
// 將entry插入父節點
parentNode.insertEntry(entry, index);
// 將新節點插入父節點
parentNode.insertChild(siblingNode, index + 1);
}
/**
* 在一個非滿節點中插入給定的項。
*
* @param node - 非滿節點
* @param entry - 給定的項
* @return true,如果B樹中不存在給定的項,否則false
*/
private boolean insertNotFull(BTreeNode<K, V> node, Entry<K, V> entry)
{
assert node.size() < maxKeySize;
if(node.isLeaf()) // 如果是葉子節點,直接插入
return node.insertEntry(entry);
else
{
/* 找到entry在給定節點應該插入的位置,那麼entry應該插入
* 該位置對應的子樹中
*/
SearchResult<V> result = node.searchKey(entry.getKey());
// 如果存在,則直接返回失敗
if(result.isExist())
return false;
BTreeNode<K, V> childNode = node.childAt(result.getIndex());
if(childNode.size() == 2*t - 1) // 如果子節點是滿節點
{
// 則先分裂
splitNode(node, childNode, result.getIndex());
/* 如果給定entry的鍵大於分裂之後新生成項的鍵,則需要插入該新項的右邊,
* 否則左邊。
*/
if(compare(entry.getKey(), node.entryAt(result.getIndex()).getKey()) > 0)
childNode = node.childAt(result.getIndex() + 1);
}
return insertNotFull(childNode, entry);
}
}
/**
* 在B樹中插入給定的鍵值對。
*
* @param key - 鍵
* @param value - 值
*/
public boolean insert(K key, V value)
{
if(root.size() == maxKeySize) // 如果根節點滿了,則B樹長高
{
BTreeNode<K, V> newRoot = new BTreeNode<K, V>(kComparator);
newRoot.setLeaf(false);
newRoot.addChild(root);
splitNode(newRoot, root, 0);
root = newRoot;
}
return insertNotFull(root, new Entry<K, V>(key, value));
}
/**
* 如果存在給定的鍵,則更新鍵關聯的值,
* 否則插入給定的項。
*
* @param node - 非滿節點
* @param entry - 給定的項
* @return true,如果B樹中不存在給定的項,否則false
*/
private V putNotFull(BTreeNode<K, V> node, Entry<K, V> entry)
{
assert node.size() < maxKeySize;
if(node.isLeaf()) // 如果是葉子節點,直接插入
return node.putEntry(entry);
else
{
/* 找到entry在給定節點應該插入的位置,那麼entry應該插入
* 該位置對應的子樹中
*/
SearchResult<V> result = node.searchKey(entry.getKey());
// 如果存在,則更新
if(result.isExist())
return node.putEntry(entry);
BTreeNode<K, V> childNode = node.childAt(result.getIndex());
if(childNode.size() == 2*t - 1) // 如果子節點是滿節點
{
// 則先分裂
splitNode(node, childNode, result.getIndex());
/* 如果給定entry的鍵大於分裂之後新生成項的鍵,則需要插入該新項的右邊,
* 否則左邊。
*/
if(compare(entry.getKey(), node.entryAt(result.getIndex()).getKey()) > 0)
childNode = node.childAt(result.getIndex() + 1);
}
return putNotFull(childNode, entry);
}
}
/**
* 如果B樹中存在給定的鍵,則更新值。
* 否則插入。
*
* @param key - 鍵
* @param value - 值
* @return 如果B樹中存在給定的鍵,則返回之前的值,否則null
*/
public V put(K key, V value)
{
if(root.size() == maxKeySize) // 如果根節點滿了,則B樹長高
{
BTreeNode<K, V> newRoot = new BTreeNode<K, V>(kComparator);
newRoot.setLeaf(false);
newRoot.addChild(root);
splitNode(newRoot, root, 0);
root = newRoot;
}
return putNotFull(root, new Entry<K, V>(key, value));
}
/**
* 從B樹中刪除一個與給定鍵關聯的項。
*
* @param key - 給定的鍵
* @return 如果B樹中存在給定鍵關聯的項,則返回刪除的項,否則null
*/
public Entry<K, V> delete(K key)
{
return delete(root, key);
}
/**
* 從以給定<code>node</code>爲根的子樹中刪除與給定鍵關聯的項。
* <p/>
* 刪除的實現思想請參考《算法導論》第二版的第18章。
*
* @param node - 給定的節點
* @param key - 給定的鍵
* @return 如果B樹中存在給定鍵關聯的項,則返回刪除的項,否則null
*/
private Entry<K, V> delete(BTreeNode<K, V> node, K key)
{
// 該過程需要保證,對非根節點執行刪除操作時,其關鍵字個數至少爲t。
assert node.size() >= t || node == root;
SearchResult<V> result = node.searchKey(key);
/*
* 因爲這是查找成功的情況,0 <= result.getIndex() <= (node.size() - 1),
* 因此(result.getIndex() + 1)不會溢出。
*/
if(result.isExist())
{
// 1.如果關鍵字在節點node中,並且是葉節點,則直接刪除。
if(node.isLeaf())
return node.removeEntry(result.getIndex());
else
{
// 2.a 如果節點node中前於key的子節點包含至少t個項
BTreeNode<K, V> leftChildNode = node.childAt(result.getIndex());
if(leftChildNode.size() >= t)
{
// 使用leftChildNode中的最後一個項代替node中需要刪除的項
node.removeEntry(result.getIndex());
node.insertEntry(leftChildNode.entryAt(leftChildNode.size() - 1), result.getIndex());
// 遞歸刪除左子節點中的最後一個項
return delete(leftChildNode, leftChildNode.entryAt(leftChildNode.size() - 1).getKey());
}
else
{
// 2.b 如果節點node中後於key的子節點包含至少t個關鍵字
BTreeNode<K, V> rightChildNode = node.childAt(result.getIndex() + 1);
if(rightChildNode.size() >= t)
{
// 使用rightChildNode中的第一個項代替node中需要刪除的項
node.removeEntry(result.getIndex());
node.insertEntry(rightChildNode.entryAt(0), result.getIndex());
// 遞歸刪除右子節點中的第一個項
return delete(rightChildNode, rightChildNode.entryAt(0).getKey());
}
else // 2.c 前於key和後於key的子節點都只包含t-1個項
{
Entry<K, V> deletedEntry = node.removeEntry(result.getIndex());
node.removeChild(result.getIndex() + 1);
// 將node中與key關聯的項和rightChildNode中的項合併進leftChildNode
leftChildNode.addEntry(deletedEntry);
for(int i = 0; i < rightChildNode.size(); ++ i)
leftChildNode.addEntry(rightChildNode.entryAt(i));
// 將rightChildNode中的子節點合併進leftChildNode,如果有的話
if(!rightChildNode.isLeaf())
{
for(int i = 0; i <= rightChildNode.size(); ++ i)
leftChildNode.addChild(rightChildNode.childAt(i));
}
return delete(leftChildNode, key);
}
}
}
}
else
{
/*
* 因爲這是查找失敗的情況,0 <= result.getIndex() <= node.size(),
* 因此(result.getIndex() + 1)會溢出。
*/
if(node.isLeaf()) // 如果關鍵字不在節點node中,並且是葉節點,則什麼都不做,因爲該關鍵字不在該B樹中
{
logger.info("The key: " + key + " isn't in this BTree.");
return null;
}
BTreeNode<K, V> childNode = node.childAt(result.getIndex());
if(childNode.size() >= t) // // 如果子節點有不少於t個項,則遞歸刪除
return delete(childNode, key);
else // 3
{
// 先查找右邊的兄弟節點
BTreeNode<K, V> siblingNode = null;
int siblingIndex = -1;
if(result.getIndex() < node.size()) // 存在右兄弟節點
{
if(node.childAt(result.getIndex() + 1).size() >= t)
{
siblingNode = node.childAt(result.getIndex() + 1);
siblingIndex = result.getIndex() + 1;
}
}
// 如果右邊的兄弟節點不符合條件,則試試左邊的兄弟節點
if(siblingNode == null)
{
if(result.getIndex() > 0) // 存在左兄弟節點
{
if(node.childAt(result.getIndex() - 1).size() >= t)
{
siblingNode = node.childAt(result.getIndex() - 1);
siblingIndex = result.getIndex() - 1;
}
}
}
// 3.a 有一個相鄰兄弟節點至少包含t個項
if(siblingNode != null)
{
if(siblingIndex < result.getIndex()) // 左兄弟節點滿足條件
{
childNode.insertEntry(node.entryAt(siblingIndex), 0);
node.removeEntry(siblingIndex);
node.insertEntry(siblingNode.entryAt(siblingNode.size() - 1), siblingIndex);
siblingNode.removeEntry(siblingNode.size() - 1);
// 將左兄弟節點的最後一個孩子移到childNode
if(!siblingNode.isLeaf())
{
childNode.insertChild(siblingNode.childAt(siblingNode.size()), 0);
siblingNode.removeChild(siblingNode.size());
}
}
else // 右兄弟節點滿足條件
{
childNode.insertEntry(node.entryAt(result.getIndex()), childNode.size() - 1);
node.removeEntry(result.getIndex());
node.insertEntry(siblingNode.entryAt(0), result.getIndex());
siblingNode.removeEntry(0);
// 將右兄弟節點的第一個孩子移到childNode
// childNode.insertChild(siblingNode.childAt(0), childNode.size() + 1);
if(!siblingNode.isLeaf())
{
childNode.addChild(siblingNode.childAt(0));
siblingNode.removeChild(0);
}
}
return delete(childNode, key);
}
else // 3.b 如果其相鄰左右節點都包含t-1個項
{
if(result.getIndex() < node.size()) // 存在右兄弟,直接在後面追加
{
BTreeNode<K, V> rightSiblingNode = node.childAt(result.getIndex() + 1);
childNode.addEntry(node.entryAt(result.getIndex()));
node.removeEntry(result.getIndex());
node.removeChild(result.getIndex() + 1);
for(int i = 0; i < rightSiblingNode.size(); ++ i)
childNode.addEntry(rightSiblingNode.entryAt(i));
if(!rightSiblingNode.isLeaf())
{
for(int i = 0; i <= rightSiblingNode.size(); ++ i)
childNode.addChild(rightSiblingNode.childAt(i));
}
}
else // 存在左節點,在前面插入
{
BTreeNode<K, V> leftSiblingNode = node.childAt(result.getIndex() - 1);
childNode.insertEntry(node.entryAt(result.getIndex() - 1), 0);
node.removeEntry(result.getIndex() - 1);
node.removeChild(result.getIndex() - 1);
for(int i = leftSiblingNode.size() - 1; i >= 0; -- i)
childNode.insertEntry(leftSiblingNode.entryAt(i), 0);
if(!leftSiblingNode.isLeaf())
{
for(int i = leftSiblingNode.size(); i >= 0; -- i)
childNode.insertChild(leftSiblingNode.childAt(i), 0);
}
}
// 如果node是root並且node不包含任何項了
if(node == root && node.size() == 0)
root = childNode;
return delete(childNode, key);
}
}
}
}
/**
* 一個簡單的層次遍歷B樹實現,用於輸出B樹。
*/
public void output()
{
Queue<BTreeNode<K, V>> queue = new LinkedList<BTreeNode<K, V>>();
queue.offer(root);
while(!queue.isEmpty())
{
BTreeNode<K, V> node = queue.poll();
for(int i = 0; i < node.size(); ++ i)
System.out.print(node.entryAt(i) + " ");
System.out.println();
if(!node.isLeaf())
{
for(int i = 0; i <= node.size(); ++ i)
queue.offer(node.childAt(i));
}
}
}
}
測試類:
public class Test {
public static void main(String[] args)
{
Random random = new Random();
BTree<Integer, Integer> btree = new BTree<Integer, Integer>(3);
List<Integer> save = new ArrayList<Integer>();
for(int i = 0; i < 10; ++ i)
{
int r = random.nextInt(100);
save.add(r);
System.out.println(r);
btree.insert(r, r);
}
System.out.println("----------------------");
btree.output();
System.out.println("----------------------");
btree.delete(save.get(0));
btree.output();
}
}
測試結果:
65
30
15
86
54
43
30
8
81
65
----------------------
54:54
8:8 15:15 30:30 43:43
65:65 81:81 86:86
----------------------
54:54
8:8 15:15 30:30 43:43
81:81 86:86
五、B+樹
5.1 什麼是B+樹?
B+樹是在B樹上的擴展,查詢性能更優秀。
一個m階的B+樹具有如下幾個特徵:
- 有k個子樹的中間節點包含有k個元素(B樹中是k-1個元素),每個元素不保存數據,只用來索引,所有數據都在葉子節點。
- 所有葉子結點中包含全部元素的信息,及指向含這些元素記錄的指針,且葉子結點本身依關鍵字的大小自小而大順序鏈接。
- 所有的中間節點元素都同時存在於子節點,在子節點元素中是最大(或最小)元素。
上面就是一個B+樹,首先父節點的元素都出現在了子節點中,且是子節點中最大(或最小)的元素,根節點中最大元素等同於整棵樹的最大元素,在刪除新增元素時這個都不會變,保證最大元素在根節點;因爲父節點的元素都出現在了子節點中,所以所有葉子節點包含了全部的元素信息。葉子節點是通過指針連接的,形成了有序鏈表,因此B+樹可以支持範圍查詢。
5.2 B+樹和B樹區別
- B+樹相同情況下比B樹需要的IO次數更少
- B+樹的查詢必須最終查詢到子節點,B樹只要找到元素即可,不管是不是在也是節點
- B+樹支持範圍查找,B樹是一次一次遍歷
(ps:還有一種B*樹的,B*樹是在B+樹基礎上,爲非葉子結點也增加鏈表指針)
六、其他
6.1 B樹和紅黑樹的區別
B樹與紅黑樹最大的不同在於,B樹的結點可以有許多子女,從幾個到幾千個。