谈谈B-Tree 和 B+Tree

前言

周末在看《High Performance Mysql》(中文译为《高性能Mysql》)一书的时候,在index部分看到了B-Tree的概念,实际上这个名词于我而言并不陌生,毕竟之前为了应付面试的时候翻过很多博客,但今天再次看到这个名词时突然回忆不起来B-Tree到底是个什么东西了,于是用度娘去搜了一下,结果搜出来的定义千奇百怪(中文社区里面真的是一堆错误文章抄来抄去,很容易造成误导,例如之前看见了一篇文章说B-Tree是Balance Binary Tree作者是Adelson-Velskii和Landis,看得我一脸懵逼,你这样让AVL-Tree情何以堪?),后面又去外网搜了一下,也发现挺多不一样的定义的,真的是越看越不懂了,后面没办法了就直接找到了原作者R. BAYER and E. MCCREIGHT1971年发布的论文看了一遍,算是真的去了解了一下这个数据结构。

 

什么是B-Tree

 

关于B-Tree这个名词的说明

首先B-Tree这个名词是在R. BAYER and E. MCCREIGHT 1971年发布的一篇文章《Organization and Maintenance of Large Ordered Indexes》中提到的,原文如下:

The pages of the index are organized in a special datastructure, so-called B-trees.

看到了吧,虽然B-Tree确实是一颗自平衡的树但是B-Tree的全名就是"B-Tree",而不是什么Balanced Tree之类的缩写,可以说Balanced Tree包含B-tree但不仅仅只有b-tree

ps:其实我更觉得这个B是作者 R. BAYER 的缩写

B-Tree 的定义

在文章中,B-tree的定义如下:

假定h为大于等于0的整数,k为一个自然数,那么满足以下条件的有向树T (T ∈ τ(k,h) )就被称为B-Tree:

  1. 根节点到所有叶子节点的路径长度都相同,且该路径节点数为h
  2. 除根节点和叶子节点之外,每个节点都至少包含k+1个子节点;对于根节点而言要么它同时也是叶子节点,要么至少包含2个子节点
  3. 每个节点最多包含2k + 1个子节点

是的你没看错,以上就是B-Tree的完整原始定义,就是这么简洁,没有什么花里胡哨的m阶cell(m/2)之类的东西

B-Tree在索引存储上的使用

在上述定义的基础上,当使用B-Tree的节点作为索引储存的页的时候,B-tree还会有以下性质:

  1. 除了根页节点能保存的key的个数范围为1~2k之外,其他所有页节点能保存的key的个数的范围为k~2k
  2. 假定一个非叶子页节点为P,并且该节点上保存的key的个数为l,那么此节点上会有l+1个子节点
  3. 在页节点P 中的所有key都是单调递增的,形如:x0,x1,x2,......,xl;k <= l <= 2k,若P为根节点的话则:1 <= l <= 2k,此外在P中还会包含l + 1个指针用于指向P的子节点,这些指针用p0,p1,......,pl表示。

用于存储索引的一页,同时也是B-Tree的一个节点P的内部结构大概长得像下面这样:

引用一下论文中的图

p0,(x1,p1),(x2,p2),.....(xl,pl)

图中的p0是一个指针,指向所有key都小于x1的节点,随后就是索引里面包含的值x1,然后是x1附带的一些附加信息α1,随后是指针p1指向所有key都大于等于x1,但小于等于x2的节点......,最后是个指针pl,指向所有key都大于xl的节点,了解了这种结构之后就能知道为什么一个节点最多只能保存2k个key:因为指针数会等于子节点数等于2k+1,而key的个数会是指针数-1

B-Tree支持的基本操作

查找

对于B-Tree来说查找是比较简单的一个操作,我们假定要查的key 为 y,当前页为P 查询过程用语言来描述就是:

  1. 二分查找y在 P中是否存在,若存在则直接返回
  2. 若y在P不存在,并且P为叶子节点,则返回不存在
  3. 若y在P不存在,并且P不为叶子节点,则进入P的子节点,该子节点的指针为pi,满足xi-1 < y < xi+1

插入

插入操作的逻辑其实很自然而然就能想到,首先,我们假设需要插入的元素为e,那么在不考虑平衡性的情况下,插入逻辑应该是这样的:

  • 利用上面查找的逻辑,查找当前Btree中有无已存在的e
    • 若有,直接返回插入成功
    • 若无,则在搜索到最后一步的叶子页中插入e

那么上面的逻辑有一个很明显的问题,那就是叶子页的元素个数有可能超过2k,这在Btree的定义中是不被允许的,那么为了解决这个问题,我们就还需要做一步操作:页分裂

页分裂

所谓的页分裂就是把一个拥有2k + 1个元素的页进行分裂,产出一个带父节点的新页,分裂之后原来的页中保留前k个元素,分裂出来的页中保留后k个元素,同时分裂前原页中的第k+1个元素会作为分裂出来的页的父节点。

就上述逻辑举个例子,假设原来的叶子节点为P,P中有2k个数据,P的父页为Q,那么该P中的数据存储情况应该是这样的:

p0,(x1,p1),(x2,p2),.....(x2k,p2k)

当插入了e之后,现在的P会长这样:

p0,(x1,p1),(x2,p2),.....,(xk+1,pk+1),......(x2k+1,p2k+1)

进行页分裂操作之后,P会长这样:

p0,(x1,p1),(x2,p2),.....,(xk,pk)

产生的新页我们假设为P’P’长这样:

pk+1,(xk+2,pk+2),(xk+3,pk+3),.....,(x2k,p2k)

同时P’的父节点为:

(xk+1,p')

其中p‘指向的页就是产生的新页P’,随后把此节点再插入P的父页Q中,这样一次完整的页分裂操作就做完了。

所以,完整版的插入逻辑如下:

  • 利用上面查找的逻辑,查找当前Btree中有无已存在的e
    • 若有,直接返回插入成功
    • 若无,则在搜索到最后一步的叶子页中插入e
      • 插入e后叶子页元素个数没有超过2k,插入结束
      • 插入e后叶子页元素个数超过2k,执行页分裂,并向上递归

删除

关于删除,不考虑平衡性的话同样很容易就能想到一个朴素的操作办法,流程如下:

  • 利用上面查找的逻辑,查找当前Btree中有无已存在的e
    • 若无,直接返回删除成功
    • 若有,直接删除该元素

那么这个操作流程肯定是有问题的,问题就出在删除元素上,一共有两个问题:

  • 若该元素不在叶子页上,那一旦该元素被删除了,那该元素指向的子页怎么办?
  • 若该元素在叶子页上,那么一旦叶子页中的元素个数被删至低于k个了,怎么办?

第一个问题比较好解决,如果该元素不在叶子节点上的话,就将该元素与它的右子节点的第一个元素进行交换,直到交换至叶子节点。

要解决第二个问题的话就需要引入一个新的操作:页合并

页合并

页合并就是指把两个兄弟页合并为一个页的操作,具体怎么操作,我们等会用一个例子来说明,现在先约定一些名词,假设页Q中的元素e的左指针指向页P,右指针指向页P',那么PP'就被称为兄弟页,页合并操作都是在兄弟页之间进行的,同时页合并也有两种操作,分别被称为CatenationUnderflow

Catenation

Catenation发生在两个兄弟页中的元素之和小于2K的情况下,这种情况只需要把父页中的元素e给删掉,然后用e作为连接元素将P'中的所有元素合并至P中,随后删除掉页P',用图表示如下:

合并前:

合并后:

当然父节点中的元素被删掉之后也可能出现元素个数小于k的情况,所以一旦执行了Catenation操作,必须要向上递归执行页合并操作

Underflow

Underflow发生在两个兄弟页中的元素之和大于等于2k的情况下,这种情况下,我们首先还是先对这两个页做一遍Catenation操作,现在P中的元素个数就一定是大于等于2k + 1了,随后我们再对P执行一遍页分裂操作即可。

虽然页分裂操作会向父页中插入一个值,但是由于在执行页分裂操作前我们就从父页中删除了一个值,所以父页中的元素个数是不会变的,因此Underflow执行一次即可,无需递归执行

B-Tree java代码实现

public class BTree {

private int k;
private Page root;

public BTree(int k) {
    this.k = k;
    root = new Page();
}

public Page getRoot() {
    return root;
}

/**
 * b-tree的节点
 */
private class Page {
    private Element parent;
    private Element[] elements = new Element[(k + 1) * 2];
    private int elementsSize = 1;

    Page() {
        elements[0] = new Element(null);
        elements[0].setIndex(0);
        elements[0].setPage(this);
    }

    public int getElementsSize() {
        return elementsSize;
    }

    public void setElementsSize(int elementsSize) {
        this.elementsSize = elementsSize;
    }

    public Element getParent() {
        return parent;
    }

    public void setParent(Element parent) {
        this.parent = parent;
    }

    public void setElements(Element[] elements) {
        this.elements = elements;
    }

    public Element[] getElements() {
        return elements;
    }

    public boolean addElement(Element value) {
        int index = bsSearch(elements, 0, elementsSize - 1, value.getV());
        return addElement(index + 1, value);
    }

    public boolean addElement(int index, Element value) {
        value.setPage(this);
        for (int i = elementsSize; i > index; i--) {
            elements[i] = elements[i - 1];
            elements[i].setIndex(i);
        }
        value.setIndex(index);
        elements[index] = value;
        elementsSize++;
        return true;
    }

    /**
     * 返回小于等于v的第一个元素
     *
     * @param v
     * @return
     */
    public Element search(int v) {
        int index = bsSearch(elements, 0, elementsSize - 1, v);
        return elements[index];
    }

    public Element getElement(int index) {
        return elements[index];
    }

    public boolean delete(int index) {
        for (int i = index; i < elementsSize; i++) {
            elements[i] = elements[i + 1];
            if (elements[i] != null) {
                elements[i].setIndex(i);
            }
        }
        elementsSize--;
        return true;
    }

    /**
     * 返回小于等于当前v的第一个元素下标.若不存在则返回-1
     *
     * @param elements
     * @param v
     * @return
     */
    private int bsSearch(Element[] elements, int l, int r, int v) {
        for (; l < r; ) {
            int mid = (l + r + 1) / 2;
            if (elements[mid].getV() != null && elements[mid].getV() > v) {
                r = mid - 1;
            } else {
                l = mid;
            }
        }
        return l;
    }
}

/**
 * 节点中的元素
 */
private class Element {
    private Page rightIndex;
    private Page page;
    private int index;
    private Integer v;

    public Element(Integer v) {
        this.v = v;
    }

    public Page getRightIndex() {
        return rightIndex;
    }

    public void setRightIndex(Page rightIndex) {
        this.rightIndex = rightIndex;
        if (rightIndex != null) {
            rightIndex.setParent(this);
        }
    }

    public Integer getV() {
        return v;
    }

    public void setV(Integer v) {
        this.v = v;
    }

    public Page getPage() {
        return page;
    }

    public void setPage(Page page) {
        this.page = page;
    }

    public int getIndex() {
        return index;
    }

    public void setIndex(int index) {
        this.index = index;
    }
}

public Element search(int v) {
    return search(root, v);
}

public boolean insert(int v) {
    return insert(root, v);
}

public boolean delete(int v) {
    Element vEle = search(v);
    if (vEle == null) {
        return false;
    }
    // 叶子节点
    if (vEle.getRightIndex() == null) {
        Page currentPage = vEle.getPage();
        currentPage.delete(vEle.getIndex());
        adjust(currentPage);
    } else {
        Page tmpPage = vEle.getRightIndex();
        for (; ; ) {
            Element element = tmpPage.getElement(0);
            if (element.getRightIndex() != null) {
                tmpPage = element.getRightIndex();
                continue;
            }
            element = tmpPage.getElement(1);
            vEle.setV(element.getV());
            tmpPage.delete(element.getIndex());
            adjust(tmpPage);
            break;
        }
    }
    return true;
}

private Element search(Page node, int v) {
    if (node == null) {
        return null;
    }
    Element element = node.search(v);
    if (element.getV() != null && element.getV() == v) {
        return element;
    }
    return search(element.getRightIndex(), v);
}


private boolean insert(Page node, int v) {
    Element vEle = node.search(v);
    if (vEle.getRightIndex() == null) {
        node.addElement(new Element(v));
        pageSplit(node);
        return true;
    }
    if (vEle.getV() != null && vEle.getV() == v) {
        return true;
    }
    return insert(vEle.getRightIndex(), v);
}

/**
 * 0. 判断当前页是否需要进行页分裂,不需要直接返回
 * 1. 将当前页节点 P 中间第k个元素 E(k) 取出
 * 2. 新建一个页节点 Pn 将 E(k) 的右指针指向 Pn
 * 3. 将 P 中的第k+1 ~ 2k + 1个元素移至 Pn 中
 * 4. 若 P 为root,则将E(k) 的左指针指向 P ,并新建一个页节点 Pr ,将 E(k)放入 Pr 并将Pr置为为root
 * 5. 否则 将E(k) 插入P的父页中,递归上述操作
 *
 * @param currentPage
 */
private void pageSplit(Page currentPage) {
    if (currentPage.getElementsSize() - 1 <= k << 1) {
        return;
    }
    Element midElement = currentPage.getElement(k + 1);
    currentPage.getElements()[k + 1] = null;
    Page pn = new Page();
    for (int i = k + 2; i < currentPage.getElementsSize(); i++) {
        pn.addElement(i - k - 1, currentPage.getElement(i));
        currentPage.getElements()[i] = null;
    }
    pn.getElement(0).setRightIndex(midElement.getRightIndex());
    midElement.setRightIndex(pn);

    currentPage.setElementsSize(currentPage.getElementsSize() - k - 1);
    if (currentPage.getParent() == null) {
        Page pr = new Page();
        pr.getElement(0).setRightIndex(currentPage);
        pr.addElement(midElement);
        root = pr;
        return;
    }
    Page parentPage = currentPage.getParent().getPage();
    parentPage.addElement(midElement);
    pageSplit(parentPage);
}

private void adjust(Page currentPage) {
    Element parentElement = currentPage.getParent();
    // root 就不用管了
    if (parentElement == null) {
        return;
    }
    //元素个数大于等于k,也不用管
    if (currentPage.getElementsSize() >= k + 1) {
        return;
    }
    Page parentPage = parentElement.getPage();
    int left = parentElement.getIndex() - 1 < 0 ? parentElement.getIndex() + 1 : parentElement.getIndex();
    int right = parentElement.getIndex() + 1 >= parentPage.getElementsSize() ? parentElement.getIndex() : parentElement.getIndex() + 1;
    for (int i = left; i <= right; i++) {
        int leftSize = parentPage.getElement(i - 1).getRightIndex().getElementsSize();
        int rightSize = parentPage.getElement(i).getRightIndex().getElementsSize();
        if (leftSize + rightSize - 2 < k << 1) {
            catenated(parentPage.getElement(i));
            adjust(parentPage);
            return;
        }
        if (leftSize + rightSize - 2 >= k << 1) {
            underflow(parentPage.getElement(i));
            return;
        }
    }
}

private void catenated(Element element) {
    Page leftPage = element.getPage().getElement(element.getIndex() - 1).getRightIndex();
    Page rightPage = element.getRightIndex();
    //删除父页中的元素
    element.getPage().delete(element.getIndex());
    //将删除的元素添加至右页中
    rightPage.getElement(0).setV(element.getV());
    for (int i = 0; i < rightPage.getElementsSize(); i++) {
        leftPage.addElement(rightPage.getElement(i));
    }

}

private void underflow(Element element) {
    Page leftPage = element.getPage().getElement(element.getIndex() - 1).getRightIndex();
    Page rightPage = element.getRightIndex();
    List<Element> tmp = new ArrayList<Element>();
    for (int i = 0; i < leftPage.getElementsSize(); i++) {
        tmp.add(leftPage.getElement(i));
    }
    rightPage.getElement(0).setV(element.getV());
    for (int i = 0; i < rightPage.getElementsSize(); i++) {
        tmp.add(rightPage.getElement(i));
    }
    //计算出中值索引
    int midIndex = tmp.size() / 2;
    //替换元素值
    element.setV(tmp.get(midIndex).getV());
    tmp.get(midIndex).setV(null);
    //更新左页
    for (int i = 1; i < leftPage.getElementsSize(); i++) {
        leftPage.getElements()[i] = null;
    }
    leftPage.setElementsSize(1);
    for (int i = 1; i < midIndex; i++) {
        leftPage.addElement(i, tmp.get(i));
    }
    //更新右页
    for (int i = 0; i < rightPage.getElementsSize(); i++) {
        rightPage.getElements()[i] = null;
    }
    rightPage.setElementsSize(0);
    for (int i = midIndex; i < tmp.size(); i++) {
        rightPage.addElement(i - midIndex, tmp.get(i));
    }
}

public static void dfs(Page tmp) {
    if (tmp == null) {
        return;
    }
    for (int i = 0; i < tmp.getElementsSize(); i++) {
        if (tmp.getElement(i).getV() != null) {
            System.out.print(" " + tmp.getElement(i).getV());
        }
        dfs(tmp.getElement(i).getRightIndex());
    }
}

public static void main(String[] args) {

    BTree bTree = new BTree(2);
    for (int i = 100; i >= 1; i--) {
        bTree.insert(i);
        dfs(bTree.getRoot());
        System.out.println();
    }
    for (int i = 1; i <= 25; i++) {
        System.out.println(bTree.search(i).getV());
    }
    for (int i = 1; i <= 25; i++) {
        Random random = new Random();
        int x = random.nextInt(100) % 100;
        System.out.print("delete:" + x + " ");
        System.out.println(bTree.delete(x));
        dfs(bTree.getRoot());
        System.out.println();
    }

}
}

B+Tree

谈完了Btree我们再来谈谈B+Tree,所谓的B+Tree一直没有找到明确的出处和定义,所以这里谈的B+Tree指的是使用最多的定义,也就是mysql中innodb引擎被使用得最多的索引结构

定义

B+Tree 的定义与BTree大体上一致,只有一个唯一的不同点,那就是所有的数据都保存在叶子节点中,非叶子节点中只保留索引信息

查询,插入,删除

这些操作都同BTree保持一致

innodb索引的页分裂和页合并

想了解在实际使用中innodb是如何管理索引树的合并和分裂的,可以查看这篇文章

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值