B 树

ai 问题列表

请用中文回答:B-树历史

请用中文回答:100万的数据使用 avl 树来存储,树高是多少? 20

请用中文回答:100万的数据,如果存储到B-树(最小度数是500),那么树高大约是多少? 3

请用中文回答:B-树的特性有哪些?

概述

历史

B树(B-Tree)结构是一种高效存储和查询数据的方法,它的历史可以追溯到1970年代早期。B树的发明人Rudolf Bayer和Edward M. McCreight分别发表了一篇论文介绍了B树。这篇论文是1972年发表于《ACM Transactions on Database Systems》中的,题目为"Organization and Maintenance of Large Ordered Indexes"。

这篇论文提出了一种能够高效地维护大型有序索引的方法,这种方法的主要思想是将每个节点扩展成多个子节点,以减少查找所需的次数。B树结构非常适合应用于磁盘等大型存储器的高效操作,被广泛应用于关系数据库和文件系统中。

B树结构有很多变种和升级版,例如B+树,B*树和SB树等。这些变种和升级版本都基于B树的核心思想,通过调整B树的参数和结构,提高了B树在不同场景下的性能表现。

总的来说,B树结构是一个非常重要的数据结构,为高效存储和查询大量数据提供了可靠的方法。它的历史可以追溯到上个世纪70年代,而且在今天仍然被广泛应用于各种场景。

B 的含义

B-树的名称是由其发明者Rudolf Bayer提出的。Bayer和McCreight从未解释B代表什么,人们提出了许多可能的解释,比如Boeing、balanced、between、broad、bushy和Bayer等。但McCreight表示,越是思考B-trees中的B代表什么,就越能更好地理解B-trees

特性

一棵 B-树具有以下性质

特性1:每个节点 x 具有

  • 属性 n,表示节点 x 中 key 的个数
  • 属性 leaf,表示节点是否是叶子节点
  • 节点 key 可以有多个,以升序存储

特性2:每个非叶子节点中的孩子数是 n + 1、叶子节点没有孩子

特性3:最小度数t(节点的孩子数称为度)和节点中键数量的关系如下:

最小度数t 键数量范围
2 1 ~ 3
3 2 ~ 5
4 3 ~ 7
n (n-1) ~ (2n-1)

其中,当节点中键数量达到其最大值时,即 3、5、7 … 2n-1,需要分裂

特性4:叶子节点的深度都相同

问:

B-树为什么有最小度数的限制?

答:

B树中有最小度数的限制是为了保证B树的平衡特性。

在B树中,每个节点都可以有多个子节点,这使得B树可以存储大量的键值,但也带来了一些问题。如果节点的子节点数量太少,那么就可能导致B树的高度过高,从而降低了B树的效率。此外,如果节点的子节点数量太多,那么就可能导致节点的搜索、插入和删除操作变得复杂和低效。

最小度数的限制通过限制节点的子节点数量,来平衡这些问题。在B树中,每个节点的子节点数量都必须在一定的范围内,即t到2t之间(其中t为最小度数)

B-树与 2-3 树、2-3-4 树的关系

可以这样总结它们之间的关系:

  1. 2-3树是最小度数为2的B树,其中每个节点可以包含2个或3个子节点。
  2. 2-3-4树是最小度数为2的B树的一种特殊情况,其中每个节点可以包含2个、3个或4个子节点。
  3. B树是一种更加一般化的平衡树,可以适应不同的应用场景,其节点可以包含任意数量的键值,节点的度数取决于最小度数t的设定。

实现

定义节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static class Node {
boolean leaf = true;
int keyNumber;
int t;
int[] keys;
Node[] children;

public Node(int t) {
this.t = t;
this.keys = new int[2 * t - 1];
this.children = new Node[2 * t];
}

@Override
public String toString() {
return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));
}
}
  • leaf 表示是否为叶子节点
  • keyNumber 为 keys 中有效 key 数目
  • t 为最小度数,它决定了节点中key 的最小、最大数目,分别是 t-1 和 2t-1
  • keys 存储此节点的 key
  • children 存储此节点的 child
  • toString 只是为了方便调试和测试,非必须

实际 keys 应当改为 entries 以便同时保存 key 和 value,刚开始简化实现

多路查找

为上面节点类添加 get 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
Node get(int key) {
int i = 0;
while (i < keyNumber && keys[i] < key) {
i++;
}
if (i < keyNumber && keys[i] == key) {
return this;
}
if (leaf) {
return null;
}
return children[i].get(key);
}

插入 key 和 child

为上面节点类添加 insertKey 和 insertChild 方法

1
2
3
4
5
6
7
8
9
10
void insertKey(int key, int index) {
System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
keys[index] = key;
keyNumber++;
}

void insertChild(Node child, int index) {
System.arraycopy(children, index, children, index + 1, keyNumber - index);
children[index] = child;
}

作用是向 keys 数组或 children 数组指定 index 处插入新数据,注意

  • 由于使用了静态数组,并且不会在新增或删除时改变它的大小,因此需要额外的 keyNumber 来指定数组内有效 key 的数目
    • 插入时 keyNumber++
    • 删除时减少 keyNumber 的值即可
  • children 不会单独维护数目,它比 keys 多一个
  • 如果这两个方法同时调用,注意它们的先后顺序,insertChild 后调用,因为它计算复制元素个数时用到了 keyNumber

定义树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class BTree {
final int t;
final int MIN_KEY_NUMBER;
final int MAX_KEY_NUMBER;
Node root;

public BTree() {
this(2);
}

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

插入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void put(int key) {
doPut(null, 0, root, key);
}

private void doPut(Node parent, int index, Node node, int key) {
int i = 0;
while (i < node.keyNumber && node.keys[i] < key) {
i++;
}
if (i < node.keyNumber && node.keys[i] == key) {
return;
}
if (node.leaf) {
node.insertKey(key, i);
} else {
doPut(node, i, node.children[i], key);
}
if (isFull(node)) {
split(parent, index, node);
}
}
  • 首先查找本节点中的插入位置 i,如果没有空位(key 被找到),应该走更新的逻辑,目前什么没做
  • 接下来分两种情况
    • 如果节点是叶子节点,可以直接插入了
    • 如果节点是非叶子节点,需要继续在 children[i] 处继续递归插入
  • 无论哪种情况,插入完成后都可能超过节点 keys 数目限制,此时应当执行节点分裂
    • 参数中的 parent 和 index 都是给分裂方法用的,代表当前节点父节点,和分裂节点是第几个孩子

判断依据为:

1
2
3
boolean isFull(Node node) {
return node.keyNumber == MAX_KEY_NUMBER;
}

分裂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void split(Node parent, int index , Node left) {
if (parent == null) {
Node newRoot = new Node(this.t);
newRoot.leaf = false;
newRoot.insertChild(root, 0);
root = newRoot;
parent = newRoot;
}
Node right = new Node(this.t);
right.leaf = left.leaf;
right.keyNumber = t - 1;
System.arraycopy(left.keys, t, right.keys, 0, t - 1);
if (!left.leaf) {
System.arraycopy(left.children, t, right.children, 0, t);
}
left.keyNumber = t - 1;
int mid = left.keys[t - 1];
parent.insertKey(mid, index);
parent.insertChild(right, index + 1);

}

分两种情况:

  • 如果 parent == null 表示要分裂的是根节点,此时需要创建新根,原来的根节点作为新根的 0 孩子
  • 否则
    • 创建 right 节点(分裂后大于当前 left 节点的),把 t 以后的 key 和 child 都拷贝过去
    • t-1 处的 key 插入到 parent 的 index 处,index 指 left 作为孩子时的索引
    • right 节点作为 parent 的孩子插入到 index + 1 处

删除

case 1:当前节点是叶子节点,没找到

case 2:当前节点是叶子节点,找到了

case 3:当前节点是非叶子节点,没找到

case 4:当前节点是非叶子节点,找到了

case 5:删除后 key 数目 < 下限(不平衡)

case 6:根节点

完整代码

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
package com.itheima.algorithm.btree;

import java.util.Arrays;

/**
* <h3>B-树</h3>
*/
@SuppressWarnings("all")
public class BTree {

static class Node {
int[] keys; // 关键字
Node[] children; // 孩子
int keyNumber; // 有效关键字数目
boolean leaf = true; // 是否是叶子节点
int t; // 最小度数 (最小孩子数)

public Node(int t) { // t>=2
this.t = t;
this.children = new Node[2 * t];
this.keys = new int[2 * t - 1];
}

public Node(int[] keys) {
this.keys = keys;
}

@Override
public String toString() {
return Arrays.toString(Arrays.copyOfRange(keys, 0, keyNumber));
}

// 多路查找
Node get(int key) {
int i = 0;
while (i < keyNumber) {
if (keys[i] == key) {
return this;
}
if (keys[i] > key) {
break;
}
i++;
}
// 执行到此时 keys[i]>key 或 i==keyNumber
if (leaf) {
return null;
}
// 非叶子情况
return children[i].get(key);
}

// 向 keys 指定索引处插入 key
void insertKey(int key, int index) {
System.arraycopy(keys, index, keys, index + 1, keyNumber - index);
keys[index] = key;
keyNumber++;
}

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

int removeKey(int index) {
int t = keys[index];
System.arraycopy(keys, index + 1, keys, index, --keyNumber - index);
return t;
}

int removeLeftmostKey() {
return removeKey(0);
}

int removeRightmostKey() {
return removeKey(keyNumber - 1);
}

Node removeChild(int index) {
Node t = children[index];
System.arraycopy(children, index + 1, children, index, keyNumber - index);
children[keyNumber] = null;
return t;
}

Node removeLeftmostChild() {
return removeChild(0);
}

Node removeRightmostChild() {
return removeChild(keyNumber);
}

void moveToLeft(Node left) {
int start = left.keyNumber;
if (!leaf) {
for (int i = 0; i <= keyNumber; i++) {
left.children[start + i] = children[i];
}
}
for (int i = 0; i < keyNumber; i++) {
left.keys[left.keyNumber++] = keys[i];
}
}

Node leftSibling(int index) {
return index > 0 ? children[index - 1] : null;
}

Node rightSibling(int index) {
return index == keyNumber ? null : children[index + 1];
}
}

Node root;

int t; // 树中节点最小度数
final int MIN_KEY_NUMBER; // 最小key数目
final int MAX_KEY_NUMBER; // 最大key数目

public BTree() {
this(2);
}

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

// 1. 是否存在
public boolean contains(int key) {
return root.get(key) != null;
}

// 2. 新增
public void put(int key) {
doPut(root, key, null, 0);
}

private void doPut(Node node, int key, Node parent, int index) {
int i = 0;
while (i < node.keyNumber) {
if (node.keys[i] == key) {
return; // 更新
}
if (node.keys[i] > key) {
break; // 找到了插入位置,即为此时的 i
}
i++;
}
if (node.leaf) {
node.insertKey(key, i);
} else {
doPut(node.children[i], key, node, i);
}
if (node.keyNumber == MAX_KEY_NUMBER) {
split(node, parent, index);
}
}

/**
* <h3>分裂方法</h3>
*
* @param left 要分裂的节点
* @param parent 分裂节点的父节点
* @param index 分裂节点是第几个孩子
*/
void split(Node left, Node parent, int index) {
// 分裂的是根节点
if (parent == null) {
Node newRoot = new Node(t);
newRoot.leaf = false;
newRoot.insertChild(left, 0);
this.root = newRoot;
parent = newRoot;
}
// 1. 创建 right 节点,把 left 中 t 之后的 key 和 child 移动过去
Node right = new Node(t);
right.leaf = left.leaf;
System.arraycopy(left.keys, t, right.keys, 0, t - 1);
// 分裂节点是非叶子的情况
if (!left.leaf) {
System.arraycopy(left.children, t, right.children, 0, t);
for (int i = t; i <= left.keyNumber; i++) {
left.children[i] = null;
}
}
right.keyNumber = t - 1;
left.keyNumber = t - 1;
// 2. 中间的 key (t-1 处)插入到父节点
int mid = left.keys[t - 1];
parent.insertKey(mid, index);
// 3. right 节点作为父节点的孩子
parent.insertChild(right, index + 1);
}

// 3. 删除
public void remove(int key) {
doRemove(root, key, null, 0);
}

private void doRemove(Node node, int key, Node parent, int index) {
int i = 0;
while (i < node.keyNumber) {
if (node.keys[i] >= key) {
break;
}
i++;
}
if (node.leaf) {
if (notFound(node, key, i)) { // case 1
return;
}
node.removeKey(i); // case 2
} else {
if (notFound(node, key, i)) { // case 3
doRemove(node.children[i], key, node, i);
} else { // case 4
Node s = node.children[i + 1];
while (!s.leaf) {
s = s.children[0];
}
int k = s.keys[0];
node.keys[i] = k;
doRemove(node.children[i + 1], k, node, i + 1);
}
}
if (node.keyNumber < MIN_KEY_NUMBER) { // case 5
balance(node, parent, index);
}
}

private boolean notFound(Node node, int key, int i) {
return i >= node.keyNumber || (i < node.keyNumber && node.keys[i] != key);
}

private void balance(Node node, Node parent, int i) {
if (node == root) {
if (root.keyNumber == 0 && root.children[0] != null) {
root = root.children[0];
}
return;
}
Node leftSibling = parent.leftSibling(i);
Node rightSibling = parent.rightSibling(i);
if (leftSibling != null && leftSibling.keyNumber > MIN_KEY_NUMBER) {
rightRotate(node, leftSibling, parent, i);
return;
}
if (rightSibling != null && rightSibling.keyNumber > MIN_KEY_NUMBER) {
leftRotate(node, rightSibling, parent, i);
return;
}
if (leftSibling != null) {
mergeToLeft(leftSibling, parent, i - 1);
} else {
mergeToLeft(node, parent, i);
}
}


private void mergeToLeft(Node left, Node parent, int i) {
Node right = parent.removeChild(i + 1);
left.insertKey(parent.removeKey(i), left.keyNumber);
right.moveToLeft(left);
}

private void rightRotate(Node node, Node leftSibling, Node parent, int i) {
node.insertKey(parent.keys[i - 1], 0);
if (!leftSibling.leaf) {
node.insertChild(leftSibling.removeRightmostChild(), 0);
}
parent.keys[i - 1] = leftSibling.removeRightmostKey();
}

private void leftRotate(Node node, Node rightSibling, Node parent, int i) {
node.insertKey(parent.keys[i], node.keyNumber);
if (!rightSibling.leaf) {
node.insertChild(rightSibling.removeLeftmostChild(), node.keyNumber + 1);
}
parent.keys[i] = rightSibling.removeLeftmostKey();
}
}
红黑树 哈希表