手撕B樹

一、概述

1.歷史

B樹(B-Tree)結(jié)構(gòu)是一種高效存儲(chǔ)和查詢數(shù)據(jù)的方法,它的歷史可以追溯到1970年代早期。B樹的發(fā)明人Rudolf Bayer和Edward M. McCreight分別發(fā)表了一篇論文介紹了B樹。這篇論文是1972年發(fā)表于《ACM Transactions on Database Systems》中的,題目為"Organization and Maintenance of Large Ordered Indexes"。

這篇論文提出了一種能夠高效地維護(hù)大型有序索引的方法,這種方法的主要思想是將每個(gè)節(jié)點(diǎn)擴(kuò)展成多個(gè)子節(jié)點(diǎn),以減少查找所需的次數(shù)。B樹結(jié)構(gòu)非常適合應(yīng)用于磁盤等大型存儲(chǔ)器的高效操作,被廣泛應(yīng)用于關(guān)系數(shù)據(jù)庫(kù)和文件系統(tǒng)中。

B樹結(jié)構(gòu)有很多變種和升級(jí)版,例如B+樹,B*樹和SB樹等。這些變種和升級(jí)版本都基于B樹的核心思想,通過(guò)調(diào)整B樹的參數(shù)和結(jié)構(gòu),提高了B樹在不同場(chǎng)景下的性能表現(xiàn)。

總的來(lái)說(shuō),B樹結(jié)構(gòu)是一個(gè)非常重要的數(shù)據(jù)結(jié)構(gòu),為高效存儲(chǔ)和查詢大量數(shù)據(jù)提供了可靠的方法。它的歷史可以追溯到上個(gè)世紀(jì)70年代,而且在今天仍然被廣泛應(yīng)用于各種場(chǎng)景。

2.B-樹的優(yōu)勢(shì)

B樹和AVL樹、紅黑樹相比,B樹更適合磁盤的增刪改查,而AVL和紅黑樹更適合內(nèi)存的增刪改查。

假設(shè)存儲(chǔ)100萬(wàn)的數(shù)據(jù):

  • 使用AVL來(lái)存儲(chǔ),樹高為:log_21000000≈20 (20次的磁盤IO很慢,但是20次的內(nèi)存操作很快)
  • 使用B-樹存儲(chǔ),最小度數(shù)為500,樹高為:3

B樹優(yōu)勢(shì):

  • 磁盤存儲(chǔ)比內(nèi)存存儲(chǔ)慢很多,尤其是訪問磁盤的延遲相對(duì)較高。每次訪問磁盤都需要消耗更多的時(shí)間,而B樹的設(shè)計(jì)可以最大化地減少對(duì)磁盤的訪問次數(shù)。
  • 磁盤訪問一般是按塊讀取的,而B樹的節(jié)點(diǎn)通常設(shè)計(jì)為與磁盤塊大小一致。由于B樹是多路的,單次磁盤訪問通常會(huì)加載多個(gè)數(shù)據(jù)項(xiàng),而不是像AVL樹和紅黑樹那樣每次只讀取一個(gè)節(jié)點(diǎn)。
  • 在磁盤中存儲(chǔ)B樹時(shí),操作系統(tǒng)通常會(huì)將樹的部分結(jié)構(gòu)加載到內(nèi)存中以便快速查詢,避免了頻繁的磁盤訪問。
  • 在數(shù)據(jù)庫(kù)和文件系統(tǒng)中,數(shù)據(jù)通常是大規(guī)模的,存儲(chǔ)在外部存儲(chǔ)介質(zhì)上。B樹特別適合大規(guī)模數(shù)據(jù)的增刪改查,因?yàn)樗鼫p少了不必要的磁盤訪問,能夠高效地執(zhí)行復(fù)雜的數(shù)據(jù)操作。

二、特性

1.度和階

  • 度(degree):節(jié)點(diǎn)的孩子數(shù)
  • 階(order):所有節(jié)點(diǎn)孩子最大值

2.特性

  • 每個(gè)節(jié)點(diǎn)具有

    • 屬性 n,表示節(jié)點(diǎn)中 key 的個(gè)數(shù)
    • 屬性 leaf,表示節(jié)點(diǎn)是否是葉子節(jié)點(diǎn)
    • 節(jié)點(diǎn) key 可以有多個(gè),以升序存儲(chǔ)
  • 每個(gè)非葉子節(jié)點(diǎn)中的孩子數(shù)是 n + 1、葉子節(jié)點(diǎn)沒有孩子

  • 最小度數(shù)t(節(jié)點(diǎn)的孩子數(shù)稱為度)和節(jié)點(diǎn)中鍵數(shù)量的關(guān)系如下:

最小度數(shù)t 鍵數(shù)量范圍
2 1 ~ 3
3 2 ~ 5
4 3 ~ 7
... ...
n (n-1) ~ (2n-1)

其中,當(dāng)節(jié)點(diǎn)中鍵數(shù)量達(dá)到其最大值時(shí),即 3、5、7 ... 2n-1,需要分裂

  • 葉子節(jié)點(diǎn)的深度都相同

三、實(shí)現(xiàn)

1.定義節(jié)點(diǎn)

static class Node {
    // 關(guān)鍵字
    int[] keys;
    // 關(guān)鍵字?jǐn)?shù)量
    int keyNum;
    // 孩子節(jié)點(diǎn)
    Node[] children;
    // 是否是葉子節(jié)點(diǎn)
    boolean leafFlag = true;
    // 最小度數(shù):最少孩子數(shù)(決定樹的高度,度數(shù)越大,高度越小)
    int t;

    // ≥2
    public Node(int t) {
        this.t = t;
        // 最多的孩子數(shù)(約定)
        this.children = new Node[2 * t];
        this.keys = new int[2 * t -1];
    }
}

1.1 節(jié)點(diǎn)類相關(guān)方法

查找key:查找目標(biāo)22,在當(dāng)前節(jié)點(diǎn)的關(guān)鍵字?jǐn)?shù)組中依次查找,找到了返回;沒找到則從孩子節(jié)點(diǎn)找:

  • 當(dāng)前節(jié)點(diǎn)是葉子節(jié)點(diǎn):目標(biāo)不存在
  • 非葉子結(jié)點(diǎn):當(dāng)key循環(huán)到25,大于目標(biāo)22,此時(shí)從索引4對(duì)應(yīng)的孩子key數(shù)組中繼續(xù)查找,依次遞歸,直到找到為止。


    image.png

根據(jù)key獲取節(jié)點(diǎn)

/**
 * 根據(jù)key獲取節(jié)點(diǎn)
 * @param key
 * @return
 */
Node get(int key) {
    // 先從當(dāng)前key數(shù)組中找
    int i = 0;
    while (i < keyNum) {
        if (keys[i] == key) {
            // 在當(dāng)前的keys關(guān)鍵字?jǐn)?shù)組中找到了
            return this;
        }
        if (keys[i] > key) {
            // 當(dāng)數(shù)組比當(dāng)前key大還未找到時(shí),退出循環(huán)
            break;
        }
        i++;
    }
    // 如果是葉子節(jié)點(diǎn),沒有孩子了,說(shuō)明key不存在
    if (leafFlag) {
        return null;
    } else {
        // 非葉子節(jié)點(diǎn),退出時(shí)i的值就是對(duì)應(yīng)范圍的孩子節(jié)點(diǎn)數(shù)組的索引,從對(duì)應(yīng)的這個(gè)孩子數(shù)組中繼續(xù)找
        return children[i].get(key);
    }
}

向指定索引插入key

/**
 * 向keys數(shù)組中指定的索引位置插入key
 * @param key
 * @param index
 */
void insertKey(int key,int index) {
    /**
     * [0,1,2,3]
     * src:源數(shù)組
     * srcPos:起始索引
     * dest:目標(biāo)數(shù)組
     * destPos: 目標(biāo)索引
     * length:拷貝的長(zhǎng)度
     */
    System.arraycopy(keys, index, keys, index + 1, keyNum - index);
    keys[index] = key;
    keyNum++;
}

向指定索引插入child

/**
 * 向children指定索引插入child
 *
 * @param child
 * @param index
 */
void insertChild(Node child, int index) {
    System.arraycopy(children, index, children, index + 1, keyNum - index);
    children[index] = child;
}

2.定義樹

public class BTree {

    // 根節(jié)點(diǎn)
    private Node root;

    // 樹中節(jié)點(diǎn)最小度數(shù)
    int t;

    // 最小key數(shù)量 在創(chuàng)建樹的時(shí)候就指定好
    final int MIN_KEY_NUM;

    // 最大key數(shù)量
    final int MAX_KEY_NUM;

    public BTree() {
        // 默認(rèn)度數(shù)設(shè)置為2
        this(2);
    }

    public BTree(int t) {
        this.t = t;
        root = new Node(t);
        MIN_KEY_NUM = t - 1;
        MAX_KEY_NUM = 2 * t - 1;
    }
}    

判斷key在樹中是否存在

/**
 * 判斷key在樹中是否存在
 * @param key
 * @return
 */
public boolean contains(int key) {
    return root.get(key) != null;
}

3.新增key

  • 1.查找插入位置:從根節(jié)點(diǎn)開始,沿著樹向下查找,直到找到一個(gè)葉子節(jié)點(diǎn),這個(gè)葉子節(jié)點(diǎn)包含的鍵值范圍覆蓋了要插入的鍵值。
  • 2.插入鍵值:在找到的葉子節(jié)點(diǎn)中插入新的鍵值。如果葉子節(jié)點(diǎn)中的鍵值數(shù)量沒有超過(guò)B樹的階數(shù)(即每個(gè)節(jié)點(diǎn)最多可以包含的鍵值數(shù)量),則插入操作完成。
  • 3.分裂節(jié)點(diǎn):如果葉子節(jié)點(diǎn)中的鍵值數(shù)量超過(guò)了B樹的階數(shù),那么這個(gè)節(jié)點(diǎn)需要分裂。

如果度為3,最大key數(shù)量為:2*3-1=5,當(dāng)插入了8后,此時(shí)達(dá)到了最大數(shù)量5,需要分裂:


image.png

分裂邏輯:
分裂節(jié)點(diǎn)數(shù)據(jù)一分為三:

  • 左側(cè)數(shù)據(jù):本身左側(cè)的數(shù)據(jù)留在該節(jié)點(diǎn)
  • 中間數(shù)據(jù):中間索引2(度-1)的數(shù)據(jù)6移動(dòng)到父節(jié)點(diǎn)的索引1(被分裂節(jié)點(diǎn)的索引)處。
  • 右側(cè)數(shù)據(jù):從索引3(度)開始的數(shù)據(jù),移動(dòng)到新節(jié)點(diǎn),新節(jié)點(diǎn)的索引值為分裂節(jié)點(diǎn)的index+1

如果分裂的節(jié)點(diǎn)是非葉子節(jié)點(diǎn):
需要多一步操作:右側(cè)數(shù)據(jù)需要和孩子一起連帶到新節(jié)點(diǎn)去:


非葉子節(jié)點(diǎn)分裂.png

分裂的是根節(jié)點(diǎn):
需要再創(chuàng)建多一個(gè)節(jié)點(diǎn)來(lái)當(dāng)做根節(jié)點(diǎn),此根節(jié)點(diǎn)為父親,存入中間的數(shù)據(jù)。
其他步驟同上。


根節(jié)點(diǎn)分裂.png

分裂方法:

/**
 * 節(jié)點(diǎn)分裂
 * 左側(cè)數(shù)據(jù):本身左側(cè)的數(shù)據(jù)留在該節(jié)點(diǎn)
 * 中間數(shù)據(jù):中間索引2(度-1)的數(shù)據(jù)6移動(dòng)到父節(jié)點(diǎn)的索引1(被分裂節(jié)點(diǎn)的索引)處
 * 右側(cè)數(shù)據(jù):從索引3(度)開始的數(shù)據(jù),移動(dòng)到新節(jié)點(diǎn),新節(jié)點(diǎn)的索引值為分裂節(jié)點(diǎn)的index+1
 * @param node 要分裂的節(jié)點(diǎn)
 * @param index 分裂節(jié)點(diǎn)的索引
 * @param parent 要分裂節(jié)點(diǎn)的父節(jié)點(diǎn)
 *
 */
public void split(Node node, int index, Node parent) {
    // 沒有父節(jié)點(diǎn),當(dāng)前node為根節(jié)點(diǎn)
    if (parent == null) {
        // 創(chuàng)建出新的根來(lái)存儲(chǔ)中間數(shù)據(jù)
        Node newRoot = new Node(t);
        newRoot.leafFlag = false;
        newRoot.insertChild(node, 0);
        // 更新根節(jié)點(diǎn)為新創(chuàng)建的newRoot
        this.root = newRoot;
        parent = newRoot;
    }

    // 1.處理右側(cè)數(shù)據(jù):創(chuàng)建新節(jié)點(diǎn)存儲(chǔ)右側(cè)數(shù)據(jù)
    Node newNode = new Node(t);
    // 新創(chuàng)建的節(jié)點(diǎn)跟原本分裂節(jié)點(diǎn)同級(jí)
    newNode.leafFlag = node.leafFlag;
    // 新創(chuàng)建節(jié)點(diǎn)的數(shù)據(jù)從 原本節(jié)點(diǎn)【度】位置索引開始拷貝 拷貝長(zhǎng)度:t-1
    System.arraycopy(node.keys, t, newNode.keys, 0, t - 1);
    // 如果node不是葉子節(jié)點(diǎn),還需要把node的一部分孩子也同時(shí)拷貝到新節(jié)點(diǎn)的孩子中
    if (!node.leafFlag) {
        System.arraycopy(node.children, t, newNode.children, 0, t);
    }
    // 更新新節(jié)點(diǎn)的keyNum
    newNode.keyNum = t - 1;

    // 更新原本節(jié)點(diǎn)的keyNum
    node.keyNum = t - 1;

    // 2.處理中間數(shù)據(jù):【度-1】索引處的數(shù)據(jù) 移動(dòng)到父節(jié)點(diǎn)【分裂節(jié)點(diǎn)的索引】索引處
    // 要插入父節(jié)點(diǎn)的數(shù)據(jù):
    int midKey = node.keys[t - 1];
    parent.insertKey(midKey, index);

    // 3. 新創(chuàng)建的節(jié)點(diǎn)作為父親的孩子
    parent.insertChild(newNode, index + 1);

    // parent的keyNum在對(duì)應(yīng)的方法中已經(jīng)更新了
}

新增方法:

/**
 * 新增key
 *
 * @param key
 */
public void put(int key) {
    doPut(root, key, 0, null);
}

/**
 * 執(zhí)行新增key
 * 1.查找插入位置:從根節(jié)點(diǎn)開始,沿著樹向下查找,直到找到一個(gè)葉子節(jié)點(diǎn),這個(gè)葉子節(jié)點(diǎn)包含的鍵值范圍覆蓋了要插入的鍵值。
 * 2.插入鍵值:在找到的葉子節(jié)點(diǎn)中插入新的鍵值。如果葉子節(jié)點(diǎn)中的鍵值數(shù)量沒有超過(guò)B樹的階數(shù)(即每個(gè)節(jié)點(diǎn)最多可以包含的鍵值數(shù)量),則插入操作完成。
 * 3.分裂節(jié)點(diǎn):如果葉子節(jié)點(diǎn)中的鍵值數(shù)量超過(guò)了B樹的階數(shù),那么這個(gè)節(jié)點(diǎn)需要分裂。
 * @param node 待插入元素的節(jié)點(diǎn)
 * @param key 插入的key
 * @param nodeIndex  待插入元素節(jié)點(diǎn)的索引
 * @param nodeParent 待插入節(jié)點(diǎn)的父節(jié)點(diǎn)
 */
public void doPut(Node node, int key, int nodeIndex, Node nodeParent) {
    // 查找插入位置
    int index = 0;
    while (index < node.keyNum) {
        if (node.keys[index] == key ) {
            // 找到了 做更新操作 (因?yàn)闆]有維護(hù)value,所以就不用處理了)
            return;
        }
        if (node.keys[index] > key) {
            // 沒找到該key, 退出循環(huán),index的值就是要插入的位置
            break;
        }
        index++;
    }
    // 如果是葉子節(jié)點(diǎn),直接插入
    if (node.leafFlag) {
        node.insertKey(key, index);
    } else {
        // 非葉子節(jié)點(diǎn),繼續(xù)從孩子中找到插入位置 父親的這個(gè)待插入的index正好就是元素要插入的第x個(gè)孩子的位置
        doPut(node.children[index], key , index, node);
    }
    // 處理節(jié)點(diǎn)分裂邏輯 : keyNum數(shù)量達(dá)到上限,節(jié)點(diǎn)分裂
    if (node.keyNum == MAX_KEY_NUM) {
        split(node, nodeIndex, nodeParent);
    }
}

4.刪除key

情況一:刪除的是葉子節(jié)點(diǎn)的key

節(jié)點(diǎn)是葉子節(jié)點(diǎn),找到了直接刪除,沒找到返回。

情況二:刪除的是非葉子節(jié)點(diǎn)的key

沒有找到key,繼續(xù)在孩子中找。
找到了,把要?jiǎng)h除的key和替換為后繼key,刪掉后繼key。

平衡樹:該key被刪除后,key數(shù)目<key下限(t-1),樹不平衡,需要調(diào)整

  • 如果左邊兄弟節(jié)點(diǎn)的key是富裕的,可以直接找他借:右旋,把父親的旋轉(zhuǎn)下來(lái),把兄弟的旋轉(zhuǎn)上去。


    image.png
  • 如果右邊兄弟節(jié)點(diǎn)的key是富裕的,可以直接找他借:左旋,把父親的旋轉(zhuǎn)下來(lái),把兄弟的旋轉(zhuǎn)上去。
    image.png
  • 當(dāng)沒有兄弟是富裕時(shí),沒辦法借,采用向左合并:父親和失衡節(jié)點(diǎn)都合并到左側(cè)的節(jié)點(diǎn)中


    image.png

詳細(xì)右旋流程:


旋轉(zhuǎn).png

處理孩子:


處理孩子.png

向左合并詳細(xì)流程:


向左合并詳細(xì)流程.png

根節(jié)點(diǎn)調(diào)整的情況:


image.png

失衡調(diào)整代碼

/**
 * 樹的平衡
 * @param node 失衡節(jié)點(diǎn)
 * @param index 失衡節(jié)點(diǎn)索引
 * @param parent 失衡節(jié)點(diǎn)父節(jié)點(diǎn)
 */
public void balance(Node node, int index, Node parent) {
    if (node == root) {
        // 如果是根節(jié)點(diǎn) 當(dāng)調(diào)整到根節(jié)點(diǎn)只剩下一個(gè)key時(shí),要替換根節(jié)點(diǎn) (根節(jié)點(diǎn)不能為null,要保證右孩子才替換)
        if (root.keyNum == 0 && root.children[0] != null) {
            root = root.children[0];
        }
        return;
    }
    // 拿到該節(jié)點(diǎn)的左右兄弟,判斷節(jié)點(diǎn)是不是富裕的,如果富裕,則找兄弟借
    Node leftBrother = parent.childLeftBrother(index);
    Node rightBrother = parent.childRightBrother(index);

    // 左邊的兄弟富裕:右旋
    if (leftBrother != null && leftBrother.keyNum > MIN_KEY_NUM) {
        // 1.要旋轉(zhuǎn)下來(lái)的key:父節(jié)點(diǎn)中【失衡節(jié)點(diǎn)索引-1】的key:parent.keys[index-1];插入到失衡節(jié)點(diǎn)索引0位置
        // (這里父親節(jié)點(diǎn)旋轉(zhuǎn)走的不用刪除,因?yàn)榈葧?huì)左側(cè)的兄弟旋轉(zhuǎn)上來(lái)會(huì)覆蓋掉)
        node.insertKey(parent.keys[index - 1], 0);

        // 2.0 如果左側(cè)節(jié)點(diǎn)不是葉子節(jié)點(diǎn),有孩子,當(dāng)旋轉(zhuǎn)一個(gè)時(shí),只需要留下原本孩子數(shù)-1 ,把最大的孩子過(guò)繼給失衡節(jié)點(diǎn)的最小索引處(先處理后事)
        if (!leftBrother.leafFlag) {
            node.insertChild(leftBrother.removeRightMostChild(), 0);
        }

        // 2.1 要旋轉(zhuǎn)上去的key:左側(cè)兄弟最大的索引key,刪除掉,插入到父節(jié)點(diǎn)中【失衡節(jié)點(diǎn)索引-1】位置(此位置就是剛才在父節(jié)點(diǎn)旋轉(zhuǎn)走的key的位置)
        // 這里要直接覆蓋,不能調(diào)插入方法,因?yàn)檫@個(gè)是當(dāng)初旋轉(zhuǎn)下去的key。
        parent.keys[index - 1] = leftBrother.removeRightMostKey();

        return;
    }
    // 右邊的兄弟富裕:左旋
    if (rightBrother != null && rightBrother.keyNum > MIN_KEY_NUM) {
        // 1.要旋轉(zhuǎn)下來(lái)的key:父節(jié)點(diǎn)中【失衡節(jié)點(diǎn)索引】的key:parent.keys[index];插入到失衡節(jié)點(diǎn)索引最大位置keyNum位置
        // (這里父親節(jié)點(diǎn)旋轉(zhuǎn)走的不用刪除,因?yàn)榈葧?huì)右側(cè)的兄弟旋轉(zhuǎn)上來(lái)會(huì)覆蓋掉)
        node.insertKey(parent.keys[index], node.keyNum);

        // 2.0 如果右側(cè)節(jié)點(diǎn)不是葉子節(jié)點(diǎn),有孩子,當(dāng)旋轉(zhuǎn)一個(gè)時(shí),只需要留下原本孩子數(shù)-1 ,把最小的孩子過(guò)繼給失衡節(jié)點(diǎn)的最大索引處(孩子節(jié)點(diǎn)的索引比父親要多1)
        if (!rightBrother.leafFlag) {
            node.insertChild(rightBrother.removeLeftMostChild(), node.keyNum + 1);
        }

        // 2.1 要旋轉(zhuǎn)上去的key:右側(cè)兄弟最小的索引key,刪除掉,插入到父節(jié)點(diǎn)中【失衡節(jié)點(diǎn)索引-1】位置(此位置就是剛才在父節(jié)點(diǎn)旋轉(zhuǎn)走的key的位置)
        // 這里要直接覆蓋,不能調(diào)插入方法,因?yàn)檫@個(gè)是當(dāng)初旋轉(zhuǎn)下去的key。
        parent.keys[index] = rightBrother.removeLeftMostKey();

        return;
    }
    // 左右兄弟都不夠,往左合并
    if (leftBrother != null) {
        // 向左兄弟合并
        // 1.把失衡節(jié)點(diǎn)從父親中移除
        parent.removeChild(index);

        // 2.插入父節(jié)點(diǎn)的key到左兄弟 將父節(jié)點(diǎn)中【失衡節(jié)點(diǎn)索引-1】的key移動(dòng)到左側(cè)
        leftBrother.insertKey(parent.removeKey(index - 1), leftBrother.keyNum);

        // 3.插入失衡節(jié)點(diǎn)的key及其孩子到左兄弟
        node.moveToTarget(leftBrother);
    } else {
        // 右兄弟向自己合并
        // 1.把右兄弟從父親中移除
        parent.removeChild(index + 1);
        // 2.把父親的【失衡節(jié)點(diǎn)索引】 處的key移動(dòng)到自己這里
        node.insertKey(parent.removeKey(index), node.keyNum);
        // 3.把右兄弟完整移動(dòng)到自己這里
        rightBrother.moveToTarget(node);
    }
}

刪除key

/**
 * 刪除指定key
 * @param node 查找待刪除key的起點(diǎn)
 * @param parent 待刪除key的父親
 * @param nodeIndex 待刪除的key的索引
 * @param key 待刪除的key
 */
public void doRemove(Node node, Node parent, int nodeIndex, int key) {
    // 找到被刪除的key
    int index = 0;
    // 循環(huán)查找待刪除的key
    while (index < node.keyNum) {
        if (node.keys[index] >= key) {
            //找到了或者沒找到
            break;
        }
        index++;
    }
    // 如果找到了 index就是要?jiǎng)h除的key索引;
    // 如果沒找到,index就是要在children的index索引位置繼續(xù)找

    // 一、是葉子節(jié)點(diǎn)
    if (node.leafFlag) {
        // 1.1 沒找到
        if (!found(node, key, index)) {
            return;
        }
        // 1.2 找到了
        else {
            // 刪除當(dāng)前節(jié)點(diǎn)index處的key
            node.removeKey(index);
        }
    }
    // 二、不是葉子節(jié)點(diǎn)
    else {
        // 1.1 沒找到
        if (!found(node, key, index)) {
            // 繼續(xù)在孩子中找 查找的孩子的索引就是當(dāng)前index
            doRemove(node.children[index], node, index, key);
        }
        // 1.2 找到了
        else {
            // 找到后繼節(jié)點(diǎn),把后繼節(jié)點(diǎn)復(fù)制給當(dāng)前的key,然后刪除后繼節(jié)點(diǎn)。
            // 在索引+1的孩子里開始,一直往左找,直到節(jié)點(diǎn)是葉子節(jié)點(diǎn)為止,就找到了后繼節(jié)點(diǎn)
            Node deletedSuccessor = node.children[index + 1];
            while (!deletedSuccessor.leafFlag) {
                // 更新為最左側(cè)的孩子
                deletedSuccessor = deletedSuccessor.children[0];
            }
            // 1.2.1 當(dāng)找到葉子節(jié)點(diǎn)之后,最左側(cè)的key就是后繼key
            int deletedSuccessorKey = deletedSuccessor.keys[0];
            // 1.2.2 把后繼key賦值給待刪除的key
            node.keys[index] = deletedSuccessorKey;
            // 1.2.3 刪除后繼key 再調(diào)用該方法,走到情況一,刪除掉該后繼key: 起點(diǎn)為索引+1的孩子處,刪除掉后繼key
            doRemove(node.children[index + 1], node, index + 1, deletedSuccessorKey);
        }
    }

    // 樹的平衡:
    if (node.keyNum < MIN_KEY_NUM) {
        balance(node, nodeIndex, parent);
    }
}

節(jié)點(diǎn)相關(guān)方法:

        /**
         * 移除指定索引處的key
         * @param index
         * @return
         */
        int removeKey(int index) {
            int deleted = keys[index];
            System.arraycopy(keys, index + 1, keys, index, --keyNum - index);
            return deleted;
        }

        /**
         * 移除最左索引處的key
         * @return
         */
        int removeLeftMostKey(){
            return removeKey(0);
        }

        /**
         * 移除最右邊索引處的key
         * @return
         */
        int removeRightMostKey() {
            return removeKey(keyNum - 1);
        }

        /**
         * 移除指定索引處的child
         * @param index
         * @return
         */
        Node removeChild(int index) {
            Node deleted = children[index];
            System.arraycopy(children, index + 1, children, index, keyNum - index);
            children[keyNum] = null;
            return deleted;
        }

        /**
         * 移除最左邊的child
         * @return
         */
        Node removeLeftMostChild() {
            return removeChild(0);
        }

        /**
         * 移除最右邊的child
         * @return
         */
        Node removeRightMostChild() {
            return removeChild(keyNum);
        }

        /**
         * 獲取指定children處左邊的兄弟
         * @param index
         * @return
         */
        Node childLeftBrother(int index) {
            return index > 0 ? children[index - 1] : null;
        }

        /**
         * 獲取指定children處右邊的兄弟
         * @param index
         * @return
         */
        Node childRightBrother(int index) {
            return index == keyNum ? null : children[index + 1];
        }

        /**
         * 復(fù)制當(dāng)前節(jié)點(diǎn)到目標(biāo)節(jié)點(diǎn)(key和child)
         * @param target
         */
        void moveToTarget(Node target) {
            int start = target.keyNum;
            // 當(dāng)前節(jié)點(diǎn)不是葉子節(jié)點(diǎn) 說(shuō)明有孩子
            if (!leafFlag) {
                // 復(fù)制當(dāng)前節(jié)點(diǎn)的孩子到目標(biāo)節(jié)點(diǎn)的孩子中
                for (int i = 0; i <= keyNum; i++) {
                    target.children[start + i] = children[i];
                }
            }
            // 復(fù)制key到目標(biāo)節(jié)點(diǎn)的keys中
            for (int i = 0; i < keyNum; i++) {
                target.keys[target.keyNum++] = keys[i];
            }
        }
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,185評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,446評(píng)論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,951評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,189評(píng)論 0 287
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,718評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,602評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,800評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,045評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評(píng)論 1 281
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,420評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,755評(píng)論 2 371

推薦閱讀更多精彩內(nèi)容