B树的引入
在前面的几篇博文中,我已经简单介绍了:
搜索二叉树搜索二叉树
AVL树AVL树
红黑树红黑树
上面这三个都是典型的二叉搜索结构,查找的时间复杂度和树的高度相关达到O(log2N);
数据杂乱无规律的时间复杂度————————->线性搜索 O(n);
数据有序————————————->二分查找 O(log2N) 最差退化为O(n)
二叉搜索树/AVL树/红黑树—————————->O(log2N);
上述的几种情况全部是由二叉树来实现的,对于大数据处理就会出现以下几个问题:
(1):数据不能全部加载到内存中--->(数据元素访问的时候肯定会出错)
(2):数据元素能加载到内存中,但是树的高度太高------------>(增加了磁盘访问的次数,效率低下)
所以对于以上两个问题解决方法:
(1):提高I/O的时间;(不太现实)
(2):降低树的高度------->(平衡多叉树)
由此,我们就可以进入B树的介绍;
B树的定义
一颗m阶的B树,或为空树,或为满足下列特性的m叉树:
(1):根节点至少有两个孩子;
(2):每个非根节点至少有 M/2(上去整)个孩子,至多有M个孩子;
(3):每个非根节点至少有M/2-1(上取整)个关键字,至多有M-1个关键字,并且以升序排列;
(4):key[i]和key[i+1]之间的孩子节点的值介于key[i]、key[i+1]之间;
(5):所有叶子节点都在一层;
由定义,我们可以知道在B树上的查找的过程和二叉排序树查找类似,都可以是左子树<根<右子树;
所以,在B树上进行查找的过程是一个顺指针查找节点和在节点的关键字中进行查找交叉进行的过程。
我们可以给出B树的节点类型:
template<class K, size_t M>
struct BTreeNode
{
K _keys[M]; //关键码集合 本来里面应该只有M-1个元素,但是在搬移的时候为了能够让多一个元素插入,搬移时方便,所以就多给一个位置
BTreeNode<K, M>* _pParent; //双亲
BTreeNode<K, M>* _pSub[M + 1]; // 孩子集合,孩子节点一定比关键码多一个
size_t _size; //有效元素的个数
BTreeNode() //初始化,将指针数组里的元素初始化为NULL;
: _size(0)
, _pParent(NULL)
{
for (size_t idx = 0; idx < M; idx++)
_pSub[idx] = NULL;
}
};
B树的查找
因为我们在定义节点的时候是定义了一个指针数组来保存一个结点的,所以我们在查找懂啊一个元素的时候返回的是它这个节点以及它在这个节点中的位置(光返回一个节点,我们不知道具体位置还是要继续查找);
所以用一个pair(first,second)来存放返回值;
C++11中解释pair:
一个人pair保存两个数据成员。类似容器,pair是一个用来生产特定类型的模板。当创建一个pair时,我们必须提供两个类型名,pair的数据类型成员将具有对应的类型。两个类型不要求一样;pair
pair<Node*, int> Find(const K& key) //查找节点
{
Node* pCur = _pRoot;
Node* pParent = NULL;
while (pCur)
{
size_t idx = 0;
while (idx < pCur->_size)
{
if (key < pCur->_keys[idx]) //key小于pCur的话,直接跳出走到他的孩子节点中
break;
else if (key > pCur->_keys[idx]) //key大于pCur的话,直接走到他的下一个指针域中
idx++;
else
return make_pair(pCur, idx);//相等的话直接就返回键值对,(他的指针和M阶中所指的第几个)
}
pParent = pCur; //要记录他的双亲,因为是空的话,返回的是他的双亲,所以要保存起来
pCur = pCur->_pSub[idx];
}
return make_pair(pParent, -1); //没有找到就直接返回-1,以及他的双亲
}
上述,查找函数,我们返回值用的就是make_pair()生成的,返回值类型就是用一个pair来接收;
B树的插入
B树的插入原理
B树种的节点插入和我们前面接触得二叉树的插入不太一样,因为,在二叉树种,我们都知道是从根节点向下进行比较,再进行插入;而B树中我们插入的时候是将一个指针数组插入满以后,从中间进行向上分裂;
B树的插入过程
1、先判断树是否为空;
2、树如果是空,直接new一个节点;
3、判断Find()的返回值是否存在;
4、进行节点的插入;
5、节点个数如果等于阶数进行向上分裂;
图示
下面进行简单的模块代码实现:
bool Insert(const K& key)
{
//找到插入的位置
if (NULL == _pRoot)
{
_pRoot = new Node;
_pRoot->_keys[0] = key;
_pRoot->_size++;
return true;
}
pair<Node*, int> ret = Find(key);
if (ret.second > -1) //当返回值的Second的值==-1时,就说明他找到的该节点,所以就不用插入了;
return false;
K k = key;
Node* pCur = ret.first;
Node* pSub = NULL;
while (1)
{
//插入一个节点,不用实现分裂
_InsertKey(pCur, k, pSub);
if (pCur->_size < M)
return true;
//实现分裂
size_t mid = M / 2;
Node* pNewNode = new Node;
// size_t index = 0;
for (size_t idx = mid + 1; idx < pCur->_size; ++idx) //给新结点进行分裂后的赋值
{
pNewNode->_keys[pNewNode->_size++] = pCur->_keys[idx];
}
pCur->_size = pCur->_size - pNewNode->_size - 1; //原结点剩余的元素个数
for (size_t j = 0; j < pNewNode->_size + 1; j++) //进入双层以后,双亲层如果需要分裂的话,分裂后,对原来的(M+1)个孩子节点也要进行重新的赋给正确的双亲
{
pNewNode->_pSub[j] = pCur->_pSub[mid + 1 + j];
if (pNewNode->_pSub[j])
pNewNode->_pSub[j]->_pParent = pNewNode;
pCur->_pSub[mid + 1 + j] = NULL;
}
if (pCur->_pParent == NULL) //双亲不存在
{
_pRoot = new Node;
_pRoot->_keys[0] = pCur->_keys[mid];
_pRoot->_pSub[0] = pCur;
pCur->_pParent = _pRoot;
_pRoot->_pSub[1] = pNewNode;
pNewNode->_pParent = _pRoot;
_pRoot->_size = 1;
return true;
}
else
{
k = pCur->_keys[mid];
pCur = pCur->_pParent;
pSub = pNewNode;
}
}
}
void _InsertKey(Node* pCur, const K& key, Node* pSub) //搬移元素,因为关键码是有序的,所以每个插入的关键码都要排序
{
assert(pCur);
size_t j = 0;
int end = pCur->_size - 1;
while (end >= 0)
{
if (pCur->_keys[end] > key) //如果大于的话就搬移到后面去;
{
pCur->_keys[end + 1] = pCur->_keys[end];
pCur->_pSub[end + 2] = pCur->_pSub[end + 1];
}
else
break;
end--;
}
pCur->_keys[end + 1] = key;
pCur->_size++;
pCur->_pSub[end + 2] = pSub; //
if (pSub)
pSub->_pParent = pCur;
}
B树的遍历
我们在创建一个树以后,想要检查它树是否正确,可以直接在调试器中看,但是数据太多的话,我们就查看起来很麻烦,最直接简单的拌饭就是通过一个中序遍历直接查看是否有序和数据元素是否漏缺:在二叉树中,我们都会遍历;在B树种其实也很简单:
void _InOrder(Node* pRoot)
{
if (pRoot)
{
_InOrder(pRoot->_pSub[0]);
for (size_t idx = 0; idx < pRoot->_size; ++idx)
{
cout << pRoot->_keys[idx] << " ";
}
_InOrder(pRoot->_pSub[pRoot->_size]);
}
}
B-树的完整代码依上传GitHub B-Tree;
B+Tree
B+树是应文件系统所需要而出的一种B-树的变型树。一颗m阶B+树和m阶B-树的差异在于:
(1):有n颗子树的节点中含有n个关键字;
(2):所有的叶子节点中包含了全部关键字的信息,及指向这些关键字记录的指针,且叶子节点本身依关键字的大小自小而大顺序链接;
(3):所有的非终端节点可以看成是索引部分,节点中仅含有起子树(根节点)中的最大(或最小)关键字。
B*Tree
B*树是B+树的变形,在B+树的非根节点和非叶子结点在增加指向兄弟的指针;