B树原理以及Go语言简单实现
B树基础原理及Go简单实现

B树的定义

B树是一颗多路平衡查找树。我们描述一颗B树时需要指定它的阶数 M ,阶数 M 代表一个树节点最多有多少个查找路径,M=M路,当M=2则是2叉树,M=3则是3叉。

一棵M阶B树,有如下特性:

  • 若根节点不是叶子结点,则至少有两棵树。
  • 每一个节点最多M棵子树,最多有M-1关键字(键值对)。
  • 除根节点外,其他的每个分支至少有ceil(M/2)个子树,至少含有ceil(M/2)-1个关键字。
  • 每个节点中的关键字都按照大小顺序排列,每个关键字的左子树的所有关键字都小于它,每个关键字的右子树都大于它。
  • 所有叶子节点都位于同一层,或者说根节点到每个叶子节点的长度都相同。

B树

上图是一棵5阶B树。实际B树常用在数据库和文件系统中,为了减少定位记录时所经历的中间过程(树的高度),实际的阶数M都非常大,所以即使存储大量的数据,B树的高度仍然比较小。

B树相对平衡二叉树在节点空间的利用率上进行改进,B树在每个节点保存更多的数据,减少了树的高度,从而提升了查找的性能,在数据库应用中,B树的每个节点存储的数据量大约为4K, 这是因为考虑到磁盘数据存储是采用块的形式存储的,每个块的大小为4K,每次对磁盘进行IO数据读取时,同一个磁盘块的数据会被一次性读取出来,所以每一次磁盘IO都可以读取到B树中一个节点的全部数据。

每个结点中存储了关键字(key)和关键字对应的数据(data),以及孩子结点的指针。在数据库中B树(和B+树)作为索引结构,可以加快查询速度,此时B树中的key就表示键,而data表示了这个键对应的条目在硬盘上的逻辑地址。

B树的插入操作

B树设计初衷就是为了查找,所以查找是最简单的,相反插入、删除反而异常繁琐,每次操作都会多次使用查找。 插入操作,一般搜索树而言,插入就是找到相应的位置,直接加入节点,B树为了数据能够快速被查找,在插入的时候不仅要插入到相应的位置,还需要根据情况来调整树的高度以及宽度,尽可能使树高度下降。

插入操作是指插入一条记录,即(key, value)的键值对。如果B树中已存在需要插入的键值对,则用需要插入的value替换旧的value。若B树不存在这个key,则一定是在叶子结点中进行插入操作

  1. 根据要插入的key的值,找到叶子结点并插入。
  2. 判断当前结点key的个数是否小于等于M-1,若满足则结束,否则进行第3步。
  3. 以结点中间的key为中心分裂成左右两部分,然后将这个中间的key插入到父结点中,这个key的左子树指向分裂后的左半部分,这个key的右子支指向分裂后的右半部分,然后将当前结点指向父结点,继续进行第3步。

下面以5阶B树为例,执行插入操作。在5阶B树中,结点最多有4个key,最少有2个key。

  • 在空树中插入4

B树

此时根结点就一个key,根结点也是叶子结点

  • 插入:20,44,89,96

B树

插入96后此节点超过了最大允许的关键字个数4,此时需要以44为中心进行分裂,新的根节点中包含44这个key

B树

  • 插入:25,30,33

B树

此时左侧节点关键字个数大于4,同样进行分裂操作,得到下图

B树

25这个中间key合并到父节点中,左右两侧的key分裂到两个节点中并作为根节点的两个子节点存在

  • 插入:60,75,81

B树

此时最右侧节点关键字个数大于4,同样进行分裂操作,得到下图

B树

  • 插入:85,110,120

B树

同样进行分裂操作,得到下图

B树

  • 插入:101,150,158

B树

同样进行分裂操作,得到下图

B树

我们发现此时父节点关键字个数大于4,需要对根节点执行分裂操作,根节点的分裂必然导致树的高度增加,这也是B树增加高度的唯一方法,最终得到下图

B树

在实现B树的代码中,为了使代码编写更加容易,我们可以将结点中存储记录的数组长度定义为M而非M-1,这样方便底层的结点由于分裂向上层插入一个记录时(中间状态),上层有多余的位置存储这个记录。同时,每个结点还可以存储它的父结点的引用,这样就不必编写递归程序。

一般来说,对于确定的M和确定类型的记录,结点大小是固定的,无论它实际存储了多少个记录。但是分配固定结点大小的方法会存在浪费的情况,比如下图

B树

key为31,32所在的结点,已经不可能继续在插入任何值了,因为这个结点的前序key是30,后继key是33,所有整数值都用完了。所以如果记录先按key的大小排好序, 再插入到B树中,结点的使用率就会很低。

B树的删除操作

B树的删除操作按照如下几个步骤:

  1. 如果待删除的key位于非叶子结点上,则用后继key(右子树中最小的key)覆盖待删除的key,然后在后继key所在的节点S中删除该后继key。这个后继key一定是在叶子节点上的。执行第2步

  2. 判断S结点key个数是否大于等于ceil(M/2)-1,结束删除操作,否则执行第3步。

  3. 如果S结点的左(或者右)兄弟结点key个数大于ceil(M/2)-1,则父结点中的key下移到该结点,兄弟结点中的一个key上移,删除操作结束。否则将父结点中的key下移与当前结点及它的任一兄弟结点中的key合并,形成一个新的结点。原父结点中的key的两个孩子指针就变成了一个孩子指针,指向这个新结点。然后当前结点的指针指向父结点,此时父节点少了一个key,首先看这个父节点是否是根节点,是的话结束删除操作。否则需要判断是否满足B树的节点个数限制,递归重复第2步。

  4. 如果待删除的key位于叶子结点上,则直接执行第2步。

上面的步骤没有包含一种特殊情况,需要单独列出:

  • B树只有一个根节点,此时删除key没有任何限制

下面同样5阶B树为例:删除44

  • 首先找到后继节点S [60,75]

B树删除

  • 使用最小的60替换44,并删除60

B树删除

  • 此时S节点key数目为1,不满足最低的2个。继续查找它的兄弟节点,发现左兄弟节点[30,33] 不能借。只能合并兄弟节点

B树删除

  • 此时[25]节点key数目为1,不满足最低的2个。继续查找它的兄弟节点,发现右兄弟节点[96,120] 不能借。只能合并兄弟节点

B树删除

  • 移除没有key的根节点,最终得到

B树删除

上面的例子没有体现出当兄弟节点可以借的情况。当兄弟接口可以借时,不要忘记将被借节点中相应的子节点也移动到目标节点的子节点中。

下面是一个简化的go实现,忽略了Data,只使用key来表示一个关键字。

// Package btree github.com/leolu9527/algorithm/btree
package btree

import (
	"math"
	"sort"
)

type position int

const (
	left  = position(-1)
	none  = position(0)
	right = position(+1)
)

type Int int64

func (a Int) Less(b Item) bool {
	return a < b.(Int)
}

type Item interface {
	Less(than Item) bool
}

// NewBtree 创建m阶B树
func NewBtree(m int) *BTree {
	if m < 3 {
		panic("m >= 3")
	}
	var bt = &BTree{}
	bt.root = createNode(bt.m)
	bt.m = m
	bt.minItems = int(math.Ceil(float64(m)/2) - 1)
	bt.maxItems = m - 1
	return bt
}

func createNode(m int) *node {
	var node = &node{}
	node.items = make([]Item, m+1)
	node.items = node.items[0:0]

	return node
}

type items []Item

func (s items) find(key Item) (index int, exist bool) {
	i := sort.Search(len(s), func(i int) bool {
		return key.Less(s[i])
	})
	if i > 0 && !s[i-1].Less(key) {
		return i - 1, true
	}
	return i, false
}

type children []*node

type node struct {
	items    items    //关键字
	children children //叶子节点
	parent   *node
}

// 获取key
func (n *node) get(key Item) Item {
	i, found := n.items.find(key)
	if found {
		return n.items[i]
	} else if len(n.children) > 0 {
		return n.children[i].get(key)
	}
	return nil
}

// 获取key
func (n *node) getKey(key Item) (*node, int) {
	i, found := n.items.find(key)
	if found {
		return n, i
	} else if len(n.children) > 0 {
		return n.children[i].getKey(key)
	}
	return nil, 0
}

func (n *node) insert(bt *BTree, key Item) {
	i := sort.Search(len(n.items), func(i int) bool {
		return key.Less(n.items[i])
	})

	newItems := append(n.items[0:i], append(items{key}, n.items[i:]...)...)
	n.items = newItems

	bt.split(n)
}

// 获取key节点的左右子节点
func (n *node) getChildNodes(index int) (l, r *node) {
	if len(n.children) == 0 {
		return nil, nil
	}
	return n.children[index], n.children[index+1]
}

// 获取key节点的左右兄弟节点
func (n *node) getBrotherNodes() (l, r *node) {
	if n.parent == nil {
		return nil, nil
	}

	index := 0
	for i, pn := range n.parent.children {
		if pn == n {
			index = i
			break
		}
	}

	if index == 0 {
		return nil, n.parent.children[1]
	} else {
		if len(n.parent.children) == index+1 {
			return n.parent.children[index-1], nil
		} else {
			return n.parent.children[index-1], n.parent.children[index+1]
		}
	}
}

// 获取后继节点
func (n *node) getSuccessorNode(index int) *node {
	ln := n.children[index+1]
	if len(ln.children) == 0 {
		return ln
	}
	for nod := ln.children[0]; nod != nil; ln = ln.children[0] {
	}
	return ln
}

func (n *node) delete(index int) Item {
	key := n.items[index]
	if index == 0 {
		n.items = append(items{}, n.items[1:]...)
	} else {
		n.items = append(n.items[0:index], n.items[index+1:]...)
	}
	return key
}

// 移除一个子节点
//TODO 下标越界可能
func (n *node) deleteChild(index int) {
	if index == 0 {
		n.children = append(children{}, n.children[1:]...)
	} else {
		n.children = append(n.children[0:index], n.children[index+1:]...)
	}
}

//前后插入一个子节点
func (n *node) addChild(move *node, p position) {
	if p == left {
		n.children = append(children{move}, n.children...)
	}

	if p == right {
		n.children = append(n.children, move)
	}

	move.parent = n
}

// 获取node在父节点中的index
func (n *node) getIndexInParent() int {
	if n.parent == nil {
		panic("n is root node")
	}
	index := 0
	for i, pn := range n.parent.children {
		if pn == n {
			index = i
			break
		}
	}
	return index
}

type BTree struct {
	root     *node
	m        int //阶
	minItems int //除根节点外单个节点最少包含的元素数量
	maxItems int //单个节点最多包含的元素数量
}

// Get 查找
func (bt *BTree) Get(key Item) Item {
	return bt.root.get(key)
}

// InsertMultiple 批量插入
func (bt *BTree) InsertMultiple(keys []int) {
	for _, v := range keys {
		bt.Insert(Int(v))
	}
}

// Insert 插入数据
func (bt *BTree) Insert(key Item) {
	if bt.Get(key) != nil {
		return
	}
	node := getRightNode(bt.root, key)
	node.insert(bt, key)
}

// Delete 删除key
// 借不到 合
func (bt *BTree) Delete(key Item) {
	n, i := bt.root.getKey(key)
	if n == nil {
		return
	}

	if len(n.children) == 0 && n.parent == nil { //只有一个根节点
		n.delete(i)
		return
	}

	if len(n.children) == 0 && n.parent != nil && len(n.items) > bt.minItems { //叶子节点 & 节点数量充足
		n.delete(i)
		return
	}

	if len(n.children) == 0 && n.parent != nil && len(n.items) <= bt.minItems { //叶子节点、节点不足
		n.delete(i)
		bt.reBalance(n)
		return
	}

	if len(n.children) != 0 { // 非叶子节点
		sn := n.getSuccessorNode(i)
		n.items[i] = sn.items[0] //后继替换
		sn.delete(0)             //后继key删除
		bt.reBalance(sn)
		return
	}
}

//合并两个节点
func (bt *BTree) mergeNodes(l, r *node) {
	ix := l.getIndexInParent()
	k := l.parent.delete(ix)
	rChildren := r.children

	l.items = append(l.items, k)
	l.items = append(l.items, r.items...)

	l.parent.deleteChild(ix + 1)

	// 右子分支合并到左分支中
	if len(l.children) > 0 {
		for _, v := range rChildren {
			v.parent = l
		}
		l.children = append(l.children, rChildren...)
	}

	if l.parent.parent == nil && len(l.parent.items) == 0 { //根节点
		l.parent.children = make(children, 0)
		l.parent = nil
		bt.root = l
	} else {
		//递归向根验证
		bt.reBalance(l.parent)
	}
}

//再平衡
func (bt *BTree) reBalance(n *node) {

	if n.parent == nil {
		return
	} //根节点

	if len(n.items) >= bt.minItems {
		return
	}

	//判断兄弟节点是否可借
	var bn *node = nil
	p := none
	l, r := n.getBrotherNodes()
	if l != nil && len(l.items) > bt.minItems {
		bn = l
		p = left
	}

	if bn == nil && r != nil && len(r.items) > bt.minItems {
		bn = r
		p = right
	}

	if bn != nil { //可借
		var key, pk Item
		ix := bn.getIndexInParent()
		if p == left {
			key = bn.delete(len(bn.items) - 1)
			pk = bn.parent.items[ix]
			bn.parent.items[ix] = key
			n.items = append(items{pk}, n.items...)

			//借完需要处理子节点的归属问题
			if len(bn.children) > 0 {
				moveChild := bn.children[len(bn.children)-1]
				bn.deleteChild(len(bn.children) - 1)
				n.addChild(moveChild, left)
			}
		} else {
			key = bn.delete(0)
			pk = bn.parent.items[ix-1]
			bn.parent.items[ix-1] = key
			n.items = append(n.items, pk)

			//借完需要处理子节点的归属问题
			if len(bn.children) > 0 {
				moveChild := bn.children[0]
				bn.deleteChild(0)
				n.addChild(moveChild, right)
			}
		}

	} else { //不可借
		if l != nil {
			bt.mergeNodes(l, n)
		} else {
			bt.mergeNodes(n, r)
		}
	}
}

// 插入拆分
func (bt *BTree) split(n *node) {
	if len(n.items) <= bt.maxItems {
		return
	}
	middle := bt.m / 2
	l := make(items, middle)
	r := make(items, middle)
	copy(l, n.items[:middle])
	copy(r, n.items[middle+1:])
	middleItem := n.items[middle]
	n.items = l

	//右节点
	rNode := createNode(bt.m)
	rNode.items = r

	if n.parent != nil { //有父节点
		i := n.getIndexInParent()

		if i == 0 {
			n.parent.items = append(items{middleItem}, n.parent.items[:]...)
			n.parent.children = append(n.parent.children[0:1], append(children{rNode}, n.parent.children[1:]...)...)
		} else {
			n.parent.items = append(n.parent.items[0:i], append(items{middleItem}, n.parent.items[i:]...)...)
			n.parent.children = append(n.parent.children[0:i+1], append(children{rNode}, n.parent.children[i+1:]...)...)
		}

		rNode.parent = n.parent
		//递归处理父节点
		bt.split(n.parent)
	} else { //没有父节点
		newRoot := createNode(bt.m)
		newRoot.items = append(newRoot.items, middleItem)
		newRoot.children = append(newRoot.children, n, rNode)
		n.parent = newRoot
		bt.root = newRoot
		rNode.parent = bt.root

		if len(n.children) > 0 { //处理被拆分节点的子节点
			rChildren := make(children, bt.m-middle)
			copy(rChildren, n.children[middle+1:])
			n.children = n.children[0 : middle+1]
			rNode.children = rChildren
			for _, v := range rNode.children {
				v.parent = rNode
			}
		}
	}
}

// 获取待插入的叶子节点
func getRightNode(root *node, key Item) *node {
	//根节点
	if len(root.children) == 0 {
		return root
	}

	for index, item := range root.items {
		if key.Less(item) {
			return getRightNode(root.children[index], key)
		}
	}
	return getRightNode(root.children[len(root.items)], key)
}

example_test.go

package btree_test

import (
	"fmt"
	"github.com/leolu9527/algorithm/btree"
)

func Example() {
	bt := btree.NewBtree(5)
	bt.Insert(btree.Int(4))
	bt.InsertMultiple([]int{20, 44, 89, 96, 25, 30, 33, 60, 75, 81, 85, 110, 120, 101, 150, 158, 130, 135, 138})
	fmt.Println(bt.Delete(btree.Int(44)))
	fmt.Println(bt.Delete(btree.Int(138)))
	fmt.Println(bt.Delete(btree.Int(99)))

	// Output:
	// true
	// true
	// false
}

附录

代码:https://github.com/leolu9527/go-algorithms/tree/main/btree

References

  1. https://github.com/google/btree

Last modified on 2022-05-31

Comments Disabled.