飞道的博客

B+树原理分析及Java代码实现

492人阅读  评论(0)

今天我们分析B+树原理及Java代码实现,以前我写过一篇关于mysql 存储引擎B+Tree、 B-Tree和hash三种原理及区别,可以先参考

我们都知道B+树,是B树的一个变种,需要先明确一下B树的定义:

一、B/B+树的基本定义:

1、B 树可以看作是对2-3查找树的一种扩展,即他允许每个节点有M-1个子节点。

  • 根节点至少有两个子节点
  • 每个节点有M-1个key,并且以升序排列
  • 位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间
  • 其它节点至少有M/2个子节点

下图是一个M=4 阶的B树:

可以看到B树是2-3树的一种扩展,他允许一个节点有多于2个的元素。

B树的插入及平衡化操作和2-3树很相似,这里就不介绍了。下面是往B树中依次插入

6 10 4 14 5 11 15 3 2 12 1 7 8 8 6 3 6 21 5 15 15 6 32 23 45 65 7 8 6 5 4

的演示动画:

2、B+树既然是对B树的一种变种树,它与B树的差异在于:

  • 有k个子结点的结点必然有k个关键码;
  • 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
  • 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。

如下图,是一个M=4的B+树:

下图是B+树的插入动画:动画参考

3、B和B+树的区别在于,B+树的非叶子结点只包含导航索引信息,不包含实际的值(这样可以放更多的key索引),所有的叶子结点和相连的节点使用链表相连,便于区间查找和遍历。

4、B+ 树的优点在于:

  • 由于B+树在内部节点上不包含数据信息,因此在内存页中能够存放更多的key。 数据存放的更加紧密,具有更好的空间局部性。因此访问叶子节点上关联的数据也具有更好的缓存命中率。
  • B+树的叶子结点都是相链的,因此对整棵树的便利只需要一次线性遍历叶子结点即可。而且由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。

但是B树也有优点,其优点在于,由于B树的每一个节点都包含key和value,因此经常访问的元素可能离根节点更近,因此访问也更迅速。 

二、代码实现:我们先贴出代码,在逐个方法分析

1、核心代码:


  
  1. /**
  2. * @author wanghuainan
  3. * @date 2021/4/14 19:05
  4. */
  5. public class CatalogBTree<Key extends Comparable<Key>, Value> {
  6. //参数M 定义每个节点的链接数
  7. private static final int M = 4;
  8. private Node root;
  9. //树的高度 最底层为0 表示外部节点 根具有最大高度,此高度是根之后的高度
  10. private int height;
  11. //键值对总数
  12. private int n;
  13. //节点数据结构定义
  14. private static final class Node{
  15. //值的数量
  16. private int m;
  17. private Entry[] children = new Entry[M];
  18. private Node(int k){
  19. m = k;
  20. }
  21. }
  22. //节点内部每个数据项定义
  23. private static class Entry{
  24. private Comparable key;
  25. private final Object val;
  26. //下一个节点
  27. private Node next;
  28. public Entry(Comparable key, Object val, Node next){
  29. this.key = key;
  30. this.val = val;
  31. this.next = next;
  32. }
  33. @Override
  34. public String toString() {
  35. StringBuilder builder = new StringBuilder();
  36. builder.append( "Entry [key=");
  37. builder.append(key);
  38. builder.append( "]");
  39. return builder.toString();
  40. }
  41. }
  42. public CatalogBTree(){
  43. root = new Node( 0);
  44. }
  45. public int size(){
  46. return n;
  47. }
  48. public boolean isEmpty(){
  49. return size() == 0;
  50. }
  51. public int height(){
  52. return height;
  53. }
  54. /**
  55. * 查询接口
  56. * @param key
  57. * @return
  58. */
  59. public Value get(Key key){
  60. return search(root, key, height);
  61. }
  62. private Value search(Node x, Key key, int ht) {
  63. Entry[] children = x.children; //首次进入,相当于根节点
  64. //外部节点 这里使用顺序查找
  65. //如果M很大 可以改成二分查找
  66. if(ht == 0){ //到了有数据的子节点(也可能是根节点,如果键值对小于M),此次查询数据。
  67. for( int j= 0; j<x.m; j++){
  68. if(equal(key, x.children[j].key)) //遍历查询
  69. return (Value)children[j].val;
  70. }
  71. }
  72. //内部节点 寻找下一个节点
  73. else{
  74. for( int j= 0; j<x.m; j++){
  75. //最后一个节点 或者 插入值小于某个孩子节点值
  76. if(j+ 1==x.m || less(key, x.children[j+ 1].key)) //定位数据在哪个节点下,然后继续递归查询
  77. return search(x.children[j].next, key, ht- 1); //去下一层继续查询
  78. }
  79. }
  80. return null;
  81. }
  82. /**
  83. * 新增数据接口
  84. * @param key
  85. * @param val
  86. */
  87. public void put(Key key, Value val){
  88. //插入后的节点,如果节点分裂,则返回分裂后产生的新节点
  89. Node u = insert(root, key, val, height);
  90. n++; //键值对总数加一
  91. if(u == null) return; //根节点没有分裂 直接返回
  92. //分裂根节点,已满节点进行分裂,将已满节点后M/2节点生成一个新节点,并将新节点的第一个元素指向父节点,此处是M>>1 = 2(因为M=4)
  93. Node t = new Node(M>> 1);
  94. //旧根节点第一个孩子和新分裂节点第一个孩子 共同 组成新节点作为根
  95. t.children[ 0] = new Entry(root.children[ 0].key, null, root);
  96. t.children[ 1] = new Entry(u.children[ 0].key, null, u);
  97. root = t; //新的根节点
  98. height++; //节点高度加1
  99. }
  100. private Node insert(Node h, Key key, Value val, int ht) {
  101. int j;
  102. //新建待插入数据数据项
  103. Entry t = new Entry(key, val, null);
  104. if(ht == 0){ //高度为零时,直接遍历
  105. for(j= 0; j<h.m; j++){ // 外部节点 找到带插入的节点位置j
  106. if(less(key, h.children[j].key))
  107. break;
  108. }
  109. } else{
  110. //内部节点 找到合适的分支节点
  111. for(j= 0; j<h.m; j++){
  112. if(j+ 1==h.m || less(key, h.children[j+ 1].key)){
  113. Node u = insert(h.children[j++].next, key, val, ht- 1);
  114. if(u == null) return null;
  115. t.key = u.children[ 0].key;
  116. t.next = u;
  117. break;
  118. }
  119. }
  120. }
  121. //j为带插入位置,将顺序数组j位置以后后移一位(从后往前处理) 将t插入
  122. for( int i=h.m; i>j; i--){
  123. h.children[i] = h.children[i- 1];
  124. }
  125. System.out.println(j + t.toString());
  126. h.children[j] = t; //此处插入成功
  127. h.m++; //值的数量加一
  128. if(h.m < M) return null;
  129. //如果节点已满 则执行分类操作(从根节点开始)
  130. else return split(h);
  131. }
  132. private Node split(Node h) {
  133. //将已满节点h的后,一般M/2节点后的节点分裂到新节点并返回
  134. Node t = new Node(M/ 2);
  135. h.m = M / 2;
  136. for( int j= 0; j<M/ 2; j++){
  137. t.children[j] = h.children[M/ 2+j]; //把h节点中M/2节点后的节点分裂到新节点
  138. h.children[M/ 2+j] = null; //把h节点中M/2节点后的节点设置为空,以尽快GC
  139. }
  140. return t; //返回新节点
  141. }
  142. /**
  143. * 删除数据
  144. * @param key
  145. */
  146. public void remove(Key key){
  147. remove(root, key, height);
  148. }
  149. private void remove(Node x, Key key, int ht){
  150. Entry[] children = x.children; //首次进入,相当于根节点
  151. //外部节点 这里使用顺序查找
  152. //如果M很大 可以改成二分查找
  153. if(ht == 0){ //到了有数据的子节点(也可能是根节点,如果键值对小于M),此次查询数据。
  154. for( int j= 0; j<x.m; j++){
  155. if(equal(key, x.children[j].key)){ //遍历查询
  156. children[j] = null; //删除此节点数据
  157. x.m--; //值的数量减一
  158. }
  159. }
  160. }
  161. //内部节点 寻找下一个节点
  162. else{
  163. for( int j= 0; j<x.m; j++){
  164. //最后一个节点 或者 插入值小于某个孩子节点值
  165. if(j+ 1==x.m || less(key, x.children[j+ 1].key)) //定位数据在哪个节点下,然后继续递归查询
  166. remove(x.children[j].next, key, ht- 1); //去下一层继续查询
  167. }
  168. }
  169. }
  170. private boolean equal(Comparable k1, Comparable k2){
  171. return k1.compareTo(k2)== 0;
  172. }
  173. private boolean less(Comparable k1, Comparable k2){
  174. return k1.compareTo(k2)< 0;
  175. }
  176. public String toString() {
  177. return toString(root, height, "") + "\n";
  178. }
  179. private String toString(Node h, int ht, String indent) {
  180. StringBuilder s = new StringBuilder();
  181. Entry[] children = h.children; //此数据相当于根节点
  182. //外部节点
  183. if (ht == 0) {
  184. for ( int j = 0; j < h.m; j++) {
  185. s.append(indent + children[j].key + " " + children[j].val + "\n");
  186. }
  187. }
  188. else {
  189. for ( int j = 0; j < h.m; j++) {
  190. s.append(indent + "(" + children[j].key + ")\n");
  191. s.append(toString(children[j].next, ht- 1, indent + " "));
  192. }
  193. }
  194. return s.toString();
  195. }
  196. }

2、分析测试添加数据put方法:

insert等方法可以去代码里查看,下面进行main方法测试打印:


  
  1. public static void main(String[] args) {
  2. /**
  3. * 添加的顺序不同,最后树的结构也不同
  4. */
  5. CatalogBTree<Integer,String> bt = new CatalogBTree<>();
  6. bt.put( 5, "a");
  7. bt.put( 8, "b");
  8. // bt.put(9,"c");
  9. bt.put( 10, "d");
  10. bt.put( 15, "e");
  11. // bt.put(18,"f");
  12. bt.put( 20, "g");
  13. bt.put( 26, "h");
  14. // bt.put(27,"i");
  15. bt.put( 28, "i");
  16. bt.put( 30, "j");
  17. // bt.put(33,"k");
  18. bt.put( 35, "l");
  19. bt.put( 38, "m");
  20. // bt.put(50,"n");
  21. bt.put( 56, "o");
  22. bt.put( 60, "p");
  23. // bt.put(63,"q");
  24. bt.put( 65, "r");
  25. bt.put( 73, "s");
  26. // bt.put(79,"t");
  27. bt.put( 80, "u");
  28. bt.put( 85, "v");
  29. // bt.put(88,"w");
  30. bt.put( 90, "x");
  31. bt.put( 96, "y");
  32. bt.put( 99, "z");
  33. /**
  34. * 插入第三个元素
  35. */
  36. bt.put( 9, "c");
  37. bt.put( 18, "f");
  38. bt.put( 27, "i");
  39. bt.put( 33, "k");
  40. bt.put( 50, "n");
  41. bt.put( 63, "q");
  42. bt.put( 79, "t");
  43. bt.put( 88, "w");
  44. System. out.println( "高度:"+bt.height());
  45. System. out.println( "查询:"+bt. get( 9));
  46. System. out.println( bt.toString());
  47. }

用此种顺序添加,最后的结构图是这样的:

控制台打印也可以验证:(自己可以测试验证)

 

一张没有截完,在来一张 

 

注意:添加节点时经常会出现分裂节点,这块一定要详细琢磨。  比如添加一个34 节点:

添加的后面过程中会分裂出一个新的节点;如下图:

3、分析测试查询数据get方法:比如get(99),会一直向下循环,循环三次就找到;代码如下图:

 

4、分析测试删除数据remove方法:此方法和get方法类似,遍历找到后设置为空就行,然后数值减一,如下图:

删除节点时可能出现合并节点, 

比如现在删除一个 80 节点:删除后此处还有一个分支,然后就会合并到左边,如图:

到此,B+树的原理和实现分析告一段落。

另外,B/B+树经常用做数据库的索引,这方面推荐您直接看张洋的MySQL索引背后的数据结构及算法原理 这篇文章,这篇文章对MySQL中的如何使用B+树进行索引有比较详细的介绍,推荐阅读。

总结

在前面两篇文章介绍了平衡查找树中的2-3树红黑树之后,本文介绍了文件系统和数据库系统中常用的B/B+ 树,他通过对每个节点存储个数的扩展,使得对连续的数据能够进行较快的定位和访问,能够有效减少查找时间,提高存储的空间局部性从而减少IO操作。他广泛用于文件系统及数据库中,如:

  • Windows:HPFS文件系统
  • Mac:HFS,HFS+文件系统
  • Linux:ResiserFS,XFS,Ext3FS,JFS文件系统
  • 数据库:ORACLE,MYSQL,SQLSERVER等中

希望本文对您了解B/B+ 树有所帮助。参考文章如下:

参考

参阅

参考文章


转载:https://blog.csdn.net/nandao158/article/details/115701213
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场