B-Tree那点事儿

B树(B-Tree)是一种自平衡的树,能够保证数据有序.同时它还保证了在查找、插入、删除等操作时性能都能保持在$O(log;n)$.需要注意的一点是,B-Tree并不是一棵自平衡的二叉查找树,它拥有多个分叉,且为大块数据的读写操作做了优化,同时它也可以用来描述外部存储(支持对保存在磁盘或者网络上的符号表进行外部查找).

在当今的互联网环境下,数据量已经大到无法想象,而能够在巨型数据集合中快速地进行查找操作是非常重要的,而B-Tree的神奇之处正在于: 只需要使用4~5个指向一小块数据的引用即可有效支持在数百亿甚至更多元素的符号表中进行查找和插入等操作.

B-Tree的主要应用在于文件系统与数据库系统,例如Mysql中的InnoDB存储引擎就使用到了B-Tree来实现索引.

本文作者为: SylvanasSun.转载请务必将下面这段话置于文章开头处(保留超链接).
本文转发自SylvanasSun Blog,原文链接: https://sylvanassun.github.io/2017/08/13/2017-08-13-BTrees/

数据表示


我们使用页来表示一块连续的数据,访问一页的数据需要将它读入本地内存.一个页可能是本地计算机上的一个文件,也可能是服务器上的某个文件的一部分等等.页的访问次数(无论读写)即是外部查找算法的成本模型.

首先,构造一棵B-Tree不会将数据保存在树中,而是会构造一棵由键的副本组成的树,每个副本都关联着一条链接.这种方法能够将索引与符号表进行分离,同时我们还需要遵循以下的规定:

  • 选择一个参数M来构造一棵多向树(M一般为偶数),每个节点最多含有M - 1对键和链接.
  • 每个节点最少含有M / 2对键和链接,根节点例外(它最少可以含有2对).
  • .使用M阶的B-Tree来指定M的值,例如: 在一棵4阶B-Tree中,每个节点都含有至少2对至多3对.
  • B-Tree含有两种不同类型的节点,内部节点与外部节点.
  • 内部节点含有与页相关联的键的副本: 每个键都与一个节点相关联(一条链接),以此节点为根的子树中,所有的键都大于等于与此节点关联的键,但小于原内部节点中更大的键(如果存在的话).
  • 外部节点含有指向实际数据的引用: 每个键都对应着实际的值,外部节点就是一张普通的符号表.
    // max children per B-tree node = M - 1
    // must be even and greater than 2
    private static final int M = 4;

    // root of the B-tree
    private Node root;

    // height of the B-tree
    private int height;

    // number of key-value paris int the B-tree
    private int N;
    
    // B-tree node data type
    private static final class Node {
        private int children_length;
        private Entry[] children = new Entry[M];

        // create a node with k children
        private Node(int k) {
            children_length = k;
        }
    }

    // internal nodes : only use key and next
    // external nodes : only use key and value
    private static class Entry {
        private Comparable key;
        private final Object value;
        private Node next;

        private Entry(Comparable key, Object value, Node next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

查找


B-Tree中进行查找操作每次都会结束于一个外部节点.在查找时,从根节点开始,根据被查找的键来选择当前节点中的适当区间并根据对应的链接从一个节点移动到下一层节点.最终,查找过程会到达树底的一个含有键的页(也就是外部节点),如果被查找的键在该页中,查找命中并结束,如果不在,则查找未命中.

    public Value get(Key key) {
        validateKey(key, "argument key to get() is null.");
        return search(root, key, height);
    }

    private Value search(Node x, Key key, int height) {
        while (x != null) {
            Entry[] children = x.children;
            int children_length = x.children_length;

            // 当树的高度已经递减为0时,也就到达了树的底部(一个外部节点)
            // 遍历当前节点的每个键进行比较,如果找到则查找命中返回对应的值.
            if (height == 0) {
                for (int j = 0; j < children_length; j++) {
                    if (eq(key, children[j].key))
                        return (Value) children[j].value;
                }
            } else {
                // 当还是内部节点时,根据键来查找适当的区间
                for (int j = 0; j < children_length; j++) {
                    if (j + 1 == children_length || less(key, children[j + 1].key)) {
                        // 找到适当的区间后,移动到下一层节点
                        x = children[j].next;
                        height--;
                        break;
                    }
                }
            }
        }
        return null;
    }

插入


插入操作也要先从根节点不断递归地查找到合适的区间,但需要注意一点,如果查找到的外部节点已经满了怎么办呢?

解决方法也很简单,我们允许被插入的节点暂时"溢出",然后在递归调用自底向上不断地进行分裂.例如:当M为5时,根节点溢出为6-节点,只需要将它分裂为连接了两个3-节点2-节点.即将一个M-的父节点k分裂为连接着两个(M / 2)-节点的(k + 1)-节点.

    public void put(Key key, Value value) {
        validateKey(key, "argument key to put() is null.");

        Node u = insert(root, key, value, height);
        N++;
        if (u == null)
            return;

        // need to split root
        Node t = new Node(2);
        t.children[0] = new Entry(root.children[0].key, null, root);
        t.children[1] = new Entry(u.children[0].key, null, u);
        root = t;
        height++;
    }

    private Node insert(Node x, Key key, Value value, int height) {
        int j;
        Entry t = new Entry(key, value, null);
        Entry[] children = x.children;
        int children_length = x.children_length;

        // external node
        if (height == 0) {
            for (j = 0; j < children_length; j++) {
                if (less(key, children[j].key))
                    break;
            }
        } else {
            // internal node
            for (j = 0; j < children_length; j++) {
                if (j + 1 == children_length || less(key, children[j + 1].key)) {
                    // 找到合适的区间后继续递归调用
                    Node u = insert(children[j++].next, key, value, height - 1);
                    // 如果下一层没有进行过分裂操作,直接返回null
                    if (u == null)
                        return null;    
                    t.key = u.children[0].key;
                    t.next = u;
                    break;
                }
            }
        }

        // 将j之后的元素全部右移(为了腾出j的插入位置)
        for (int i = children_length; i > j; i--) {
            children[i] = children[i - 1];
        }
        
        children[j] = t;
        x.children_length++;
        if (x.children_length < M)
            return null;
        else
            return split(x); // 如果空间已满,进行分裂
    }   
    
  // 将x分裂为两个含有new_length对键的节点
    private Node split(Node x) {
        int new_length = M / 2;
        Node t = new Node(new_length);
        x.children_length = new_length;
        for (int j = 0; j < new_length; j++)
            t.children[j] = x.children[new_length + j];
        return t;
    }   

参考文献


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • B树的定义 一棵m阶的B树满足下列条件: 树中每个结点至多有m个孩子。 除根结点和叶子结点外,其它每个结点至少有m...
    文档随手记阅读 13,183评论 0 25
  • 原文链接 B树 1.前言: 动态查找树主要有:二叉查找树(Binary Search Tree),平衡二叉查找树(...
    非典型程序员阅读 1,148评论 0 3
  • B树 1.前言: 动态查找树主要有:二叉查找树(Binary Search Tree),平衡二叉查找树(Balan...
    铁甲依然在_978f阅读 1,443评论 0 4
  • 原文出处:http://www.cnblogs.com/maybe2030/p/4715035.html引文出处:...
    明教de教主阅读 9,121评论 0 7
  • 怎样的机缘巧合让我们来到这个世界 怎样的无可奈何让我们远离这个世界 怎样的三生有幸让我们看到这个世界 欣欣向荣的生...
    遺釋悟塵阅读 235评论 0 1