一.简介
B+tree是B树的扩展,叶子结点以上各层存储索引使用(也称为索引节点),B+tree的一种定义方式是关键字个数(key)和孩子结点个数相同;另一种同B-tree相同,即n个关键字有n+1个孩子节点,这种方式是和B树基本等价。
B-tree 数据和索引遍布整棵树,B+tree 只有叶子节点才存储数据和索引,非叶子节点只存储索引
B+tree叶子节点使用指针连接成有序链表,便于范围查询,而B-tree 范围检索只能中序遍历
B+tree插入数据:
同B-tree基本一样,只是叶子节点分裂为子节点时子节点数据项个数之和等于分裂前数据项个数(即子节点数据项个数之和为树的阶,转移给父节点的只是作为索引使用,故子节点应当包含完整的数据。而B-tree每个节点都是数据和索引,故B-tree中分裂后的子节点的数据项个数之和等于m-1)
B+tree删除数据:节点平衡操作分为叶子节点平衡操作和索引节点平衡操作
1.叶子节点没有待删除数据,结束操作,否则删除叶子节点包含的数据执行2
2.删除数据的叶子节点数据项大于等于ceil((m-1)/2)-1 则结束操作
3.删除数据的叶子节点的兄弟节点的数据项大于ceil((m-1)/2)-1,向兄弟节点借一条数据更新父节点索引并添加到删除数据的叶子节点,结束操作
4.删除数据的叶子节点的兄弟节点数据项小于等于ceil((m-1)/2)-1,与其中一个兄弟节点合并为一个节点,并从父节点删除对应的索引和一个子节点路径,父节点递归处理索引节点平衡(合并操作减少了父节点索引,可能导致父节点不平衡)。索引节点平衡处理同B-tree删除数据平衡处理相同(5-6-7都是处理索引节点平衡)
5.索引结点key的个数大于等于ceil((m-1)/2)-1,则操作结束
6.索引节点的兄弟节点的key个数大于ceil((m-1)/2)-1,与B-tree操作相同,索引节点的父节点索引key移动到索引节点(意味着索引节点需要增加一条索引节点的兄弟节点的子节点指针路径),索引节点的兄弟节点key移动到父节点
7.索引节点的兄弟节点的key个数小于等于ceil((m-1)/2)-1,索引节点key、索引节点父节点的一个key、索引节点兄弟节点key合并为一个索引节点,索引节点的父节点的一个key参与合并后可能导致key小于ceil((m-1)/2)-1,递归处理索引节点的父节点
二.实现
package com.vincent;
import lombok.Data;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class BPlusTree<T extends Comparable<T>> {
public static void main(String[] args) {
BPlusTree<Integer> tree = new BPlusTree<>(5);
List<Integer> datas = Arrays.asList(4,3,6,5,7,8,9,10,11,12,13);
for(int i=0;i<datas.size();i++){
tree.add(datas.get(i));
}
System.out.println(datas);
System.out.println(tree.infixEach());
for(Integer data : Arrays.asList(11,10,9,4,3)){
tree.remove(data);
System.out.println(tree.infixEach());
}
}
@Data
static class Node<T extends Comparable>{
List<T> items = new ArrayList<>();
List<Node<T>> children = new ArrayList<>();
Node<T> parent,next,prev;
@Override
public String toString() {
return items.toString();
}
}
//根节点
private Node<T> root,headLeaf;
//树的阶
private int degree;
public BPlusTree(int degree){
this.degree = degree;
}
public void add(T item){
if(root==null){
Node<T> node = new Node<>();
node.items.add(item);
root = node;
this.headLeaf = root;
}else {
Node<T> node = this.searchInsertionNode(item,root);
node.items.add(item);
this.rebalance(node);
}
}
private Node<T> searchInsertionNode(T item,Node<T> node){
if(node.children.size() == 0){
return node;
}
for(int i=0;i<node.items.size();i++){
if(node.items.get(i).compareTo(item) >= 0){
return this.searchInsertionNode(item,node.children.get(i));
}
}
return this.searchInsertionNode(item,node.children.get(node.children.size()-1));
}
public List<T> infixEach(){
List<T> dst = new ArrayList<>();
Node<T> node = this.headLeaf;
while(node != null){
dst.addAll(node.items);
node = node.next;
}
return dst;
}
private void rebalance(Node<T> node){
Collections.sort(node.items);
//分裂操作
if(node.items.size() == degree){
int index = node.items.size() / 2;
T item = node.items.get(index);
Node<T> left = new Node<>();
Node<T> right = new Node<>();
if(node.children == null || node.children.size() == 0){//分裂叶子节点
left.items.addAll(node.items.subList(0,index+1));
right.items.addAll(node.items.subList(index+1,node.items.size()));
left.next = right;
right.prev = left;
if(node == this.headLeaf){
this.headLeaf = left;
}else{
left.prev = node.prev;
node.prev.next = left;
}
right.next = node.next;
if(node.next != null){
node.next.prev = right;
}
}else{//分裂非叶子节点
left.items.addAll(node.items.subList(0,index));
right.items.addAll(node.items.subList(index+1,node.items.size()));
left.children.addAll(node.children.subList(0,index+1));
right.children.addAll(node.children.subList(index+1,node.children.size()));
}
//设置分裂子节点的父节点
if(node.children != null && node.children.size() > 0){
for(int i=0;i<left.children.size();i++){
left.children.get(i).parent = left;
}
for(int i=0;i<right.children.size();i++){
right.children.get(i).parent = right;
}
}
if(node.parent == null){//根节点
node.items.clear();
node.children.clear();
node.items.add(item);
node.children.add(left);
node.children.add(right);
left.parent = node;
right.parent = node;
}else{
Node<T> parent = node.parent;
parent.items.add(item);
int pindex = parent.children.indexOf(node);
parent.children.set(pindex,left);
if(pindex < parent.children.size()-1){
parent.children.add(pindex+1,right);
}else{
parent.children.add(right);
}
left.parent = parent;
right.parent = parent;
this.rebalance(parent);
}
}
}
public void remove(T item){
Node<T> node = headLeaf;
while(node != null){
if(node.items.contains(item)){
node.items.remove(item);
break;
}
node = node.next;
}
if(node.parent == null && (node.children==null || node.children.size()==0)){
if(node.items.size() == 0){
this.root = null;
this.headLeaf = null;
}
return;
}
rebalanceOnWipe(node);
}
private void rebalanceOnWipe(Node<T> node){
Node<T> parent = node.parent;
int threshold = degree/2;
if(parent == null || node.items.size() >= threshold){
if(root.items.size() == 0){
root.items.addAll(root.children.get(0).items);
root.children.clear();
}
return;
}
int index = parent.children.indexOf(node);
if(node.children == null || node.children.size() == 0) {//叶子节点
Node<T> sibling;
if(index == 0) {
sibling = parent.children.get(index + 1);
if (sibling.items.size() <= threshold) {
node.items.addAll(sibling.items);
parent.children.remove(sibling);
parent.items.remove(index);
node.next = sibling.next;
if (sibling.next != null) {
sibling.next.prev = node;
}
this.rebalanceOnWipe(parent);
} else {
node.items.add(sibling.items.remove(0));
parent.items.set(index, node.items.get(node.items.size()-1));
}
}else{
sibling = parent.children.get(index-1);
if(sibling.items.size() <= threshold){
sibling.items.addAll(node.items);
parent.children.remove(node);
parent.items.remove(index-1);
sibling.next = node.next;
if (node.next != null) {
node.next.prev = sibling;
}
this.rebalanceOnWipe(parent);
}else{
node.items.add(0,sibling.items.remove(sibling.items.size()-1));
parent.items.set(index-1,sibling.items.get(sibling.items.size()-1));
}
}
}else{//非叶子节点
Node<T> sibling;
if(index == 0) {
sibling = parent.children.get(index + 1);
if (sibling.items.size() <= threshold) {
node.items.add(parent.items.remove(index));
node.items.addAll(sibling.items);
parent.children.remove(sibling);
node.children.addAll(sibling.children);
for(int i=0;i<(sibling.children==null?0:sibling.children.size());i++){
sibling.children.get(i).parent = node;
}
this.rebalanceOnWipe(parent);
} else {
node.items.add(parent.items.get(index));
parent.items.set(index,sibling.items.remove(0));
node.children.add(sibling.children.remove(0));
node.children.get(node.children.size()-1).parent = node;
}
}else{
sibling = parent.children.get(index-1);
if(sibling.items.size() <= threshold){
sibling.items.add(parent.items.remove(index-1));
sibling.items.addAll(node.items);
parent.children.remove(node);
sibling.children.addAll(node.children);
for(int i=0;i<node.children.size();i++){
node.children.get(i).parent = sibling;
}
this.rebalanceOnWipe(parent);
}else{
node.items.add(0,parent.items.get(index-1));
parent.items.set(index-1,sibling.items.remove(sibling.items.size()-1));
node.children.add(sibling.children.remove(sibling.children.size()-1));
node.children.get(node.children.size()-1).parent = node;
}
}
}
}
}
效果:
1.数据插入完成后:
2.输出效果:
三.总结
1.B+tree 数据都在叶子节点,非叶子节点只做索引使用
2.数据检索要到叶子节点才结束,查询性能更稳定
3.叶子节点按顺序连接为链表,便于范围查询
4.相比B-tree数据和索引遍布整棵树,B+tree索引节点能存储更多索引,树的高度会更矮,IO次数更少