B树算法与实现

B树的定义

假设B树的度为t(t>=2),则B树满足如下要求:(参考算法导论)

(1)  每个非根节点至少包含t-1个关键字,t个指向子节点的指针;至多包含2t-1个关键字,2t个指向子女的指针(叶子节点的子女为空)。

(2)  节点的所有key按非降序存放,假设节点的关键字分别为K[1], K[2] … K[n], 指向子女的指针分别为P[1], P[2]…P[n+1],其中n为节点关键字的个数。则有:

P[1] <= K[1] <= P[2] <= K[2] …..<= K[n] <= P[n+1]   // 这里P[n]也指其指向的关键字

(3)  若根节点非空,则根节点至少包含两个子女;

(4)  所有的叶子节点都在同一层。

 

B树的搜索,search(root, target)

从root出发,对每个节点,找到大于或等于target关键字中最小的K[i],如果K[i]与target相等,则查找成功;否则在P[i]中递归搜索target,直到到达叶子节点,如仍未找到则说明关键字不在B树中,查找失败。

 

B树的插入,insert(root, target)

B树的插入需要沿着搜索的路径从root一直到叶节点,根据B树的规则,每个节点的关键字个数在[t-1, 2t-1]之间,故当target要加入到某个叶子时,如果该叶子节点已经有2t-1个关键字,则再加入target就违反了B树的定义,这时就需要对该叶子节点进行分裂,将叶子以中间节点为界,分成两个包含t-1个关键字的子节点,同时把中间节点提升到该叶子的父节点中,如果这样使得父节点的关键字个数超过2t-1,则要继续向上分裂,直到根节点,根节点的分裂会使得树加高一层。

 

上面的过程需要回溯,那么能否从根下降到叶节点后不回溯就能完成节点的插入呢?答案是肯定的,核心思想就是未雨绸缪,在下降的过程中,一旦遇到已满的节点(关键字个数为2t-1),就就对该节点进行分裂,这样就保证在叶子节点需要分裂时,其父节点一定是非满的,从而不需要再向上回溯。

 

B树的删除,delete(root, target)

在删除B树节点时,为了避免回溯,当遇到需要合并的节点时就立即执行合并,B树的删除算法如下:从root向叶子节点按照search规律遍历:

(1)  如果target在叶节点x中,则直接从x中删除target,情况(2)和(3)会保证当再叶子节点找到target时,肯定能借节点或合并成功而不会引起父节点的关键字个数少于t-1。

(2)  如果target在分支节点x中:

(a)  如果x的左分支节点y至少包含t个关键字,则找出y的最右的关键字prev,并替换target,并在y中递归删除prev。

(b)  如果x的右分支节点z至少包含t个关键字,则找出z的最左的关键字next,并替换target,并在z中递归删除next。

(c)  否则,如果y和z都只有t-1个关键字,则将targe与z合并到y中,使得y有2t-1个关键字,再从y中递归删除target。

(3)  如果关键字不在分支节点x中,则必然在x的某个分支节点p[i]中,如果p[i]节点只有t-1个关键字。

(a)  如果p[i-1]拥有至少t个关键字,则将x的某个关键字降至p[i]中,将p[i-1]的最大节点上升至x中。

(b)  如果p[i+1]拥有至少t个关键字,则将x个某个关键字降至p[i]中,将p[i+1]的最小关键字上升至x个。

(c)  如果p[i-1]与p[i+1]都拥有t-1个关键字,则将p[i]与其中一个兄弟合并,将x的一个关键字降至合并的节点中,成为中间关键字。

 

B树的实现

数据结构

  1. /**
  2.  * @brief the degree of btree
  3.  * key per node: [M1, 2M1]
  4.  * child per node: [M, 2M]
  5.  */
  6. #define M 2   // M为B树的度
  7. typedef struct btree_node {
  8.     int k[2*M1];
  9.     struct btree_node *p[2*M];
  10.     int num;
  11.     bool is_leaf;
  12. } btree_node;


创建B树

  1. btree_node *btree_node_new()
  2. {
  3.     btree_node *node = (btree_node *)malloc(sizeof(btree_node));
  4.     if(NULL == node) {
  5.         return NULL;
  6.     }
  7.     for(int i = 0; i < 2 * M 1; i++) {  // 初始化key
  8.         node>k[i] = 0;
  9.     }
  10.     for(int i = 0; i < 2 * M; i++) {     // 初始化pointer
  11.         node>p[i] = NULL;
  12.     }
  13.     node>num = 0;
  14.     node>is_leaf = true;  // 默认为叶子
  15. }
  16. btree_node *btree_create()
  17. {
  18.     btree_node *node = btree_node_new();
  19.     if(NULL == node) {
  20.         return NULL;
  21.     }
  22.     return node;
  23. }


插入节点

  1. // 当child满时,将其进行分裂,child = parent->p[pos]
  2. int btree_split_child(btree_node *parent, int pos, btree_node *child)
  3. {
  4.     // 创建新的节点
  5.     btree_node *new_child = btree_node_new();
  6.     if(NULL == new_child) {
  7.         return 1;
  8.     }

      
     // 新节点的is_leaf与child相同,key的个数为M-1

  1.     new_child>is_leaf = child>is_leaf;
  2.     new_child>num = M  1;
  3.     
  4.     // 将child后半部分的key拷贝给新节点
  5.     for(int i = 0; i < M  1; i++) {
  6.         new_child>k[i] = child>k[i+M];
  7.     }

      
     // 如果child不是叶子,还需要把指针拷过去,指针比节点多1

  1.     if(false == new_child>is_leaf) {
  2.         for(int i = 0; i < M; i++) {
  3.             new_child>p[i] = child>p[i+M];
  4.         }
  5.     }
  6.     child>num = M  1;

      
     // child的中间节点需要插入parent的pos处,更新parent的key和pointer

  1.     for(int i = parent>num; i > pos; i) {
  2.         parent>p[i+1] = parent>p[i];
  3.     }
  4.     parent>p[pos+1] = new_child;
  5.     for(int i = parent>num  1; i >= pos; i) {
  6.         parent>k[i+1] = parent>k[i];
  7.     }
  8.     parent>k[pos] = child>k[M1];
  9.     
  10.     parent>num += 1;
  11. }

 
 // 执行该操作时,node->num < 2M-1 

  1. void btree_insert_nonfull(btree_node *node, int target)
  2. {
  3.     if(== node>is_leaf) { // 如果在叶子中找到,直接删除
  4.         int pos = node>num;
  5.         while(pos >= 1 && target < node>k[pos1]) {
  6.             node>k[pos] = node>k[pos1];
  7.             pos;
  8.         }
  9.         node>k[pos] = target;
  10.         node>num += 1;
  11.     } else {   // 沿着查找路径下降
  12.         int pos = node>num;
  13.         while(pos > 0 && target < node>k[pos1]) {
  14.             pos;
  15.         }
  16.         if(* M == node>p[pos]>num) {
  17.             btree_split_child(node, pos, node>p[pos]); // 如果路径上有满节点则分裂
  18.             if(target > node>k[pos]) {
  19.                 pos++;
  20.             }
  21.         }
  22.         
  23.         btree_insert_nonfull(node>p[pos], target);
  24.     }
  25. }

 //插入入口

  1. btree_node* btree_insert(btree_node *root, int target)
  2. {
  3.     if(NULL == root) {
  4.         return NULL;
  5.     }
  6.     
  7.     // 对根节点的特殊处理,如果根是满的,唯一使得树增高的情形
  8.     // 先申请一个新的
  9.     if(* M  1 == root>num) {
  10.         btree_node *node = btree_node_new();
  11.         if(NULL == node) {
  12.             return root;
  13.         }
  14.         
  15.         node>is_leaf = 0;
  16.         node>p[0] = root;
  17.         btree_split_child(node, 0, root);
  18.         btree_insert_nonfull(node, target);
  19.         return node;
  20.     } else {
  21.         btree_insert_nonfull(root, target); 
  22.         return root;
  23.     }
  24. }


删除节点

  1. // 将y,root->k[pos], z合并到y节点,并释放z节点,y,z各有M-1个节点
  2. void btree_merge_child(btree_node *root, int pos, btree_node *y, btree_node *z)
  3. {
  4.     // 将z中节点拷贝到y的后半部分
  5.     y>num = 2 * M  1;
  6.     for(int i = M; i < 2 * M  1; i++) {
  7.         y>k[i] = z>k[iM];
  8.     }
  9.     y>k[M1] = root>k[pos]; // k[pos]下降为y的中间节点
  10.     
  11.     // 如果z非叶子,需要拷贝pointer
  12.     if(false == z>is_leaf) {
  13.         for(int i = M; i < 2 * M; i++) {
  14.             y>p[i] = z>p[iM];
  15.         }
  16.     }

     
      // k[pos]下降到y中,更新key和pointer

  1.     for(int j = pos + 1; j < root>num; j++) {
  2.         root>k[j1] = root>k[j];
  3.         root>p[j] = root>p[j+1];
  4.     }
  5.     root>num = 1;
  6.     free(z);
  7. }

  
 // 删除入口

  1. btree_node *btree_delete(btree_node *root, int target)
  2. {
  3.     // 特殊处理,当根只有两个子女,切两个子女的关键字个数都为M-1时,合并根与两个子女
  4.     // 这是唯一能降低树高的情形
  5.     if(== root>num) {
  6.         btree_node *= root>p[0];
  7.         btree_node *= root>p[1];
  8.         if(NULL != y && NULL != z &&
  9.                 M  1 == y>num && M  1 == z>num) {
  10.             btree_merge_child(root, 0, y, z);
  11.             free(root);
  12.             btree_delete_nonone(y, target);
  13.             return y;
  14.         } else {
  15.             btree_delete_nonone(root, target);
  16.             return root;
  17.         }
  18.     } else {
  19.         btree_delete_nonone(root, target);    
  20.         return root;
  21.     }
  22. }

  
 // root至少有个t个关键字,保证不会回溯

  1. void btree_delete_nonone(btree_node *root, int target)
  2. {
  3.     if(true == root>is_leaf) {  // 如果在叶子节点,直接删除
  4.         int i = 0;
  5.         while(< root>num && target > root>k[i]) i++;
  6.         if(target == root>k[i]) {
  7.             for(int j = i + 1; j < 2 * M  1; j++) {
  8.                 root>k[j1] = root>k[j];
  9.             }
  10.             root>num = 1;
  11.         } else {
  12.             printf(“target not found\n”);
  13.         }
  14.     } else {  // 在分支中
  15.         int i = 0;
  16.         btree_node *= NULL, *= NULL;
  17.         while(< root>num && target > root>k[i]) i++
  18.         if(< root>num && target == root>k[i]) { // 如果在分支节点找到target
  19.             y = root>p[i];
  20.             z = root>p[i+1];
  21.             if(y>num > M  1) {  
  22. // 如果左分支关键字多于M-1,则找到左分支的最右节点prev,替换target
  23.                 // 并在左分支中递归删除prev,情况2(a)
  24.                 int pre = btree_search_predecessor(y);
  25.                 root>k[i] = pre;
  26.                 btree_delete_nonone(y, pre);
  27.             } else if(z>num > M  1) {
  28.                 // 如果右分支关键字多于M-1,则找到右分支的最左节点next,替换target
  29.                 // 并在右分支中递归删除next,情况2(b)
  30.                 int next = btree_search_successor(z);
  31.                 root>k[i] = next;
  32.                 btree_delete_nonone(z, next);
  33.             } else {
  34.                 // 两个分支节点数都为M-1,则合并至y,并在y中递归删除target,情况2(c)
  35.                 btree_merge_child(root, i, y, z);
  36.                 btree_delete(y, target);
  37.             }
  38.         } else {   // 在分支没有找到,肯定在分支的子节点中
  39.             y = root>p[i];
  40.             if(< root>num) {
  41.                 z = root>p[i+1];
  42.             }
  43.             btree_node *= NULL;
  44.             if(> 0) {
  45.                 p = root>p[i1];
  46.             }
  47.             if(y>num == M  1) {
  48.                 if(> 0 && p>num > M  1) {
  49.                     // 左邻接节点关键字个数大于M-1
  50.                     btree_shift_to_right_child(root, i1, p, y); //情况3(a)
  51.                 } else if(< root>num && z>num > M  1) {
  52.                     // 右邻接节点关键字个数大于M-1
  53.                     btree_shift_to_left_child(root, i, y, z); // 情况3(b)
  54.                 } else if(> 0) {
  55.                     btree_merge_child(root, i1, p, y);  // 情况3(c)
  56.                     y = p;
  57.                 } else {
  58.                     btree_merge_child(root, i, y, z); // 情况3(c)
  59.                 }
  60.                 btree_delete_nonone(y, target);
  61.             } else {
  62.                 btree_delete_nonone(y, target);
  63.             }
  64.         }
  65.     }
  66. }

 //寻找rightmost,以root为根的最大关键字

  1. int btree_search_predecessor(btree_node *root)
  2. {
  3.     btree_node *= root;
  4.     while(false == y>is_leaf) {
  5.         y = y>p[y>num];
  6.     }
  7.     return y>k[y>num1];
  8. }

 // 寻找leftmost,以root为根的最小关键字

  1. int btree_search_successor(btree_node *root) 
  2. {
  3.     btree_node *= root;
  4.     while(false == z>is_leaf) {
  5.         z = z>p[0];
  6.     }
  7.     return z>k[0];
  8. }

 // z向y借节点,将root->k[pos]下降至z,将y的最大关键字上升至root的pos处

  1. void btree_shift_to_right_child(btree_node *root, int pos, 
  2.         btree_node *y, btree_node *z)
  3. {
  4.     z>num += 1;
  5.     for(int i = z>num 1; i > 0; i) {
  6.         z>k[i] = z>k[i1];
  7.     }
  8.     z>k[0]= root>k[pos];
  9.     root>k[pos] = y>k[y>num1];
  10.     if(false == z>is_leaf) {
  11.         for(int i = z>num; i > 0; i) {
  12.             z>p[i] = z>p[i1];
  13.         }
  14.         z>p[0] = y>p[y>num];
  15.     }
  16.     y>num = 1
  17. }



 // y向借节点,将root->k[pos]下降至y,将z的最小关键字上升至root的pos处

  1. void btree_shift_to_left_child(btree_node *root, int pos,
  2.         btree_node *y, btree_node *z)
  3. {
  4.     y>num += 1;
  5.     y>k[y>num1] = root>k[pos];
  6.     root>k[pos] = z>k[0];
  7.     for(int j = 1; j < z>num; j++) {
  8.         z>k[j1] = z>k[j];
  9.     }
  10.     if(false == z>is_leaf) {
  11.         y>p[y>num] = z>p[0];
  12.         for(int j = 1; j <= z>num; j++) {
  13.             z>p[j1] = z>p[j];
  14.         }
  15.     } 
  16.     z>num = 1;
  17. }


插入与删除过程(图片为层序遍历的结果)

插入序列[18, 31, 12, 10, 15, 48, 45, 47, 50, 52, 23, 30, 20]

《B树算法与实现》

 

删除序列[15, 18, 23, 30, 31, 52, 50, 48, 47, 45, 20, 12, 10]

《B树算法与实现》

B+树

与B树不同的时,B+树的关键字都存储在叶子节点,分支节点均为索引,在实现上大致与B树类似,在几个细节稍有不同。

 

(1) 数据结构中增加prev,next指针,用于将叶子节点串成有序双向链表。

(2) 在节点分裂的时候,如果分裂的节点为叶子,则需要把中间节点保留在左(或右)边的分支上,并且需要更新prev和next。

(3) 在节点合的时候,如果合并的节点为叶子,不需要把跟节点下降为中间节点,并且需要更新prev和next。

(4) 在向邻接节点借节点时,借来的关键字并不是父节点的关键字,而是邻接点的关键字,并根据实际情况更新父节点的索引。

    原文作者:B树
    原文地址: https://blog.csdn.net/theam/article/details/9180917
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注