当前位置:   article > 正文

【数据结构与算法】B_树_数据结构b

数据结构b

目录

前言:

一、B树

1、B树概念

2、B树查找

3、B树插入

4、B树前序遍历

5、B树性能

二、B+、B*树

1、B+树概念

2、B+树的插入

2、B*树概念

3、总结

三、B系列树的应用

总结


前言:

我们已经有很多索引的数据结构了

例如:

顺序查找 、二分查找 、二叉搜索树 、二叉平衡树(AVL树和红黑树) 、哈希
以上结构适合于数据量相对较小,能够一次性存放在内存中,进行数据查找
如果数据量很大,一次性无法放进内存中,那么只能存放到磁盘上,如果要进行搜索,只能将关键字映射的数据的地址存放到搜索树的节点中,要访问数据,就要先去磁盘中读取
磁盘的速度是远低于内存的,虽然平衡搜索树的时间复杂度是O(log H)(H是高度)
100亿个数据也就仅仅需要查找10次,但是这是在内存的情况,10次IO的速度和在内存中查找10次
的速度相差十分的大
哈希表虽然能够达到O(1)但是极端情况下哈希冲突非常严重,效率下降很多
所以为了解决大量数据的查询,在平衡搜索树的基础上提出了B树
B树主要进行了两点优化
1、压缩高度,二叉树变成多叉树
2、一个节点里面有多个关键字及映射的值

一、B树

1、B树概念

1970年,R.Bayer和E.mccreight提出了一种适用于外查找的,它是一种平衡的多叉树,称为B树(或B-树、B_树)。

一棵m阶B树(balanced tree of order m)是一棵平衡的m路搜索树。它或者是空树,或者是满足下列性质的树:

1. 根节点至少有两个孩子
2. 每个分支节点都包含 k-1 个关键字和 k 个孩子,其中 ceil(m/2) ≤ k ≤ m ceil 是向上取整函数
3. 每个叶子节点都包含 k-1 个关键字,其中 ceil(m/2) ≤ k ≤ m
4. 所有的叶子节点都在同一层
5. 每个节点中的关键字从小到大排列,节点当中 k-1 个元素正好是 k 个孩子包含的元素的值域划
6. 每个结点的结构为:( n A0 K1 A1 K2 A2 Kn An 其中, Ki(1≤i≤n) 为关键
字,且 Ki<Ki+1(1≤i≤n-1) Ai(0≤i≤n) 为指向子树根结点的指针。且 Ai 所指子树所有结点中的关键字均小于Ki+1 。 n为结点中关键字的个数,满足ceil(m/2)-1≤n≤m-1
根至少有两个孩子的原因会在分裂时讲解
关键字的个数比孩子的数量少一个
例如M = 10
最少有 4个关键字, 5个孩子

最多有9个关键字,10个孩子

向上取整的原因会在插入时说明

同时每一层的关键字按特定的顺序排列(升序,降序,可以使用仿函数来控制)

 

2、B树查找

还是以上图为例,现在要找53

寻找的思路与二叉搜索树类型,不过这次是在一个数组中寻找,

它既要在纵向上搜索,也要在横向上搜索

先在横向搜索,从左向右遍历找到与它的key相等的节点,如果不相等比它小,就继续向右遍历

比它小就到下一层寻找,直到找到该节点或者将整棵树遍历完都没有找到

为了提高效率,因为对于每一个节点来说,它是有序的,可以使用二分查找提高查找效率

同时为了方便实现Insert,我们将Find的返回值类型定义为pair<Node*, int>

如果能够找到就返回该节点的地址,及它在该节点位置的下标

找不到就返回它的父节点及-1,可以根据pair的second来判断是否找到该节点

找到它的父节点就可以方便进行插入了

  1. // 顺序遍历
  2. std::pair<Node *, int> Find(const K &key)
  3. {
  4. Node *cur = _root;
  5. Node *parent = nullptr;
  6. while (cur)
  7. {
  8. size_t i = 0;
  9. while (i < cur->_n)
  10. {
  11. if (key < cur->_keys[i])
  12. {
  13. break;
  14. }
  15. else if (key > cur->_keys[i])
  16. {
  17. i++;
  18. }
  19. else
  20. {
  21. return std::make_pair(cur, i);
  22. }
  23. }
  24. parent = cur;
  25. cur = cur->_subs[i];
  26. }
  27. return std::make_pair(parent, -1);
  28. }
  29. // 二分查找
  30. std::pair<Node *, int> Find(const K &key)
  31. {
  32. Node *cur = _root;
  33. Node *parent = nullptr;
  34. while (cur)
  35. {
  36. int left = 0;
  37. int right = cur->_n - 1;
  38. while (left <= right)
  39. {
  40. int mid = (left + right) >> 1;
  41. if (key < cur->_keys[mid])
  42. {
  43. right = mid - 1;
  44. }
  45. else if (key > cur->_keys[mid])
  46. {
  47. left = mid + 1;
  48. }
  49. else
  50. {
  51. return std::make_pair(cur, mid);
  52. }
  53. }
  54. parent = cur;
  55. cur = cur->_subs[left];
  56. }
  57. return std::make_pair(parent, -1);
  58. }

3、B树插入

B树的插入,如果不考虑分裂是比较简单的

  1. bool Insert(const K &key, const V &val)
  2. {
  3. if (_root == nullptr)
  4. {
  5. _root = new Node;
  6. _root->_keys[0] = key;
  7. _root->_val[0] = val;
  8. _root->_n++;
  9. return true;
  10. }
  11. return true;
  12. }

接下来看具体的插入过程

 

 

 

 

 

 

 

 

 

 

 

  1. void InsertKey(Node *node, const K &key, const V &val, Node *child)
  2. {
  3. int end = node->_n - 1;
  4. while (end >= 0)
  5. {
  6. if (key < node->_keys[end])
  7. {
  8. node->_keys[end + 1] = node->_keys[end];
  9. node->_val[end + 1] = node->_val[end];
  10. // child 也要对应上
  11. node->_subs[end + 2] = node->_subs[end + 1];
  12. end--;
  13. }
  14. else
  15. {
  16. break;
  17. }
  18. }
  19. // 先插入key
  20. node->_keys[end + 1] = key;
  21. node->_val[end + 1] = val;
  22. // 最后插入child,关键字比节点数少一
  23. node->_subs[end + 2] = child;
  24. if (child)
  25. {
  26. child->_parent = node;
  27. }
  28. node->_n++;
  29. }

  1. bool Insert(const K &key, const V &val)
  2. {
  3. if (_root == nullptr)
  4. {
  5. _root = new Node;
  6. _root->_keys[0] = key;
  7. _root->_val[0] = val;
  8. _root->_n++;
  9. return true;
  10. }
  11. // 插入节点过程
  12. K newKey = key;
  13. V newVal = val;
  14. Node *child = nullptr;
  15. std::pair<Node *, int> ret = Find(newKey);
  16. //说明已经存在该节点了,不用插入
  17. if (ret.second >= 0)
  18. {
  19. return false;
  20. }
  21. Node *parent = ret.first;
  22. while (true)
  23. {
  24. InsertKey(parent, newKey, newVal, child);
  25. if (parent->_n < M)
  26. {
  27. return true;
  28. }
  29. // B树满了,开始分裂,创建新节点,并且将原节点的一半拷贝给brother
  30. size_t mid = M / 2;
  31. Node *brother = new Node;
  32. size_t j = 0;
  33. size_t i = mid + 1;
  34. for (; i < M; i++)
  35. {
  36. brother->_keys[j] = parent->_keys[i];
  37. brother->_val[j] = parent->_keys[i];
  38. brother->_subs[j] = parent->_subs[i];
  39. // parent->child->parent = brother
  40. if (parent->_subs[i])
  41. {
  42. parent->_subs[i]->_parent = brother;
  43. }
  44. // 处理干净,指针必须处理,val可以不处理
  45. parent->_keys[i] = K();
  46. parent->_val[i] = V();
  47. parent->_subs[i] = nullptr;
  48. j++;
  49. }
  50. // child比key多一个,处理最后的右子树
  51. brother->_subs[j] = parent->_subs[i];
  52. if (parent->_subs[i])
  53. {
  54. parent->_subs[i]->_parent = brother;
  55. }
  56. parent->_subs[i] = nullptr;
  57. brother->_n = j;
  58. parent->_n -= j + 1; // 还有一个节点给了parent
  59. K midKey = parent->_keys[mid];
  60. K midVal = parent->_val[mid];
  61. parent->_keys[mid] = K();
  62. parent->_val[mid] = V();
  63. //说明刚才分裂的是根节点
  64. if (parent->_parent == nullptr)
  65. {
  66. _root = new Node;
  67. _root->_keys[0] = midKey;
  68. _root->_val[0] = midVal;
  69. _root->_subs[0] = parent;
  70. _root->_subs[1] = brother;
  71. _root->_n = 1;
  72. parent->_parent = _root;
  73. brother->_parent = _root;
  74. break;
  75. }
  76. else
  77. {
  78. // 转化为parent->parent 中插入 newKey和brother
  79. newKey = midKey;
  80. newVal = midVal;
  81. child = brother;
  82. parent = parent->_parent;
  83. }
  84. }
  85. return true;
  86. }

4、B树前序遍历

它的前序遍历就是多叉树的前序遍历,同时不要忘记最后的右子树

  1. void _InOrder(Node *root)
  2. {
  3. if (root == nullptr)
  4. {
  5. return;
  6. }
  7. //左树,根,左树,根,左树,根……
  8. size_t i = 0;
  9. for (; i < root->_n; i++)
  10. {
  11. _InOrder(root->_subs[i]);
  12. std::cout << "Key " << root->_keys[i] << " : "
  13. << "val " << root->_val[i] << std::endl;
  14. }
  15. //右数
  16. _InOrder(root->_subs[i]);
  17. }
  18. void InOrder()
  19. {
  20. _InOrder(_root);
  21. }

5、B树性能

对于一棵节点为N度为M的B-树,查找和插入需要log{M-1}N~log{M/2}N次比较,这个很好证明:
对于度为M的B-树,每一个节点的子节点个数为M/2 ~(M-1)之间,因此树的高度应该在要 log{M-1}Nlog{M/2}N之间,在定位到该节点后,再采用二分查找的方式可以很快的定位 到该元素
B-树的效率是很高的,对于N = 62*1000000000个节点,如果度M为1024,则log_{M/2}N <=
4,即在620亿个元素中,如果这棵树的度为1024,则需要小于4次即可定位到该节点,然后利用
二分查找可以快速定位到该元素,大大减少了读取磁盘的次数

二、B+、B*树

1、B+树概念

B+ 树是 B 树的变形,是在 B 树基础上优化的多路平衡搜索树, B+ 树的规则跟 B 树基本类似,但是又
B 树的基础上做了以下几点改进优化:
1、分支节点的子树指针与关键字个数相同
2、  分支节点的子树指针 p[i] 指向关键字值大小在 [k[i] k[i+1]) 区间之间
3、所有叶子节点增加一个链接指针链接在一起
4、  所有关键字及其映射数据都在叶子节点出现

总结:

分支节点跟叶子节点有重复的值,分支节点存的是叶子结点的索引

父亲中存的是孩子节点中的最小值做索引

B+树特性:

所有关键字都出现在叶子节点的链表中,且链表中的节点都是有序的

在分支节点无法获取value

分支节点相当于是叶子节点的索引,叶子节点才是存储数据的

 

2、B+树的插入

B+树的分裂:
当一个结点满时,分配一个新的结点,并将原结点中1/2的数据复制到新结点,最后在父结点中增
加新结点的指针;B+树的分裂只影响原结点和父结点,而不会影响兄弟结点,所以它不需要指向
兄弟的指针。

 

 

 

 

 

 

2、B*树概念

B* 树是 B+ 树的变形,在 B+ 树的非根和非叶子节点再增加指向兄弟节点的指针。
B* 树的分裂:
当一个结点满时,如果它的下一个兄弟结点未满,那么将一部分数据移到兄弟结点中,再在原结
点插入关键字,最后修改父结点中兄弟结点的关键字(因为兄弟结点的关键字范围改变了);如
果兄弟也满了,则在原结点与兄弟结点之间增加新结点,并各复制1/3的数据到新结点,最后在父
结点增加新结点的指针。
所以,B*树分配新结点的概率比B+树要低,空间使用率更高;

 

B*树主要是为了弥补B+树的空间利用率低的缺点

3、总结

通过以上介绍,大致将B树,B+树,B*树总结如下:
B树:有序数组+平衡多叉树;
B+树:有序数组链表+平衡多叉树;
B*树:一棵更丰满的,空间利用率更高的B+树

三、B系列树的应用

B-树最常见的应用就是用来做索引。索引通俗的说就是为了方便用户快速找到所寻之物,比如:
书籍目录可以让读者快速找到相关信息,hao123网页导航网站,为了让用户能够快速的找到有价
值的分类网站,本质上就是互联网页面中的索引结构。
MySQL官方对索引的定义为:索引(index)是帮助MySQL高效获取数据的数据结构,简单来说:
索引就是数据结构
当数据量很大时,为了能够方便管理数据,提高数据查询的效率,一般都会选择将数据保存到数
据库,因此数据库不仅仅是帮助用户管理数据,而且数据库系统还维护着满足特定查找算法的数
据结构,这些数据结构以某种方式引用数据,这样就可以在这些数据结构上实现高级查找算法,
该数据结构就是索引。


总结


以上就是今天要讲的内容,本文仅仅简单介绍了B树的相关概念

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/792241
推荐阅读
相关标签
  

闽ICP备14008679号