前言:
动态查找树主要有:二叉查找树(Binary Search Tree),平衡二叉查找树(Balanced Binary Search Tree),红黑树(Red-Black Tree ),B-tree。前三者是典型的二叉查找树结构,其查找的时间复杂度O(log2N)与树的深度相关,那么降低树的深度自然会提高查找效率。
但是咱们有面对这样一个实际问题:就是大规模数据存储中,实现索引查询这样一个实际背景下,树节点存储的元素数量是有限的(如果元素数量非常多的话,查找就退化成节点内部的线性查找了),这样导致二叉查找树结构由于树的深度过大而造成磁盘I/O读写过于频繁,进而导致查询效率低下(为什么会出现这种情况,待会在外部存储器-磁盘中有所解释),那么如何减少树的深度(当然是不能减少查询的数据量),一个基本的想法就是:采用多叉树结构(由于树节点元素数量是有限的,自然该节点的子树数量也就是有限的)。
也就是说,因为磁盘的操作费时费资源,如果过于频繁的多次查找势必效率低下。那么如何提高效率,即如何避免磁盘过于频繁的多次查找呢?根据磁盘查找存取的次数往往由树的高度所决定,所以,只要我们通过某种较好的树结构减少树的结构尽量减少树的高度,那么是不是便能有效减少磁盘查找存取的次数呢?那这种有效的树结构是一种怎样的树呢?
这样我们就提出了一个新的查找树结构——多路查找树。根据平衡二叉树的启发,自然就想到平衡多路查找树结构,也就是这篇文章所要阐述的第一个主题B~tree,即B树结构
B-树:
具体讲解之前,有一点,再次强调下:B-树,即为B树。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是一种一种树。而事实上是,B-tree就是指的B树。特此说明。
我们知道,B 树是为了磁盘或其它存储设备而设计的一种多叉(下面你会看到,相对于二叉,B树每个内结点有多个分支,即多叉)平衡查找树。与本blog之前介绍的红黑树很相似,但在降低磁盘I/0操作方面要更好一些。许多数据库系统都一般使用B树或者B树的各种变形结构,如下文即将要介绍的B+树,B*树来存储信息。
B树与红黑树最大的不同在于,B树的结点可以有许多子女,从几个到几千个。那为什么又说B树与红黑树很相似呢?因为与红黑树一样,一棵含n个结点的B树的高度也为O(lgn),但可能比一棵红黑树的高度小许多,应为它的分支因子比较大。所以,B树可以在O(logn)时间内,实现各种如插入(insert),删除(delete)等动态集合操作。
如下图所示,即是一棵B树,一棵关键字为英语中辅音字母的B树,现在要从树种查找字母R(包含n[x]个关键字的内结点x,x有n[x]+1]个子女(也就是说,一个内结点x若含有n[x]个关键字,那么x将含有n[x]+1个子女)。所有的叶结点都处于相同的深度,带阴影的结点为查找字母R时要检查的结点):
相信,从上图你能轻易的看到,一个内结点x若含有n[x]个关键字,那么x将含有n[x]+1个子女。如含有2个关键字D H的内结点有3个子女,而含有3个关键字Q T X的内结点有4个子女。
B 树又叫平衡多路查找树。一棵m阶的B 树 (m叉树)的特性如下:
-
树中每个结点最多含有m个孩子(m>=2);
-
除根结点和叶子结点外,其它每个结点至少有[ceil(m / 2)]个孩子(其中ceil(x)是一个取上限的函数);
-
若根结点不是叶子结点,则至少有2个孩子(特殊情况:没有孩子的根结点,即根结点为叶子结点,整棵树只有一个根节点);
-
所有叶子结点都出现在同一层,叶子结点不包含任何关键字信息(可以看做是外部接点或查询失败的接点,实际上这些结点不存在,指向这些结点的指针都为null);(读者反馈@冷岳:这里有错,叶子节点只是没有孩子和指向孩子的指针,这些节点也存在,也有元素。@JULY:其实,关键是把什么当做叶子结点,因为如红黑树中,每一个NULL指针即当做叶子结点,只是没画出来而已)。
-
每个非终端结点中包含有n个关键字信息: (n,P0,K1,P1,K2,P2,......,Kn,Pn)。其中:
a) Ki (i=1...n)为关键字,且关键字按顺序升序排序K(i-1)< Ki。
b) Pi为指向子树根的接点,且指针P(i-1)指向子树种所有结点的关键字均小于Ki,但都大于K(i-1)。
c) 关键字的个数n必须满足: [ceil(m / 2)-1]<= n <= m-1。如下图所示:
针对上面第5点,再阐述下:B树中每一个结点能包含的关键字(如之前上面的D H和Q T X)数有一个上界和下界。这个下界可以用一个称作B树的最小度数(算法导论中文版上译作度数,最小度数即内节点中节点最小孩子数目)t(t>=2)表示。
-
每个非根的结点必须至少含有t-1个关键字。每个非根的内结点至少有t个子女。如果树是非空的,则根结点至少包含一个关键字;
-
每个结点可包含之多2t-1个关键字。所以一个内结点至多可有2t个子女。如果一个结点恰好有2t-1个关键字,我们就说这个结点是满的(而稍后介绍的B*树作为B树的一种常用变形,B*树中要求每个内结点至少为2/3满,而不是像这里的B树所要求的至少半满);
-
当关键字数t=2(t=2的意思是,t
min=2,t可以>=2)时的B树是最简单的
(有很多人会因此误认为B树就是二叉查找树,但二叉查找树就是二叉查找树,B树就是B树,B树的真正最准确的定义为:一棵含有t(t>=2)个关键字的平衡多路查找树)。每个内结点可能因此而含有2个、3个或4个子女,亦即一棵2-3-4树,然而在实际中,通常采用大得多的t值。
B树中的每个结点根据实际情况可以包含大量的关键字信息和分支(当然是不能超过磁盘块的大小,根据磁盘驱动(disk drives)的不同,一般块的大小在1k~4k左右);这样树的深度降低了,这就意味着查找一个元素只要很少结点从外存磁盘中读入内存,很快访问到要查找的数据。
C++代码:
#pragma once
//
//
B树(B-Tree)
// By Rendy
// C++版(vs2008编译通过)
// 按照《算法导论》的算法实现,节点没有父指针,不需要回溯
///
template <typename T>
class CBTree
{
//最小度数M=2
static const int M = 2;
static const int KEY_MAX = 2*M-1;
static const int KEY_MIN = M-1;
static const int CHILD_MAX = 2*M;
static const int CHILD_MIN = M;
typedef struct Node{
int
n;
//关键码的个数
T
k[KEY_MAX];
//关键码
Node*
c[CHILD_MAX];
//子节点指针
bool
leaf;
//是否是叶子节点
} NODE, *PNODE;
public:
CBTree(void)
{
m_pRoot = NULL;
}
~CBTree(void)
{
DestoryTree(m_pRoot);
m_pRoot = NULL;
}
public:
bool Insert(T key)
{
//已经插入了,直接返回成功
if (Search(m_pRoot, key))
return true;
if (NULL == m_pRoot)
m_pRoot = CreateEmptyTree();
if (m_pRoot->n == KEY_MAX)
{
PNODE pNew = NewNode();
pNew->n = 0;
pNew->leaf = false;
pNew->c[0] = m_pRoot;
SplitChild(pNew, 0);
InsertNoneFull(pNew, key);
m_pRoot = pNew;
}
else
{
InsertNoneFull(m_pRoot, key);
}
return true;
}
bool Delete(T key)
{
if (NULL == m_pRoot)
return true;
//不存在,不用删了
if (!Search(m_pRoot, key))
return true;
if (m_pRoot->n==1)
//特殊情况处理
{
if (m_pRoot->leaf)
{
DestoryTree(m_pRoot);
m_pRoot = NULL;
return true;
}
else
{
PNODE pC1 = m_pRoot->c[0];
PNODE pC2 = m_pRoot->c[1];
if (pC1->n==KEY_MIN && pC2->n==KEY_MIN)
{
MergeChild(m_pRoot,0);
DeleteNode(m_pRoot);
m_pRoot = pC1;
}
}
}
DeleteNoneEmpty(m_pRoot, key);
return true;
}
bool Find(T key)
{
return Search(m_pRoot, key);
}
protected:
bool Search(PNODE pNode, T key)
{
if (pNode == NULL)
return false;
int i = 0;
while(i<pNode->n && key>pNode->k[i])
{
i++;
}
if (i<pNode->n && (key==pNode->k[i]))
{
return true;
}
else if (pNode->leaf)
{
return false;
}
else
{
return Search(pNode->c[i], key);
}
}
//将pParent的第nChildIndex个孩子分裂
bool SplitChild(PNODE pParent, int nChildIndex)
{
PNODE pChild = pParent->c[nChildIndex];
//为新分裂出的节点分配空间
PNODE pChild2 = NewNode();
if (NULL == pChild2)
return false;
//与被分裂点同级
pChild2->leaf = pChild->leaf;
//设置分裂节点key数
pChild2->n = KEY_MIN;
//复制数据
for (int i=0;i<KEY_MIN;i++)
{
pChild2->k[i] = pChild->k[i+M];
}
//如果不是叶节点,复制指针
if (!pChild->leaf)
{
for (int i=0;i<CHILD_MIN;i++)
{
pChild2->c[i] = pChild->c[i+M];
}
}
pChild->n = KEY_MIN;
//将中间数作为索引插入到双亲节点中
//插入点后面的关键字和指针都往后移动一个位置
for (int i=pParent->n; i>nChildIndex; i--)
{
pParent->k[i] = pParent->k[i-1];
pParent->c[i+1] = pParent->c[i];
}
pParent->k[nChildIndex] = pChild->k[M-1];
pParent->n++;
pParent->c[nChildIndex+1] = pChild2;
return true;
}
void InsertNoneFull(PNODE pNode , T key)
{
int i = pNode->n;
PNODE pChild = NULL;
if (pNode->leaf)
{
while (i>0 && (key < pNode->k[i-1]))
{
pNode->k[i] = pNode->k[i-1];
i--;
}
pNode->k[i] = key;
pNode->n++;
}
else
{
while(i>0 && key < pNode->k[i-1])
{
i--;
}
pChild = pNode->c[i];
//如果子节点key满了
if (pChild->n == KEY_MAX)
{
//分裂子节点
SplitChild(pNode, i);
//如果大于分裂后提上来的key,插入分裂后的右边,否则左边
if (key > pNode->k[i])
{
i++;
pChild = pNode->c[i];//分裂后的右部分
}
}
InsertNoneFull(pChild, key);
}
}
void MergeChild(PNODE pParent, int index)
{
PNODE pChild1 = pParent->c[index];
PNODE pChild2 = pParent->c[index+1];
//将c2数据合并到c1
pChild1->n = KEY_MAX;
pChild1->k[M-1] = pParent->k[index];
for (int i=M ; i<KEY_MAX ; i++)
{
pChild1->k[i] = pChild2->k[i-M];
}
if (!pChild1->leaf)
{
for (int i=M ; i<CHILD_MAX ; i++)
{
pChild1->c[i] = pChild2->c[i-M];
}
}
//父节点删除index的key,index后的往前移动一位
pParent->n--;
for (int i=index ; i<(pParent->n) ; i++)
{
pParent->k[i] = pParent->k[i+1];
pParent->c[i+1] = pParent->c[i+2];
}
//删除child2
DeleteNode(pChild2);
}
void DeleteNoneEmpty(PNODE pNode, T key)
{
int i = pNode->n;
if (pNode->leaf)
{
while (i>0 && (key < pNode->k[i-1])) { i--; }
if (key == pNode->k[i-1])
//1
{
for(int j=i ; j<pNode->n ; j++)
{
pNode->k[j-1] = pNode->k[j];
}
pNode->n--;
}
}
else
{
while (i>0 && (key < pNode->k[i-1])) { i--; }
if (i>0 && key==pNode->k[i-1])
//2
{
PNODE pChild1 = pNode->c[i-1];
PNODE pChild2 = pNode->c[i];
//左Child多,从左Child移一个
if (pChild1->n > KEY_MIN)
//2a
{
T preKey = GetPredecessor(pChild1);
DeleteNoneEmpty(pChild1, preKey);
pNode->k[i-1] = preKey;
}
//右Child多,从右Child移一个
else if (pChild2->n > KEY_MIN)
//2b
{
T sucKey = GetSuccessor(pChild2);
DeleteNoneEmpty(pChild2, sucKey);
pNode->k[i-1] = sucKey;
}
//左右都是M-1,合并
else
//2c
{
MergeChild(pNode, i-1);
DeleteNoneEmpty(pChild1, key);
}
}
else
//3
{
//含有key的子树
PNODE pSub = pNode->c[i];
if (pSub->n == KEY_MIN)
{
PNODE pLeft = (i>0 ? pNode->c[i-1] : NULL);
PNODE pRight = (i<pNode->n ? pNode->c[i+1] : NULL);
if (pLeft && pLeft->n > KEY_MIN)
//3a1
{
GetFromLeftBrother(pNode, i-1, pSub, pLeft);
}
else if (pRight && pRight->n > KEY_MIN)
//3a2
{
GetFromRightBrother(pNode, i, pSub, pRight);
}
else if (pLeft && pLeft->n == KEY_MIN)
//3b1
{
MergeChild(pNode, i-1);
pSub = pLeft;
}
else if (pRight && pRight->n == KEY_MIN)
//3b2
{
MergeChild(pNode, i);
}
}
DeleteNoneEmpty(pSub, key);
}
}
}
T GetPredecessor(PNODE pNode)
{
while(!pNode->leaf)
{
pNode = pNode->c[pNode->n];
}
return pNode->k[pNode->n-1];
}
T GetSuccessor(PNODE pNode)
{
while(!pNode->leaf)
{
pNode = pNode->c[0];
}
return pNode->k[0];
}
//把left最大key给pParent(index),把pParent(index)给pNode
void GetFromLeftBrother(PNODE pParent, int index, PNODE pNode, PNODE pLeft)
{
for(int i=pNode->n ; i>0 ; i--)
{
pNode->k[i] = pNode->k[i-1];
}
pNode->k[0] = pParent->k[index];
if (!pNode->leaf)
{
for(int i=pNode->n ; i>=0 ; i--)
{
pNode->c[i+1] = pNode->c[i];
}
pNode->c[0] = pLeft->c[pLeft->n];
}
pNode->n++;
//parent
pParent->k[index] = pLeft->k[pLeft->n-1];
//left
pLeft->n--;
}
//把right最小key给pParent(index),把pParent(index)给pNode
void GetFromRightBrother(PNODE pParent, int index, PNODE pNode, PNODE pRight)
{
pNode->k[pNode->n] = pParent->k[index];
if (!pNode->leaf)
{
pNode->c[pNode->n+1] = pRight->c[0];
}
pNode->n++;
//parent
pParent->k[index] = pRight->k[0];
//right
for(int i=1 ; i<pRight->n ; i++)
{
pRight->k[i-1] = pRight->k[i];
}
if (!pRight->leaf)
{
for(int i=0 ; i<pRight->n ; i++)
{
pRight->c[i] = pRight->c[i+1];
}
}
pRight->n--;
}
//
PNODE NewNode()
{
PNODE pNode = new NODE;
if (NULL == pNode)
return pNode;
//init key count
pNode->n = 0;
//init children
for (int i=0 ; i<CHILD_MAX ; i++)
pNode->c[i] = NULL;
return pNode;
}
void DeleteNode(PNODE &pNode)
{
if (pNode)
{
delete pNode;
pNode = NULL;
}
}
//
PNODE CreateEmptyTree (){
PNODE pRet = NewNode();
pRet->n = 0;
pRet->leaf = true;
return pRet;
}
void DestoryTree(PNODE pNode)
{
if (pNode)
{
for(int i=0 ; i<=pNode->n ; i++)
{
if (pNode->leaf)
break;
if (pNode->c[i])
DestoryTree(pNode->c[i]);
}
DeleteNode(pNode);
}
}
public:
void Display()
{
Display(m_pRoot, 2);
printf ( "\n" );
}
private:
void Display(PNODE t, int level)
{
int i=0;
if (t)
{
while(i<level-2)
{
printf(" ");
i++;
}
printf ( "[ " );
for (int i=0; i<t->n; i++)
{
printf ( "%d " , t->k[i]);
}
printf ( "]\n" );
for (int i=0; i<=t->n; i++)
{
Display(t->c[i], level+3);
}
}
}
private:
PNODE m_pRoot;
};