前言

本文为系列文章

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

阅读本文前,建议先复习前两篇文章,以便更好的理解本文。

从删除的数据所在的节点可分为两种情况:

  1. 从叶子节点删除数据
  2. 从非叶子节点删除数据

无论从叶子节点还是非叶子节点删除数据时都需要保证B树的特性:非根节点每个节点的 key 数量都在 [t-1, 2t-1] 之间

借此保证B树的平衡性。

之前介绍的插入数据关注的是这个范围的上限 2t-1,插入时,如果节点的 key 数量大于 2t-1,就需要进行数据的分裂。

而删除数据则关注是下限 t-1,如果节点的 key 数量小于 t-1,就需要进行数据的移动或者合并。

删除数据时,需要考虑的情况比较多,本文会分别讨论这些情况,但一些比较边缘的情况为避免描述过于复杂,不再文中讨论,而是在代码中进行了注释。

因为删除逻辑比较复杂,请结合完整代码进行阅读。

https://github.com/eventhorizon-cli/EventHorizon.BTree/blob/b51881719146a86568669cdc78f8524299bee33d/src/EventHorizon.BTree/BTree.cs#L139

从叶子节点删除数据

如果待删除的数据在叶子节点,且该节点的 Item 数量大于 t-1,那么直接删除该数据即可。

从非叶子节点删除数据

如果待删除的数据在非叶子节点,那么需要先找到该数据的左子节点,然后将左子节点的数据替换到待删除的数据,最后再删除左子节点的数据。

这样能保证被删除数据的节点的 Item 数量不变,保证 B树 有 k 个子节点的非叶子节点拥有 k − 1 个键的特性不受破坏。

提前扩充只有 t-1 的 Item 的节点:维持 B树 平衡的核心算法

在数据插入的时候,为了避免回溯性的节点分裂,我们提前将已满的子节点进行分裂。

同样的在数据删除,不断往下递归查找时,如果遇到只有 t-1 个 Item 的节点,我们也需要提前将其扩充,以避免回溯性的节点处理。

扩充的节点不一定是最后数据所在的节点,只是向下查找过程中遇到的节点。

节点扩充的分为两类,一个是从兄弟节点借用 Item,一个是合并兄弟节点,被借用的兄弟节点需要满足 Item 数量大于 t-1。具体可分为以下三种情况:

从左兄弟节点借用 Item

待扩充节点的左兄弟节点存在且左兄弟节点的 Item 数量 > t-1 时,从左兄弟节点借用 Item 进行扩充。

为了保证 B树 数据的顺序特性:任意 Item 的左子树中的 Key 均小于该 Item 的 Key,右子树中的 Key 均大于该 Item 的 Key。需要交换左兄弟节点的最右边的 Item 和父节点中对应位置的 Item(位于左兄弟节点右侧)。

以下图为例进行说明:

从右兄弟节点借用 Item

待扩充节点的左兄弟节点不存在或者左兄弟节点的 Item 数量 只有 t-1 时,无法外借。但右兄弟节点存在且右兄弟节点的 Item 数量 > t-1 时,从右兄弟节点借用 Item 进行扩充。

以下图为例进行说明:

从兄弟节点进行扩充可以概括为:借用,交换,插入。

与左兄弟节点或者右兄弟节点合并

如果待扩充节点的左兄弟节点和右兄弟节点都不存在或者都只有 t-1 个 Item 时,无法外借。此时需要与左兄弟节点或者右兄弟节点进行合并。

以下图为例进行说明:

最值的删除

之前章节介绍过 B树 最值的查找:

  1. 最小值:从根节点开始,一直往左子树走,直到叶子节点。
  2. 最大值:从根节点开始,一直往右子树走,直到叶子节点。

最值的删除就是先找到最值的位置并将其删除,在向下寻找的过程中,需要和普通的数据删除一样,对节点进行扩充或者合并。

代码实现

最值删除是删除的特殊情况,我们定义一个枚举用来区分普通数据的删除,最小值的删除以及最大值的删除,这三种方式只在数据查找的时候有所区分,其他的逻辑都是一样的。

internal enum RemoveType
{
Item,
Min,
Max
} public sealed class BTree<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue?>>
{
public bool TryRemove([NotNull] TKey key, out TValue? value)
{
ArgumentNullException.ThrowIfNull(key); return TryRemove(key, RemoveType.Item, out value);
} public bool TryRemoveMax(out TValue? value) => TryRemove(default, RemoveType.Max, out value); public bool TryRemoveMin(out TValue? value) => TryRemove(default, RemoveType.Min, out value); private bool TryRemove(TKey? key, RemoveType removeType, out TValue? value)
{
if (_root == null || _root.IsItemsEmpty)
{
value = default;
return false;
} bool removed = _root.TryRemove(key, removeType, out var item);
if (_root.IsItemsEmpty && !_root.IsLeaf)
{
// 根节点原来的两个子节点进行了合并,根节点唯一的元素被移动到了子节点中,需要将合并后的子节点设置为新的根节点
_root = _root.GetChild(0);
} if (removed)
{
_count--;
value = item!.Value;
return true;
} value = default;
return removed;
}
}

主要的逻辑定义在 Node 中,不断向下递归

internal class Node<TKey, TValue>
{
public bool TryRemove(TKey? key, RemoveType removeType, [MaybeNullWhen(false)] out Item<TKey, TValue?> item)
{
int index = 0;
bool found = false;
if (removeType == RemoveType.Max)
{
if (IsLeaf)
{
if (_items.Count == 0)
{
item = default;
return false;
} // 如果是叶子节点,直接删除最后一个元素,就是删除最大的 Item
item = _items.RemoveLast();
return true;
} // 当前节点不是叶子节点,需要找到最大的子节点,继续向下查找并删除
index = ItemsCount;
} if (removeType == RemoveType.Min)
{
if (IsLeaf)
{
if (_items.Count == 0)
{
item = default;
return false;
} // 当前节点是叶子节点,直接删除第一个元素,就是删除最小的 Item
item = _items.RemoveAt(0);
return true;
} // 当前节点不是叶子节点,需要找到最小的子节点,继续向下查找并删除
index = 0;
} if (removeType == RemoveType.Item)
{
// 如果没有找到,index 表示的是 key 可能在的子树的索引
found = _items.TryFindKey(key!, out index); if (IsLeaf)
{
// 如果是叶子节点,能找到就删除,找不到就返回 false,表示删除失败
if (found)
{
item = _items.RemoveAt(index);
return true;
} item = default;
return false;
}
} // 如果当前节点的左子节点的 Item 个数小于最小 Item 个数,就需要进行合并或者借元素
// 这个处理对应两种情况:
// 1. 要删除的 Item 不在当前节点的子节点中,为避免删除后导致数据所在节点的 Item 个数小于最小 Item 个数,需要先进行合并或者借元素。
// 2. 要删除的 Item 就在当前节点中,为避免删除后导致当前节点的 Item 个数小于最小 Item 个数,需要先从左子节点中借一个 Item 过来,保证当前节点的 Item 数量不变。
// 为此先要保证左子节点被借用后的 Item 个数不小于最小 Item 个数。
if (_children[index].ItemsCount <= _minItems)
{
return GrowChildrenAndTryRemove(index, key!, removeType, out item);
} var child = _children[index]; if (found)
{
// 如果在当前节点找到了,就删除当前节点的 Item,然后将 左子节点 中的最大的 Item 移动到当前节点中
// 以维持当前节点的 Item 个数不变,保证 B树 有 k 个子节点的非叶子节点拥有 k − 1 个键的特性。
item = _items[index];
child.TryRemove(default!, RemoveType.Max, out var stolenItem);
_items[index] = stolenItem;
return true;
} return child.TryRemove(key!, removeType, out item);
} private bool GrowChildrenAndTryRemove(
int childIndex,
TKey key,
RemoveType removeType,
[MaybeNullWhen(false)] out Item<TKey, TValue?> item)
{
if (childIndex > 0 && _children[childIndex - 1].ItemsCount > _minItems)
{
// 如果左边的子节点存在且左边的子节点的item数量大于最小值,则从左边的子节点借一个item
var child = _children[childIndex];
var leftChild = _children[childIndex - 1];
var stolenItem = leftChild._items.RemoveLast();
child._items.InsertAt(0, _items[childIndex - 1]);
_items[childIndex - 1] = stolenItem;
if (!leftChild.IsLeaf)
{
// 非叶子节点的子节点需要保证数量比item多1,item数量变了,子节点数量也要变
// 所以需要从左边的子节点中移除最后一个子节点,然后插入到当前子节点的第一个位置
child._children.InsertAt(0, leftChild._children.RemoveLast());
}
}
else if (childIndex < ChildrenCount - 1 && _children[childIndex + 1].ItemsCount > _minItems)
{
// 如果右边的子节点存在且右边的子节点的item数量大于最小值,则从右边的子节点借一个item
var child = _children[childIndex];
var rightChild = _children[childIndex + 1];
var stolenItem = rightChild._items.RemoveAt(0);
child._items.Add(_items[childIndex]);
_items[childIndex] = stolenItem;
if (!rightChild.IsLeaf)
{
// 非叶子节点的子节点需要保证数量比item多1,item数量变了,子节点数量也要变
// 所以需要从右边的子节点中移除第一个子节点,然后插入到当前子节点的最后一个位置
child.AddChild(rightChild._children.RemoveAt(0));
}
}
else
{
// 如果当前节点左右两边的子节点的item数量都不大于最小值(例如正好等于最小值 t-1 ),则合并当前节点和右边的子节点或者左边的子节点
// 优先和右边的子节点合并,如果右边的子节点不存在,则和左边的子节点合并
if (childIndex >= ItemsCount)
{
// ItemCount 代表最的子节点的索引,如果 childIndex 大于等于 ItemCount,说明右边的子节点不存在,需要和左边的子节点合并
childIndex--;
} var child = _children[childIndex];
var mergeItem = _items.RemoveAt(childIndex);
var mergeChild = _children.RemoveAt(childIndex + 1);
child._items.Add(mergeItem);
child._items.AddRange(mergeChild._items);
child._children.AddRange(mergeChild._children);
} return TryRemove(key, removeType, out item);
}
}

Benchmarks:与 优先队列 PriorityQueue 的比较

我们实现的 BTree 支持自定义排序规则,也实现最值的删除,意味着可以充当优先队列使用。

我们使用 PriorityQueue 与 BTree 进行性能对比来看看 B树 能否充当优先队列使用。

入队性能

public class BTree_PriorityQueue_EnequeueBenchmarks
{
[Params(1000, 1_0000, 10_0000)] public int DataSize; [Params(2, 4, 8, 16)] public int Degree; private HashSet<int> _data; [IterationSetup]
public void Setup()
{
var random = new Random();
_data = new HashSet<int>();
while (_data.Count < DataSize)
{
var value = random.Next();
_data.Add(value);
}
} [Benchmark]
public void BTree_Add()
{
var btree = new BTree<int, int>(Degree); foreach (var value in _data)
{
btree.Add(value, value);
}
} [Benchmark]
public void PriorityQueue_Enqueue()
{
var priorityQueue = new PriorityQueue<int, int>(DataSize); foreach (var value in _data)
{
priorityQueue.Enqueue(value, value);
}
}
}

出队性能

public class BTree_PriorityQueue_DequeueBenchmarks
{
[Params(1000, 1_0000, 10_0000)] public int DataSize; [Params(2, 4, 8, 16)] public int Degree; private BTree<int, int> _btree; private PriorityQueue<int, int> _priorityQueue; [IterationSetup]
public void Setup()
{
var random = new Random();
_btree = new BTree<int, int>(Degree);
_priorityQueue = new PriorityQueue<int, int>(DataSize); while (_btree.Count < DataSize)
{
var value = random.Next();
_btree.Add(value, value);
_priorityQueue.Enqueue(value, value);
}
} [Benchmark]
public void BTree_Remove()
{
while (_btree.Count > 0)
{
_btree.RemoveMin();
}
} [Benchmark]
public void PriorityQueue_Dequeue()
{
while (_priorityQueue.Count > 0)
{
_priorityQueue.Dequeue();
}
}
}

可以看到,B树 虽然在入队性能上比 PriorityQueue 差。但在数据量和 degree 较大时,出队性能比 PriorityQueue 好,是有能力充当优先队列使用的。

总结

B树 在 degree 较大时,树的高度较低,删除的效率较高,可充当优先队列使用。

B树 的插入,删除,查找都是基于递归的,递归的深度为树的高度。

B树 对数据的查找基于二分查找,时间复杂度为 O(log n),B树 的插入和删除基于 B树的查找算法,都要找到数据所在的节点,然后在该节点进行插入和删除。因此,B树 的插入和删除的时间复杂度也为 O(log n)。

B树 是对二叉树的一种优化,使得树的高度更低,但是在插入,删除的过程中,需要进行大量的节点分裂,合并,借用,交换等操作,使得算法的复杂度更高。

参考资料

Google 用 Go 实现的内存版 B树 https://github.com/google/btree

B树 维基百科 https://zh.m.wikipedia.org/zh-hans/B树

图解B树及C#实现(3)数据的删除的更多相关文章

  1. InnoDB一棵B+树可以存放多少行数据?

    一个问题? InnoDB一棵B+树可以存放多少行数据?这个问题的简单回答是:约2千万.为什么是这么多呢?因为这是可以算出来的,要搞清楚这个问题,我们先从InnoDB索引数据结构.数据组织方式说起. 我 ...

  2. 面试题:InnoDB中一棵B+树能存多少行数据?

    阅读本文大概需要 5 分钟. 作者:李平 | 来源:个人博客 一.InnoDB 一棵 B+ 树可以存放多少行数据? InnoDB 一棵 B+ 树可以存放多少行数据? 这个问题的简单回答是:约 2 千万 ...

  3. MySQL(四)InnoDB中一棵B+树能存多少行数据

    一.InnoDB一棵B+树可以存放多少行数据?(约2千万) 我们都知道计算机在存储数据的时候,有最小存储单元,这就好比我们今天进行现金的流通最小单位是一毛.在计算机中磁盘存储数据最小单元是扇区,一个扇 ...

  4. innodb中一颗B+树能存储多少条数据

    如图,为B+树组织数据的方式: 实际存储时当然不会每个节点只存3条数据. 以InnoDB引擎为例,简单计算一下一颗B+树可以存放多少行数据. B+树特点:只有叶子节点存储数据,而非叶子节点存放的是用来 ...

  5. Web jquery表格组件 JQGrid 的使用 - 7.查询数据、编辑数据、删除数据

    系列索引 Web jquery表格组件 JQGrid 的使用 - 从入门到精通 开篇及索引 Web jquery表格组件 JQGrid 的使用 - 4.JQGrid参数.ColModel API.事件 ...

  6. mysql插入数据与删除重复记录的几个例子(收藏)

    mysql插入数据与删除重复记录的几个例子 12-26shell脚本实现mysql数据的批量插入 12-26mysql循环语句插入数据的例子 12-26mysql批量插入数据(insert into ...

  7. MVC5 + EF6 + Bootstrap3 (13) 查看详情、编辑数据、删除数据

    Slark.NET-博客园 http://www.cnblogs.com/slark/p/mvc5-ef6-bs3-get-started-rud.html 系列教程:MVC5 + EF6 + Boo ...

  8. MYSQL中delete删除多表数据与删除关联数据

    在mysql中删除数据方法有很多种,最常用的是使用delete来删除记录,下面我来介绍delete删除单条记 录与删除多表关联数据的一些简单实例. 1.delete from t1 where 条件 ...

  9. ASP.NET MVC+EF框架+EasyUI实现权限管理系列(18)-过滤器的使用和批量删除数据(伪删除和直接删除)

    原文:ASP.NET MVC+EF框架+EasyUI实现权限管理系列(18)-过滤器的使用和批量删除数据(伪删除和直接删除) ASP.NET MVC+EF框架+EasyUI实现权限管系列 (开篇)   ...

  10. oracle_自动备份用户数据,删除N天前的旧数据(非rman,bat+vbs)

    有时数据没有实时备份恢复那么高的安全性需求,但每天 ,或者定期备份表结构 和数据依旧是很有必要的,介绍一种方法 在归档和非归档模式均可使用的自动备份方法. 预期效果是备份用户下的数据含表结构,备份文件 ...

随机推荐

  1. 【Azure 事件中心】Event Hub 无法连接,出现 Did not observe any item or terminal signal within 60000ms in 'flatMapMany' 的错误消息

    问题描述 使用Java SDK连接Azure Event Hub,一直出现 java.util.concurrent.TimeoutException 异常, 消息为:java.util.concur ...

  2. python基础类型,字符串

    python基本类型小结 # str,可以用索引取值,但是不能通过索引改变值, # a = "123" a[0]=10,直接TypeError因为字符串是不可变类型 # list, ...

  3. miniconda使用

    基本指令 conda create -n xxx python=3.7 // 创建Python3.7的名为xxx虚拟环境 conda env list // 显示所有的虚拟环境 conda activ ...

  4. Java开发学习(四十)----MyBatisPlus入门案例与简介

    一.入门案例 MybatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发.提供效率. SpringBoot它能快速构建Spring开发环境用以整合其他技术,使用起来 ...

  5. selenium被某些网页检测不允许正常访问、登录等,解决办法

    网站通过什么方式检测 function b() { return "$cdc_asdjflasutopfhvcZLmcfl_"in u || d.webdriver } 通过上方的 ...

  6. (C++) 笔记 C++11 std::mutex std::condition_variable 的使用

    #include <atomic> #include <chrono> #include <condition_variable> #include <ios ...

  7. PGL图学习之图神经网络ERNIESage、UniMP进阶模型[系列八]

    PGL图学习之图神经网络ERNIESage.UniMP进阶模型[系列八] 原项目链接:fork一下即可:https://aistudio.baidu.com/aistudio/projectdetai ...

  8. combotree 的简单使用2

    上一次我在 combotree 的简单使用 中介绍了一种combotree的写法,不过有一个缺点,就是当输的结构非常大的时候,分级较多时,消耗内存的现象会比较严重,下面介绍的一种方法,使combotr ...

  9. 大数据HDFS凭啥能存下百亿数据?

    欢迎关注大数据系列课程 前言 大家平时经常用的百度网盘存放电影.照片.文档等,那有想过百度网盘是如何存下那么多文件的呢?难到是用一台计算机器存的吗?那得多大磁盘啊?显然不是的,那本文就带大家揭秘. 分 ...

  10. markdown语法使用

    markdown语法使用 标题系列 ​ 1.警号 ​ 2.快捷键 ​ ctrl + 数字(1~6) 小标题系列 * 文本 无序标题 + 文本 无序标题 数字 文本 有序标题 语言环境 表格制作 表情制 ...