[CLRS][CH 18]B树

B树的简介

B树的性质

一棵B树T是具有一下性质的有根树:
  1. 每个结点 x 具有如下属性:
    a. x.n 存储当前节点 x 中关键字的个数;
    b. x.n 个关键字本身 x.key1, x.key2, ..., x.keyx.n 以非降序存放,使得 x.key1 ≤ x.key2 ≤ ... ≤ x.keyx.n
    c. x.isLeaf 是一个bool值,表示 x 是否为叶节点,或者是内部结点。
  2. 每个内结点 x 还包含 x.n+1 个指针,指向其孩子 x.c1, x.c2, ..., x.cx.n+1。叶节点没有孩子。
  3. 关键字 x.keyi 对存储在各子树中的关键字范围加以分割。
  4. 叶节点具有相同的深度,即树的高度 h。
  5. 每个节点包含的关键字个数有上下界,用固定整数 t≥2,即B树的最小度数表示:
    a. 除了根节点以外的每个结点必须至少有 t-1 个关键字,因此除了根节点每个节点至少有 t 个子节点。如果树非空,根节点至少有一个关键字;
    b. 每个节点至多可包含 2t-1 个关键字,因此一个内部结点至多有 2t 个子节点。当一个节点恰好有 2t-1 个关键字时,称之为满结点。

当 t= 2 时,这就是一棵 2-3-4 树。因为每个结点有 t-1=1 ~ 2t-1=3 个关键字,有 t=2 ~ 2t=4 个子节点。

B树的高度

定理:如果 n≥1,那么对于任意一棵包含了 n 个关键字,高度为h,最小度数 t≥2 的B树T,有h ≤ logt[(n+1)/2]。高度为 h = logtn。

跟普通BST一样,B树上大部分操作(所需的磁盘读写次数)过程跟高度成正比。B树的相对于红黑树的优势在于,深度非常浅,因为检查人一个节点都要访问一次磁盘,所以B树大大减少了磁盘访问的次数。如图,B树根至少包含一个关键字,其他内结点包含至少 t-1 个关键字。因此高度为 h 的B树在深度1至少包含2个结点,深度2至少包含2t个结点,深度h至少包含 2th-1个结点。

B树上的基本操作:

始终保持如下约定:
B树根节点始终保存在主存中,这样就无需读盘。修改根节点时需要写入磁盘;
任何被当做参数的节点在被传递之前,需要读盘。
所有操作都是单程算法,从树根向下,但不向上返回。

搜索B树

跟BST搜索过程很像,但是对于每一个内部结点x,做一个 (x.n + 1) 路的分支选择。

该过程访问磁盘页面数为 O(h) = O(logtn)。所用CPU时间为 O(th) = O(tlogtn)。

下面代码中,x为指向根节点的指针,key为要搜索的关键字,如果 key 在树中,则返回结点 y 和使得 y.keyi == key 的下标 i 组成的序对(y, i),否则返回NULL。

B-Tree-Search(Node *x, int key) {
i = 1;
while (i <= x.n && k > x.key[i])      // 找出最小下标i,使得 key ≤ x.key[i],若找不,则令 i = x.n + 1
    i++;
if (i <= x.n and k == x.key[i])       // 如果找到,就返回该关键字结点
    return (x, i);
else if (x.isLeaf == True)            // 如果没找到,并且是叶节点,则返回失败结果NULL
    return NULL;
else DISK-READ(x, c[i])               // 如果没找到,并且不是叶节点,就继续向下查找。这里需要读一次盘
    return B-Tree-Search(x.c[i], k);
}

创建一棵空的B树

为了构造一棵空的B树,先用B-Tree-Create创建一个空根节点,再调用B-Tree-Insert插入操作。这邪恶过程都需要调用辅助过程Allocate-Node,它在O(1)时间内为新节点分配磁盘页。

该过程需要O(1)次磁盘操作和O(1)的CPU时间。

B-Tree-Create(Node *newtree) {
newtree = Allocate-Node();
newtree.isLeaf = True;
newtree.n = 0;
Disk-Write(newtree);
}

向B树中插入一个关键字

B树的插入过程跟 2-3-4 树插入过程一样(因为 2-3-4 树就是B树的一种)。当我们从树根向下插入过程中,分裂沿途遇到的所有满节点,因此每当分裂结点时,可确保其父节点不是满节点。另外,在B树中插入关键字,必须插入到已存在的叶节点上。

分裂B树中的结点

分裂过程 B-Tree-Split-Child 的参数为 x 和 i,其中 x 为被分裂结点的父节点,i 为被分裂结点在其父节点 x 中的下标,即被分裂的节点为x.ci。该过程把 x.ci 分裂成两个结点,并将 x.ci 的的中间关键字上浮之 x 中。如果被分裂的结点为根,树的高度增加1,分裂根是令B树高度增加的唯一操作。

B-Tree-Split-Child(Node *x, int i) {
  y = x.c[i]                   // 设置 y 为被分裂结点 x.c[i]
  z = Allocate-Node()          // 新建结点 z 为分裂后多出来的新结点
  z.isLeaf = y.isLeaf          // 设置结点 z 的 isLeaf 属性
  z.n = t-1                    // 设置结点 z 的关键字数量
  for j = (1 to t-1)           // 设置结点 z 的关键字
      z.key[j] = y.key[j+t]    // 结点 z 的关键字为结点 y 的关键字的后半部分
  if (y.isLeaf == False)       // 如果被分裂结点不是叶节点的话,还要进一步设置结点 z 的子节点
      for j = (1 to t)
          z.c[j] = y.c[j+t]
  y.n = t-1                    // 重置结点 y 的关键字数量
  for j = (x.n+1 downto i+1)   // 将分裂结点 y 的父节点部分子节点后移一位,以便插入新结点
      x.c[j+1] = x.c[j]
  x.c[i+1] = z                 // 将结点 z 插入父节点
  for j = (x.n downto i)       // 将分裂结点 y 的父节点部分关键字后移一位,以便插入新关键字
      x.key[j+1] = x.key[j]
  x.key[i] = y.key[t]          // 将分裂结点 y 的中间关键字上浮插入至父节点
  x.n++                        // 重置结点 x 的关键字数量
  Disk-Write(y)                // 将被调整关键字写入磁盘
  Disk-Write(z)
  Disk-Write(x)
}

如图是一个B树分裂示意图,分裂一个 t=4 的结点。结点 y=x.ci 分为两个节点 y 和 z,y的中间关键字 S 被升到其父节点 x 中。

以沿树单程下行的方式向B树插入关键字

过程 B-Tree-Insert 通过调用 B-Tree-Split-Child 来保证递归始终不会降到一个满节点上。

B-Tree-Insert(Node *root, int key) {
    r = root;
    if (r.n == 2t-1)                  // 如果根节点为满节点,则需要分裂根,通过设置空节点 s 作为根节点的父节点,进行分裂
        s = Allocate-Node()
        root = s
        s.isLeaf = False
        s.n = 0
        s.c[1] = r
        B-Tree-Split-Child(s, 1)
        B-Tree-Insert-NonFull(s, key) // 分裂完成后,调用 B-Tree-Insert-NonFull(s,k)进行插入
    else
        B-Tree-Insert-NonFull(r, key) // 如果根节点不为满节点,则直接插入
}

辅助过程 B-Tree-Insert-NonFull 将关键字 k 插入结点 x,调用该过程时应该保证结点 x 不是满节点,这一点可以通过 B-Tree-Insert 和 B-Tree-Insert-NonFull 递归调用得以保证。

B-Tree-Insert-NonFull(Node *x, int key) {
    i = x.n
    if (x.isLeaf == True)                     // 如果 x 是叶节点则直接进行插入
        while (i >= 1 && key < x.key[i])      // 寻找合适的插入 key 的位置
            x.key[i+1] = x.key[i]
            i--
        x.key[i+1] = key                      // 插入key
        x.n++                                 // 重置结点 x 的节点数量
        Disk-Write(x)                         // 写入磁盘
    else                                      // 如果结点 x 不是叶节点,则需要向下寻找适合插入 key 的一个子节点
        while (i >= 1 && k < key[i])          // 寻找合适的 key 的位置
            i--
        i++
        Disk-Read(x, c[i])                    // 读取要进行写入的子节点
        if (x.c[i] == 2t-1)                   // 如果被写入子节点为满节点,则需要分裂
            B-Tree-Split-Child(x, i)
            if (key > x.key[i])               // 确定插入位置
                i++
        B-Tree-Insert-NonFull(x.c[i], key)    // 插入 key 至新的结点 x.c[i] 中
}

从B树中删除关键字

时间: 2024-10-09 08:53:16

[CLRS][CH 18]B树的相关文章

《Code Complete》ch.18 表驱动法

是什么 一种scheme,用表来做信息存取,代替逻辑语句(if/else) 为什么 简化逻辑语句,避免大量嵌套的 if/else 或者 switch/case 怎么用 三种访问表的方式 直接访问:将源数据作为key 索引访问:构建KV表 阶梯访问:分为连续区间,遍历或者二分查找 例子 // get the full name of weekday // good String[] weekdays = { "Sunday", "Monday", "Tues

[CLRS][CH 20] van Emde Boas 树

vEB树简介 当关键字是有界范围内整数时,能够避免排序的 Ω(nlgn) 的下界限制.vEB树支持动态集合上运行时间为 O(lglgn) 的操作:Search, Insert, Delete, Min, Max, Successor 和 Predecessor. 接下来,用 n 表示集合中当前元素的个数,用 u 表示元素的可能取值范围.这样vEB树的操作运行时间就为 O(lglgu). 太难了...放弃先......

hdu 6162 Ch’s gift(树链剖分+主席树)

题目链接:hdu 6162 Ch's gift 题意: 给你一棵树,树上每个点有一个权值,现在有m个询问,每次询问给你一个s,t,L,R,问你从s到t的路径上,权值在[L,R]内的总和为多少. 题解: 我感觉我写复杂了,用树链剖分来维护路径,然后用主席树来建立权值线段树乱搞. 1 #include<bits/stdc++.h> 2 #define mst(a,b) memset(a,b,sizeof(a)) 3 #define F(i,a,b) for(int i=(a);i<=(b);

[CLRS][CH 15.5]最优二叉查找树

背景铺垫 假设我们正在设计一个翻译程序,讲英语翻译成法语,需要用一棵BST存储文章中出现的单词及等价的法语.因为要频繁地查找这棵树,所以我们希望查找时间越短越好.当然我们可以考虑使用红黑树,或者可能更适用的伸展树,来实现这一操作.但是这仍然不能满足我们的需要:由于单词出现频率不同,如果mycophagist这种奇怪的单词出现在根节点,而the.a.is这些常用单词出现在叶节点,即使O(lgn)的查找速度也极大的浪费了时间. 因此我们需要这样一种BST:频繁出现的单词出现在离根节点较近的地方.假设

[CLRS][CH 19]斐波那契堆

斐波那契堆简介 斐波那契堆(Fibnacci Heap)有两种用途:第一,支持一系列操作,这些操作构成了所谓的可合并堆.第二,其一些操作可以在常数时间内完成,这使得这种数据结构非常适合于需要频繁调用这些操作的应用. 可合并堆(Mergeable Heap)支持如下五种操作:Make-Heap(), Insert(H, x), Minmun(H), Extract-Min(H), Union(H1, H2).事实上,就是具备了快速合并操作的堆(Heap). 斐波那契堆还支持额外两种操作:Decre

剑指offer 18:树的子结构

题目描述 输入两棵二叉树A,B,判断B是不是A的子结构.(ps:我们约定空树不是任意一个树的子结构) 解题思路 验证B是不是A的子树,直觉做法,按照任意次序遍历A树,一旦出现和B树根节点相同的子节点,就将以此节点为根的子树与B树相比较,满足则查找成功,否则查找失败.树的先序遍历最为直观,此处以先序遍历为例,给出C++实现代码: /* struct TreeNode { int val; struct TreeNode *left; struct TreeNode *right; TreeNode

6444. 【GDOI2020模拟01.18】树高

题目 正解 这题也不是给人写出来的-- 先不要考虑操作二. 有一种比较新奇的思路:将点的颜色挂在边上,然后搞出个边的连通块. 这些连通块的维护是很舒服的,直接上数据结构即可. 考虑边的连通块和点的连通块的关系. 假如有\(x\)和\(y\)和\(z\)三个点相连,\(x\)为\(y\)父亲,\(y\)为\(z\)父亲. \((x,y)\)和\((y,z)\)的颜色相同,意味着\(y\)和\(z\)的颜色相同. 推广一下,我们可以发现,对于一个边连通块而言,除了根节点(需要特判是不是整棵树的根节点

[CLRS][CH 32]字符串匹配

问题简介 对于给定文本 T[n] 和模式 P[m],找到一个位移量 s,使得 T[s + j] = P[j] (0 <= s <= n-m, 1 <= j <= m),则说明模式 P 在文本 T 中出现且位移为 s.所以字符串问题就是在给定文本 T 中,找出指定模式 P 出现的所有有效位移的问题. 记号和术语 长度为0的空字符串用 ? 表示.字符串 x 的长度用 |x| 表示.字符串 x, y 的连接表示为xy,长度为 |x|+|y|.对于字符串 x, y, w,如果 x = wy

[CLRS][CH 15.4] 最长公共子序列

---恢复内容开始--- 摘要 介绍了最长公共子序列的概念及解题思路. 子序列概念 子序列:一个给定序列的子序列就是该给定序列中,去掉零个或多个元素.一般来说,给定一个序列 X = <x1, x2, ..., xm>,另一个序列 Z = <z1, z2, ..., zk> 如果存在X的一个严格递增下标序列<i1, i2, ..., ik>,使得所有的j = 1, 2, ..., k,有xij = zj,则Z是X的一个子序列.例如,Z = <B, C, D, B&g