8.2 B-Tree

系统存储容量的增长速度 << 应用问题规模的增长速度
不同容量的存储器,访问速度差异悬殊。存储系统多数分级组织(Caching),最常用的数据尽可能放在更高层、更小的存储器中。
从磁盘中读写1B,与读写1KB速度几乎相同。批量式访问,以页(page)或块(block)为单位,使用缓冲区,最终单位字节的1KB访问时间大大缩短。

#define BUFSIZ 512 // 缓冲区默认容量
int setvbuf(FILE* fp, char* buf, int _Mode, size_t size); // 定制缓冲区(流,缓冲区,_IOFBF | _IOLBF | _IONBF,缓冲区容量)
int fflush(FILE* fp); // 强制清空缓冲区

B-Tree,平衡的多路(multi-way)搜索树,经适当合并得到超级节点,每d代合并m = 2d路,m - 1个关键码
B-Tree逻辑上与BBST完全等价,多级存储系统中使用可针对外部查找,极大降低I/O次数,充分利用外存对批量访问的高效支持,每下降一层,以超级节点为单位,读入一组关键码
m阶B-Tree,即m路平衡搜索树(m ≥ 2)
external nodes深度相等,internal nodes深度相等
内部节点有不超过m - 1个关键码(K1 < K2 < ... < Kn),不超过m个分支(A0, A1, A2, ... , An)
内部节点的分支数不可过少,根节点2 \le n+1,其它节点\left \lceil m/2 \right \rceil \le n+1,故亦可称作(\left \lceil m/2 \right \rceil, m)-Tree,例如(2, 4)-Tree

template <typename T> struct BTNode { // B-Tree节点
    BTNodePosi(T) parent; // 父节点
    Vector<T> key; // 数值向量
    Vector<BTNodePosi(T)> child; // 子向量,长度总比key多1
    BTNode() {
        parent = NULL;
        child.insert(0, NULL);
    }
    BTNode(T e, BTNodePosi(T) lc = NULL, BTNodePosi(T) rc = NULL) {
        parent = NULL; // 作为根节点
        key.insert(0, e); // 初始时仅1个关键码
        child.insert(0, lc);
        child.insert(1, rc); // 2个子节点
        if (lc)
            lc->parent = this;
        if (rc)
            rc->parent = this;
    }
}
#define BTNodePosi(T) BTNode<T>* // B-Tree节点位置
template <typename T> class BTree { // B-Tree
protected:
    int _size; // 关键码总数
    int _order; // 阶次
    BTNodePosi(T) _root; // 根
    BTNodePosi(T) _hot; // search()最后访问的非空节点位置
    void solveOverflow(BTNodePosi(T)); // 因插入而上溢后的分裂处理
    void solveUnderflow(BTNodePosi(T)); // 因删除而下溢后的合并处理
public:
    BTNodePosi(T) search(const T & e); // 查找
    bool insert(const T & e); // 插入
    bool remove(const T & e); // 删除
}

查找

template <typename T> BTNodePosi(T) BTree<T>::search(const T & e) {
    BTNodePosi(T) v = _root;
    _hot = NULL; // 从根节点出发
    while (v) { // 逐层查找
        Rank r = v->key.search(e); // 在当前节点对应向量中顺序查找
        if (0 <= r && e == v->key[r])
            return v; // 若成功,则返回
        _hot = v;
        v = v->child[r + 1]; // 沿引用转至对应的下层子树,并载入其根I/O
    } // 若因!v退出,则抵达外部节点
    return NULL; // 失败
}

最大高度
含N个关键码的m阶B-Tree,各层内部节点数为n_{k} = 2 \times \left \lceil m/2 \right \rceil ^{k-1}
外部节点所在层N+1=n_{h}\ge 2\times\left\lceil m/2\right\rceil^{h-1},可推得h\le 1+\log_{\left\lceil m/2\right\rceil}{\left\lfloor\left(N+1\right)/2\right\rfloor}=O\left(\log_{m}{N}\right)
相对于BBST,\log_{\left \lceil m/2 \right \rceil }{(N/2)}/\log_{2}{N}=1/(\log_{2}{m}-1),若取m = 256,则树高(I/O次数)降至约1/7

最小高度
含N个关键码的m阶B-Tree,各层内部节点数为n_{h}=m^{h}
外部节点所在层N+1=n_{h} \le m^{h},可推得h \ge \log_{m}{(N+1)} = \Omega (\log_{m}{N})
相对于BBST,(\log_{m}{N}-1)/\log_{2}{N}=\log_{m}{2}-\log_{n}{2}\approx 1/\log_{2}{m},若取m = 256,则树高(I/O次数)降至约1/8

插入

template <typename T> bool BTree<T>::insert(const T & e) {
    BTNodePosi(T) v = search(e);
    if (v)
        return false; // 确认e不存在
    Rank r = _hot->key.search(e); // 在节点_hot中确定插入位置
    _hot->key.insert(r + 1, e); // 将新关键码插入至对应位置
    _hot->child.insert(r + 2, NULL); // 创建一个空子树指针
    _size++;
    solveOverflow(_hot); // 若发生上溢,则分裂
    return true; // 插入成功
}

分裂
设上溢节点中关键码为k0, k1, ... , km-1
取中位数s = \left \lfloor m/2 \right \rfloor,以关键码ks为界划分k0, ... , ks-1, ks, ks+1, ... , km-1
关键码ks上升一层,并分裂(split)以所得的2个节点作为左右子节点
若上溢节点的父节点已饱和,则在接纳被提升的关键码后上溢,继续分裂
上溢可能持续发生,并逐层向上传播,最坏情况即分裂至根

template <typename T> void BTree::solveOverflow(BTNodePosi(T) v) { // 上溢修复
    if (_order >= v->child.size())
        return; // 不再上溢
    Rank s = _order / 2; // 轴点,此时_order = key.size() = child.size() - 1
    BTNodePosi(T) u = new BTNode<T>(); // 新节点已有一个空子节点
    for (Rank j = 0; j < _order - s - 1; j++) { // 分裂出右侧节点u
        u->child.insert(j, v->child.remove(s + 1)); // v右侧_order-s-1个子节点
        u->key.insert(j, v->key.remove(s + 1)); // v右侧_order-s-1个关键码
    }
    u->child[_order - s -1] = v->child.remove(s + 1); // 移动v最右子节点
    if (u->child[0]) // 若u子节点非空,则统一令其以u为父节点
        for (Rank j = 0; j < _order - s; j++)
            u->child[j]->parent = u;
    BTNodePosi(T) p = v->parent; // v当前父节点p
    if (!p) { // 若p为空,则创建,全树增加1层,新根节点恰好2度
        _root = p = new BTNode<T>();
        p->child[0] = v;
        v->parent = p;
    }
    Rank r = 1 + p->key.search(v->key[0]); // p中指向u的指针的秩
    p->key.insert(r, v->key.remove(s)); // 轴点关键码上升
    p->child.insert(r + 1, u);
    u->parent = p; // 新节点u与父节点p互联
    solveOverflow(p); // 上升一层,若有必要则继续分裂,至多递归O(logn)层
}

删除

template <typename T> bool BTree<T>::remove(const T & e) {
    BTNodePosi(T) v =search(e);
    if (!v)
        return false; // 确认e存在
    Rank r = v->key.search(e) // 确定e在v中的秩
    if (v->child[0]) { // 若v非叶节点
        BTNodePosi(T) u = v->child[r + 1]; // 则在右子树中一直向左
        while (u->child[0])
            u = u->child[0]; // 即可找到e的后继(必属于叶节点)
        v->key[r] = u->key[0];
        v = u;
        r = 0; // 并交换位置
    } // 至此v必然位于最底层,且其中第r个关键码即待删除者
    v->key.remove(r);
    v->child.remove(r + 1);
    _size--;
    solveUnderflow(v); // 若发生下溢,则需旋转或合并
    return true;
}

旋转与合并
节点V下溢时,必恰好包含\left \lceil m/2 \right \rceil - 2个关键码与\left \lceil m/2 \right \rceil - 1个分支
视其左右兄弟节点L与R的规模,可分3种情况处理
(1) 若L存在,且至少包含\left \lceil m/2 \right \rceil个关键码,则将P中分界关键码y移至V中(作为最小关键码),将L中最大关键码x移至P中(取代原关键码y),经此旋转后,局部乃至全树重新满足B-Tree条件,下溢修复完毕
(2) 若R存在,且至少包含\left \lceil m/2 \right \rceil个关键码,亦可旋转,完全对称
(3) 若L或R不存在,或均不足\left \lceil m/2 \right \rceil个关键码,L与R仍必有其一(以L为例),且恰含�\left \lceil m/2 \right \rceil - 1个关键码,从P中抽取介于L与V之间的分界关键码y,通过y粘接,将L与V合成一个节点,同时合并此前y的子节点引用
此处下溢得以修复,但可能继而导致P下溢,若如此,则继续旋转或合并
下溢可能持续发生并向上传播,但至多不超过O(h)层

template <typename T> void BTree<T>::solveUnderflow(BTNodePosi(T) v) { // 下溢修复
    if ((_order + 1) >> 1 <= v->child.size())
        return; // v未下溢
    BTNodePosi(T) p = v->parent;
    if (!p) { // 已至根节点
        if (!v->key.size() && v->child[0]) { // 若v已经不含有关键码,但有唯一的非空child时
            _root = v->child[0];
            _root->par = NULL; // 跳过该节点
            v->child[0] = NULL;
            release(v);  // 释放v
        } // 树高减1
        return;
    }
    Rank r = 0;
    while (p->child[r] != v)
        r++; // 确定v是p的第r个子节点
    if (0 < r) { // 若v不是p的第1个子节点
        BTNodePosi(T) ls = p->child[r - 1]; // 则左兄弟节点必存在
        if ((_order + 1) >> 1 < ls->child.size()) { // 若左兄弟节点数量足够多
            v->key.insert(0, p->key[r - 1]); // 则p借出一个关键码给v,作为最小关键码
            p->key[r - 1] = ls->key.remove(ls->key.size() - 1); // ls最大关键码转入p
            v->child.insert(0, ls->child.remove(ls->child.size() - 1)); // 同时ls最右子节点给v,作v最左子节点
            if (v->child[0])
                v->child[0]->parent = v; // 调整指针
            return; // 至此,通过右旋已完成当前层(及所有层)的下溢处理
        }
    }
    if (p->child.size() - 1 > r) { // 若v不是p的最后1个子节点
        BTNodePosi(T) rs = p->child[r + 1]; // 则右兄弟节点必存在
        if ((_order + 1) >> 1 < rs->child.size()) { // 若右兄弟节点数量足够多
            v->key.insert(v->key.size(), p->key[r]); // 则p借出一个关键码给v,作为最大关键码
            p->key[r] = rs->key.remove(0); // rs最小关键码转入p
            v->child.insert(v->child.size(), rs->child.remove(0)); // 同时rs最左子节点给v,作v最右子节点
            if (v->child[v->child.size() - 1])
                v->child[v->child.size() - 1]->parent = v; // 调整指针
            return; // 至此,通过左旋已完成当前层(及所有层)的下溢处理
    }
    if (0 < r) { // 与左兄弟节点合并
        BTNodePosi(T) ls = p->child[r - 1]; // 左兄弟节点必存在
        ls->key.insert(ls->key.size(), p->key.remove(r - 1));
        p->child.remove(r); // p的第r-1个关键码转入ls,v不再是p的第r个子节点
        ls->child.insert(ls->child.size(), v->child.remove(0));
        if (ls->child[ls->child.size() - 1] // v最左子节点给ls,作最右子节点
            ls->child[ls->child.size() - 1]->parent = ls;
        while (!v->key.empty()) { // 若v中元素仍非空,则将其余元素依次转入ls
            ls->key.insert(ls->key.size(), v->key.remove(0));
            ls->child.insert(ls->child.size(), v->child.remove(0));
            if(ls->child[ls->child.size() - 1])
                ls->child[ls->child.size() - 1]->parent = ls;
        }
        release(v); // 合并后该局部的根已空,因此释放,合并后节点作为新的根
    } else { // 与右兄弟节点合并
        BTNodePosi(T) rs = p->child[r + 1]; // 右兄弟节点必存在
        rs->key.insert(0, p->key.remove(r));
        p->child.remove(r); // p的第r个关键码转入rs
        rs->child.insert(0, v->child.remove(v->child.size() - 1));
        if (rs->child[0] // v最右子节点给rs,作最左子节点
            rs->child[0]->parent = rs;
        while (!v->key.empty()) { // 若v中元素仍非空,则将其余元素依次转入rs
            rs->key.insert(0, v->key.remove(v->key.size() - 1));
            rs->child.insert(0, v->child.remove(v->child.size() - 1));
            if(rs->child[0])
                rs->child[0]->parent = rs;
        }
        release(v); // 合并后该局部的根已空,因此释放,合并后节点作为新的根
    }
    solveUnderflow(p); // 上升1层,继续分裂,至多递归O(logn)层
    return;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342