B 樹 - Java 實現

當數據規模大到內存已不足以容納時, 常規的平衡二叉搜索樹的效率將大打折扣。其原因在於,查找過程對外存的訪問次數過多。

磁盤等外部存儲器的一個特點是: 讀取物理地址連續的一千個字節,與讀取單個字節所消耗的時間幾乎是沒有區別的。

此時,可以使用多路搜索樹。多路搜索樹可以由二叉搜索樹變換得到。如,

在這裏插入圖片描述
此時,搜索過程每下降一層,都以“大節點”爲單位從外存中讀取一組(而不再是一個)關鍵碼。更爲重要的是,這組關鍵碼在邏輯上與物理上都彼此相鄰,故可以批量方式從外存一次性讀出,且所需時間與讀取單個關鍵碼幾乎一樣。

B 樹

  • 所謂 m 階 B-樹 , 即 m 路平衡搜索樹(m2m \ge 2)。
  • 所有外部節點深度相等
  • 設一個內部節點存有 n 個關鍵碼, 以及用以指示對應分支的 n+1 個指針。除以外的所有內部節點,都應滿足:m/2分支數m\lceil m/2 \rceil \le \text{分支數} \le m ,而在非空的 B-樹中,根節點應滿足:2m2 \le 分支數 \le m
  • 每個內部節點中的關鍵碼按順序排列。
  • 由於各節點的分支數介於m/2\lceil m/2 \rceilmm 之間,故m階B-樹也稱 (m/2,m)(\lceil m/2 \rceil , m)-樹 。(關鍵碼數介於m/21\lceil m/2 \rceil - 1m1m - 1 之間)

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-樹的高度 h=Θ(logmN)h = \Theta(\log_mN)

// 用於表示搜索結果(作爲內部類)
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 個分支)。

取分裂點位置 s=m/2s = \lfloor m/2 \rfloor ,則可將 v 的關鍵碼分爲 3部分:[0…s)、s、[s+1…) 。其中,[0…s) 部分作爲左節點、[s+1…) 部分作爲右節點,s 處的關鍵碼則被提升至父節點中。

至於,被提升的關鍵碼,有 3種可能的情況:

  1. 父節點未飽和,則將關鍵碼插到合適的位置即可;
  2. 父節點已飽和,強行插入關鍵碼之後,父節點也需要分裂;
  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 包含的分支數等於 m/21\lceil m/2 \rceil - 1,則違背了 B-樹的限定條件。

解決下溢問題

根據節點 V 的左、右兄弟所包含的關鍵碼數,可分爲 3種情況處理:

注:V 的左、右兄弟至少有一個不爲空。(如果左右兄弟都爲空,則說明節點 V 之父只有一個孩子,即只有一個分支、0個關鍵碼)

(1)左兄弟存在,且至少包含 m/2\lceil m/2 \rceil 個關鍵碼

設 V 之左兄弟爲 L,L 與 V 分別是父節點 P 中關鍵碼 y 的左右孩子,L 中的最大的關鍵碼爲 x 。

在這裏插入圖片描述
將 y 從節點 P 轉移至節點 V 中(作爲最小關鍵碼),再將 x 從 L 轉移至 P 中(取代原關鍵碼 y)。至此,局部乃至整樹都重新滿足B-樹條件,下溢修復完畢。

(2)右兄弟存在,且至少包含 m/2\lceil m/2 \rceil 個關鍵碼

與前一種情況對稱。

設 V 之右兄弟爲 R,V 與 R 分別是父節點 P 中關鍵碼 y 的左右孩子,R 中的最小的關鍵碼爲 x 。

在這裏插入圖片描述
將 y 從節點 P 轉移至節點 V 中(作爲最大關鍵碼),再將 x 從 R 轉移至 P 中(取代原關鍵碼 y)。至此,局部乃至整樹都重新滿足B-樹條件,下溢修復完畢。

(3)左右兄弟要麼不存在,要麼包含的關鍵碼數少於 m/2\lceil m/2 \rceil

當然,左右兄弟不可能同時不存在。

不失一般性,假設 V 之左兄弟 L 存在,則 L 此時應該恰好包含 m/21\lceil m/2 \rceil - 1 個關鍵碼。

在這裏插入圖片描述
從父節點 P 中抽出介於 L 和 V 之間的關鍵碼 y, 並通過該關鍵碼將節點 L 和 V “粘接” 成一個節點。合併得到的新節點,其含有的關鍵碼數量爲 (m/21)+1+(m/22)=2m/22m1(\lceil m/2 \rceil - 1) + 1 + (\lceil m/2 \rceil - 2) = 2\cdot\lceil m/2 \rceil - 2 \le m - 1

當左兄弟不存在時,右兄弟必定存在,可按照類似方法合併節點。

// 刪除
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);
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章