图解B树及C#实现(2)数据的读取及遍历

2023年 9月 28日 29.2k 0

前言

本文为系列文章

  • B树的定义及数据的插入
  • 数据的读取及遍历(本文)
  • 数据的删除
  • 前一篇文章为大家介绍了 B树 的基本概念及其插入算法。本文将基于前一篇的内容,为大家介绍插入到 B树 中的数据该怎么读取及遍历,

    本文的代码基于前一篇文章的代码,已经实现的功能可能会被省略,只介绍新增的功能。

    在本文开始前,再次复习下 B树 的顺序特性:

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

    理解数据的顺序性对本文的理解至关重要。

    查询数据

    算法说明

    B树 是基于二分查找算法进行设计的,某些资料中你也会看到用 多路搜索树 来归类 B树。

    在 B树 中查找数据时,二分体现在两个方面:

  • 在节点中查找数据时,使用二分查找算法。
  • 当节点中找不到数据时,使用二分查找算法找到下一个节点。
  • 具体的查找过程如下:

  • 从根节点开始,在节点中使用二分查找算法查找数据。
  • 如果没有找到数据,则根据查找的 Key 值与节点中的 Key 值的大小关系,决定下一个节点的位置。
  • 重复步骤 1 和 2,直到找到数据或者找到叶子节点。如果在叶子节点中也没有找到数据,则说明数据不存在。
  • 举例说明:
    在下面的 B树 中,查找 Key 为 8 的数据。

  • 从根节点开始,使用二分查找算法没有找到数据
  • 根据 Key 值与节点中的 Key 值的大小关系,决定下一个节点的位置应该在 6 和 9 之间,也就是 6 的右子树。
  • 在 6 的右子树中,使用二分查找算法找到了数据。
  • 代码实现

    前一篇文章我们定义了 Items 类,用于存储节点中的数据,并且在一开始就定义了一个二分查找算法,用于在 Items 查找 Item。

    前一篇用它来找到合适的插入位置,现在我们用寻找已经存在的数据。

    在当前节点找到 Item 时,index 对应的就是 Item 的位置。没找到时则代表下一个子树的索引。

    理解代码时请参考下图:

    internal class Items
    {
        public bool TryFindKey(TKey key, out int index)
        {
            if (_count == 0)
            {
                index = 0;
                return false;
            }
    
            // 二分查找
            int left = 0;
            int right = _count - 1;
            while (left  itemsCount)
            {
                foreach (var item in _children[childrenCount - 1].InOrderTraversal())
                {
                    yield return item;
                }
            }
        }
    }
    

    BTree 实现了 IEnumerable 接口,以便我们可以使用 foreach 循环来遍历 BTree 中的所有 Item,其代码只要调用 Node 的 InOrderTraversal 方法即可:

    public sealed class BTree : IEnumerable
    {
        public IEnumerator GetEnumerator()
        {
            foreach (var item in _root!.InOrderTraversal())
            {
                yield return new KeyValuePair(item.Key, item.Value);
            }
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
    

    Benchmarks

    最后,我们来看一下 Degree 对 BTree 的性能的影响。

    注意,我们这里只考虑 B树的数据量远大于 Degree 的情况。

    我们使用 BenchmarkDotNet 来测试,测试代码如下:

    public class BTreeWriteBenchmarks
    {
        [Params(2, 3, 4, 5, 6)] public int Degree { get; set; }
        
        private HashSet _randomKeys;
        
        [GlobalSetup]
        public void Setup()
        {
            _randomKeys = new HashSet();
            var random = new Random();
            while (_randomKeys.Count < 1000)
            {
                _randomKeys.Add(random.Next(0, 100000));
            }
        }
        
        [Benchmark]
        public void WriteSequential()
        {
            var bTree = new BTree(Degree);
            for (var i = 0; i < 1000; i++)
            {
                bTree.Add(i, i);
            }
        }
        
        [Benchmark]
        public void WriteRandom()
        {
            var bTree = new BTree(Degree);
            foreach (var key in _randomKeys)
            {
                bTree.Add(key, key);
            }
        }
    }
    
    public class BenchmarkConfig : ManualConfig
    {
        public BenchmarkConfig()
        {
            Add(DefaultConfig.Instance);
            Add(MemoryDiagnoser.Default);
                
            ArtifactsPath = Path.Combine(AppContext.BaseDirectory, "artifacts", DateTime.Now.ToString("yyyy-mm-dd_hh-MM-ss"));
        }
    }
    
    new BenchmarkSwitcher(new[]
    {
        typeof(BTreeReadBenchmarks),
    }).Run(args, new BenchmarkConfig());
    

    我们测试了 4 项性能指标,分别是顺序读、随机读、最小值、最大值、遍历,测试结果如下:

    可以看到,在相同的数据量下,Degree 越大,性能越好,这是因为 Degree 越大,BTree 的高度越小,所以每次查找的时候,需要遍历的节点越少,性能越好。

    但是不是真的 Degree 越大就越好呢,我们再来看下写入性能的测试结果:

    public class BTreeWriteBenchmarks
    {
        [Params(2, 3, 4, 5, 6)] public int Degree { get; set; }
        
        private HashSet _randomKeys;
        
        [GlobalSetup]
        public void Setup()
        {
            _randomKeys = new HashSet();
            var random = new Random();
            while (_randomKeys.Count < 1000)
            {
                _randomKeys.Add(random.Next(0, 100000));
            }
        }
        
        [Benchmark]
        public void WriteSequential()
        {
            var bTree = new BTree(Degree);
            for (var i = 0; i < 1000; i++)
            {
                bTree.Add(i, i);
            }
        }
        
        [Benchmark]
        public void WriteRandom()
        {
            var bTree = new BTree(Degree);
            foreach (var key in _randomKeys)
            {
                bTree.Add(key, key);
            }
        }
    }
    

    测试结果如下:

    可以看到,Degree 越大,写入性能也越好,每个节点的容量够大,需要分裂的次数就变少了。

    总结

    • B树是一种多路平衡查找树,可以基于二分查找的思路来查询数据。
    • B树的数据量远大于 Degree 的情况下,B树的 Degree 越大,读写性能越好。如果是磁盘中的实现,每个节点要考虑到磁盘页的大小,Degree 会有上限。

    参考资料

    Google 用 Go 实现的内存版 B树 github.com/google/btre…

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论