B-树引入
当我们从一堆数据里查找某个数据的时候,常使用如下方法:
数据杂乱无规律—>线性搜索 —-> O(N)
数据有序—->二分查找—->O(log2N)—>最差情况下退化成单只树O(N)
二叉搜索树/AVL树/红黑树—->O(log2N)
其中二叉搜索树、 AVL树、 红黑树都是动态查找树, 典型的二叉搜索树结构,查找的时间复杂度和树的高度相关O(log2N)。
这几种树的结构,很大程度上,提高了数据的查找效率,但是数据一般保存在磁盘上,若数据量过大不能全部加载到内存,那么将导致这几种数据结构的树的高度太高, 增大访问磁盘的次数, 从而导致效率低下。为了访问所有数据, 使用如下搜索树结构保存数据: 树的结点中保存权值(关键字)和磁盘的地址
由此,我们引入B-树。
B-树定义
1970年, R.Bayer和E.mccreight提出了一种适合外查找的树, 它是一种平衡的多叉树, 称为B树。( 有些地方写的是B-树,注意不要误读成”B减树”)一棵M阶(M>2)的B树, 是一棵平衡的M路平衡搜索树, 可以是空树或者满足一下性质:
1. 根节点至少有两个孩子——假设根节点只有一个关键字,那么至少有两个关键字(一个大于根,一个小于根)存在,作为其孩子。
2. 每个非根节点至少有M/2(上取整)个孩子,至多有M个孩子——假设M为3,则该节点至少有(3/2+1)个孩子,至多有3个孩子。
3. 每个非根节点至少有M/2-1(上取整)个关键字,至多有M-1个关键字, 并且以升序排列
4. key[i]和key[i+1]之间的孩子节点的值介于key[i]、 key[i+1]之间——B树为有序树,每两个键值之间的所有孩子节点的键值大小必然介于两双亲节点之间。
5. 所有的叶子节点都在同一层——B树不同于其他树从上向下生长,而是自下而上,层层分裂。
- 关于性质,从图解中详细介绍。
图解B树
以下所有图M值取3。
- 树为空时
插入20时,对已有键值10和20进行比较,按照从左到右从小到大的顺序插入。
当20插入根节点以后,节点size等于M,此时需要对节点进行分裂。若不分裂,则该节点孩子为四个,违反了性质2。
这里节点结构定义时多给了一格,以便插入键值时键值数组不会越界。
分裂过程如下图:
分裂时,创建两个新节点,一个作为根节点用以存放节点中间键值为20的节点,一个用来存放中间键值右边的所有键值,其次,更新孩子双亲指向关系。
- 树不空
依次插入40和50,自上而下查找插入位置,根据大小排列,插入30所在节点,50插入后需要再次分裂节点,此时,因该节点非根节点,则,分裂时,将中间键值之后的键值移入新节点中,中间键值存入双亲节点中,在此例中,其双亲为根节点,往双亲插入中间键值时,按照从左向右,从小到大的顺序,即键值的插入顺序。
再次插入80,70。图示如下:
分裂图示如下:
上图中,70插入后,该节点需要分裂,分裂完毕之后,70存入根节点,此时根节点也需要分裂,以满足B树性质。需要注意的是,分裂过程中,各个节点的孩子双亲指向需要及时更改,否则出错,具体细节见代码实现。
B树代码实现
#include<iostream>
using namespace std;
template <typename K, size_t M>
struct BTreeNode
{
K _keys[M]; // 关键字的集合——键值数组 -->多出的一格防止数组越界
BTreeNode* _pSons[M + 1]; // 孩子节点的集合-->多出的一格备用
BTreeNode* _pParent; // 双亲节点
size_t _size; // 有效关键字的个数——当前节点内当前关键字的数目
BTreeNode() // 构造函数-->对各成员进行初始化,初始时size为0,双亲节点为空
: _size(0)
, _pParent(NULL)
{
size_t i = 0;
for (i = 0; i < M; i++)
_pSons[i] = NULL; // 初始化前M个孩子为空
_pSons[i] = NULL; // 备用的那一格置空
}
};
template <typename K, size_t M>
class BTree
{
public:
typedef BTreeNode<K, M> Node; // 类型重命名
BTree()
:_pRoot(NULL)
{}
// pair类由C++库提供,它将一对值配对,这可能是不同类型(T1和T2)。可通过其第一和第二公共成员访问。
pair<Node*, int> Find(const K& key) // 查找键值为key的节点,返回该节点以及节点内位置下标
{
Node* pCur = _pRoot;
Node* pParent = NULL;
// 只要没找到且pCur不为空,继续查找
while (pCur)
{
// 从根节点找起,只要下标i不越界且该位置的键值小于key,就继续向后查找。若大于key则跳出
size_t i = 0;
while (i < pCur->_size)
{
if (key < pCur->_keys[i])
break;
else if (key > pCur->_keys[i])
i++;
else
return pair<Node*, int>(pCur, i);
}
// pParent记录pCur为空时的双亲节点
pParent = pCur;
pCur = pCur->_pSons[i];
}
// 未找到——>返回pParent,位置返回-1
return pair<Node*, int>(pParent, -1);
}
bool Insert(const K& key) //插入
{
// 若树为空,直接插入,更新keys,size
if (_pRoot == NULL)
{
_pRoot = new Node;
_pRoot->_keys[0] = key;
_pRoot->_size = 1;
return true;
}
//找插入位置,若要插入的键值已存在,返回false
pair<Node*, int> pos = Find(key);
if (pos.second >= 0)
return false;
//插入
Node* pCur = pos.first; // 要插入的位置的坐在节点
Node* pSon = NULL; // 标志pCur位置上新的孩子
K k = key;
// 循环检查树是否正确,对其及时进行调整,直到插入成功返回true
while (true)
{
// 在pCur节点里插入键值k
InsertKey(pCur, pSon, k);
// 插入后若pCur的size<M,说明节点不需要分裂,直接返回
if (pCur->_size < M)
return true;
// 分裂节点
size_t mid = pCur->_size >> 1;
Node* newNode = new Node;
// 搬移mid右边键值到新节点newNode,且更新搬移键值的孩子的指向
size_t i = 0;
for (i = mid+1; i < pCur->_size; i++)
{
newNode->_keys[newNode->_size] = pCur->_keys[i];
newNode->_pSons[newNode->_size++] = pCur->_pSons[i];
if (pCur->_pSons[i])
pCur->_pSons[i]->_pParent = newNode;
}
newNode->_pSons[newNode->_size] = pCur->_pSons[i];
if (pCur->_pSons[i])
pCur->_pSons[i]->_pParent = newNode;
// 更新pCur的size
pCur->_size = pCur->_size - newNode->_size - 1;
// 若pCur已经调整到根节点还未合格,则再次分裂,更新根节点后直接返回true
if (_pRoot == pCur)
{
_pRoot = new Node;
_pRoot->_keys[0] = pCur->_keys[mid];
_pRoot->_size = 1;
// 更新新的根节点的孩子以及孩子双亲的指向
_pRoot->_pSons[0] = pCur;
pCur->_pParent = _pRoot;
_pRoot->_pSons[1] = newNode;
newNode->_pParent = _pRoot;
return true;
}
else // 若pCur不为根,且仍旧不平衡,则pCur向上更新即指向其双亲,pSon指向新分裂出来的节点,并更新需要调整的键值
{
k = pCur->_keys[mid];
pCur = pCur->_pParent;
pSon = newNode;
}
}
}
// 中序遍历
void InOrder()
{
cout << "InOrder:" << endl;
_InOrder(_pRoot);
cout << endl;
}
protected:
void _InOrder(Node* pRoot)
{
if (pRoot)
{
int i = 0;
for (; i < pRoot->_size; i++)
{
_InOrder(pRoot->_pSons[i]);
cout << pRoot->_keys[i] << " ";
}
_InOrder(pRoot->_pSons[pRoot->_size]); // 处理该节点最右边的孩子
}
}
void InsertKey(Node* pCur, Node* pSon, const K& key)
{
int end = pCur->_size - 1; //标志pCur的最后一个有效键值位置
while (end >= 0)
{
// 比较当前位置上的键值与key的大小,找插入位置
// 若当前位置键值大于key
if (pCur->_keys[end] > key)
{
pCur->_keys[end + 1] = pCur->_keys[end]; // 向后移动当前位置上的键值
pCur->_pSons[end + 2] = pCur->_pSons[end + 1]; // 键值移动后,相应的更新孩子指向
}
else // 找到位置后退出循环
break;
end--;
}
// 插入key,并更新对应位置上pCur的孩子指向以及size
pCur->_keys[end + 1] = key;
pCur->_pSons[end + 2] = pSon;
pCur->_size++;
// 若孩子不为空,更新双亲为pCur
if (pSon)
pSon->_pParent = pCur;
}
private:
Node* _pRoot;
};
void Test()
{
BTree<int, 3> t;
t.Insert(10);
t.Insert(30);
t.Insert(20);
t.Insert(40);
t.Insert(50);
t.Insert(80);
t.Insert(70);
t.InOrder();
}