前言
本章节将带领大家进入B树的学习,主要介绍B树的概念和B树的插入代码的实现,删除代码不做讲解,最后简单介绍B+树和B*树。
B树的概念
1970年,R.Bayer和E.mccreight提出了一种适合外查找的树,它是一种平衡的多叉树,称为B树(有些地方写的是B-树,注意不要误读成"B减树")。
如果B树是一颗三叉平衡树的话,上面一层是关键字区域,下面一层存放的是孩子结点:
 
我们来直观感受一下插入的过程:
B树的插入过程
一棵M阶(M>2)的B树,是一棵平衡的M路平衡搜索树,可以是空树或者满足一下性质:
- 根节点至少有两个孩子
- 每个非根节点至少有 【M/2(向上取整) - 1】 个关键字,至多有M-1个关键字,并且以升序排列
- 每个非根节点至少有【M/2(向上取整)】个孩子,至多有M个孩子
- key[i]和key[i+1]之间的孩子节点的值介于key[i]、key[i+1]之间
- 所有的叶子节点都在同一层
B树的实现
这里实现B树的插入代码。
结点定义
这里以三叉树为演示例子,定义 M 为 3,在结点初始化的时候,我们分别在keys 和 subs 域都增加一个空间,这样会方便我们后续的结点分裂。
    public static final int M = 3;
    public Node root;
    static class Node {
        public int[] keys;//关键字
        public Node[] subs;//孩子结点
        public Node parent;//双亲结点
        public int usedSize;//使用的空间个数
        public Node() {
            //多分配一个空间是为了后面便于我们分裂结点
            this.keys = new int[M];
            this.subs = new Node[M+1];
        }
    }
插入分析
首先如果根节点为空的话,直接插入即可:
//根节点为空,直接插入
        if(root == null) {
            root = new Node();
            root.keys[0] = key;
            root.usedSize = 1;
            return;
        }
然后这里我们实现的B树是不能插入相同的数据的,所以我们需要先查找是否已经存在过 key 值,先写一个查找代码:
当遇到和key 值是一样的情况下,我们直接返回即可,如果没有遇到,我们需要继续查找下去。
结点的 keys 是连续的数组,我们需要遍历这个数组:
 如果发现 key 大于数组元素,需要继续向后遍历,如果发现 key 小于数组元素,我们则需要进入到对应的孩子结点继续寻找 key 。
最后我们要考虑返回值,我们应该返回什么样的数据?
 如果至少单纯判断是否存在,也就是返回布尔值,如果存在某个数据就是返回true,这时候是不需要进行插入操作的,但是如果不存在,你返回的是 false ,那我们应该从哪个结点进行插入操作,所以我们需要获得具体的结点,这时候就需要在查找的循环过程中保存上一个 cur 结点,当cur 走到空的时候,cur 的上一个结点就是我们需要插入数据的结点了。
但是如果返回结点,那就意味着最后的返回值不可能为空,那就无法判断是否存在了 key,所以我们需要接收两个返回值,这时候我们可以定义一个泛型类,用来创建对象保存两个数据,一个是结点,一个是下标,当不存在的时候直接返回 -1。
public class Pair<K, V> {
    private K key;
    private V val;
    public Pair(K key, V val) {
        this.key = key;
        this.val = val;
    }
    public K getKey() {
        return key;
    }
    public void setKey(K key) {
        this.key = key;
    }
    public V getVal() {
        return val;
    }
    public void setVal(V val) {
        this.val = val;
    }
}
    //查找
    public Pair<Node,Integer> find(int key) {
        Node cur = root;
        Node prev = null;
        while(cur != null) {
            int i = 0;
            while(i < cur.usedSize) {
                if(cur.keys[i] == key) {
                    //存在该节点
                    return new Pair<>(cur,i);
                } else if(cur.keys[i] > key) {
                    //需要进入孩子结点继续查找
                    break;
                } else {
                    //继续查找
                    i++;
                }
            }
            prev = cur;
            cur = cur.subs[i];
        }
        //找不到,返回双亲结点
        return new Pair<>(prev,-1);
    }
如果不存在,我们就需要插入key ,在获取到的 prev 上进行直接插入,最后我们就要考虑是否超过了M,如果超过了M,就需要进行结点的分裂:
这里要注意的是,我们插入过程都是在叶子结点上进行的,所以不需要进行孩子域 subs 的调整。
        //不存在,需要进行插入
        Node cur = find.getKey();
        //插入是在叶子结点进行的,不需要调整孩子结点
        int i = cur.usedSize - 1;
        for (; i >= 0; i--) {
            if(cur.keys[i] > key) {
                cur.keys[i+1] = cur.keys[i];
            } else {
                break;
            }
        }
        cur.keys[i+1] = key;
        cur.usedSize++;
        //是否需要进行分裂
        if(cur.usedSize == M) {
            split(cur);
        }
分裂分析
我们来看一下非根结点的分裂过程:
 
 我们需要获取中间的关键字,然后从中间的关键字的下一个开始拷贝到新结点上,然后中间的关键字需要提取到上面去,也就是需要调整 双亲结点将 中间值插入进去,最后调整三个结点即可。
由于你往双亲结点上插入了一个数据,所以可能导致双亲结点超过容量,所以最后还需要查看双亲结点是否需要进行分裂
		Node newNode = new Node();
        Node parent = cur.parent;
        //进行keys和孩子结点的拷贝
        int mid = M / 2;
        int i = 0;
        int j = mid + 1;
        for(; j < cur.usedSize; j++) {
            newNode.keys[i] = cur.keys[j];
            newNode.subs[i] = cur.subs[j];
            //如果拷贝过来的孩子结点不为空,需要修改孩子结点的双亲结点
            if(newNode.subs[i] != null) {
                newNode.subs[i].parent = newNode;
            }
            //usedSize 随之修改
            newNode.usedSize++;
            i++;
        }
        //还差一个孩子结点没有拷贝,再次拷贝孩子结点
        newNode.subs[i] = cur.subs[j];
        if(newNode.subs[i] != null) {
            newNode.subs[i].parent = newNode;
        }
        //新结点的双亲结点设置为 parent
        newNode.parent = parent;
        //设置 cur 的 usedSize 数值
        cur.usedSize = mid;
        //需要提取的中间关键字
        int midVal = cur.keys[mid];
        //特殊情况:当分裂的结点正好是根结点
        if(cur == root) {
            root = new Node();
            root.keys[0] = midVal;
            root.subs[0] = cur;
            root.subs[1] = newNode;
            cur.parent = newNode.parent = root;
            root.usedSize = 1;
            return;
        }
        //处理 parent 结点
        //将 cur 的中间关键值提到 parent;
        int end = parent.usedSize - 1;
        for(; end >= 0; end--) {
            if(parent.keys[end] > midVal) {
                parent.keys[end+1] = parent.keys[end];
                parent.subs[end+2] = parent.subs[end+1];
            } else {
                break;
            }
        }
        parent.keys[end+1] = midVal;
        parent.subs[end+2] = newNode;
        parent.usedSize++;
        //是否需要继续分裂
        if(parent.usedSize == M) {
            split(parent);
        }
如果分裂的是根节点的话,就有一点不一样了:我们需要为中间值创建一个新结点作为新的 根节点
 
 
		//特殊情况:当分裂的结点正好是根结点
        if(cur == root) {
            root = new Node();
            root.keys[0] = midVal;
            root.subs[0] = cur;
            root.subs[1] = newNode;
            cur.parent = newNode.parent = root;
            root.usedSize = 1;
            return;
        }
根节点的插入和非根结点的插入区别就在于中间值的处理,所以在前面拷贝的过程的代码可以保留,最后进行特殊情况的判断处理即可。
    private void split(Node cur) {
        Node newNode = new Node();
        Node parent = cur.parent;
        //进行keys和孩子结点的拷贝
        int mid = M / 2;
        int i = 0;
        int j = mid + 1;
        for(; j < cur.usedSize; j++) {
            newNode.keys[i] = cur.keys[j];
            newNode.subs[i] = cur.subs[j];
            //如果拷贝过来的孩子结点不为空,需要修改孩子结点的双亲结点
            if(newNode.subs[i] != null) {
                newNode.subs[i].parent = newNode;
            }
            //usedSize 随之修改
            newNode.usedSize++;
            i++;
        }
        //还差一个孩子结点没有拷贝,再次拷贝孩子结点
        newNode.subs[i] = cur.subs[j];
        if(newNode.subs[i] != null) {
            newNode.subs[i].parent = newNode;
        }
        //新结点的双亲结点设置为 parent
        newNode.parent = parent;
        //设置 cur 的 usedSize 数值
        cur.usedSize = mid;
        //需要提取的中间关键字
        int midVal = cur.keys[mid];
        //特殊情况:当分裂的结点正好是根结点
        if(cur == root) {
            root = new Node();
            root.keys[0] = midVal;
            root.subs[0] = cur;
            root.subs[1] = newNode;
            cur.parent = newNode.parent = root;
            root.usedSize = 1;
            return;
        }
        //处理 parent 结点
        //将 cur 的中间关键值提到 parent;
        int end = parent.usedSize - 1;
        for(; end >= 0; end--) {
            if(parent.keys[end] > midVal) {
                parent.keys[end+1] = parent.keys[end];
                parent.subs[end+2] = parent.subs[end+1];
            } else {
                break;
            }
        }
        parent.keys[end+1] = midVal;
        parent.subs[end+2] = newNode;
        parent.usedSize++;
        //是否需要继续分裂
        if(parent.usedSize == M) {
            split(parent);
        }
    }
最终代码
package mybtree;
public class Btree {
    public static final int M = 3;
    public Node root;
    static class Node {
        public int[] keys;//关键字
        public Node[] subs;//孩子结点
        public Node parent;//双亲结点
        public int usedSize;//使用的空间个数
        public Node() {
            //多分配一个空间是为了后面便于我们分裂结点
            this.keys = new int[M];
            this.subs = new Node[M+1];
        }
    }
    //插入
    public void insert(int key) {
        //根节点为空,直接插入
        if(root == null) {
            root = new Node();
            root.keys[0] = key;
            root.usedSize = 1;
            return;
        }
        //先查找是否存在key
        Pair<Node,Integer> find = find(key);
        //如果已经存在,直接返回
        if(find.getVal() != -1) {
            return;
        }
        //不存在,需要进行插入
        Node cur = find.getKey();
        //插入是在叶子结点进行的,不需要调整孩子结点
        int i = cur.usedSize - 1;
        for (; i >= 0; i--) {
            if(cur.keys[i] > key) {
                cur.keys[i+1] = cur.keys[i];
            } else {
                break;
            }
        }
        cur.keys[i+1] = key;
        cur.usedSize++;
        //是否需要进行分裂
        if(cur.usedSize == M) {
            split(cur);
        }
    }
    private void split(Node cur) {
        Node newNode = new Node();
        Node parent = cur.parent;
        //进行keys和孩子结点的拷贝
        int mid = M / 2;
        int i = 0;
        int j = mid + 1;
        for(; j < cur.usedSize; j++) {
            newNode.keys[i] = cur.keys[j];
            newNode.subs[i] = cur.subs[j];
            //如果拷贝过来的孩子结点不为空,需要修改孩子结点的双亲结点
            if(newNode.subs[i] != null) {
                newNode.subs[i].parent = newNode;
            }
            //usedSize 随之修改
            newNode.usedSize++;
            i++;
        }
        //还差一个孩子结点没有拷贝,再次拷贝孩子结点
        newNode.subs[i] = cur.subs[j];
        if(newNode.subs[i] != null) {
            newNode.subs[i].parent = newNode;
        }
        //新结点的双亲结点设置为 parent
        newNode.parent = parent;
        //设置 cur 的 usedSize 数值
        cur.usedSize = mid;
        //需要提取的中间关键字
        int midVal = cur.keys[mid];
        //特殊情况:当分裂的结点正好是根结点
        if(cur == root) {
            root = new Node();
            root.keys[0] = midVal;
            root.subs[0] = cur;
            root.subs[1] = newNode;
            cur.parent = newNode.parent = root;
            root.usedSize = 1;
            return;
        }
        //处理 parent 结点
        //将 cur 的中间关键值提到 parent;
        int end = parent.usedSize - 1;
        for(; end >= 0; end--) {
            if(parent.keys[end] > midVal) {
                parent.keys[end+1] = parent.keys[end];
                parent.subs[end+2] = parent.subs[end+1];
            } else {
                break;
            }
        }
        parent.keys[end+1] = midVal;
        parent.subs[end+2] = newNode;
        parent.usedSize++;
        //是否需要继续分裂
        if(parent.usedSize == M) {
            split(parent);
        }
    }
    //查找
    public Pair<Node,Integer> find(int key) {
        Node cur = root;
        Node prev = null;
        while(cur != null) {
            int i = 0;
            while(i < cur.usedSize) {
                if(cur.keys[i] == key) {
                    //存在该节点
                    return new Pair<>(cur,i);
                } else if(cur.keys[i] > key) {
                    //需要进入孩子结点继续查找
                    break;
                } else {
                    //继续查找
                    i++;
                }
            }
            prev = cur;
            cur = cur.subs[i];
        }
        //找不到,返回双亲结点
        return new Pair<>(prev,-1);
    }
    public void inorder(Node root){
        if(root == null)
            return;
        for(int i = 0; i < root.usedSize; ++i){
            inorder(root.subs[i]);
            System.out.println(root.keys[i]);
        }
        inorder(root.subs[root.usedSize]);
    }
}
B+树介绍
B+树是B-树的变形,也是一种多路搜索树:
 其定义基本与B-树相同,除了:
- 非叶子节点的子树指针与关键字个数相同
- 非叶子节点的子树指针p[i],指向关键字值属于【k[i],k[i+1]】的子树【这句话的意思是B+树在B树的基础上只存在右子树,也就是说keys 数组第一个区域是不存在左孩子的,然后每一个孩子结点的范围是 k[i] 到 k[i+1] 之间的】
- 所有叶子节点通过双向链表进行连接
- 所有关键字都在叶子节点出现 
B+树的应用:
 在MySQL中使用B+树来对数据进行管理,在下一篇MySQL的索引中我会进行详细的讲解。
B* 树介绍
B*树是B+树的变形,在B+树的非根和非叶子节点再增加指向兄弟节点的指针。
 
 这样做的好处是可以节约存储空间,结点在进行分裂的时候,会优先先看看兄弟结点是否已满,如果没有满,会将数值插入到兄弟结点上。










![[Linux]进程控制详解](https://i-blog.csdnimg.cn/direct/508659c0ca6c49ff815a29685eb59ed5.png)








