圖解B樹及C#實現(1)

前言

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 包進行體驗

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 按升序排序。

internal class Item<TKey, TValue>
{
    #region Constructors

    public Item(TKey key, TValue? value)
    {
        Key = key;
        Value = value;
    }

    #endregion

    #region Properties

    public TKey Key { get; }

    public TValue? Value { get; set; }
    
    #endregion
}

定義 ItemsChildren 兩個類型分別用於存儲 Item 集合和子節點集合。爲了簡化設計以及減少動態擴容帶來的性能損失,作爲數據實際容器的數組在第一開始就會按最大的 capacity 進行創建。同時也預先給 ItemsChildren 定義好後面會被用到的基本方法。

internal class Items<TKey, TValue>
{
    #region Fields

    private readonly Item<TKey, TValue?>?[] _items;
    private readonly int _capacity;
    private readonly IComparer<TKey> _comparer;

    private int _count;

    #endregion

    #region Constructors

    public Items(int capacity, IComparer<TKey> comparer)
    {
        _capacity = capacity;
        _items = new Item<TKey, TValue?>[capacity];
        _comparer = comparer;
    }

    #region Properties

    public int Count => _count;

    #endregion

    #region Indexers

    public Item<TKey, TValue?> this[int index]
    {
        get
        {
            if (index < 0 || index >= _count)
            {
                throw new IndexOutOfRangeException();
            }

            return _items[index]!;
        }
        set => _items[index] = value;
    }

    #endregion

    #endregion

    #region Public Methods

    /// <summary>
    /// 查找指定的鍵,並返回它的索引,如果找不到則返回key可以插入的位置
    /// </summary>
    /// <param name="key">指定的key</param>
    /// <param name="index">key的索引或者其可以插入的位置</param>
    /// <returns>指定的key是否存在</returns>
    public bool TryFindKey(TKey key, out int index)
    {
        if (_count == 0)
        {
            index = 0;
            return false;
        }

        // 二分查找
        int left = 0;
        int right = _count - 1;
        while (left <= right)
        {
            int middle = (left + right) / 2;
            var compareResult = _comparer.Compare(key, _items[middle]!.Key);
            if (compareResult == 0)
            {
                index = middle;
                return true;
            }

            if (compareResult < 0)
            {
                right = middle - 1;
            }
            else
            {
                left = middle + 1;
            }
        }

        index = left;
        return false;
    }

    public void InsertAt(int index, Item<TKey, TValue?> item)
    {
        if (_count == _capacity)
            throw new InvalidOperationException("Cannot insert into a full list.");

        if (index < _count)
            Array.Copy(_items, index, _items, index + 1, _count - index);

        _items[index] = item;
        _count++;
    }

    public void Add(Item<TKey, TValue?> item) => InsertAt(_count, item);

    public void AddRange(Items<TKey, TValue?> items)
    {
        if (_count + items.Count > _capacity)
            throw new InvalidOperationException("Cannot add items to a full list.");

        Array.Copy(items._items, 0, _items, _count, items.Count);
        _count += items.Count;
    }

    public Item<TKey, TValue?> RemoveAt(int index)
    {
        if (index >= _count)
            throw new ArgumentOutOfRangeException(nameof(index));

        var item = _items[index];

        if (index < _count - 1)
            Array.Copy(_items, index + 1, _items, index, _count - index - 1);

        _items[_count - 1] = null;
        _count--;

        return item!;
    }


    public Item<TKey, TValue?> RemoveLast() => RemoveAt(_count - 1);

    public void Truncate(int index)
    {
        if (index >= _count)
            throw new ArgumentOutOfRangeException(nameof(index));

        for (int i = index; i < _count; i++)
        {
            _items[i] = null;
        }

        _count = index;
    }

    #endregion
}
internal class Children<TKey, TValue>
{
    #region Fields

    private readonly Node<TKey, TValue?>?[] _children;
    private readonly int _capacity;

    private int _count;

    #endregion

    #region Constructors

    public Children(int capacity)
    {
        _capacity = capacity;
        _children = new Node<TKey, TValue?>[_capacity];
    }

    #endregion

    #region Properties

    public int Count => _count;

    #endregion

    #region Indexers

    public Node<TKey, TValue?> this[int index]
    {
        get
        {
            if (index < 0 || index >= _count)
            {
                throw new IndexOutOfRangeException();
            }

            return _children[index]!;
        }
    }

    #endregion

    #region Public Methods

    public void InsertAt(int index, Node<TKey, TValue?> child)
    {
        if (_count == _capacity)
            throw new InvalidOperationException("Cannot insert into a full list.");

        if (index < _count)
            Array.Copy(_children, index, _children, index + 1, _count - index);

        _children[index] = child;
        _count++;
    }

    public void Add(Node<TKey, TValue?> child) => InsertAt(_count, child);

    public void AddRange(Children<TKey, TValue?> children)
    {
        if (_count + children.Count > _capacity)
            throw new InvalidOperationException("Cannot add to a full list.");

        Array.Copy(children._children, 0, _children, _count, children.Count);
        _count += children.Count;
    }

    public Node<TKey, TValue?> RemoveAt(int index)
    {
        if (index >= _count)
            throw new ArgumentOutOfRangeException(nameof(index));

        var child = _children[index];

        if (index < _count - 1)
            Array.Copy(_children, index + 1, _children, index, _count - index - 1);

        _children[_count - 1] = null;
        _count--;

        return child!;
    }

    public Node<TKey, TValue?> RemoveLast() => RemoveAt(_count - 1);

    public void Truncate(int index)
    {
        if (index >= _count)
            throw new ArgumentOutOfRangeException(nameof(index));

        for (var i = index; i < _count; i++)
            _children[i] = null;

        _count = index;
    }

    #endregion
}

Node 來表示每個節點,支持傳入 Comparer 用於實現自定義的排序方式。

internal class Node<TKey, TValue>
{
    #region Fields

    private readonly IComparer<TKey> _comparer;
    private readonly int _degree;
    private readonly int _minItems;
    private readonly int _maxItems;
    private readonly int _maxChildren;

    private readonly Items<TKey, TValue?> _items;
    private readonly Children<TKey, TValue?> _children;

    #endregion

    #region Constructors

    public Node(int degree, IComparer<TKey> comparer)
    {
        _degree = degree;
        _comparer = comparer;
        _minItems = degree - 1;
        _maxItems = 2 * degree - 1;
        _maxChildren = 2 * degree;

        _items = new Items<TKey, TValue?>(_maxItems, _comparer);
        _children = new Children<TKey, TValue?>(_maxChildren);
    }

    #endregion

    #region Properties

    public int ItemsCount => _items.Count;

    public int ChildrenCount => _children.Count;

    public bool IsItemsFull => ItemsCount == _maxItems;
    public bool IsItemsEmpty => ItemsCount == 0;

    public bool IsLeaf => ChildrenCount == 0;

    #endregion

    // ...
}
public sealed class BTree<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue?>>
{
    #region Fields

    private readonly int _degree;
    private readonly IComparer<TKey> _comparer;
    private int _count;
    private Node<TKey, TValue?>? _root;

    #endregion

    #region Constructors

    public BTree(int degree) : this(degree, Comparer<TKey>.Default)
    {
    }

    public BTree(int degree, IComparer<TKey> comparer)
    {
        if (degree < 2)
        {
            throw new ArgumentOutOfRangeException(nameof(degree), "Degree must be at least 2.");
        }

        ArgumentNullException.ThrowIfNull(comparer);

        _degree = degree;
        _comparer = comparer;
    }

    #endregion

    #region Properties

    public int Count => _count;

    public int Degree => _degree;

    public IComparer<TKey> Comparer => _comparer;

    #endregion

    // ...
}

插入數據的過程

先重複一下上文提到的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 插入數據時不作任何處理。

對應枚舉如下:

internal enum InsertionBehavior
{
    /// <summary>
    /// 默認操作,如果 key 已經存在,則不會更新 value
    /// </summary>
    None = 0,

    /// <summary>
    /// 如果 key 已經存在,則更新 value
    /// </summary>
    OverwriteExisting = 1,

    /// <summary>
    /// 如果 key 已經存在,則拋出異常
    /// </summary>
    ThrowOnExisting = 2
}

並定義對應的處理結果枚舉

internal enum InsertionResult
{
    None = 0,
    Added = 1,
    Updated = 2,
}
public sealed class BTree<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue?>>
{
    #region Indexers

    public TValue? this[[NotNull] TKey key]
    {
        get
        {
            if (TryGetValue(key, out var value))
            {
                return value;
            }

            throw new KeyNotFoundException();
        }
        set => TryInsert(key, value, InsertionBehavior.OverwriteExisting);
    }    

    #endregion

    #region Public Methods

    /// <summary>
    /// 往B樹中添加一個鍵值對
    /// </summary>
    /// <param name="key">要添加的元素的key</param>
    /// <param name="value">要添加的元素的value</param>
    /// <exception cref="ArgumentNullException">key是null</exception>
    /// <exception cref="ArgumentException">key已經存在</exception>
    public void Add([NotNull] TKey key, TValue? value) =>
        TryInsert(key, value, InsertionBehavior.ThrowOnExisting);

    /// <summary>
    /// 嘗試往B樹中添加一個鍵值對
    /// </summary>
    /// <param name="key">要添加的元素的key</param>
    /// <param name="value">要添加的元素的value</param>
    /// <returns>true:添加成功;false:添加失敗</returns>
    public bool TryAdd([NotNull] TKey key, TValue? value) =>
        TryInsert(key, value, InsertionBehavior.None);

    #endregion
}

插入算法

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

internal class Node<TKey, TValue>
{
    /// <summary>
    /// 將當前<see cref="Node{TKey,TValue}"/>分裂成兩個<see cref="Node{TKey,TValue}"/>。
    /// </summary>
    /// <returns>中間位置的<see cref="Item{TKey,TValue}"/>和分裂後的第二個<see cref="Node{TKey,TValue}"/></returns>
    public (Item<TKey, TValue?> MiddleItem, Node<TKey, TValue?> SecnodNode) Split()
    {
        int middleIndex = ItemsCount / 2;
        var middleItem = _items[middleIndex];
        var secondNode = new Node<TKey, TValue?>(_degree, _comparer);

        // 將中間位置後的所有Item移動到新的Node中
        for (int i = middleIndex + 1; i < ItemsCount; i++)
        {
            secondNode._items.Add(_items[i]);
        }

        _items.Truncate(middleIndex);

        if (!IsLeaf)
        {
            // 將中間位置後的所有子節點移動到新的Node中
            for (int i = middleIndex + 1; i < ChildrenCount; i++)
            {
                secondNode._children.Add(_children[i]);
            }

            _children.Truncate(middleIndex + 1);
        }

        return (middleItem, secondNode);
    }

    /// <summary>
    /// 如果指定的子節點已滿,則將其分裂爲兩個子節點,並將中間的 <see cref="Item{TKey,TValue}"/>> 插入到當前節點中。
    /// </summary>
    /// <param name="childIndex">指定的子節點的索引</param>
    /// <returns>True 表示已經分裂了子節點,False 表示沒有分裂子節點</returns>
    private bool MaybeSplitChildren(int childIndex)
    {
        var childNode = _children[childIndex];
        if (childNode.IsItemsFull)
        {
            var (middleItem, secondNode) = childNode.Split();
            _items.InsertAt(childIndex, middleItem);
            // 將新node插入到當前node的children中
            _children.InsertAt(childIndex + 1, secondNode);
            return true;
        }

        return false;
    }
}

在 BTree 中定義插入方法

public sealed class BTree<TKey, TValue>
    private bool TryInsert([NotNull] TKey key, TValue? value, InsertionBehavior behavior)
    {
        ArgumentNullException.ThrowIfNull(key);

        if (_root == null)
        {
            _root = new Node<TKey, TValue?>(_degree, _comparer);
            _root.Add(new Item<TKey, TValue?>(key, value));
            _count++;
            return true;
        }

        if (_root.IsItemsFull)
        {
            // 根節點已滿,需要分裂
            var (middleItem, secondNode) = _root.Split();
            var oldRoot = _root;
            _root = new Node<TKey, TValue?>(_degree, _comparer);
            // 將原來根節點中間的元素添加到新的根節點
            _root.Add(middleItem);
            // 將原來根節點分裂出來的節點添加到新的根節點
            _root.AddChild(oldRoot);
            _root.AddChild(secondNode);
        }

        // 從根節點開始插入,如果插入的 Key 已經存在,會按照 behavior 的值進行處理
        var insertionResult = _root.TryInsert(key, value, behavior);
        if (insertionResult == InsertionResult.Added) _count++;

        return insertionResult != InsertionResult.None;
    }
}

在 Node 中定義插入方法,遞歸調用直至找到葉子節點,然後在葉子節點中插入

internal class Node<TKey, TValue>
{
    public InsertionResult TryInsert(TKey key, TValue? value, InsertionBehavior behavior)
    {
        // 如果當前key已經存在, 根據插入行爲決定是否替換
        if (_items.TryFindKey(key, out int index))
        {
            switch (behavior)
            {
                case InsertionBehavior.OverwriteExisting:
                    _items[index].Value = value;
                    return InsertionResult.Updated;
                case InsertionBehavior.ThrowOnExisting:
                    throw new ArgumentException($"An item with the same key has already been added. Key: {key}");
                default:
                    return InsertionResult.None;
            }
        }

        // 如果當前節點是葉子節點,則直接插入
        if (IsLeaf)
        {
            // index 是新的 item 應該插入的位置,items 按順序排列
            _items.InsertAt(index, new Item<TKey, TValue?>(key, value));
            return InsertionResult.Added;
        }

        // 如果當前節點的子節點已經滿了,則需要分裂
        // 如果當前節點的子節點沒有滿,則不需要分裂
        // 如果當前節點的子節點分裂了,則需要判斷當前key是否大於分裂後的中間key
        // 如果當前key大於分裂後的中間key,則需要向右邊的子節點插入
        // 如果當前key小於分裂後的中間key,則需要向左邊的子節點插入

        // index 是新的 item 應該插入的位置,如果當做children的索引,則代表應該插入的位置的右邊的子節點
        if (MaybeSplitChildren(index))
        {
            // rightmostItem 是子節點分裂後的中間的 item,被提升到當前節點的 items 中的最後一個位置了
            var middleItemOfChild = _items[index];

            switch (_comparer.Compare(key, middleItemOfChild.Key))
            {
                case > 0:
                    // 如果當前key大於分裂後的中間key,則需要向右邊的子節點插入
                    index++;
                    break;
                case < 0:
                    // 如果當前key小於分裂後的中間key,則需要向左邊的子節點插入
                    break;
                default:
                    // 如果當前key等於分裂後的中間key,根據插入行爲決定是否替換
                    switch (behavior)
                    {
                        case InsertionBehavior.OverwriteExisting:
                            middleItemOfChild.Value = value;
                            return InsertionResult.Updated;
                        case InsertionBehavior.ThrowOnExisting:
                            throw new ArgumentException(
                                $"An item with the same key has already been added. Key: {key}");
                        default:
                            return InsertionResult.None;
                    }
            }
        }

        // 往子節點插入
        return _children[index].TryInsert(key, value, behavior);
    }
}    

總結

B樹中的數據是按照順序存儲的,所以可以使用二分查找法來查找數據,時間複雜度爲 O(log n)。

往B樹插入數據的過程是一個尋找合適的葉子節點的過程,然後在葉子節點中插入數據,時間複雜度爲 O(log n)。

B樹的節點中存儲的數據量是有限的,所以在插入數據時,可能會發生節點分裂,這樣就會導致樹的高度增加,所以在插入數據時,需要判斷是否需要分裂,如果需要分裂,就需要將中間的數據提升到父節點中,以此類推,直到根節點,如果根節點也需要分裂,就需要新建一個根節點,然後將原來的根節點和分裂出來的節點作爲新的根節點的子節點。

參考資料

PingCAP 寶寶牀邊故事集:存儲引擎

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

B樹 維基百科

Google 用 Go 實現的內存版 B樹

渴望力量系列 《算法導論第三版》

歡迎關注個人微信公衆號 EventHorizonCLI ,最新的原創技術文章將在優先這裏發佈。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章