Skip to content

Latest commit

 

History

History
139 lines (132 loc) · 5.8 KB

4D.md

File metadata and controls

139 lines (132 loc) · 5.8 KB

B+树

在第二章中,我们讨论过改善链式结构存储友好性的办法(团块化),本节则要把它应用到树上面。

type bNode[T constraints.Ordered] struct {
    inner bool                      //true表示为内节点,false表示为叶节点
    view  []T                       //数据窗口,指向派生类的data字段
}
type iNode[T constraints.Ordered] struct {
    bNode[T]                        //inner==true
    data     [iFullSize]T           //索引
    kids     [iFullSize]*iNode[T]   //子节点指针域(类型不安全,有可能为叶节点)
}
type lNode[T constraints.Ordered] struct {
    bNode[T] //inner==false
    data     [lFullSize]T           //负载
    next     *lNode[T]              //链表指针域
}

  B+树由索引节点和存储节点组成,是一种完美平衡的多叉树。其中,索引节点中的索引值为其对应子节点内容的最大值。而存储节点作为树的叶节点同时,还构成有序块链表。

       [3  6  9]
      /    |    \
[1 2 3] [4 5 6] [7 8 9]

盈则分之

                                                                         [5 9]
                                                                         /   \
       [3  6  9]         =>        [3     6     9]        =>        [3  5]   [6  9]
      /    |    \                  /     / \     \                  /   |     |   \ 
[0 1 2] [3 4 5] [6 8 9]      [1 2 3] [4 5] [5 6] [7 8 9]      [1 2 3] [4 5] [5 6] [7 8 9]

向已满节点插入新元素时,我们将该节点一分为二:

func (tr *Tree[T]) Insert(key T) bool {
    if tr.root == nil {
        //...
    }
    tr.path.Clear()
    node, pos := tr.root, 0
    if key > tr.root.ceil() {                           //超出上界
        for node.inner {
            pos = len(node.view) - 1
            node.data[pos] = key                        //一路更新
            tr.path.Push(trait[T]{node, pos})
            node = node.kids[pos]
        }
        pos = len(node.view)
    } else {
        for node.inner {
            pos = array.SearchFirstGE(node.view, key)
            if key == node.view[pos] { return false }
            tr.path.Push(trait[T]{node, pos})           //路径追踪
            node = node.kids[pos]
        }
        pos = array.SearchFirstGE(node.view, key)
        if key == node.view[pos] { return false }
    }

    peer := node.asLeaf().insert(pos, key).asIndex()
    for peer != nil {                                   //分裂出新节点,继续
        if tr.path.IsEmpty() {
            //...
        } else {
            last := tr.path.Pop()
            last.node.view[last.idx] = node.ceil()
            node, peer = last.node, last.node.insert(last.idx+1, peer)
    }   }
    return true
}

亏则合之

         [4 9]                     [4 9]                   [4 9]
         /   \                     /   \                   /   \
    [2  4]   [6  9]      =>    [2 4]  [6  9]      =>     [4] [6  9]      =>     [4  6  9]
    /   |     |   \            /  |    |   \             /    |   \             /   |   \ 
[1 2] [3 4] [5 6] [7 8 9]  [1 2] [4] [5 6] [7 8 9]  [1 2 4] [5 6] [7 8 9]  [1 2 4] [5 6] [7 8 9]

删除元素后,我们尝试对残余数据量低的节点处理:和附近节点合并或者从附近节点分流数据。

func (tr *Tree[T]) Remove(key T) bool {
    //...
    node.asLeaf().remove(pos)
    if tr.path.IsEmpty() {                                  //对根节点采用直接删除
        if len(node.view) == 0 {
            tr.root, tr.head = nil, nil
        }
        return true
    } //除了到根节点,len(node.view) >= 2
    shrink, ceil := (pos == len(node.view)), node.ceil()    //需要额外的索引修正

    last := tr.path.Pop()                                   //数据量少,考虑合并节点
    for limit, isLeaf := lQuarterSize, true; len(node.view) < limit; limit, isLeaf = iQuarterSize, false {
        peer := node
        if last.idx == len(last.node.view)-1 {              //向前
            node = last.node.kids[last.idx-1]
        } else {                                            //向后
            last.idx++
            peer, shrink = last.node.kids[last.idx], false
        }
        combined := false
        if isLeaf {
            combined = node.asLeaf().combine(peer.asLeaf())
        } else {
            combined = node.combine(peer)                  //尝试合并
        }
        last.node.view[last.idx-1] = node.ceil()           //修正索引
        if !combined { break }                             //中止
        last.node.remove(last.idx)
        if tr.path.IsEmpty() {
            if len(last.node.view) == 1 {                  //对根节点采用缩节处理
                tr.root = last.node.kids[0]
            }
            return true
        }
        node = last.node
        last = tr.path.Pop()
    }
    if shrink {                                             //合并已结束,但索引修正未完成
        last.node.view[last.idx] = ceil
        for last.idx == len(last.node.view)-1 &&            //级联
            !tr.path.IsEmpty() {
            last = tr.path.Pop()
            last.node.view[last.idx] = ceil
    }   }
    return true
}

性能分析

  B+树是查询、插入、删除都为O(logN)的三好学生,只是节点的分裂与合并涉及数据挪移,有一定开销。事实上,B+树虽然比较笨重,从上一节的评测中可以看出,其性能通常比二叉搜索树要强。造成此等结果的根源在于随机访存性能不理想,团块型结构往往比纯链式结构要吃香。


目录 上一节 下一节