经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C# » 查看文章
图解B树及C#实现(1)
来源:cnblogs  作者:黑洞视界  时间:2022/12/12 9:13:20  对本文有异议

前言

B树(B-tree),也常被记作 B-树,其中“-”不发音。B树的发明者 Rudolf Bayer 和 Edward M. McCreight 并没有给B树中的 B 明确的定义,大家也不必对此纠结太多。

B+树是B树的变体,两者的适用场景是不一样的,以后也会给大家带来B+树的介绍。

本系列将用三篇文章讲解B树的设计理念及如何用 C# 实现一个内存版本的B树:

  1. B树的定义及数据的插入(本文)
  2. 数据的读取及遍历
  3. 数据的删除

完整代码已放至github https://github.com/eventhorizon-cli/EventHorizon.BTree
或者安装 nuget 包进行体验

  1. dotnet add package EventHorizon.BTree

完整代码中包含了debug辅助代码,可以通过调试来了解B树的内部结构。

B树最早被设计出来,并不是作为一个单纯的内存数据结构,而是用作 磁盘存储引擎 的索引实现,以后也会单独写一篇文章来做说明。

本文部分说明引用自PingCAP 的公开ppt 宝宝床边故事集:存储引擎,强烈推荐给各位学习。

部分内容属于个人理解,若有不对之处,欢迎指正。

索引原理

局部性(Locality)

硬件、操作系统等等系统,绝大部分时候,执行一次操作流程 有额外的开销(overhead)。

因此很多部件、模块都设计成:连续执行类似或相同的操作、访问空间相邻的内容时,则将多次操作合并为一次,或多次之间共享上下文信息。这样能极大提升性能。

这种时间、空间上的连续性,叫做局部性。

数据的局部性

我们把数据的连续性及连续区域大小称为 局部性,连续存放的数据越多,局部性越好。

内存存储和磁盘存储

IO的访问性能有两个重要的衡量指标:

  1. IOPS(Input/Output Operations Per Second): 每秒进行IO读写操作的次数
  2. IOBW(Input/Output Bandwidth): IO带宽

磁盘的IOPS和IOBW都低于内存,IOPS更为明显。

磁盘IO是以 页(page)为单位进行数据读取的,如果数据的局部性好,只加载一个磁盘页到内存就可以实现一组有序数据的连续访问。如果数据的局部性差,则每读取一次数据都有可能要加载一个磁盘页,性能较差。

当数据局部性差时:

  • 需要更频繁地访问磁盘
  • IOPS 比 IOBW 先达到上限,性能差

当数据局部性好时:

  • IOBW 能达到硬件上限
  • IOBW 达到上限是理想的最好性能

磁盘存储适合的索引结构

综上所述,就磁盘存储而言,局部性的好坏对性能影响很大。

有序数组的局部性很好,用二分查找法查询数据的时间复杂度是O(log n)。但插入数据时,时间复杂度就成了O(n)。

二叉平衡树(Self-balancing binary search tree,常见的实现如 AVL树 和 红黑树)用二分查找法查询数据的时间复杂度是O(log n)。插入数据时也是先查询到具体位置,时间复杂度是O(log n)。

但二叉平衡树的局部性很差,这在内存中不是什么问题,因为内存访问随机数据的性能很高,但在磁盘中,不断加载不同的磁盘页,overhead 很高。

数据的局部性越好,读性能更好,但写性能会降低。
数据的局部性越差,读性能会变差,但写性能会更好。

B树则是在这两者之间寻求平衡点:

从有序数组的角度看,我们把大数组分割成了一个个小的有序数组,再用另一种有序结构把小数组组织起来,插入数据时,移动数据量减少并且可控。

从树的角度看,用一个个小的有序数组代替元素作为节点,大大增加了局部性,减少了存储 overhead。

B树简介

定义

B树中的节点分为三种:

  • 根节点(root node)
  • 内部节点(internal node):存储数据以及指向其子节点的指针。
  • 叶子节点(leaf node):叶子节点只存储数据,没有子节点。

B树只有一个节点时,根节点本身就是叶子节点。

节点中每一个数据项(下文用 item 代替)都是一组键值对。item 的数量范围需要预定义,通常有以下两种定义方式:

  • 度(degree):通常简写为 t,2t-1 代表 item 数量上限。
  • 阶(order):通常简写为 m,m 代表 item 数量上限。

本文用 度(degree)进行描述,一个度是 t(t>=2) 的B树被设计为具有以下属性:

  1. 每一个节点最多有 2t 个子节点。
  2. 每一个内部节点最少有 t 个子节点。
  3. 如果根节点不是叶子节点,那么它至少有两个子节点。
  4. 有 k 个子节点的非叶子节点拥有 k ? 1 个键。
  5. 所有的叶子节点都在同一层。

这5个属性都是为了维持B树的平衡。其中前4个是在 度 被定义后就可以控制的,而第5个是源于B树新增数据的方式,稍后会做解释。

B树中数据的有序性

  • 每个 节点 中的 Item 按 Key 有序排列(规则可以是自定义的)。
  • 升序排序时,每个 Item 左子树 中的 Item 的 Key 均小于当前 Item 的 Key。
  • 升序排序时,每个 Item 右子树 中的 Item 的 Key 均大于当前 Item 的 Key。

用C#定义数据结构

开始算法讲解前,我们需要先定义下将会用到的数据结构。

虽然代码太多可能影响阅读体验,但考虑到 gayhub 可能访问不稳定,还是尽量贴全了。

下图所示是一个 degree 是 3 的 B树,Key 按升序排序。

  1. internal class Item<TKey, TValue>
  2. {
  3. #region Constructors
  4. public Item(TKey key, TValue? value)
  5. {
  6. Key = key;
  7. Value = value;
  8. }
  9. #endregion
  10. #region Properties
  11. public TKey Key { get; }
  12. public TValue? Value { get; set; }
  13. #endregion
  14. }

定义 ItemsChildren 两个类型分别用于存储 Item 集合和子节点集合。为了简化设计以及减少动态扩容带来的性能损失,作为数据实际容器的数组在第一开始就会按最大的 capacity 进行创建。同时也预先给 ItemsChildren 定义好后面会被用到的基本方法。

  1. internal class Items<TKey, TValue>
  2. {
  3. #region Fields
  4. private readonly Item<TKey, TValue?>?[] _items;
  5. private readonly int _capacity;
  6. private readonly IComparer<TKey> _comparer;
  7. private int _count;
  8. #endregion
  9. #region Constructors
  10. public Items(int capacity, IComparer<TKey> comparer)
  11. {
  12. _capacity = capacity;
  13. _items = new Item<TKey, TValue?>[capacity];
  14. _comparer = comparer;
  15. }
  16. #region Properties
  17. public int Count => _count;
  18. #endregion
  19. #region Indexers
  20. public Item<TKey, TValue?> this[int index]
  21. {
  22. get
  23. {
  24. if (index < 0 || index >= _count)
  25. {
  26. throw new IndexOutOfRangeException();
  27. }
  28. return _items[index]!;
  29. }
  30. set => _items[index] = value;
  31. }
  32. #endregion
  33. #endregion
  34. #region Public Methods
  35. /// <summary>
  36. /// 查找指定的键,并返回它的索引,如果找不到则返回key可以插入的位置
  37. /// </summary>
  38. /// <param name="key">指定的key</param>
  39. /// <param name="index">key的索引或者其可以插入的位置</param>
  40. /// <returns>指定的key是否存在</returns>
  41. public bool TryFindKey(TKey key, out int index)
  42. {
  43. if (_count == 0)
  44. {
  45. index = 0;
  46. return false;
  47. }
  48. // 二分查找
  49. int left = 0;
  50. int right = _count - 1;
  51. while (left <= right)
  52. {
  53. int middle = (left + right) / 2;
  54. var compareResult = _comparer.Compare(key, _items[middle]!.Key);
  55. if (compareResult == 0)
  56. {
  57. index = middle;
  58. return true;
  59. }
  60. if (compareResult < 0)
  61. {
  62. right = middle - 1;
  63. }
  64. else
  65. {
  66. left = middle + 1;
  67. }
  68. }
  69. index = left;
  70. return false;
  71. }
  72. public void InsertAt(int index, Item<TKey, TValue?> item)
  73. {
  74. if (_count == _capacity)
  75. throw new InvalidOperationException("Cannot insert into a full list.");
  76. if (index < _count)
  77. Array.Copy(_items, index, _items, index + 1, _count - index);
  78. _items[index] = item;
  79. _count++;
  80. }
  81. public void Add(Item<TKey, TValue?> item) => InsertAt(_count, item);
  82. public void AddRange(Items<TKey, TValue?> items)
  83. {
  84. if (_count + items.Count > _capacity)
  85. throw new InvalidOperationException("Cannot add items to a full list.");
  86. Array.Copy(items._items, 0, _items, _count, items.Count);
  87. _count += items.Count;
  88. }
  89. public Item<TKey, TValue?> RemoveAt(int index)
  90. {
  91. if (index >= _count)
  92. throw new ArgumentOutOfRangeException(nameof(index));
  93. var item = _items[index];
  94. if (index < _count - 1)
  95. Array.Copy(_items, index + 1, _items, index, _count - index - 1);
  96. _items[_count - 1] = null;
  97. _count--;
  98. return item!;
  99. }
  100. public Item<TKey, TValue?> RemoveLast() => RemoveAt(_count - 1);
  101. public void Truncate(int index)
  102. {
  103. if (index >= _count)
  104. throw new ArgumentOutOfRangeException(nameof(index));
  105. for (int i = index; i < _count; i++)
  106. {
  107. _items[i] = null;
  108. }
  109. _count = index;
  110. }
  111. #endregion
  112. }
  1. internal class Children<TKey, TValue>
  2. {
  3. #region Fields
  4. private readonly Node<TKey, TValue?>?[] _children;
  5. private readonly int _capacity;
  6. private int _count;
  7. #endregion
  8. #region Constructors
  9. public Children(int capacity)
  10. {
  11. _capacity = capacity;
  12. _children = new Node<TKey, TValue?>[_capacity];
  13. }
  14. #endregion
  15. #region Properties
  16. public int Count => _count;
  17. #endregion
  18. #region Indexers
  19. public Node<TKey, TValue?> this[int index]
  20. {
  21. get
  22. {
  23. if (index < 0 || index >= _count)
  24. {
  25. throw new IndexOutOfRangeException();
  26. }
  27. return _children[index]!;
  28. }
  29. }
  30. #endregion
  31. #region Public Methods
  32. public void InsertAt(int index, Node<TKey, TValue?> child)
  33. {
  34. if (_count == _capacity)
  35. throw new InvalidOperationException("Cannot insert into a full list.");
  36. if (index < _count)
  37. Array.Copy(_children, index, _children, index + 1, _count - index);
  38. _children[index] = child;
  39. _count++;
  40. }
  41. public void Add(Node<TKey, TValue?> child) => InsertAt(_count, child);
  42. public void AddRange(Children<TKey, TValue?> children)
  43. {
  44. if (_count + children.Count > _capacity)
  45. throw new InvalidOperationException("Cannot add to a full list.");
  46. Array.Copy(children._children, 0, _children, _count, children.Count);
  47. _count += children.Count;
  48. }
  49. public Node<TKey, TValue?> RemoveAt(int index)
  50. {
  51. if (index >= _count)
  52. throw new ArgumentOutOfRangeException(nameof(index));
  53. var child = _children[index];
  54. if (index < _count - 1)
  55. Array.Copy(_children, index + 1, _children, index, _count - index - 1);
  56. _children[_count - 1] = null;
  57. _count--;
  58. return child!;
  59. }
  60. public Node<TKey, TValue?> RemoveLast() => RemoveAt(_count - 1);
  61. public void Truncate(int index)
  62. {
  63. if (index >= _count)
  64. throw new ArgumentOutOfRangeException(nameof(index));
  65. for (var i = index; i < _count; i++)
  66. _children[i] = null;
  67. _count = index;
  68. }
  69. #endregion
  70. }

Node 来表示每个节点,支持传入 Comparer 用于实现自定义的排序方式。

  1. internal class Node<TKey, TValue>
  2. {
  3. #region Fields
  4. private readonly IComparer<TKey> _comparer;
  5. private readonly int _degree;
  6. private readonly int _minItems;
  7. private readonly int _maxItems;
  8. private readonly int _maxChildren;
  9. private readonly Items<TKey, TValue?> _items;
  10. private readonly Children<TKey, TValue?> _children;
  11. #endregion
  12. #region Constructors
  13. public Node(int degree, IComparer<TKey> comparer)
  14. {
  15. _degree = degree;
  16. _comparer = comparer;
  17. _minItems = degree - 1;
  18. _maxItems = 2 * degree - 1;
  19. _maxChildren = 2 * degree;
  20. _items = new Items<TKey, TValue?>(_maxItems, _comparer);
  21. _children = new Children<TKey, TValue?>(_maxChildren);
  22. }
  23. #endregion
  24. #region Properties
  25. public int ItemsCount => _items.Count;
  26. public int ChildrenCount => _children.Count;
  27. public bool IsItemsFull => ItemsCount == _maxItems;
  28. public bool IsItemsEmpty => ItemsCount == 0;
  29. public bool IsLeaf => ChildrenCount == 0;
  30. #endregion
  31. // ...
  32. }
  1. public sealed class BTree<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue?>>
  2. {
  3. #region Fields
  4. private readonly int _degree;
  5. private readonly IComparer<TKey> _comparer;
  6. private int _count;
  7. private Node<TKey, TValue?>? _root;
  8. #endregion
  9. #region Constructors
  10. public BTree(int degree) : this(degree, Comparer<TKey>.Default)
  11. {
  12. }
  13. public BTree(int degree, IComparer<TKey> comparer)
  14. {
  15. if (degree < 2)
  16. {
  17. throw new ArgumentOutOfRangeException(nameof(degree), "Degree must be at least 2.");
  18. }
  19. ArgumentNullException.ThrowIfNull(comparer);
  20. _degree = degree;
  21. _comparer = comparer;
  22. }
  23. #endregion
  24. #region Properties
  25. public int Count => _count;
  26. public int Degree => _degree;
  27. public IComparer<TKey> Comparer => _comparer;
  28. #endregion
  29. // ...
  30. }

插入数据的过程

先重复一下上文提到的B树的顺序特性:

  • 每个 节点 中的 Item 按 Key 有序排列(规则可以是自定义的)。
  • 升序排序时,每个 Item 左子树 中的 Item 的 Key 均小于当前 Item 的 Key。
  • 升序排序时,每个 Item 右子树 中的 Item 的 Key 均大于当前 Item 的 Key。

插入数据的过程就是在树中找到合适的位置插入数据,同时保证树的顺序特性不变。

寻找位置的过程是递归的,从根节点开始,如果当前节点是叶子节点,那么就在当前节点中插入数据;如果当前节点不是叶子节点,那么就根据当前节点中的 Item 的 Key 和要插入的数据的 Key 的大小关系,决定是向左子树还是右子树继续寻找合适的位置。

以下面这个图例来说明插入数据的过程:

  1. 在 根节点 中,借助 二分查找法 找到 5 的位置应该在 3 和 7 之间,因为根节点不是叶子节点,所以不能在根节点直接插入,继续在 Node 2 中寻找合适的位置。Node 2 是 3 的右子树,7 的左子树,其中的 Key 都大于 3,小于 7。
  2. Node 2 是叶子节点,所以可以在 Node 2 中插入 5。按二分查找法找到 5 的位置应该在 4 和 6 之间,所以插入数据后 Node 2 中的 Item 应该是这样的:[4, 5, 6]

分裂:新节点诞生的唯一方式

上文提到单个节点最多只能有 2t-1 个 Item,如果节点已经满了,还有新 Item 需要插入的话,节点就需要进行分裂。

根节点的分裂

如果根节点满了(Item的数量达到2t-1),有需要插入新 Item 的话,就需要对根节点进行分裂,分裂后的根节点会有两个子节点,分别是原来的根节点和新的节点。

分裂分为以下几个步骤(不一定要按这个顺序):

  1. 创建一个新的节点,作为新的根节点。
  2. 将原根节点作为新根节点的第一个子节点。
  3. 将原根节点中间(索引记为mid)的 Item 移动到新的根节点中,作为新根节点的第一个 Item。
  4. 创建一个新的节点。
  5. 将原根节点中间 Item 右边的 Item(mid+1开始)移动到新节点中。
  6. 将原根节点中间 Item 右边的 子节点(mid+1开始)移动到新节点中。
  7. 将新节点作为新根节点的第二个子节点。

非根节点的分裂

假设当前节点是父节点的第 k 个子节点,也就是父节点 Items[k](用PItems代指) 的左子节点,或者说是PItems[k-1] 的右子节点。当前节点中所有 Item 的 Key 都在 (PItems[k-1], PItems[k])区间内。

分裂分为以下几个步骤:

  1. 将中间(索引记为mid)的 Item (记作MItem)提升到父节点中,插入 PItems[k],原来的 PItems[k] 移动至 PItems[k+1],父节点中的 Item 依然保持有序。
  2. 创建新的节点。
  3. 将右半部分(mid+1开始)的 Item 移至新节点。
  4. 将右半部分(mid+1开始)的 子节点 移至新节点。
  5. 将新的节点 插入父节点的子节点的第 k+1 个位置,也就是作为刚改过位置的 MItem 的右子节点,MItem 的 Key 小于 其右子树中所有 Item,顺序性也不会遭到破坏。

新插入的 Item 会根据 Key 的大小,插入到分裂后的左节点或者右节点中。

下图所示B树 degree 为 3,每个 Node 最多有 5(2*3-1)个 Item,在[4,5,6,8,9]所在节点插入 7 需先进行分裂。6 将被提升到根节点中,原来的 6 所在节点将被分裂成两个节点,7 会被插入到右侧的新节点中。

分裂导致树的高度增加

节点在分裂的时候,如果父节点已经满了,那么父节点也需要分裂,这样就会导致父节点的父节点也需要分裂,以此类推,直到根节点。

而根节点的分裂,会导致树的高度增加。

新 Item 的插入是发生在叶子节点的,分裂也是从叶子节点开始。如果一个节点一开始是叶子节点,随着数据的增加,它始终都是叶子节点,叶子节点分裂后,新的叶子节点也是同一高度的

这其实解答了上文提到的问题:为什么B树的叶子节点都在同一层。

提前分裂

B树中数据的插入过程,是一个从根节点不断 向下 寻找合适叶子节点的过程。

而分裂是一个从叶子节点不断 向上 的过程。

因此分裂算法的实际实现中,为了避免回溯性分裂(磁盘存储中,回溯带来的 overhead 很大),一般会在 向下 寻找的过程中提前去分裂已经满了的节点。

插入算法实现

在插入新 Item 的过程中,BTree 本质上只是一个入口,大部分的逻辑都是和 节点 相关的,因此我们会把主要的逻辑定义在 节点 中。

Key 已存在时的处理策略

新插入的 Item 的 Key 可能已经存在了,针对已经存在的 Key 的处理方式,这边参考 Dictionary 的处理方式:

  • 通过 Indexer 插入数据时新 Value 覆盖旧 Value。
  • 通过 Add 插入数据时扔出异常。
  • 通过 TryAdd 插入数据时不作任何处理。

对应枚举如下:

  1. internal enum InsertionBehavior
  2. {
  3. /// <summary>
  4. /// 默认操作,如果 key 已经存在,则不会更新 value
  5. /// </summary>
  6. None = 0,
  7. /// <summary>
  8. /// 如果 key 已经存在,则更新 value
  9. /// </summary>
  10. OverwriteExisting = 1,
  11. /// <summary>
  12. /// 如果 key 已经存在,则抛出异常
  13. /// </summary>
  14. ThrowOnExisting = 2
  15. }

并定义对应的处理结果枚举

  1. internal enum InsertionResult
  2. {
  3. None = 0,
  4. Added = 1,
  5. Updated = 2,
  6. }
  1. public sealed class BTree<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue?>>
  2. {
  3. #region Indexers
  4. public TValue? this[[NotNull] TKey key]
  5. {
  6. get
  7. {
  8. if (TryGetValue(key, out var value))
  9. {
  10. return value;
  11. }
  12. throw new KeyNotFoundException();
  13. }
  14. set => TryInsert(key, value, InsertionBehavior.OverwriteExisting);
  15. }
  16. #endregion
  17. #region Public Methods
  18. /// <summary>
  19. /// 往B树中添加一个键值对
  20. /// </summary>
  21. /// <param name="key">要添加的元素的key</param>
  22. /// <param name="value">要添加的元素的value</param>
  23. /// <exception cref="ArgumentNullException">key是null</exception>
  24. /// <exception cref="ArgumentException">key已经存在</exception>
  25. public void Add([NotNull] TKey key, TValue? value) =>
  26. TryInsert(key, value, InsertionBehavior.ThrowOnExisting);
  27. /// <summary>
  28. /// 尝试往B树中添加一个键值对
  29. /// </summary>
  30. /// <param name="key">要添加的元素的key</param>
  31. /// <param name="value">要添加的元素的value</param>
  32. /// <returns>true:添加成功;false:添加失败</returns>
  33. public bool TryAdd([NotNull] TKey key, TValue? value) =>
  34. TryInsert(key, value, InsertionBehavior.None);
  35. #endregion
  36. }

插入算法

在 Node 中 定义分裂和判断是否要提前分裂的方法

  1. internal class Node<TKey, TValue>
  2. {
  3. /// <summary>
  4. /// 将当前<see cref="Node{TKey,TValue}"/>分裂成两个<see cref="Node{TKey,TValue}"/>。
  5. /// </summary>
  6. /// <returns>中间位置的<see cref="Item{TKey,TValue}"/>和分裂后的第二个<see cref="Node{TKey,TValue}"/></returns>
  7. public (Item<TKey, TValue?> MiddleItem, Node<TKey, TValue?> SecnodNode) Split()
  8. {
  9. int middleIndex = ItemsCount / 2;
  10. var middleItem = _items[middleIndex];
  11. var secondNode = new Node<TKey, TValue?>(_degree, _comparer);
  12. // 将中间位置后的所有Item移动到新的Node中
  13. for (int i = middleIndex + 1; i < ItemsCount; i++)
  14. {
  15. secondNode._items.Add(_items[i]);
  16. }
  17. _items.Truncate(middleIndex);
  18. if (!IsLeaf)
  19. {
  20. // 将中间位置后的所有子节点移动到新的Node中
  21. for (int i = middleIndex + 1; i < ChildrenCount; i++)
  22. {
  23. secondNode._children.Add(_children[i]);
  24. }
  25. _children.Truncate(middleIndex + 1);
  26. }
  27. return (middleItem, secondNode);
  28. }
  29. /// <summary>
  30. /// 如果指定的子节点已满,则将其分裂为两个子节点,并将中间的 <see cref="Item{TKey,TValue}"/>> 插入到当前节点中。
  31. /// </summary>
  32. /// <param name="childIndex">指定的子节点的索引</param>
  33. /// <returns>True 表示已经分裂了子节点,False 表示没有分裂子节点</returns>
  34. private bool MaybeSplitChildren(int childIndex)
  35. {
  36. var childNode = _children[childIndex];
  37. if (childNode.IsItemsFull)
  38. {
  39. var (middleItem, secondNode) = childNode.Split();
  40. _items.InsertAt(childIndex, middleItem);
  41. // 将新node插入到当前node的children中
  42. _children.InsertAt(childIndex + 1, secondNode);
  43. return true;
  44. }
  45. return false;
  46. }
  47. }

在 BTree 中定义插入方法

  1. public sealed class BTree<TKey, TValue>
  2. private bool TryInsert([NotNull] TKey key, TValue? value, InsertionBehavior behavior)
  3. {
  4. ArgumentNullException.ThrowIfNull(key);
  5. if (_root == null)
  6. {
  7. _root = new Node<TKey, TValue?>(_degree, _comparer);
  8. _root.Add(new Item<TKey, TValue?>(key, value));
  9. _count++;
  10. return true;
  11. }
  12. if (_root.IsItemsFull)
  13. {
  14. // 根节点已满,需要分裂
  15. var (middleItem, secondNode) = _root.Split();
  16. var oldRoot = _root;
  17. _root = new Node<TKey, TValue?>(_degree, _comparer);
  18. // 将原来根节点中间的元素添加到新的根节点
  19. _root.Add(middleItem);
  20. // 将原来根节点分裂出来的节点添加到新的根节点
  21. _root.AddChild(oldRoot);
  22. _root.AddChild(secondNode);
  23. }
  24. // 从根节点开始插入,如果插入的 Key 已经存在,会按照 behavior 的值进行处理
  25. var insertionResult = _root.TryInsert(key, value, behavior);
  26. if (insertionResult == InsertionResult.Added) _count++;
  27. return insertionResult != InsertionResult.None;
  28. }
  29. }

在 Node 中定义插入方法,递归调用直至找到叶子节点,然后在叶子节点中插入

  1. internal class Node<TKey, TValue>
  2. {
  3. public InsertionResult TryInsert(TKey key, TValue? value, InsertionBehavior behavior)
  4. {
  5. // 如果当前key已经存在, 根据插入行为决定是否替换
  6. if (_items.TryFindKey(key, out int index))
  7. {
  8. switch (behavior)
  9. {
  10. case InsertionBehavior.OverwriteExisting:
  11. _items[index].Value = value;
  12. return InsertionResult.Updated;
  13. case InsertionBehavior.ThrowOnExisting:
  14. throw new ArgumentException($"An item with the same key has already been added. Key: {key}");
  15. default:
  16. return InsertionResult.None;
  17. }
  18. }
  19. // 如果当前节点是叶子节点,则直接插入
  20. if (IsLeaf)
  21. {
  22. // index 是新的 item 应该插入的位置,items 按顺序排列
  23. _items.InsertAt(index, new Item<TKey, TValue?>(key, value));
  24. return InsertionResult.Added;
  25. }
  26. // 如果当前节点的子节点已经满了,则需要分裂
  27. // 如果当前节点的子节点没有满,则不需要分裂
  28. // 如果当前节点的子节点分裂了,则需要判断当前key是否大于分裂后的中间key
  29. // 如果当前key大于分裂后的中间key,则需要向右边的子节点插入
  30. // 如果当前key小于分裂后的中间key,则需要向左边的子节点插入
  31. // index 是新的 item 应该插入的位置,如果当做children的索引,则代表应该插入的位置的右边的子节点
  32. if (MaybeSplitChildren(index))
  33. {
  34. // rightmostItem 是子节点分裂后的中间的 item,被提升到当前节点的 items 中的最后一个位置了
  35. var middleItemOfChild = _items[index];
  36. switch (_comparer.Compare(key, middleItemOfChild.Key))
  37. {
  38. case > 0:
  39. // 如果当前key大于分裂后的中间key,则需要向右边的子节点插入
  40. index++;
  41. break;
  42. case < 0:
  43. // 如果当前key小于分裂后的中间key,则需要向左边的子节点插入
  44. break;
  45. default:
  46. // 如果当前key等于分裂后的中间key,根据插入行为决定是否替换
  47. switch (behavior)
  48. {
  49. case InsertionBehavior.OverwriteExisting:
  50. middleItemOfChild.Value = value;
  51. return InsertionResult.Updated;
  52. case InsertionBehavior.ThrowOnExisting:
  53. throw new ArgumentException(
  54. $"An item with the same key has already been added. Key: {key}");
  55. default:
  56. return InsertionResult.None;
  57. }
  58. }
  59. }
  60. // 往子节点插入
  61. return _children[index].TryInsert(key, value, behavior);
  62. }
  63. }

总结

B树中的数据是按照顺序存储的,所以可以使用二分查找法来查找数据,时间复杂度为 O(log n)。

往B树插入数据的过程是一个寻找合适的叶子节点的过程,然后在叶子节点中插入数据,时间复杂度为 O(log n)。

B树的节点中存储的数据量是有限的,所以在插入数据时,可能会发生节点分裂,这样就会导致树的高度增加,所以在插入数据时,需要判断是否需要分裂,如果需要分裂,就需要将中间的数据提升到父节点中,以此类推,直到根节点,如果根节点也需要分裂,就需要新建一个根节点,然后将原来的根节点和分裂出来的节点作为新的根节点的子节点。

参考资料

PingCAP 宝宝床边故事集:存储引擎

B树、B+树索引算法原理(上)

B树 维基百科

Google 用 Go 实现的内存版 B树

渴望力量系列 《算法导论第三版》

欢迎关注个人微信公众号 EventHorizonCLI ,最新的原创技术文章将在优先这里发布。

原文链接:https://www.cnblogs.com/eventhorizon/p/16972835.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号