當數據規模大到內存已不足以容納時, 常規的平衡二叉搜索樹的效率將大打折扣。其原因在於,查找過程對外存的訪問次數過多。
磁盤等外部存儲器的一個特點是: 讀取物理地址連續的一千個字節,與讀取單個字節所消耗的時間幾乎是沒有區別的。
此時,可以使用多路搜索樹。多路搜索樹可以由二叉搜索樹變換得到。如,
此時,搜索過程每下降一層,都以“大節點”爲單位從外存中讀取一組(而不再是一個)關鍵碼。更爲重要的是,這組關鍵碼在邏輯上與物理上都彼此相鄰,故可以批量方式從外存一次性讀出,且所需時間與讀取單個關鍵碼幾乎一樣。
B 樹
- 所謂 m 階 B-樹 , 即 m 路平衡搜索樹()。
- 所有外部節點的深度相等。
- 設一個內部節點存有 n 個關鍵碼, 以及用以指示對應分支的 n+1 個指針。除根以外的所有內部節點,都應滿足: ,而在非空的 B-樹中,根節點應滿足:
- 每個內部節點中的關鍵碼按順序排列。
- 由於各節點的分支數介於 至 之間,故m階B-樹也稱 -樹 。(關鍵碼數介於 至 之間)
B+ 樹 與 B 樹 的區別
- B+ 樹 的內部節點只存放關鍵碼,不存放具體的數據,所有數據都放在葉子節點。而 B 樹的每個節點都存放有數據。
- B+ 樹的葉子節點中存有一個指向下一個葉子節點的指針(關鍵碼小的指向關鍵碼大的)
- B+ 樹中同一個關鍵碼可以在不同節點中重複出現,而 B樹中任何一個關鍵碼只能出現在一個結點中。
樹定義
public class BTree<T extends Comparable<? super T>> {
// B 樹節點
private static class BNode<T> {
BNode<T> parent;
List<T> datas; // 存放的數據(含關鍵碼)
List<BNode<T>> children; // 指向孩子節點的指針
// 初始時有0個關鍵碼和1個空孩子指針
BNode() {
datas = new LinkedList<>();
children = new LinkedList<>();
children.add(null);
}
}
private int order; // B 樹的階次
private BNode<T> root; // 樹根
public BTree(int order) {
this.order = order;
root = new BNode<>();
}
// 在 datas 中查找 e 的位置,或 e 應該插入的位置
private int find(List<T> datas, T e) {
for (int i = 0; i < datas.size(); i++) {
// 等於:就是找到了
// 小於:就是沒有找到,但 e 應該插入此處
if (e.compareTo(datas.get(i)) <= 0) {
return i;
}
}
return datas.size();
}
...
}
搜索關鍵碼
對於活躍的B-樹,其根節點會常駐於內存; 此外,任何時刻通常只有另一節點(稱作當前節點)留駐於內存。
查找過程
B-樹的查找過程, 與二叉搜索樹的查找過程基本類似:
首先以根節點作爲當前節點,若在當前節點(所包含的一組關鍵碼)中能夠找到目標關鍵碼,則成功返回。否則,則必可在當前節點中確定某一個分支(“失敗”位置) ,並通過它轉至邏輯上處於下一層的另一節點。若該節點不是外部節點, 則將其載入內存,並更新爲當前節點,然後繼續重複上述過程。
效率
對於高度爲 h 的B-樹,外存訪問不超過 O(h - 1) 次。
存有 N 個關鍵碼的 m 階B-樹的高度 。
// 用於表示搜索結果(作爲內部類)
private static class SearchResult<T> {
BNode<T> target; // 目標節點
BNode<T> parent; // 目標節點之父
int index; // 目標數據在目標節點中的位置
// getter & setter ...
}
// 查找關鍵碼 e
public SearchResult<T> search(T e) {
SearchResult<T> result = new SearchResult<>();
BNode<T> current = root, parent = null;
int index = -1;
while (current != null) {
List<T> datas = current.datas;
// 在當前節點的關鍵碼中查找,要麼命中,要麼獲知下一步走哪個分支
index = find(datas, e);
// 找到了
if (index < datas.size() && e.compareTo(datas.get(index)) == 0) {
break;
}
// 沒有找到,進入下一層
parent = current;
current = current.children.get(index);
}
result.target = current;
result.parent = parent;
result.index = index;
return result;
}
插入關鍵碼
對於 m 介 B-樹,設關鍵碼插入的節點爲 v 。
若插入新的關鍵碼之後, v 的分支數等於 m+1,則違背了 B-樹的限定條件,此時需要分裂節點 v 。
分裂節點
此時,節點 v 恰包含有 m 個關鍵碼(即 m+1 個分支)。
取分裂點位置 ,則可將 v 的關鍵碼分爲 3部分:[0…s)、s、[s+1…) 。其中,[0…s) 部分作爲左節點、[s+1…) 部分作爲右節點,s 處的關鍵碼則被提升至父節點中。
至於,被提升的關鍵碼,有 3種可能的情況:
- 父節點未飽和,則將關鍵碼插到合適的位置即可;
- 父節點已飽和,強行插入關鍵碼之後,父節點也需要分裂;
- 若上溢傳遞至樹根,則可令被提升的關鍵碼自成一個節點, 並作爲新的樹根。
// 插入:不允許存在重複關鍵碼
public void insert(T e) {
// 先確定關鍵碼是否存在
SearchResult<T> result = search(e);
// 存在
if (result.target != null) {
return;
}
// 插到合適的位置
BNode<T> node = result.parent;
int index = find(node.datas, e);
node.datas.add(index, e);
// 添加相應的孩子指針
node.children.add(index + 1, null);
// 必要時分裂節點
solveOverflow(node);
}
// 處理節點上溢情況
private void solveOverflow(BNode<T> node) {
// 未上溢
if (node.children.size() <= order) {
return;
}
// 分裂點
int s = order >> 1;
// node 分裂爲左右兩個節點,原節點繼續用作分裂後的左節點,rightNode 用作分裂後的右節點
// 注意:新節點已有一個空孩子
BNode<T> rightNode = new BNode<>();
rightNode.children.remove(0);
// rightNode:存放 node.datas[s+1:] 和 node.children[s+1:]
for (int i = 0; i < order - s - 1; i++) {
rightNode.datas.add(node.datas.remove(s + 1));
rightNode.children.add(node.children.remove(s + 1));
}
// 最右側的那個指針
rightNode.children.add(node.children.remove(s + 1));
// 如果 rightNode 的孩子非空,則讓它們認新爹
if (rightNode.children.get(0) != null) {
for (int i = 0; i < order - s; i++) {
rightNode.children.get(i).parent = rightNode;
}
}
// node 之父
BNode<T> parent = node.parent;
// node 爲樹根:被提升的關鍵碼要作爲新的樹根
if (parent == null) {
root = parent = new BNode<>();
parent.children.set(0, node);
node.parent = parent;
}
// parent 中指向 rightNode 的指針之位置
int index = find(parent.datas, node.datas.get(0));
// 分裂點處的關鍵碼上升
parent.datas.add(index, node.datas.remove(s));
parent.children.add(index + 1, rightNode);
rightNode.parent = parent;
// 有可能父節點也已上溢
solveOverflow(parent);
}
刪除關鍵碼
設節點 t 包含待刪除的關鍵碼 k 。若 t 不是葉子節點,則需要沿着適當的分支向下,直到葉子節點,並在該葉子節點中找到 k 的後繼 k2 。然後,更新節點 t 中的 k 爲 k2;最後,刪除葉子節點中的 k2 。
對於 m 介 B-樹,設葉子節點 V 包含待刪除的關鍵碼。若刪除關鍵碼之後,V 包含的分支數等於 ,則違背了 B-樹的限定條件。
解決下溢問題
根據節點 V 的左、右兄弟所包含的關鍵碼數,可分爲 3種情況處理:
注:V 的左、右兄弟至少有一個不爲空。(如果左右兄弟都爲空,則說明節點 V 之父只有一個孩子,即只有一個分支、0個關鍵碼)
(1)左兄弟存在,且至少包含 個關鍵碼
設 V 之左兄弟爲 L,L 與 V 分別是父節點 P 中關鍵碼 y 的左右孩子,L 中的最大的關鍵碼爲 x 。
將 y 從節點 P 轉移至節點 V 中(作爲最小關鍵碼),再將 x 從 L 轉移至 P 中(取代原關鍵碼 y)。至此,局部乃至整樹都重新滿足B-樹條件,下溢修復完畢。
(2)右兄弟存在,且至少包含 個關鍵碼
與前一種情況對稱。
設 V 之右兄弟爲 R,V 與 R 分別是父節點 P 中關鍵碼 y 的左右孩子,R 中的最小的關鍵碼爲 x 。
將 y 從節點 P 轉移至節點 V 中(作爲最大關鍵碼),再將 x 從 R 轉移至 P 中(取代原關鍵碼 y)。至此,局部乃至整樹都重新滿足B-樹條件,下溢修復完畢。
(3)左右兄弟要麼不存在,要麼包含的關鍵碼數少於
當然,左右兄弟不可能同時不存在。
不失一般性,假設 V 之左兄弟 L 存在,則 L 此時應該恰好包含 個關鍵碼。
從父節點 P 中抽出介於 L 和 V 之間的關鍵碼 y, 並通過該關鍵碼將節點 L 和 V “粘接” 成一個節點。合併得到的新節點,其含有的關鍵碼數量爲 。
當左兄弟不存在時,右兄弟必定存在,可按照類似方法合併節點。
// 刪除
public void remove(T e) {
// 先確定關鍵碼是否存在
SearchResult<T> result = search(e);
// 不存在
if (result.target == null) {
return;
}
// 關鍵碼所在的節點及索引
BNode<T> target = result.target;
int index = result.index;
// 若 target 不是葉子節點,則需要在葉子節點中找到 e 的後繼,然後用其更新 e,再讓後繼替死
if (target.children.get(0) != null) {
// 在右子樹中,一直往左,即可找到後繼
// 類似於二叉搜索樹(實際上,可將該 B-樹展開爲一棵二叉搜索樹)
BNode<T> node = target.children.get(index + 1);
while (node.children.get(0) != null) {
node = node.children.get(0);
}
// 更新 target 中的 e 爲後繼
target.datas.set(index, node.datas.get(0));
// 實際被刪除的關鍵碼所在的節點及其索引
target = node;
index = 0;
}
// 此時,target 必然位於最底層
target.datas.remove(index);
target.children.remove(index + 1);
// 如果下溢,則解決之
solveUnderflow(target);
}
// 解決下溢情況
private void solveUnderflow(BNode<T> node) {
// 未下溢
if (node.children.size() >= ((order + 1) >> 1)) {
return;
}
// node 之父
BNode<T> parent = node.parent;
// 已至樹根:沒有孩子下限
if (parent == null) {
// 若樹根 node 不含關鍵碼,但卻含有(唯一)一個非空孩子,則其可被刪除
if (node.datas.size() == 0 && node.children.get(0) != null) {
root = node.children.get(0);
root.parent = null;
node.children.set(0, null);
}
return;
}
// 確定 node 是 parent 的第幾個孩子
int index = 0;
while (parent.children.get(index) != node) {
index++;
}
// 情況1:向左兄弟借關鍵碼
if (index > 0) {
BNode<T> leftSilbing = parent.children.get(index - 1);
// 若左兄弟足夠胖
if (leftSilbing.children.size() > ((order + 1) >> 1)) {
// parent 借關鍵碼給 node
// 左兄弟借關鍵碼給 parent
node.datas.add(0, parent.datas.get(index - 1));
parent.datas.add(index - 1, leftSilbing.datas.remove(leftSilbing.datas.size() - 1));
node.children.add(0, leftSilbing.children.remove(leftSilbing.children.size() - 1));
// 新爹
if (node.children.get(0) != null) {
node.children.get(0).parent = node;
}
return;
}
}
// 至此,左兄弟要麼爲空,要麼太“瘦”
// 情況2:向右兄弟借關鍵碼
if (index < parent.children.size() - 1) {
BNode<T> rightSibling = parent.children.get(index + 1);
// 若右兄弟足夠胖
if (rightSibling.children.size() > ((order + 1) >> 1)) {
// parent 借關鍵碼給 node
// 右兄弟借關鍵碼給 parent
node.datas.add(node.datas.size(), parent.datas.get(index));
parent.datas.add(index, rightSibling.datas.remove(0));
node.children.add(node.children.size(), rightSibling.children.remove(0));
// 新爹
if (node.children.get(node.children.size() - 1) != null) {
node.children.get(node.children.size() - 1).parent = node;
}
return;
}
}
// 情況3:左、右兄弟要麼爲空(但不可能同時),要麼都太“瘦” ——合併兩者
if (index > 0) { // 與左兄弟合併
BNode<T> leftSibling = parent.children.get(index - 1);
leftSibling.datas.add(leftSibling.datas.size(), parent.datas.remove(index - 1));
parent.children.remove(index);
// 將 node 中的數據、指針移到左兄弟
leftSibling.children.add(leftSibling.children.size(), node.children.remove(0));
if (leftSibling.children.get(leftSibling.children.size() - 1) != null) {
leftSibling.children.get(leftSibling.children.size() - 1).parent = leftSibling;
}
while (!node.datas.isEmpty()) {
leftSibling.datas.add(leftSibling.datas.size(), node.datas.remove(0));
leftSibling.children.add(leftSibling.children.size(), node.children.remove(0));
if (leftSibling.children.get(leftSibling.children.size() - 1) != null) {
leftSibling.children.get(leftSibling.children.size() - 1).parent = leftSibling;
}
}
}
else { // 與右兄弟合併
BNode<T> rightSibling = parent.children.get(index + 1);
rightSibling.datas.add(0, parent.datas.remove(index));
parent.children.remove(index);
// 將 node 中的數據、指針移到右兄弟
rightSibling.children.add(0, node.children.remove(node.children.size() - 1));
if (rightSibling.children.get(0) != null) {
rightSibling.children.get(0).parent = rightSibling;
}
while (!node.datas.isEmpty()) {
rightSibling.datas.add(0, node.datas.remove(node.datas.size() - 1));
rightSibling.children.add(0, node.children.remove(node.children.size() - 1));
if (rightSibling.children.get(0) != null) {
rightSibling.children.get(0).parent = rightSibling;
}
}
}
// 可能父節點也下溢了
solveUnderflow(parent);
}