B+树

B+树

一、什么是 B+ 树?

B+ 树是 B-树的变种。

其结构类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
                                             ____ ___ ___
| 24 | | |
|
--------------------------------------------------------
| |
____ ____ ___ ____ ___ ___
| 15 | 20 | | | 33 | | |
| |
-------------------------------------------- -----------------------
| | | | |
____ ____ ___ ____ ____ ___ ____ ____ ___ ____ ____ ___ ____ ____ ___
| 10 | 12 | | <=> | 15 | 18 | | <=> | 20 | 21 | | <=> | 30 | 31 | | <=> | 34 | 38 | |

B+ 树和 B-树的主要区别在于:

  • 内部节点只是索引,只有叶子节点才保存数据
  • 叶子节点之间构成了一条双向链表
  • 叶子节点包含了所有关键码值,而内部节点仅包含用于索引的关键码值

B+ 树和 B-树的区别主要在于叶子节点,它们的内部节点基本是一样的。

二、为什么用 B+ 树?

  • B+ 树特别适合范围查询,因为叶子节点之间是双向连接,可以顺着双向链表寻找数据
  • B+ 树只会在叶子节点上插入和删除数据,相对于 B-树来说,实现会简单一些

三、如何实现 B+ 树?

和 B-树差不多,B+ 树也包含了几种操作:

  • 检索元素:在树中搜索指定的元素
  • 插入元素:往树中插入新的元素
  • 删除元素:从树中删除指定的元素

其中,插入和删除会对 B+ 树的结构产生影响,稍微麻烦。

3.1 检索元素

B+ 树的检索和 B-树的差不多:

  • 基于多路查找,找到对应的节点
  • 需要一直找到叶子节点,因为只有叶子节点有数据

比如,上面的 B+ 树中,要寻找元素 20,它的搜索路线就是:

1
2
3
4
5
6
7
8
9
10
11
12
                                             ____ ___ ___
| 24 | | |
|
--------------------------------------------------------
| |
____ ____ ___ ____ ___ ___
| | 20 | | | | | |
| |
-------------------------------------------- -----------------------
| | | | |
____ ____ ___ ____ ____ ___ ____ ____ ___ ____ ____ ___ ____ ____ ___
| | | | <=> | | | | <=> | 20 | | | <=> | | | | <=> | | | |

注意:虽然内部节点也有 20 这个关键码值,但是它没有保存数据,仅仅是作为索引,所以还是要一直搜索到叶子节点。

这个检索过程对应的伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**- 检索元素 -*/
private V get(BPTNode<K, V> root, K key) {
int index = root.findIndex(key);
if (root.isLeaf()) {
// 叶子节点
if (key.equals(root.getKey(index))) {
// 找到 key 对应的节点
BPTLeaf<K, V> leaf = (BPTLeaf<K, V>) root;
return leaf.getValue(index);
} else {
// 没有找到对应的节点
return null;
}
} else {
// 内部节点,往子树继续找 key 对应的节点
return get(root.getChild(index), key);
}
}

B+ 树的检索要一直到达叶子节点才行。

3.2 插入元素

B+ 树和 B-树一样:

  • 插入节点总是在叶子节点插入

B-树节点的分裂实际只有 2 种情况,但是 B+ 树的叶子节点分裂会有点特殊:

  • 叶子节点分裂后,新的左右叶子节点会保留所有的关键码值
  • 分裂出来的父节点的关键码值,等于右叶子节点中的最小关键码值

这是因为,B+ 树的数据都在叶子中保存,不能将其上溢到父节点中,要保持这种规则。

由于 B+ 树叶子节点和内部节点的分裂不同,所以会分为 4 种情况:

  1. 叶子节点空间足够,直接插入元素
  2. 叶子节点空间不足,1 个节点分裂成 3 个节点,执行上溢操作
  3. 内部节点空间足够,直接插入元素
  4. 内部节点空间不足,1 个节点分裂成 3 个节点,执行上溢操作

这 4 种情况的效果如下。

3.2.1 叶子节点-直接插入

当叶子节点空间足够时,可以直接插入新元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 ____ ____ ___ 
| 12 | | |

| 插入 20
v

____ ____ ___
| 12 | 20 | |

| 插入 9
v

____ ____ ____
| 9 | 12 | 20 |

此时的叶子节点空间足够时,直接插入元素就行了。

3.2.2 叶子节点-分裂上溢

当叶子节点空间不足时,再插入新元素,会导致节点分裂,产生上溢:

1
2
3
4
5
6
7
8
9
10
11
12
13
           ___ ____ ____ 
| 9 | 12 | 20 |

| 插入 25
v

____ ___ ___
| 20 | | |
|
---------------------
| |
___ ____ ___ ____ ____ ____
| 9 | 12 | | <=> | 20 | 25 | |

节点空间不足以插入新元素,所以 1 个节点会分裂成 3 个节点。其中,

  • 叶子分裂后,新的叶子节点会保留所有的关键码值 [9, 12][20, 25]
  • 父节点的关键码值,等于右叶子节点的最小关键码值 [20]
  • 叶子节点之间,还需要补充上双向链接

在叶子节点分裂上溢这一点上,B+ 树和 B-树有所不同。

3.2.3 内部节点-直接插入

为了保持 B+ 树的高度平衡,叶子节点会将分裂后的父节点,往上插入之前的父节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
                        ____ ___ ___ 
| 20 | | |
|
------------------------
| |
___ ____ ____ ____ ____ ____
| 9 | 12 | 18 | <=> | 20 | 25 | |


| 插入 10
v

____ ____ ___
| 12 | 20 | |
|
-----------------------------------------------
| | |
___ ____ ____ ____ ____ ____ ____ ____ ____
| 9 | 10 | | <=> | 12 | 18 | | <=> | 20 | 25 | |

左子节点 [9, 12, 18] 插入新元素 10 后会产生上溢,12 会上溢父节点 [20] 中。

因为内部节点 [20] 的空间足够,所以 12 直接插入即可。

3.2.4 内部节点-分裂上溢

节点上溢后,如果父节点空间也满了,那么父节点也会出现分裂上溢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
                                             ____ ____ ____ 
| 12 | 20 | 45 |
|
------------------------------------------------------------------
| | | |
___ ___ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____
| 8 | 9 | 10 | | 12 | 18 | | | 20 | 25 | | | 45 | 85 | |


| 插入 11
v

____ ____ ____
| 20 | | |
|
------------------------------------------------------------
| |
____ ____ ___ ____ ___ ___
| 10 | 12 | | | 45 | | |
| |
---------------------------------------------- -------------------------
| | | | |
___ ___ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____
| 8 | 9 | | <=> | 10 | 11 | | <=> | 12 | 18 | | <=> | 20 | 25 | | <=> | 45 | 85 | |

插入元素 11 之后,会传递性地导致节点 [8, 9, 10] 和节点 [12, 20, 45] 都产生分裂上溢。

注意,内部节点的分裂和叶子节点是不同的:

  • 内部节点分裂后,子节点不会出现和父节点一样的关键码值
  • 新分裂父节点的关键码值,等于插入新元素后的中间关键码值

这种分裂上溢可以一直持续到根节点,最后可能会导致 B+ 树提升一层(如上)。

上述的 4 种情况,对应的伪代码差不多是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**- 插入新元素 -*/
private BPTNode<K, V> put(BPTNode<K, V> root, K key, V value) {
int index = root.findIndex(key);
if (root.isLeaf()) {
// 叶子节点,有可能发生了分裂
BPTLeaf<K, V> leaf = (BPTLeaf<K, V>) root;
return leaf.add(key, value);
} else {
// 内部节点,添加到子树的叶子节点中
BPTInternal<K, V> node = (BPTInternal<K, V>) root;
BPTNode<K, V> child = node.getChild(index);
child = put(child, key, value);

// 添加到子树中,有可能发生了分裂
return node.overflow(index, child);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/**- 叶子节点的上溢 -*/
public BPTNode<K, V> add(K key, V value) {
int index = findIndex(key);

// 1. 空间足够,可以直接插入新元素
if (!this.isFull()) {
insertEntry(index + 1, new Entry<>(key, value));
return this;
}

// 2. 空间不足,需要分裂节点
return splitNodes(index, new Entry<>(key, value));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
/**- 内部节点的上溢 -*/
public BPTNode<K, V> overflow(int index, BPTNode<K, V> node) {
// 1. 当前空间足够,可以直接插入新节点
if (size + node.size() < m) {
for (Entry<K, V> entry : node.entries()) {
insertEntry(++index, entry);
}
return this;
}

// 2. 当前空间不够,需要分裂成 3 个节点
return splitNodes(index, node);
}

3.3 删除元素

删除元素,和插入一样,由于叶子节点的特殊性,所以实际上会有 6 种情况:

  1. 叶子节点元素足够,直接删除
  2. 叶子节点元素不足,看兄弟节点是否可以借一个过来(和内部节点不同)
  3. 叶子节点兄弟节点不能借,那就和兄弟合并,删除父节点(和内部节点不同)
  4. 内部节点元素足够,直接删除
  5. 内部节点元素不足,看兄弟节点是否可以借一个过来(和叶子节点不同)
  6. 内部节点兄弟节点不能借,那就和兄弟合并,删除父节点(和叶子节点不同)

这几种情况如下。

3.3.1 叶子节点-直接删除

如果是叶子节点,且删除元素后,元素数量仍然处于半满状态,则可以直接删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
           ____ ___ ___ 
| 20 | | |
|
-----------------------
| |
___ ____ ___ ____ ____ ____
| 9 | 12 | | <=> | 20 | 25 | |

| 删除 20
v

____ ___ ___
| 20 | | |
|
-----------------------
| |
___ ____ ___ ____ ____ ____
| 9 | 12 | | <=> | 25 | | |

叶子节点 [20, 25] 删除 20 以后,元素数量依旧满足半满状态,则可以直接删除并返回。

注意:直接删除叶子节点元素 20 后,并不会改变父节点的关键码值 20

3.3.2 叶子节点-从兄弟借

如果删除元素后,元素数量少于一半,不满足要求,则先看一下兄弟节点能不能借个元素过来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
           ____ ___ ___ 
| 20 | | |
|
-----------------------
| |
___ ____ ___ ____ ____ ____
| 9 | 12 | | <=> | 25 | | |

| 删除 25
v

____ ___ ___
| 12 | | |
|
-----------------------
| |
___ ____ ___ ____ ____ ____
| 9 | | | <=> | 12 | | |

右子节点 [25] 删除 25 以后,元素数量不满足要求了,找兄弟节点 [9, 12] 借一个元素。

和 B-树的旋转不同,B+ 树叶子节点的借用是这样的:

  • 叶子节点直接从兄弟节点借一个元素过来
  • 同时还要更新叶子节点对应的父节点关键码值

删除 25 以后,除了借 12 过来,还要更新父节点的关键码值为 12

上面是从左兄弟借,从右兄弟借也是一样的,要更新父节点的关键码值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
           ____ ___ ___ 
| 20 | | |
|
-----------------------
| |
___ ____ ___ ____ ____ ____
| 9 | | | <=> | 20 | 25 | |

| 删除 9
v

____ ___ ___
| 25 | | |
|
-----------------------
| |
____ ___ ___ ____ ____ ____
| 20 | | | <=> | 25 | | |

3.3.3 叶子节点-合并下溢

如果兄弟节点借不了元素,那就将左子节点 + 右子节点合并在一起,并删除父节点元素

1
2
3
4
5
6
7
8
9
10
11
12
13
           ____ ___ ___ 
| 25 | | |
|
-----------------------
| |
____ ___ ___ ____ ____ ____
| 20 | | | <=> | 30 | | |

| 删除 30
v

____ ____ ___
| 20 | | |

注意:叶子节点的合并下溢,并不包括父节点元素值,只是左右叶子兄弟合并,因为内部节点不是数据节点。

3.3.4 内部节点-直接删除

叶子节点合并下溢后,父节点需要删除对应的父节点元素,如果父节点空间足够,就可以直接删除:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
                       ____ ____ ___ 
| 12 | 20 | |
|
-----------------------------------------------
| | |
___ ____ ____ ____ ____ ____ ____ ____ ____
| 9 | 10 | | <=> | 12 | 18 | | <=> | 20 | | |

| 删除 20
v

____ ____ ___
| 12 | | |
|
------------------------
| |
___ ____ ____ ____ ____ ___
| 9 | 10 | | <=> | 12 | 18 | |

删除 20 以后,会造成叶子节点合并下溢,父节点元素 20 被删除。

由于父节点 [12, 20] 空间足够,删除 20 后依旧满足要求,所以直接删除即可。

3.3.5 内部节点-从兄弟借

如果内部节点删除时,发现空间不足,则先考虑从兄弟节点借一个元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
                                             ____ ____ ____ 
| 20 | | |
|
------------------------------------------------------------
| |
____ ____ ___ ____ ___ ___
| 10 | 12 | | | 45 | | |
| |
---------------------------------------------- -------------------------
| | | | |
___ ___ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____
| 8 | 9 | | <=> | 10 | | | <=> | 12 | 18 | | <=> | 20 | 25 | | <=> | 85 | | |

| 删除 85
v

____ ____ ____
| 12 | | |
|
-----------------------------------------------
| |
____ ____ ___ ____ ___ ___
| 10 | | | | 20 | | |
| |
------------------------ ------------------------
| | | |
___ ___ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____
| 8 | 9 | | <=> | 10 | | | <=> | 12 | 18 | | <=> | 20 | 25 | |

删除元素 85,会造成叶子节点 [20, 25][85] 合并,并删除父节点元素 45

但是内部节点 [45] 由于空间不足,就需要从兄弟节点 [10, 12] 借一个元素过来才行。

不过,内部节点的借用和叶子节点的借用不同:

  • 内部节点借用,实际上是旋转操作,将兄弟节点旋到父节点,父节点旋到当前节点

上面借用的元素 12,实际会旋转到父节点,然后父节点 20 再旋转到右子节点中。

3.3.6 内部节点-合并下溢

如果内部节点的兄弟节点借不了元素,只能将父子节点进行合并,产生下溢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
                                  ____ ____ ____ 
| 12 | | |
|
-----------------------------------------------
| |
____ ____ ___ ____ ___ ___
| 10 | | | | 20 | | |
| |
------------------------ ------------------------
| | | |
___ ___ ____ ____ ____ ____ ____ ____ ____ ____ ____ ____
| 8 | 9 | | <=> | 10 | | | <=> | 12 | 18 | | <=> | 20 | | |

| 删除 20
v

____ ____ ___
| 10 | 12 | |
|
-------------------------------------------
| | |
___ ___ ____ ____ ____ ____ ____ ____ ____
| 8 | 9 | | <=> | 10 | | | <=> | 12 | 18 | |

删除元素 20,会造成叶子节点 [12, 18][20] 合并,并删除父节点元素 20

删除内部节点元素 20 时,由于其空间不足,且兄弟节点无法借用,只能合并父子节点,产生下溢了。

注意,内部节点的合并和叶子节点的合并不同:

  • 内部节点的合并下溢,包括了 父节点 + 左子节点 + 右子节点

即上面的父子节点 [12][10][20] 会合并起来,形成一个新节点。

下溢和上溢一样,也可能会持续下溢,一直持续到根节点,最终导致树的层级下降。

上述情况的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**- 删除元素 -*/
private BPTNode<K, V> remove(BPTNode<K, V> root, K key) {
int index = root.findIndex(key);
if (root.isLeaf()) {
BPTLeaf<K, V> leaf = (BPTLeaf<K, V>) root;
if (key.equals(root.getKey(index))) {
// 找到对应节点后移除元素
return leaf.delete(index);
} else {
// 没找到对应节点,直接返回
return leaf;
}
} else {
BPTInternal<K, V> node = (BPTInternal<K, V>) root;
// 在子树内递归移除元素
remove(node.getChild(index), key);
// 移除后可能需要父节点下溢
return node.underflow(index);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**- 叶子节点的下溢 -*/
public BPTNode<K, V> delete(K key, V value) {
int index = findIndex(key);

// 1. 空间足够,可以直接删除元素
if (this.isEnough()) {
removeEntry(index);
return this;
}

// 2. 从兄弟节点借一个元素
if (borrowLeft(index) || borrowRight(index)) {
return this;
}

// 3. 合并左右兄弟节点
return mergeNodes(index);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**- 内部节点的下溢 -*/
public BPTNode<K, V> underflow(int index) {
// 1. 空间足够,可以直接删除元素
if (this.isEnough()) {
removeEntry(index);
return this;
}

// 2. 从兄弟节点借一个元素
if (borrowLeft(index) || borrowRight(index)) {
return this;
}

// 3. 合并父元素 + 左右子节点
return mergeNodes(index);
}

总结

  • 性质
    • 内部节点只是索引,只有叶子节点才保存数据
    • 叶子节点之间构成了一条双向链表
    • 叶子节点包含了所有关键码值,而内部节点仅包含用于索引的关键码值
  • 检索
    • 多路查找:一直找到叶子节点
  • 插入
    1. 叶子节点空间足够,直接插入
    2. 叶子节点空间不足,分裂上溢(父节点关键码值等于右叶子节点的最小关键码值)
    3. 内部节点空间足够,直接插入
    4. 内部节点空间不足,分裂上溢(节点的关键码值都不一样)
  • 删除
    1. 叶子节点空间足够,直接删除
    2. 叶子节点空间不足,从兄弟借(移动)
    3. 叶子节点兄弟节点无法借用,合并左右叶子节点,删除父元素,执行下溢(仅合并兄弟叶子节点)
    4. 内部节点空间足够,直接删除
    5. 内部节点空间不足,从兄弟借(旋转)
    6. 内部节点兄弟节点无法借用,合并父子节点,执行下溢(合并父子节点)

参考

《数据结构与算法分析(第三版)》

https://www.cnblogs.com/nullzx/p/8729425.html

附录

B+ 树接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/**
* B+ 树接口定义
*
* @author weijiaduo
* @since 2023/1/9
*/
public interface BPTree<K extends Comparable<K>, V> {

/**
* 查找指定 key 的值
*
* @param key key
* @return value
*/
V get(K key);

/**
* 添加指定 key-value 的节点
*
* @param key key
* @param value value
*/
void put(K key, V value);

/**
* 删除指定 key 的节点
*
* @param key key
*/
void remove(K key);

}

B+ 树实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
/**
* B+ 树实现类
*
* @author weijiaduo
* @since 2023/1/9
*/
public class BPTreeImpl<K extends Comparable<K>, V> implements BPTree<K, V> {

/**
* 根节点
*/
private BPTNode<K, V> root;
/**
* m 阶 B+ 树
*/
private final int m;

/**
* B+树构造器
*
* @param m 指定 m 阶 B+ 树
*/
public BPTreeImpl(int m) {
this.m = m;
}

@Override
public V get(K key) {
return get(root, key);
}

/**
* 从当前节点开始找指定 key 的值
*
* @param root 当前根节点
* @param key key
* @return value
*/
private V get(BPTNode<K, V> root, K key) {
if (root == null) {
return null;
}

// 查找当前 key 所在的位置
int index = root.findIndex(key);
if (root.isLeaf()) {
if (key.equals(root.getKey(index))) {
BPTLeaf<K, V> leaf = (BPTLeaf<K, V>) root;
return leaf.getValue(index);
} else {
return null;
}
} else {
// 往子树继续找 key 对应的节点
return get(root.getChild(index), key);
}
}

@Override
public void put(K key, V value) {
root = put(root, key, value);
}

/**
* 在当前树内添加指定的 key-value
*
* @param root 当前根节点
* @param key key
* @param value value
* @return 新的根节点
*/
private BPTNode<K, V> put(BPTNode<K, V> root, K key, V value) {
// 1. 整棵树为空时,插入新节点
if (root == null) {
BPTLeaf<K, V> leaf = new BPTLeaf<>(m);
return leaf.add(key, value);
}

int index = root.findIndex(key);
if (root.isLeaf()) {
// 2. 叶子节点
BPTLeaf<K, V> leaf = (BPTLeaf<K, V>) root;

// 验证是否已存在,存在则只需更新值
if (key.equals(leaf.getKey(index))) {
leaf.setValue(index, value);
return leaf;
}

// 添加到叶子节点中,有可能发生了分裂
return leaf.add(key, value);
} else {
// 3. 内部节点,添加到子树的叶子节点中
BPTInternal<K, V> node = (BPTInternal<K, V>) root;
BPTNode<K, V> child = node.getChild(index);
child = put(child, key, value);

// 添加到子树中,有可能发生了分裂
return node.overflow(index, child);
}
}

@Override
public void remove(K key) {
root = remove(root, key);
if (root != null && root.isEmpty()) {
// 树的高度下降
root = root.firstChild();
}
}

/**
* 在当前树内删除指定 key 的节点
*
* @param root 当前根节点
* @param key key
* @return 新的根节点
*/
private BPTNode<K, V> remove(BPTNode<K, V> root, K key) {
if (root == null) {
return null;
}

int index = root.findIndex(key);
if (root.isLeaf()) {
BPTLeaf<K, V> leaf = (BPTLeaf<K, V>) root;
if (key.equals(root.getKey(index))) {
// 找到对应节点后移除元素
return leaf.delete(index);
} else {
// 没找到对应节点,直接返回
return leaf;
}
} else {
BPTInternal<K, V> node = (BPTInternal<K, V>) root;
// 在子树内递归移除元素
remove(node.getChild(index), key);
// 移除后可能需要父节点下溢
return node.underflow(index);
}
}

}

B+ 节点定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
/**
* B+树基类节点
*
* @author weijiaduo
* @since 2023/1/10
*/
public abstract class BPTNode<K extends Comparable<K>, V> {

/**
* m 阶 B 树
*/
protected final int m;
/**
* 节点元素阈值
*/
protected final int threshold;
/**
* 节点元素数组
* <p>
* 元素的第 0 位始终是一个占位元素,专门用于存放最左子节点
* <p>
* 所以一个节点能拥有的元素个数最多是 m - 1
*/
protected final Object[] elements;
/**
* 实际元素数量
* <p>
* 取值范围是 [0, m - 1]
*/
protected int size;

public BPTNode(int m) {
this.m = m;
this.threshold = half(m);
elements = new Object[m];
// 最左元素占位,用于存放最左子节点指针
elements[0] = new Entry<K, V>(null, null);
size = 0;
}

/**
* @return 元素数量
*/
public int size() {
return size;
}

/**
* 半数阈值 ceil(m / 2) - 1
*
* @param m 当前值
* @return 半数阈值
*/
protected int half(int m) {
return (m + 1) / 2 - 1;
}

/**
* 是否已满
*
* @return true已满/false未满
*/
public boolean isFull() {
return size == m - 1;
}

/**
* 是否为空
*
* @return true为空/false非空
*/
public boolean isEmpty() {
return size == 0;
}

/**
* 节点是否合法
* <p>
* 节点的元素数量要求大于等于阈值
*
* @return true合法/false非法
*/
public boolean isLegal() {
return size >= threshold;
}

/**
* 是否可借用元素给别的节点
* <p>
* 当节点的元素数量大于阈值时,才可以外借元素
*
* @return true可借用/false不可借用
*/
protected boolean canBorrow() {
return size > threshold;
}

/**
* 是否是叶子节点
* <p>
* 没有任何一个非空子节点时才是叶子节点
*
* @return true叶子节点/false内部节点
*/
public abstract boolean isLeaf();

/**
* 获取所有的元素
*
* @return 元素集合
*/
protected List<Entry<K, V>> entries() {
List<Entry<K, V>> entries = new ArrayList<>(size);
// 元素从索引 1 开始
for (int i = 1; i <= size; i++) {
entries.add(getEntry(i));
}
return entries;
}

/**
* 获取所有的 key
*
* @return key 集合
*/
public List<K> keys() {
List<K> keys = new ArrayList<>(size);
// 元素从索引 1 开始
for (int i = 1; i <= size; i++) {
keys.add(getKey(i));
}
return keys;
}

/**
* 获取指定位置的 key
*
* @param index 索引
* @return key
*/
public K getKey(int index) {
return getEntry(index).key;
}

/**
* 获取所有的子节点
* <p>
* 注意:是所有可能的子节点,也包括了 null 子节点
*
* @return 子节点集合
*/
public List<BPTNode<K, V>> children() {
List<BPTNode<K, V>> children = new ArrayList<>(size + 1);
// 子节点从索引 0 开始
for (int i = 0; i <= size; i++) {
children.add(getChild(i));
}
return children;
}

/**
* 返回第一个子节点
* <p>
* 注意:不是第一个非空子节点,而是第 1 个元素的左子节点
*
* @return 第一个子节点
*/
public BPTNode<K, V> firstChild() {
return getChild(0);
}

/**
* 返回最后一个子节点
* <p>
* 注意:不是最后一个非空子节点,而是最后 1 个元素的右子节点
*
* @return 最后一个子节点
*/
public BPTNode<K, V> lastChild() {
return getChild(size);
}

/**
* 获取指定位置的下一层子节点
*
* @param index 指定位置
* @return 下一层子树根节点
*/
public BPTNode<K, V> getChild(int index) {
return getEntry(index).pointer;
}

/**
* 更新指定位置的子节点
*
* @param index 索引
* @param node 子节点
*/
public void setChild(int index, BPTNode<K, V> node) {
getEntry(index).pointer = node;
}

/**
* 查找指定 key 的位置
* <p>
* 返回最后一个小于等于 key 的位置
*
* @param key key
* @return key 的位置
*/
public int findIndex(K key) {
// 第 0 位是占位元素,还没算在内
return 1 + binaryle(keys(), key);
}

/**
* 二分查找,返回最后一个小于等于 key 的位置
*
* @param keys key 集合
* @param key 指定 key
* @return 最后一个小于等于 key 的位置/-1
*/
protected int binaryle(List<K> keys, K key) {
int size = keys.size();
int l = 0, r = size - 1;
while (l <= r) {
int m = l + (r - l) / 2;
K k = keys.get(m);
if (k.compareTo(key) <= 0) {
if (m == size - 1 || keys.get(m + 1).compareTo(key) > 0) {
return m;
}
l = m + 1;
} else {
r = m - 1;
}
}
return -1;
}

/**
* 获取指定位置的元素
*
* @param index 索引
* @return 元素
*/
protected Entry<K, V> getEntry(int index) {
return (Entry<K, V>) elements[index];
}

/**
* 更新指定位置的元素
* <p>
* 要求原始元素必须存在,否则应该使用 {@code insertEntry()}
*
* @param index 索引
* @param entry 元素
*/
protected void setEntry(int index, Entry<K, V> entry) {
Entry<K, V> oldEntry = getEntry(index);
if (oldEntry == null || index == 0) {
throw new IllegalStateException(String.format("index: %d, size: %d", index, size));
}

// 保留子节点,只替换元素的 key-value
elements[index] = entry;
entry.pointer = oldEntry.pointer;
}

/**
* 追加新元素
* <p>
* 此方法不会产生节点分裂,需要分裂时应调用 {@code overflow()} 方法
*
* @param entry 元素
*/
protected void addEntry(Entry<K, V> entry) {
if (isFull()) {
throw new IllegalStateException(String.format("size: %d", size));
}

elements[++size] = entry;
}

/**
* 插入新元素
* <p>
* 此方法不会产生节点分裂,需要分裂时应调用 {@code overflow()} 方法
*
* @param index 插入索引
* @param entry 元素
*/
protected void insertEntry(int index, Entry<K, V> entry) {
if (isFull() || index == 0) {
throw new IllegalStateException(String.format("index: %d, size: %d", index, size));
}

if (size >= index) {
System.arraycopy(elements, index, elements, index + 1, size - index + 1);
elements[index] = entry;
size++;
} else {
elements[index] = entry;
size = index;
}
}

/**
* 移除指定位置的元素
*
* @param index 索引
*/
protected void removeEntry(int index) {
if (isEmpty() || index == 0) {
throw new IllegalStateException(String.format("index: %d, size: %d", index, size));
}

if (size > index) {
System.arraycopy(elements, index + 1, elements, index, size - index);
}
elements[size] = null;
size--;
}

@Override
public String toString() {
String[] s = new String[size];
for (int i = 1; i <= size; i++) {
s[i - 1] = elements[i].toString();
}
return "[" + String.join(", ", s) + "]";
}

/**
* 单个元素
*/
public static class Entry<K extends Comparable<K>, V> {
/**
* key
*/
K key;
/**
* value
*/
V value;
/**
* 右子节点指针,即大于 key 的子树
*/
BPTNode<K, V> pointer;

public Entry(K key, V value) {
this.key = key;
this.value = value;
pointer = null;
}

@Override
public String toString() {
return "{" + key + " : " + value + "}";
}
}

}

B+ 树内部节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
/**
* B+树内部节点
*
* @author weijiaduo
* @since 2023/1/10
*/
public class BPTInternal<K extends Comparable<K>, V> extends BPTNode<K, V> {

public BPTInternal(int m) {
super(m);
}

@Override
public boolean isLeaf() {
return false;
}

/**
* 节点上溢处理
* <p>
* 子树添加新元素后,可能会分裂成新节点,替代原有的子节点
*
* @param index 索引
* @param node 新节点
* @return 添加后的当前节点
*/
public BPTNode<K, V> overflow(int index, BPTNode<K, V> node) {
if (node == null || node.isEmpty()) {
return this;
}

// 还是旧节点,说明子节点没有上溢,无需处理
BPTNode<K, V> cur = getChild(index);
if (cur == node) {
return this;
}

// 1. 当前空间足够,可以直接插入新节点
if (size + node.size() < m) {
// 新子节点的左子节点替代旧子节点的位置
setChild(index, node.firstChild());
for (Entry<K, V> entry : node.entries()) {
insertEntry(++index, entry);
}
return this;
}

// 2. 当前空间不够,需要分裂成 3 个节点
return splitNodes(index, node);
}

/**
* 子节点上溢后,父节点已满,则需要分裂节点,将 1 个节点拆分成 3 个节点
*
* @param index 上溢子节点的父元素索引
* @param newNode 上溢的子节点
* @return 分裂后新的父节点
*/
private BPTNode<K, V> splitNodes(int index, BPTNode<K, V> newNode) {
// 新子节点的左子节点替代旧子节点的位置
setChild(index, newNode.firstChild());

int newSize = size + newNode.size();
List<Entry<K, V>> allEntries = new ArrayList<>(newSize);
List<Entry<K, V>> curEntries = entries();
for (int i = 0; i < index && i < size; i++) {
allEntries.add(curEntries.get(i));
}
allEntries.addAll(newNode.entries());
for (int i = index; i < size; i++) {
allEntries.add(curEntries.get(i));
}

// 根节点
int mid = newSize / 2;
BPTInternal<K, V> root = new BPTInternal<>(m);
for (int i = mid; i <= mid; i++) {
root.addEntry(allEntries.get(i));
}
// 左子节点
BPTInternal<K, V> left = new BPTInternal<>(m);
for (int i = 0; i < mid; i++) {
left.addEntry(allEntries.get(i));
}
// 右子节点(和叶子节点不同,右子节点不包括根节点元素)
BPTInternal<K, V> right = new BPTInternal<>(m);
for (int i = mid + 1; i < newSize; i++) {
right.addEntry(allEntries.get(i));
}

// 更新边界指针
left.setChild(0, firstChild());
right.setChild(0, root.lastChild());
root.setChild(0, left);
root.setChild(root.size, right);
return root;
}

/**
* 节点下溢处理
* <p>
* 存在非法子节点时,可能需要对父节点进行下溢操作
*
* @param index 父节点索引
* @return 根节点
*/
public BPTNode<K, V> underflow(int index) {
// 验证子节点是否合法,合法就不用下溢
BPTNode<K, V> cur = getChild(index);
if (cur == null || cur.isLegal()) {
return this;
}

// 1. 从兄弟节点借一个元素
BPTNode<K, V> left = null;
if (index > 0) {
left = getChild(index - 1);
if (left != null && left.canBorrow()) {
return borrowLeft(index);
}
}
BPTNode<K, V> right = null;
if (index < size) {
right = getChild(index + 1);
if (right != null && right.canBorrow()) {
return borrowRight(index);
}
}
if (left == null && right == null) {
return this;
}

// 2. 合并父元素 + 左右子节点
// 始终把当前节点作为合并时的右子节点
return mergeNodes(left != null ? index : index + 1);
}

/**
* 借用左子节点的值
* <p>
* 实际就是右旋,将父节点转到右子节点,左子节点的元素转到父节点
*
* @param index 父节点索引
*/
private BPTNode<K, V> borrowLeft(int index) {
BPTNode<K, V> left = getChild(index - 1);
BPTNode<K, V> right = getChild(index);
if (left.isLeaf()) {
// 叶子节点,父元素不是数据,不能旋转到叶子节点里面
// 左叶子节点转移到右叶子节点(叶子节点的子节点都为空,不需要更新)
Entry<K, V> leftEntry = left.getEntry(left.size);
left.removeEntry(left.size);
right.insertEntry(1, leftEntry);
// 更新父节点索引
setEntry(index, new Entry<>(leftEntry.key, null));
setChild(index, right);
} else {
// 内部节点,都是索引元素,直接旋转
Entry<K, V> parentEntry = getEntry(index);
Entry<K, V> leftEntry = left.getEntry(left.size);
BPTNode<K, V> leftRight = left.lastChild();

// 父节点转到右子节点
right.insertEntry(1, parentEntry);
right.setChild(1, right.firstChild());
// 左子节点转到父节点
left.removeEntry(left.size);
setEntry(index, leftEntry);
setChild(index, right);
right.setChild(0, leftRight);
}
return this;
}

/**
* 借用右子节点的值
* <p>
* 实际就是左旋,将父元素转到左子节点,右子节点的元素转到父节点
*
* @param index 父节点索引
*/
private BPTNode<K, V> borrowRight(int index) {
BPTNode<K, V> right = getChild(index + 1);
BPTNode<K, V> left = getChild(index);
if (right.isLeaf()) {
// 叶子节点,父元素不是数据,不能旋转到叶子节点里面
// 右叶子节点转移到左叶子子节点(叶子节点的子节点都为空,不需要更新)
Entry<K, V> rightEntry = right.getEntry(1);
right.removeEntry(1);
left.addEntry(rightEntry);
// 更新父节点索引
Entry<K, V> newRightEntry = right.getEntry(1);
setEntry(index + 1, new Entry<>(newRightEntry.key, null));
setChild(index + 1, right);
} else {
// 内部节点,都是索引元素,直接旋转
Entry<K, V> parentEntry = getEntry(index + 1);
Entry<K, V> rightEntry = right.getEntry(1);
BPTNode<K, V> rightLeft = right.firstChild();

// 父节点转到左子节点
left.addEntry(parentEntry);
// 右子节点转到父节点
right.setChild(0, right.getChild(1));
right.removeEntry(1);
setEntry(index + 1, rightEntry);
setChild(index + 1, right);
left.setChild(left.size, rightLeft);
}
return this;
}

/**
* 父节点元素 + 右子节点,全都合并到左子节点
* <p>
* 因为父节点元素和右子节点是一一对应的,所以都是按照“父 + 右 --> 左”进行合并
* <p>
* 方便同时删除父节点元素和右子节点
*
* @param index 父节点索引
*/
private BPTNode<K, V> mergeNodes(int index) {
BPTNode<K, V> left = getChild(index - 1);
BPTNode<K, V> right = getChild(index);

// 父节点元素合并到左子节点
if (!left.isLeaf()) {
// 子节点是叶子节点时,合并不要加上父元素
Entry<K, V> parentEntry = getEntry(index);
left.addEntry(parentEntry);
}

// 右子节点合并到左子节点
left.setChild(left.size, right.firstChild());
for (Entry<K, V> entry : right.entries()) {
left.addEntry(entry);
}

// 移除父节点元素
removeEntry(index);

// 更新叶子节点双向链表的指针
if (left.isLeaf()) {
BPTLeaf<K, V> leftLeaf = (BPTLeaf<K, V>) left;
BPTLeaf<K, V> rightLeaf = (BPTLeaf<K, V>) right;
BPTLeaf<K, V> rightNext = rightLeaf.getNext();
if (rightNext != null) {
rightNext.setPrev(leftLeaf);
}
leftLeaf.setNext(rightNext);
}
return this;
}

}

B+ 树叶子节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
/**
* B+树叶子节点
*
* @author weijiaduo
* @since 2023/1/10
*/
public class BPTLeaf<K extends Comparable<K>, V> extends BPTNode<K, V> {

/**
* 双向链表,上一个节点
*/
private BPTLeaf<K, V> prev;
/**
* 双向链表,下一个节点
*/
private BPTLeaf<K, V> next;

public BPTLeaf(int m) {
super(m);
}

@Override
public boolean isLeaf() {
return true;
}

/**
* 添加新元素
*
* @param key key
* @param value value
*/
public BPTNode<K, V> add(K key, V value) {
int index = findIndex(key);

// 1. 空间足够,可以直接插入新元素
if (!this.isFull()) {
insertEntry(index + 1, new Entry<>(key, value));
return this;
}

// 2. 空间不足,需要分裂节点
return splitNodes(index, new Entry<>(key, value));
}

/**
* 叶子节点已满,无法插入新元素时,需分裂成 3 个节点
*
* @param index 元素插入位置
* @param newEntry 新元素
* @return 分裂后的父节点
*/
private BPTNode<K, V> splitNodes(int index, Entry<K, V> newEntry) {
List<Entry<K, V>> curEntries = entries();
curEntries.add(index, newEntry);
int newSize = curEntries.size();
int mid = newSize / 2;

// 根节点(内部节点只需要索引就够了,不需要数据)
BPTNode<K, V> root = new BPTInternal<>(m);
Entry<K, V> entry = curEntries.get(mid);
root.addEntry(new Entry<>(entry.key, null));
// 左子节点
BPTLeaf<K, V> left = new BPTLeaf<>(m);
for (int i = 0; i < mid; i++) {
left.addEntry(curEntries.get(i));
}
// 右子节点(和内部节点不同,右子节点包括了根节点元素)
BPTLeaf<K, V> right = new BPTLeaf<>(m);
for (int i = mid; i < newSize; i++) {
right.addEntry(curEntries.get(i));
}
// 设置父子节点关系
root.setChild(0, left);
root.setChild(root.size, right);

// 更新叶子节点双向链表指针
left.prev = this.prev;
if (this.prev != null) {
this.prev.next = left;
}
left.next = right;
right.prev = left;
if (this.next != null) {
this.next.prev = right;
}
right.next = this.next;
this.prev = this.next = null;
return root;
}

/**
* 删除指定位置的元素
* <p>
* 删除元素后,节点可能会产生下溢,从而返回了一个空元素节点
*
* @param index 索引
* @return 删除后的根节点
*/
public BPTNode<K, V> delete(int index) {
// 叶子节点直接删除元素,删除后可能会变成元素为空的空节点
// 空节点会由内部节点处理掉,或者由根节点处理掉
removeEntry(index);
return this;
}

/**
* 获取指定位置的 value
*
* @param index 索引
* @return value
*/
public V getValue(int index) {
return getEntry(index).value;
}

/**
* 设置指定位置的 value
*
* @param index 索引
* @param value 值
*/
public void setValue(int index, V value) {
getEntry(index).value = value;
}

/**
* @return 上一个叶子节点
*/
public BPTLeaf<K, V> getPrev() {
return prev;
}

/**
* @return 下一个叶子节点
*/
public BPTLeaf<K, V> getNext() {
return next;
}

/**
* 更新上一个叶子节点
*
* @param prev 上一个叶子节点
*/
protected void setPrev(BPTLeaf<K, V> prev) {
this.prev = prev;
}

/**
* 更新下一个叶子节点
*
* @param next 下一个叶子节点
*/
protected void setNext(BPTLeaf<K, V> next) {
this.next = next;
}

}
作者

jiaduo

发布于

2023-01-16

更新于

2023-04-03

许可协议