【数据结构】B树与B+树
简介:本文主要介绍了B树和B+树的插入、删除操作。
一、B树
在1970年,Bayer&McCreight发表的论文《ORGANIZATION AND MAINTENANCE OF LARGE ORDERED INDICES 》(大型有序索引的组织和维护)中提出了一种新的数据结构来维护大型索引,这种数据结构在论文中称为B-Tree。
1.1 B树的定义
B树也称B-树,它是一颗多路平衡查找树。我们描述一颗B树时需要指定它的阶数,阶数表示了一个结点最多有多少个子结点,一般用字母m表示阶数。当m取2时,就是我们常见的二叉搜索树。
一颗m阶的B树定义如下:
- 每个结点最多有m-1个关键字。
- 根结点最少可以只有1个关键字。
- 非根结点至少有Math.ceil(m/2)-1个关键字。
- 每个结点中的关键字都按照从小到大的顺序排列,每个关键字的左子树中的所有关键字都小于它,而右子树中的所有关键字都大于它。
- 所有叶子结点都位于同一层,或者说根结点到每个叶子结点的长度都相同。
上图是一颗阶数为4的B树。在实际应用中的B树的阶数m都非常大(通常大于100),所以即使存储大量的数据,B树的高度仍然比较小。每个结点中存储了关键字(key)和关键字对应的数据(data),以及子结点的指针。 我们将一个key和其对应的data称为一个记录 。 但为了方便描述,除非特别说明,后续文中就用key来代替(key, value)键值对这个整体 。在数据库中我们将B树(和B+树)作为索引结构,可以加快查询速速,此时B树中的key就表示键,而data表示了这个键对应的条目在硬盘上的逻辑地址。
1.2 B树的插入操作
插入操作是指插入一条记录,即(key, value)的键值对。如果B树中已存在需要插入的键值对,则用需要插入的value替换旧的value。若B树不存在这个key,则一定是在叶子结点中进行插入操作。
- 根据要插入的key的值,找到叶子结点并插入。
- 判断当前结点key的个数是否小于等于m-1,若满足则结束,否则进行第3步。
- 以结点中间的key为中心分裂成左右两部分,然后将这个中间的key插入到父结点中,这个key的左子树指向分裂后的左半部分,这个key的右子支指向分裂后的右半部分,然后将当前结点指向父结点,继续进行第3步。
下面以5阶B树为例,介绍B树的插入操作,在5阶B树中,结点最多有4个key,最少有2个key
a).在空树中插入39
此时根结点就一个key,此时根结点也是叶子结点
b). 继续插入22,97和41
根结点此时有4个key
c). 继续插入53
插入后超过了最大允许的关键字个数4,所以以key值为41为中心进行分裂,结果如下图所示,分裂后当前结点指针指向父结点,满足B树条件,插入操作结束。当阶数m为偶数时,需要分裂时就不存在排序恰好在中间的key,那么我们选择中间位置的前一个key或中间位置的后一个key为中心进行分裂即可。
d). 依次插入13,21,40,同样会造成分裂,结果如下图所示。
e). 依次插入30,27,33;36,35,34;24,29,结果如下图所示。
f). 插入key值为26的记录,插入后的结果如下图所示。
当前结点需要以27为中心分裂,并向父结点进位27,然后当前结点指向父结点,结果如下图所示。
进位后导致当前结点(即根结点)也需要分裂,分裂的结果如下图所示。
分裂后当前结点指向新的根,此时无需调整。
g). 最后再依次插入key为17,28,29,31,32的记录,结果如下图所示。
在实现B树的代码中,为了使代码编写更加容易,我们可以将结点中存储记录的数组长度定义为m而非m-1,这样方便底层的结点由于分裂向上层插入一个记录时,上层有多余的位置存储这个记录。同时,每个结点还可以存储它的父结点的引用,这样就不必编写递归程序。
一般来说,对于确定的m和确定类型的记录,结点大小是固定的,无论它实际存储了多少个记录。但是分配固定结点大小的方法会存在浪费的情况,比如key为28,29所在的结点,还有2个key的位置没有使用,但是已经不可能继续在插入任何值了,因为这个结点的前序key是27,后继key是30,所有整数值都用完了。所以如果记录先按key的大小排好序,再插入到B树中,结点的使用率就会很低,最差情况下使用率仅为50%。
1.3 B树的删除操作
删除操作是指,根据key删除记录,如果B树中的记录中不存对应key的记录,则删除失败。
- 如果当前需要删除的key位于非叶子结点上,则用后继key(这里的后继key均指后继记录的意思)覆盖要删除的key,然后在后继key所在的子支中删除该后继key。此时后继key一定位于叶子结点上,这个过程和二叉搜索树删除结点的方式类似。删除这个记录后执行第2步
- 该结点key个数大于等于Math.ceil(m/2)-1,结束删除操作,否则执行第3步。
- 如果兄弟结点key个数大于Math.ceil(m/2)-1,则父结点中的key下移到该结点,兄弟结点中的一个key上移,删除操作结束。
否则,将父结点中的key下移与当前结点及它的兄弟结点中的key合并,形成一个新的结点。原父结点中的key的两个子指针就变成了一个子指针,指向这个新结点。然后当前结点的指针指向父结点,重复上第2步。
有些结点它可能即有左兄弟,又有右兄弟,那么我们任意选择一个兄弟结点进行操作即可。
下面以5阶B树为例,介绍B树的删除操作,5阶B树中,结点最多有4个key,最少有2个key
a). 原始状态
b). 在上面的B树中删除21,删除后结点中的关键字个数仍然大于等2,所以删除结束。
c). 在上述情况下接着删除27。从上图可知27位于非叶子结点中,所以用27的后继替换它。从图中可以看出,27的后继为28,我们用28替换27,然后在28(原27)的右子结点中删除28。删除后的结果如下图所示。
删除后发现,当前叶子结点的记录的个数小于2,而它的兄弟结点中有3个记录(当前结点还有一个右兄弟,选择右兄弟就会出现合并结点的情况,不论选哪一个都行,只是最后B树的形态会不一样而已),我们可以从兄弟结点中借取一个key。所以父结点中的28下移,兄弟结点中的26上移,删除结束。结果如下图所示。
d). 在上述情况下接着32,结果如下图。
当删除后,当前结点中只key,而兄弟结点中也仅有2个key。所以只能让父结点中的30下移和这个两个子结点中的key合并,成为一个新的结点,当前结点的指针指向父结点。结果如下图所示。
当前结点key的个数满足条件,故删除结束。
e). 上述情况下,我们接着删除key为40的记录,删除后结果如下图所示。
同理,当前结点的记录数小于2,兄弟结点中没有多余key,所以父结点中的key下移,和兄弟(这里我们选择左兄弟,选择右兄弟也可以)结点合并,合并后的指向当前结点的指针就指向了父结点。
同理,对于当前结点而言只能继续合并了,最后结果如下所示。
合并后结点当前结点满足条件,删除结束。
1.4 案例
import java.util.*;
/**
* B树实现
*
* @param <K>
* @param <V>
*/
public class BTree<K, V> {
/**
* 内部类,B树中节点中的元素
*
* @param <K> 键类型
* @param <V> 值类型,可以是指向数据的索引,也可以是实体数据
*/
private class Entry<K, V> {
private K key;
private V value;
public void setKey(K key) {
this.key = key;
}
public K getKey() {
return this.key;
}
public void setValue(V value) {
this.value = value;
}
public V getValue() {
return this.value;
}
@Override
public String toString() {
return "key: " + this.key + " , ";
}
}
/**
* 内部类,封装搜索结果
*
* @param <V>
*/
private class SearchResult<V> {
private boolean isExist;
private V value;
private int index;
//构造方法,将查询结果封装入对象
public SearchResult(boolean isExist, int index, V value) {
this.isExist = isExist;
this.index = index;
this.value = value;
}
public boolean isExist() {
return isExist;
}
public V getValue() {
return value;
}
public int getIndex() {
return index;
}
}
/**
* 树的节点
*
* @param <K>
* @param <V>
*/
public class Node<K, V> {
//节点内的项
private List<Entry<K, V>> entrys;
//节点的孩子节点们
private List<Node<K, V>> sons;
//是否是叶子节点
private boolean isLeaf;
//键值比较函数对象,如果采用倒序或者其它排序方式,传入该对象
private Comparator<K> kComparator;
//比较两个key,如果没有传入自定义排序方式则采用默认的升序
private int compare(K key1, K key2) {
return this.kComparator == null ? ((Comparable<K>) key2).compareTo(key1) : kComparator.compare(key1, key2);
}
//普通构造函数
Node() {
this.entrys = new LinkedList<Entry<K, V>>();
this.sons = new LinkedList<Node<K, V>>();
this.isLeaf = false;
}
//自定义K排序方式的构造函数
Node(Comparator<K> kComparator) {
this();
this.kComparator = kComparator;
}
public void setIsLeaf(boolean isLeaf) {
this.isLeaf = isLeaf;
}
public boolean getIsLeaf() {
return this.isLeaf;
}
//返回本节点的项数
public int nodeSize() {
return this.entrys.size();
}
/**
* 在本节点内查找元素, 本质就是一个有序数组的二分查找
*
* @param key 待查找元素的key值
* @return 查找结果封装入 SearchResult
*/
public SearchResult<V> search(K key) {
int begin = 0;
int end = this.nodeSize() - 1;
// if (end == 0) {
// return new SearchResult<V>(false, 0, null);
// }
int mid = (begin + end) / 2;
boolean isExist = false;
int index = 0;
V value = null;
//二分查找
while (begin < end) {
mid = (begin + end) / 2;
Entry midEntry = this.entrys.get(mid);
int compareRe = compare((K) midEntry.getKey(), key);
//找到了
if (compareRe == 0) {
break;
} else {
if (compareRe > 0) {
//在中点右边
begin = mid + 1;
} else {
end = mid - 1;
}
}
}
//二分查找结束,判断结果;三个元素以上才是正经二分,只有两个或一个元素属于边界条件要着重考虑
if (begin < end) {
//找到了
isExist = true;
index = mid;
value = this.entrys.get(mid).getValue();
} else if (begin == end) {
K midKey = this.entrys.get(begin).getKey();
int comRe = compare(midKey, key);
if (comRe == 0) {
isExist = true;
index = begin;
value = this.entrys.get(mid).getValue();
} else if (comRe > 0) {
isExist = false;
index = begin + 1;
value = null;
} else {
isExist = false;
index = begin;
value = null;
}
} else {
isExist = false;
index = begin;
value = null;
}
return new SearchResult<V>(isExist, index, value);
}
//删除给定索引位置的项
public Entry<K, V> removeEntry(int index) {
Entry<K, V> re = this.entrys.get(index);
this.entrys.remove(index);
return re;
}
//得到index处的项
public Entry<K, V> entryAt(int index) {
return this.entrys.get(index);
}
//将新项插入指定位置
private void insertEntry(Entry<K, V> entry, int index) {
this.entrys.add(index, entry);
}
//节点内插入项
private boolean insertEntry(Entry<K, V> entry) {
SearchResult<V> result = search(entry.getKey());
if (result.isExist()) {
return false;
} else {
insertEntry(entry, result.getIndex());
return true;
}
}
//更新项,如果项存在,更新其值并返回原值,否则直接插入
public V putEntry(Entry<K, V> entry) {
SearchResult<V> re = search(entry.getKey());
if (re.isExist) {
Entry oldEntry = this.entrys.get(re.getIndex());
V oldValue = (V) oldEntry.getValue();
oldEntry.setValue(entry.getValue());
return oldValue;
} else {
insertEntry(entry);
return null;
}
}
//获得指定索引的子节点
public Node childAt(int index) {
return this.sons.get(index);
}
//删除给定索引的子节点
public void removeChild(int index) {
this.sons.remove(index);
}
//将新的子节点插入到指定位置
public void insertChild(Node<K, V> child, int index) {
this.sons.add(index, child);
}
}
//度数T,不传入则默认为 2-3 树
private Integer DEFAULT_T = 2;
//根节点
private Node<K, V> root;
private int t = DEFAULT_T;
//非根节点的最小项数,体现的是除了根节点,其余节点都是分裂而来的!
private int nodeMinSize = t - 1;
//节点的最大项数
private int nodeMaxSize = 2 * t - 1;
//比较函数对象
private Comparator<K> kComparator;
//构造一棵自然排序的B树
BTree() {
Node<K, V> root = new Node<K, V>();
this.root = root;
root.setIsLeaf(true);
}
//构造一棵度为 t 的B树
BTree(int t) {
this();
this.t = t;
nodeMinSize = t - 1;
nodeMaxSize = 2 * t - 1;
}
//构造一棵按给定排序方式排序,且度为 t 的B树
BTree(Comparator<K> com, int t) {
this(t);
this.kComparator = com;
}
//在以root为根的树内搜索key项
private V search(Node<K, V> root, K key) {
SearchResult<V> re = root.search(key);
if (re.isExist) {
return re.value;
} else {
//回归条件
if (root.isLeaf) {
return null;
}
int index = re.index;
//递归搜索子节点
return (V) search(root.childAt(index), key);
}
}
public V search(K key) {
return search(this.root, key);
}
/**
* 满子节点的分裂过程:从中间节点断开,后半部分形成新结点插入父节点。若分裂节点不是叶子节点,将子节点一并分裂到新节点
*
* @param fatherNode 待分裂节点的父节点
* @param splitNode 待分裂节点
* @param index 待分裂节点在父节点中的索引
*/
private void splitNode(Node<K, V> fatherNode, Node<K, V> splitNode, int index) {
//分裂产生的新节点
Node<K, V> newNode = new Node<K, V>(this.kComparator);
//如果原节点为叶子节点,那么新节点也是
newNode.setIsLeaf(splitNode.isLeaf);
//将 t到2*t-2 项迁移到新节点
for (int i = t; i < this.nodeMaxSize; i++) {
newNode.entrys.add(splitNode.entrys.get(i));
}
//中间节点向上融合到父节点的 index+1
Entry<K, V> midEntry = splitNode.entrys.get(t - 1);
for (int i = this.nodeMaxSize - 1; i >= t - 1; i--) {
//删除原节点中已迁移的项,删除时注意从尾部向前删除
splitNode.entrys.remove(i);
}
//如果分裂的节点不是叶子节点,子节点一并跟随分裂
if (!splitNode.getIsLeaf()) {
for (int i = t; i < this.nodeMaxSize + 1; i++) {
newNode.sons.add(splitNode.sons.get(i));
}
//删除时注意从尾部向前删除
for (int i = this.nodeMaxSize; i >= t; i--) {
splitNode.sons.remove(i);
}
}
//父节点插入分裂的中间元素,分裂出的新节点加入父节点的 sons
fatherNode.insertEntry(midEntry);
fatherNode.insertChild(newNode, index + 1);
}
/**
* 插入一个非满节点:一路向下寻找插入位置。
* 在寻找的路径上,如果碰到大小为2t-1的节点,分裂并向上融合。
* 每次插入都从叶子节点插入,通过分裂将插入动作向上反馈,直到融合到根节点,只有由根节点的分裂
* 才能增加整棵树的高度,从而维持树的平衡。
* 树在一开始就是平衡的(只有根),整棵树的高度增加必须由根节点的分裂引发,从而高度增加后还是平衡的
* 因为没次检查子节点前如果子节点满了会先分裂,所以除根节点外,其余节点被其子节点向上融合均不会导致节点满
* 仅插入一个元素的情况下,每个节点最多经历一次子节点的分裂
*
* @param root 当前节点
* @param entry 待插入元素
* @return
*/
private boolean insertNotFull(Node<K, V> root, Entry<K, V> entry) {
if (root.getIsLeaf()) {
//到达叶子节点,直接插入
return root.insertEntry(entry);
}
SearchResult<V> re = root.search(entry.getKey());
if (re.isExist) {
//已存在key,直接返回
return false;
}
int index = re.getIndex();
Node<K, V> searchChild = root.childAt(index);
//待查询子节点已满,分裂后再判断该搜索哪个子节点
if (searchChild.nodeSize() == 2 * t - 1) {
splitNode(root, searchChild, index);
if (root.compare(root.entryAt(index).getKey(), entry.getKey()) > 0) {
searchChild = root.childAt(index + 1);
}
}
return insertNotFull(searchChild, entry);
}
//插入一个新节点
public boolean insertNode(Entry<K, V> entry) {
//根节点满了,先分裂根节点
if (root.nodeSize() == 2 * t - 1) {
Node<K, V> newRoot = new Node<K, V>();
newRoot.setIsLeaf(false);
newRoot.insertChild(root, 0);
splitNode(newRoot, root, 0);
this.root = newRoot;
}
return insertNotFull(root, entry);
}
/**
* 如果Key已存在,更新value,否则直接插入entry
*
* @param root
* @param entry
* @return
*/
private V putNotFull(Node<K, V> root, Entry<K, V> entry) {
assert root.nodeSize() < nodeMaxSize;
if (root.isLeaf) {
return root.putEntry(entry);
}
SearchResult<V> re = root.search(entry.getKey());
if (re.isExist) {
//如果存在,则更新
root.entryAt(re.index).setValue(entry.getValue());
return re.value;
}
//如果不存在,继续向下搜素,先判断子节点是否需要分裂
Node<K, V> searchChild = root.childAt(re.index);
if (searchChild.nodeSize() == 2 * t - 1) {
splitNode(root, searchChild, re.index);
if (root.compare(entry.getKey(), root.entryAt(re.index).getKey()) > 0) {
searchChild = root.childAt(re.index + 1);
}
}
return putNotFull(searchChild, entry);
}
// 如果树中已存在 key 则更新并返回原 value,否则插入并返回null
public V put(Entry<K, V> entry) {
//如果根节点已满,先分裂根节点
if (this.root.nodeSize() == nodeMaxSize) {
Node<K, V> newRoot = new Node<K, V>(kComparator);
newRoot.setIsLeaf(false);
newRoot.insertChild(root, 0);
splitNode(newRoot, root, 0);
this.root = newRoot;
}
return putNotFull(root, entry);
}
private Entry<K, V> delete(Node<K, V> root, Entry<K, V> entry) {
SearchResult<V> re = root.search(entry.getKey());
if (re.isExist()) {
//回归条件,如果是叶子节点中的元素,直接删除
if (root.getIsLeaf()) {
return root.removeEntry(re.getIndex());
}
//如果不是叶子节点,判断应将待删除节点交换到左子节点还是右子节点
Node<K, V> leftChild = root.childAt(re.getIndex());
//如果左子节点包含多于 t-1 个项,转移到左子节点删除
if (leftChild.nodeSize() >= t) {
//删除过程为,将待删除项与其左子节点最后一项互换,并递归互换下去,直到将待删除节点换到叶子节点后删除
root.removeEntry(re.getIndex());
root.insertEntry(leftChild.entryAt(leftChild.nodeSize() - 1), re.getIndex());
leftChild.removeEntry(leftChild.nodeSize() - 1);
leftChild.insertEntry(entry);
return delete(leftChild, entry);
}
//左子节点不可删除项,则同样逻辑检查右子节点
Node<K, V> rightChild = root.childAt(re.getIndex() + 1);
if (rightChild.nodeSize() >= t) {
root.removeEntry(re.getIndex());
root.insertEntry(rightChild.entryAt(0), re.getIndex());
rightChild.removeEntry(0);
rightChild.insertEntry(entry);
return delete(rightChild, entry);
}
//如果左右子节点均不能删除项,将左右子节点合并,并将删除项放到新节点的合并连接处
Entry<K, V> deletedEntry = root.removeEntry(re.getIndex());
leftChild.insertEntry(deletedEntry);
root.removeChild(re.getIndex() + 1);
//左右子节点合并
for (int i = 0; i < rightChild.nodeSize(); i++) {
leftChild.insertEntry(rightChild.entryAt(i));
}
//右子节点存在子节点,则子节点也合并入左子节点子节点集合
if (!rightChild.getIsLeaf()) {
for (int i = 0; i < rightChild.sons.size(); i++) {
leftChild.insertChild(rightChild.childAt(i), leftChild.sons.size());
}
}
//合并后继续向左递归
return delete(leftChild, entry);
} else {//删除节点不在本节点
//回归条件,搜索到叶节点依然没找到,待删除节点不在树中
if (root.getIsLeaf()) {
for (int i = 0; i < root.nodeSize(); i++) {
System.out.print("++++++++++++++++++++");
System.out.print(root.entryAt(i).getKey() + ",");
System.out.print("++++++++++++++++++++");
}
throw new RuntimeException(entry.key + " is not in this tree!");
}
Node<K, V> searchChild = root.childAt(re.index);
//子节点可删除项,递归删除
if (searchChild.nodeSize() >= t) {
return delete(searchChild, entry);
}
//待旋转节点,子节点项数小于等于 t-1 ,不能删除项,准备左旋或右旋为其补充项数
Node<K, V> siblingNode = null;
int siblingIndex = -1;
//存在右兄弟
if (re.getIndex() < root.nodeSize() - 1) {
Node<K, V> rightBrother = root.childAt(re.getIndex() + 1);
if (rightBrother.nodeSize() >= t) {
siblingNode = rightBrother;
siblingIndex = re.getIndex() + 1;
}
}
//不存在右兄弟则尝试左兄嘚
if (siblingNode == null) {
if (re.getIndex() > 0) {
//尝试左兄弟节点
Node<K, V> leftBrothr = root.childAt(re.getIndex() - 1);
if (leftBrothr.nodeSize() >= t) {
siblingNode = leftBrothr;
siblingIndex = re.getIndex() - 1;
}
}
}
//至少有一个兄弟可以匀出项来
if (siblingNode != null) {
//是左兄嘚
if (siblingIndex < re.getIndex()) {
//左节点最后一项右旋
searchChild.insertEntry(root.entryAt(siblingIndex), 0);
root.removeEntry(siblingIndex);
root.insertEntry(siblingNode.entryAt(siblingNode.nodeSize() - 1), siblingIndex);
siblingNode.removeEntry(siblingNode.nodeSize() - 1);
//子节点跟着右旋
if (!siblingNode.getIsLeaf()) {
searchChild.insertChild(siblingNode.childAt(siblingNode.sons.size() - 1), 0);
siblingNode.removeChild(siblingNode.sons.size() - 1);
}
} else {
//是右兄嘚
searchChild.insertEntry(root.entryAt(re.getIndex()), searchChild.nodeSize() - 1);
root.removeEntry(re.getIndex());
root.insertEntry(siblingNode.entryAt(0), re.getIndex());
siblingNode.removeEntry(0);
if (!siblingNode.getIsLeaf()) {
searchChild.insertChild(siblingNode.childAt(0), searchChild.sons.size());
siblingNode.removeChild(0);
}
}
return delete(searchChild, entry);
}
//左右兄嘚都匀不出项来,直接由左右兄嘚节点与父项合并为一个节点
if (re.getIndex() <= root.nodeSize() - 1) {
Node<K, V> rightSon = root.childAt(re.getIndex() + 1);
searchChild.insertEntry(root.entryAt(re.getIndex()), searchChild.nodeSize());
root.removeEntry(re.getIndex());
root.removeChild(re.getIndex() + 1);
for (int i = 0; i < rightSon.nodeSize(); i++) {
searchChild.insertEntry(rightSon.entryAt(i));
}
if (!rightSon.getIsLeaf()) {
for (int j = 0; j < rightSon.sons.size(); j++) {
searchChild.insertChild(rightSon.childAt(j), searchChild.sons.size());
}
}
if (root == this.root) {
this.root = searchChild;
}
} else {
//没有右兄弟,试试左兄嘚
Node<K, V> leftSon = root.childAt(re.getIndex() - 1);
searchChild.insertEntry(root.entryAt(re.getIndex() - 1), 0);
root.removeChild(re.getIndex() - 1);
root.removeEntry(re.getIndex() - 1);
for (int i = 0; i < leftSon.nodeSize(); i++) {
searchChild.insertEntry(leftSon.entryAt(i));
}
if (!leftSon.getIsLeaf()) {
for (int i = leftSon.sons.size() - 1; i >= 0; i--) {
searchChild.insertChild(leftSon.childAt(i), 0);
}
}
if (root == this.root) {
this.root = searchChild;
}
}
// if (root == this.root && root.nodeSize() == 0) {
// root = searchChild;
// }
return delete(searchChild, entry);
}
}
public Entry<K, V> delete(K key) {
Entry<K, V> en = new Entry<K, V>();
en.setKey(key);
return delete(root, en);
}
/**
* 借助队列打印B树
*/
public void output() {
Queue<Node<K, V>> queue = new LinkedList<Node<K, V>>();
queue.offer(this.root);
while (!queue.isEmpty()) {
Node<K, V> node = queue.poll();
for (int i = 0; i < node.nodeSize(); ++i) {
System.out.print(node.entryAt(i) + " ");
}
System.out.println();
if (!node.getIsLeaf()) {
for (int i = 0; i <= node.sons.size() - 1; ++i) {
queue.offer(node.childAt(i));
}
}
}
}
public static void main(String[] args) {
Random random = new Random();
BTree<Integer, Integer> btree = new BTree<Integer, Integer>(3);
List<Integer> save = new ArrayList<Integer>(30);
// save.add(8290);
// save.add(7887);
// save.add(9460);
// save.add(9928);
// save.add(6127);
// save.add(5891);
// save.add(1592);
// save.add(14);
// save.add(8681);
// save.add(4843);
// save.add(1051);
for (int i = 0; i < 20; ++i) {
int r = random.nextInt(10000);
save.add(r);
System.out.print(r + " ");
BTree.Entry en = btree.new Entry<Integer, Integer>();
en.setKey(r);
en.setValue(r);
// BTree.Entry en = btree.new Entry<Integer, Integer>();
// en.setKey(save.get(i));
btree.insertNode(en);
}
System.out.println("----------------------");
btree.output();
System.out.println("----------------------");
btree.delete(save.get(0));
btree.output();
}
}
二、B+树
2.1 B+树的定义
各种资料上B+树的定义各有不同,一种定义方式是关键字个数和子结点个数相同。这里我们采取维基百科上所定义的方式,即关键字个数比子结点个数小1,这种方式是和B树基本等价的。上图就是一颗阶数为4的B+树。
除此之外B+树还有以下的要求。
- B+树包含2种类型的结点:内部结点(也称索引结点)和叶子结点。根结点本身即可以是内部结点,也可以是叶子结点。根结点的关键字个数最少可以只有1个。
- B+树与B树最大的不同是内部结点不保存数据,只用于索引,所有数据(或者说记录)都保存在叶子结点中。
- m阶B+树表示了内部结点最多有m-1个关键字(或者说内部结点最多有m个子树),阶数m同时限制了叶子结点最多存储m-1个记录。
- 内部结点中的key都按照从小到大的顺序排列,对于内部结点中的一个key,左树中的所有key都小于它,右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。
- 每个叶子结点都存有相邻叶子结点的指针,叶子结点本身依关键字的大小自小而大顺序链接。
2.2 B+树的插入操作
- 若为空树,创建一个叶子结点,然后将记录插入其中,此时这个叶子结点也是根结点,插入操作结束。
- 针对叶子类型结点:根据key值找到叶子结点,向这个叶子结点插入记录。插入后,若当前结点key的个数小于等于m-1,则插入结束。否则将这个叶子结点分裂成左右两个叶子结点,左叶子结点包含前m/2个记录,右结点包含剩下的记录,将第m/2+1个记录的key进位到父结点中(父结点一定是索引类型结点),进位到父结点的key左子指针向左结点,右子指针向右结点。将当前结点的指针指向父结点,然后执行第3步。
- 针对索引类型结点:若当前结点key的个数小于等于m-1,则插入结束。否则,将这个索引类型结点分裂成两个索引结点,左索引结点包含前(m-1)/2个key,右结点包含m-(m-1)/2个key,将第m/2个key进位到父结点中,进位到父结点的key左子指向左结点,进位到父结点的key右子指向右结点。将当前结点的指针指向父结点,然后重复第3步。
下面是一颗5阶B树的插入过程,5阶B数的结点最少2个key,最多4个key。
a). 空树中插入5
b). 依次插入8,10,15
c). 插入16
插入16后超过了关键字的个数限制,所以要进行分裂。在叶子结点分裂时,分裂出来的左结点2个记录,右边3个记录,中间key成为索引结点中的key,分裂后当前结点指向了父结点(根结点)。结果如下图所示。
当然我们还有另一种分裂方式,给左结点3个记录,右结点2个记录,此时索引结点中的key就变为15。
d). 插入17
e). 插入18,插入后如下图所示
当前结点的关键字个数大于5,进行分裂。分裂成两个结点,左结点2个记录,右结点3个记录,关键字16进位到父结点(索引类型)中,将当前结点的指针指向父结点。
当前结点的关键字个数满足条件,插入结束。
f). 插入若干数据后
g). 在上图中插入7,结果如下图所示
当前结点的关键字个数超过4,需要分裂。左结点2个记录,右结点3个记录。分裂后关键字7进入到父结点中,将当前结点的指针指向父结点,结果如下图所示。
当前结点的关键字个数超过4,需要继续分裂。左结点2个关键字,右结点2个关键字,关键字16进入到父结点中,将当前结点指向父结点,结果如下图所示。
当前结点的关键字个数满足条件,插入结束。
2.3 B+树的删除操作
如果叶子结点中没有相应的key,则删除失败。否则执行下面的步骤
- 删除叶子结点中对应的key。删除后若结点的key的个数大于等于Math.ceil(m-1)/2 – 1,删除操作结束,否则执行第2步。
- 若兄弟结点key有富余(大于Math.ceil(m-1)/2 – 1),向兄弟结点借一个记录,同时用借到的key替换父结(指当前结点和兄弟结点共同的父结点)点中的key,删除结束。否则执行第3步。
- 若兄弟结点中没有富余的key,则当前结点和兄弟结点合并成一个新的叶子结点,并删除父结点中的key(父结点中的这个key两边的子指针就变成了一个指针,正好指向这个新的叶子结点),将当前结点指向父结点(必为索引结点),执行第4步(第4步以后的操作和B树就完全一样了,主要是为了更新索引结点)。
- 若索引结点的key的个数大于等于Math.ceil(m-1)/2 – 1,则删除操作结束。否则执行第5步
- 若兄弟结点有富余,父结点key下移,兄弟结点key上移,删除结束。否则执行第6步
- 当前结点和兄弟结点及父结点下移key合并成一个新的结点。将当前结点指向父结点,重复第4步。
注意,通过B+树的删除操作后,索引结点中存在的key,不一定在叶子结点中存在对应的记录。
下面是一颗5阶B树的删除过程,5阶B数的结点最少2个key,最多4个key。
a). 初始状态
b). 删除22,删除后结果如下图
删除后叶子结点中key的个数大于等于2,删除结束
c). 删除15,删除后的结果如下图所示
删除后当前结点只有一个key,不满足条件,而兄弟结点有三个key,可以从兄弟结点借一个关键字为9的记录,同时更新将父结点中的关键字由10也变为9,删除结束。
d). 删除7,删除后的结果如下图所示
当前结点关键字个数小于2,(左)兄弟结点中的也没有富余的关键字(当前结点还有个右兄弟,不过选择任意一个进行分析就可以了,这里我们选择了左边的),所以当前结点和兄弟结点合并,并删除父结点中的key,当前结点指向父结点。
此时当前结点的关键字个数小于2,兄弟结点的关键字也没有富余,所以父结点中的关键字下移,和两个子结点合并,结果如下图所示。
参考文章
https://www.cnblogs.com/jeasonit/p/12330125.html
https://www.cnblogs.com/niuyourou/p/12367242.html
https://www.cnblogs.com/skyice/p/10624876.html