欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

谈谈B-Tree 和 B+Tree

程序员文章站 2022-05-14 17:38:39
...

前言

周末在看《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的内部结构大概长得像下面这样:

引用一下论文中的图

谈谈B-Tree 和 B+Tree

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',用图表示如下:

合并前:

谈谈B-Tree 和 B+Tree

合并后:

谈谈B-Tree 和 B+Tree

当然父节点中的元素被删掉之后也可能出现元素个数小于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是如何管理索引树的合并和分裂的,可以查看这篇文章

相关标签: 杂项