B-Tree的简单实现:
GitHub源码地址
注:
B-Tree的原理介绍在这里就不赘述了,相信其它博客会讲的更透彻。我们主要关注实现过程的细节问题。
以下条目对应的代码实现会在代码中标出,例如第3条对应的代码中的位置标识为[3]
代码最后写了2个调试使用的代码,可能对理解实现过程和逻辑结构有帮助。
- B-Tree单个节点的结构是:
BTNode {
id; //1)节点对象的唯一标识
parent; //2)父节点的id
indexNum;//3)当前索引数量
childNum;//4)指向的子节点数量,比indexNum少1
indexMap;//5)数组,该节点包含的索引,索引升序排列
children;//6)数组,键为indexMap中的索引(index),值为指向的子节点的id。指向的子节点中的索引,均大于index
}
若B-Tree的阶为m,即m阶B-Tree:
- 节点的索引数量indexNum (ceil(m/2) - 1) <= indexNum <= (m-1) [节点分裂后,一个索引上升到父节点,新生成的节点继承原节点剩余的一半索引,所以索引数量>= ceil(m/2) - 1]
- 同理,节点的子节点数量childNum ceil(m/2) <= indexNum <= m-1
插入都是在叶节点开始的。[3]
- 节点索引数量>=m时,需要分裂节点:
- 处于中间位置的索引,上升到父节点[4-1]
- 重新创建一个节点,上升到父节点的中间索引指向的节点就是这个新节点。而这个新节点的索引是原节点中间之后的索引,指向的子节点也是原节点中间索引之后的索引指向的子节点。这里需要注意的是需要把这些指向的子节点的parent改为新创建的节点。[4-2]
- 原节点的parent不变,只是将中间索引和中间索引之后的索引和指向的子节点删除。[4-3]
- 如果中间索引上升到父节点,造成父节点索引数量>=m,则需要分裂父节点,并依次递归分裂。[4-4]
<?php
class BTNode
{
public $id;
public $parent = 0;
public $indexNum = 0;
public $childNum = 0;
public $indexMap = [];
public $children = [];
public function __construct(BTree $btree)
{
$this->id = uniqid();
$btree->nodeMap[$this->id] = $this;
}
public function add($index = 0, $child = null)
{
if ($index !== 0) {
$this->indexMap[$this->indexNum++] = $index;
sort($this->indexMap);
}
$this->children[$index] = $child;
ksort($this->children);
$this->childNum++;
}
public function isFull($order)
{
return $this->indexNum >= $order;
}
}
class BTree
{
/**
* @var BTNode
*/
public $root = null;
public $order;
public $keyNumMin;
public $keyNumMax;
public $nodeMap = [];
public function __construct($order = 3)
{
$this->order = $order;
$this->keyNumMin = ceil($order / 2) - 1;
$this->keyNumMax = $order - 1;
}
public function insert($index)
{
if ($this->isEmpty()) {
$node = new BTNode($this);
$node->add(0, 0);
$node->add($index, 0);
$this->root = $node;
} else {
$nextNode = $this->root;
$prevNode = $this->root;
//只有叶节点的子节点才会为null,所以插入都从叶节点开始[3]
while ($nextNode != null) {
$indexMap = $nextNode->indexMap;
$indexNum = $nextNode->indexNum;
$pos = $indexMap[$indexNum - 1];
$prevNode = $nextNode;
for ($i = 0; $i < $indexNum; $i++) {
if ($index < $indexMap[$i]) {
if ($i !== 0) {
$pos = $indexMap[$i - 1];
} else {
$pos = 0;
}
break;
} else if ($index == $indexMap[$i]) {
return false;
}
}
$nextNode = $this->getNode($nextNode->children[$pos]);
}
$prevNode->add($index, 0);
if ($prevNode->isFull($this->order)) {
$this->split($prevNode);
}
}
}
public function find($index)
{
if (!$this->isEmpty()) {
$nextNode = $this->root;
while ($nextNode != null) {
$indexMap = $nextNode->indexMap;
$indexNum = $nextNode->indexNum;
$pos = $indexMap[$indexNum - 1];
for ($i = 0; $i < $indexNum; $i++) {
if ($index < $indexMap[$i]) {
if ($i !== 0) {
$pos = $indexMap[$i - 1];
} else {
$pos = 0;
}
break;
} else if ($index == $indexMap[$i]) {
return true;
}
}
$nextNode = $this->getNode($nextNode->children[$pos]);
}
}
return false;
}
public function isEmpty()
{
return is_null($this->root);
}
public function split(BTNode $node)
{
$indexMap = $node->indexMap;
$children = $node->children;
$indexNum = $node->indexNum;
$middle = intval(floor($indexNum / 2));
$middleIndex = $indexMap[$middle];
$middleChild = $children[$middleIndex];
//重新创建一个节点[4-2]
$newNode = new BTNode($this);
$newNode->add(0, $middleChild);
for ($i = 0; $i < $indexNum; $i++) {
if ($i >= $middle) {
if ($i != $middle) {
//把原节点中间索引之后的索引加入新节点[4-2]
$child = $children[$indexMap[$i]];
$newNode->add($indexMap[$i], $child);
if ($child != 0) {
$tmp = $this->getNode($child);
//这里需要注意的是需要把这些指向的子节点的parent改为新创建的节点。[4-2] $tmp->parent = $newNode->id;
}
}
//原节点删除中间索引及之后的索引[4-3] unset($node->indexMap[$i]);
unset($node->children[$indexMap[$i]]);
$node->indexNum--;
$node->childNum--;
}
}
$pid = $node->parent;
if ($pid === 0) {
//根节点分裂,树高度增加
$parent = new BTNode($this);
$parent->add(0, $node->id);
$this->root = $parent;
$pid = $parent->id;
} else {
$parent = $this->getNode($pid);
}
$node->parent = $pid;
$newNode->parent = $pid;
//处于中间位置的索引,上升到父节点[4-1]
$parent->add($middleIndex, $newNode->id);
//父节点容量超出,进行分裂[4-4]
if ($parent->isFull($this->order)) {
$this->split($parent);
}
}
/**
* @param $id
* @return BTNode | null
*/
public function getNode($id)
{
return isset($this->nodeMap[$id]) ? $this->nodeMap[$id] : null;
}
}
$testList = [3, 4, 5, 10, 22, 32, 7, 6, 2, 1, 999, 101];
$btree = new BTree(3);
echo '<pre>';
foreach ($testList as $value) {
$btree->insert($value);
}
/************************************ 插入B树的所有元素 **********************************/
echo '插入B树的所有元素: <br>';
foreach ($testList as $value) {
echo $value.',';
}
echo '<hr>';
/******************************* 测试find方法 *****************************************/
$tt = [3, 4, 5, 10, 22, 32, 7, 6, 2, 1, 999, 22, 33, 55, 1, 9, 17, 101];
echo '测试find方法:<br>';
foreach ($tt as $v) {
if ($btree->find($v)) {
$msg = in_array($v, $testList) ? 'true' : 'false';
echo $v.' 存在! 是否正确:['.$msg.']';
} else {
$msg = !in_array($v, $testList) ? 'true' : 'false';
echo $v. ' 不存在! 是否正确:['.$msg.']';
}
echo '<br>';
}
echo '<hr>';
/****************************************** 调试代码: 打印节点信息 ************************************************************/
echo '调试: 打印节点信息:<br>';
function dump(BTree $btree) {
$stack = [$btree->root->id, '关联索引:0'];
while (!empty($stack)) {
$tmpNode = array_shift($stack);
if (is_string($tmpNode) && strpos($tmpNode, '关联索引:') === 0) {
echo '('.$tmpNode.')';
echo '<br>';
continue;
}
$tmpNode = $btree->getNode($tmpNode);
if (!is_null($tmpNode) && is_array($tmpNode->children) && !empty($tmpNode->children)) {
echo '节点索引数:['.$tmpNode->indexNum.'] ';
echo '节点列表: ';
foreach ($tmpNode->indexMap as $index) {
echo $index.',';
}
echo ' | ';
foreach ($tmpNode->children as $key => $value) {
if ($value != 0) {
array_push($stack, $value);
array_push($stack, '关联索引:'.$key);
}
}
}
}
}
dump($btree);
echo '<hr>';
/*************************************** 调试代码: 打印所有树节点 *****************************************/
echo '调试: 打印所有树节点:<br>';
foreach ($btree->nodeMap as $each) {
var_dump($each);
}
echo '<hr>';