标签:b树 b+树 红黑树 数据结构
B-树
B-tree树即B树,B即Balanced,平衡的意思,B-树又称为多路平衡查找树。因为B树的原英文名称为B-tree,而国内很多人喜欢把B-tree译作B-树,其实,这是个非常不好的直译,很容易让人产生误解。如人们可能会以为B-树是一种树,而B树又是另一种树。而事实上是,B-tree就是指的B树。
一、定义
B-树是一种多路搜索树(并不一定是二叉的)
1970年,R.Bayer和E.mccreight提出了一种适用于外查找的树,它是一种平衡的多叉树,称为B树(或B-树、B_树)。
一棵m阶B树(balanced tree of order m)是一棵平衡的m路搜索树。它或者是空树,或者是满足下列性质的树:
1、根结点至少有两个子女;
2、每个非根节点所包含的关键字个数 j 满足:┌m/2┐ - 1 <= j <= m - 1;
3、除根结点以外的所有结点(不包括叶子结点)的度数正好是关键字总数加1,故内部子树个数 k 满足:┌m/2┐ <= k <= m ;
4、所有的叶子结点都位于同一层。
在B-树中,每个结点中关键字从小到大排列,并且当该结点的孩子是非叶子结点时,该k-1个关键字正好是k个孩子包含的关键字的值域的分划。
因为叶子结点不包含关键字,所以可以把叶子结点看成在树里实际上并不存在外部结点,指向这些外部结点的指针为空,叶子结点的数目正好等于树中所包含的关键字总个数加1。
B-树中的一个包含n个关键字,n+1个指针的结点的一般形式为: (n,P0,K1,P1,K2,P2,…,Kn,Pn)
其中,Ki为关键字,K1<K2<…<Kn, Pi 是指向包括Ki到Ki+1之间的关键字的子树的指针。
二、概念
-
3.除根结点以外的非叶子结点的儿子数为[M/2, M];
4.每个结点存放至少M/2-1(取上整)和至多M-1个
关键字;(至少2个关键字,根
节点至少一个关键字);
5.非叶子结点的关键字个数=指向儿子的
指针个数-1;
6.非叶子结点的关键字:K[1], K[2], …, K[m-1],m<M+1;且K[i]< K[i+1] ;
7.非叶子结点的指针:P[1], P[2], …, P[m];其中P[1]指向
关键字小于K[1]的子树,P[m]指向关键字大于K[m-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树;
如:(M=3)
M=3的B-树
B-树的搜索,从
根结点开始,对结点内的关键字(有序)序列进行
二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子
指针为空,或已经是
叶子结点。
三、特性
2.任何一个关键字出现且只出现在一个结点中;
5.自动层次控制;
由于限制了除根结点以外的非叶子结点,至少含有M/2个儿子,确保了结点的至少利用率,其最底搜索性能为:
其中,M为设定的非叶子结点最多子树个数,N为关键字总数;
所以B-树的性能总是等价于二分查找(与M值无关),也就没有B树平衡的问题;
由于M/2的限制,在插入结点时,如果结点已满,需要将结点分裂为两个各占M/2的结点;删除结点时,需将两个不足M/2的兄弟节点合并.
(1)根结点只有1个,关键字字数的范围[1,m-1],分支数量范围[2,m];
(2)除根以外的非叶结点,每个结点包含分支数范围[[m/2],m],即关键字字数的范围是[[m/2]-1,m-1],其中[m/2]表示取大于m/2的最小整数;
(3)非叶结点是由叶结点分裂而来的,所以叶结点关键字个数也满足[[m/2]-1,m-1];
四、B树查找的算法思想
1、B-树的查找
B-树的查找过程:根据给定值查找结点和在结点的关键字中进行查找交叉进行。首先从根结点开始重复如下过程:
若比结点的第一个关键字小,则查找在该结点第一个指针指向的结点进行;若等于结点中某个关键字,则查找成功;若在两个关键字之间,则查找在它们之间的指针指向的结点进行;若比该结点所有关键字大,则查找在该结点最后一个指针指向的结点进行;若查找已经到达某个叶结点,则说明给定值对应的数据记录不存在,查找失败。
2. B-树的插入
插入的过程分两步完成:
(1)利用前述的B-树的查找算法查找关键字的插入位置。若找到,则说明该关键字已经存在,直接返回。否则查找操作必失败于某个最低层的非终端结点上。
(2)判断该结点是否还有空位置。即判断该结点的关键字总数是否满足n<=m-1。若满足,则说明该结点还有空位置,直接把关键字k插入到该结点的合适位置上。若不满足,说明该结点己没有空位置,需要把结点分裂成两个。
分裂的方法是:生成一新结点。把原结点上的关键字和k按升序排序后,从中间位置把关键字(不包括中间位置的关键字)分成两部分。左部分所含关键字放在旧结点中,右部分所含关键字放在新结点中,中间位置的关键字连同新结点的存储位置插入到父结点中。如果父结点的关键字个数也超过(m-1),则要再分裂,再往上插。直至这个过程传到根结点为止。
3、B-树的删除
在B-树上删除关键字k的过程分两步完成:
(1)利用前述的B-树的查找算法找出该关键字所在的结点。然后根据
k所在结点是否为叶子结点有不同的处理方法。
(2)若该结点为非叶结点,且被删关键字为该结点中第i个关键字key[i],则可从指针son[i]所指的子树中找出最小关键字Y,代替key[i]的位置,然后在叶结点中删去Y。
因此,把在非叶结点删除关键字k的问题就变成了删除叶子结点中的关键字的问题了。
在B-树叶结点上删除一个关键字的方法是
首先将要删除的关键字k直接从该叶子结点中删除。然后根据不同情况分别作相应的处理,共有三种可能情况:
(1)如果被删关键字所在结点的原关键字个数n>=ceil(m/2),说明删去该关键字后该结点仍满足B-树的定义。这种情况最为简单,只需从该结点中直接删去关键字即可。
(2)如果被删关键字所在结点的关键字个数n等于ceil(m/2)-1,说明删去该关键字后该结点将不满足B-树的定义,需要调整。
调整过程为:如果其左右兄弟结点中有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目大于ceil(m/2)-1。则可将右(左)兄弟结点中最小(大)关键字上移至双亲结点。而将双亲结点中小(大)于该上移关键字的关键字下移至被删关键字所在结点中。
(3)如果左右兄弟结点中没有“多余”的关键字,即与该结点相邻的右(左)兄弟结点中的关键字数目均等于ceil(m/2)-1。这种情况比较复杂。需把要删除关键字的结点与其左(或右)兄弟结点以及双亲结点中分割二者的关键字合并成一个结点,即在删除关键字后,该结点中剩余的关键字加指针,加上双亲结点中的关键字Ki一起,合并到Ai(是双亲结点指向该删除关键字结点的左(右)兄弟结点的指针)所指的兄弟结点中去。如果因此使双亲结点中关键字个数小于ceil(m/2)-1,则对此双亲结点做同样处理。以致于可能直到对根结点做这样的处理而使整个树减少一层。
总之,设所删关键字为非终端结点中的Ki,则可以指针Ai所指子树中的最小关键字Y代替Ki,然后在相应结点中删除Y。对任意关键字的删除都可以转化为对最下层关键字的删除。
如图示:
a、被删关键字Ki所在结点的关键字数目不小于ceil(m/2),则只需从结点中删除Ki和相应指针Ai,树的其它部分不变。
b、被删关键字Ki所在结点的关键字数目等于ceil(m/2)-1,则需调整。调整过程如上面所述。
c、被删关键字Ki所在结点和其相邻兄弟结点中的的关键字数目均等于ceil(m/2)-1,假设该结点有右兄弟,且其右兄弟结点地址由其双亲结点指针Ai所指。则在删除关键字之后,它所在结点的剩余关键字和指针,加上双亲结点中的关键字Ki一起,合并到Ai所指兄弟结点中(若无右兄弟,则合并到左兄弟结点中)。如果因此使双亲结点中的关键字数目少于ceil(m/2)-1,则依次类推。
五、B-树的C语言描述
1、存储结构
2、插入
3、查找
六、B-树的C语言实现
#include "stdio.h"
#include "stdlib.h"
#include "math.h"
#define OK 1
#define ERROR -1
#define m 3 //3阶树
#define N 16 //数据元素个数
#define MAX 5 //字符串最大长度+1
typedef int KeyType;
struct Others //记录的其它部分
{
char info[MAX];
};
struct Record
{
KeyType key; //关键字
Others others; //其它部分
};
typedef struct BTNode
{
int keynum; //结点中关键字个数
BTNode *parent;//指向双亲节点
struct Node //结点向量类型
{
KeyType key; //关键字向量
BTNode *ptr;//子树指针向量
Record *recptr; //记录向量指针
}node[m+1]; //key,recptr的0号单元未用
}BTNode,*BTree;
struct Result //B树的查找结果类型
{
BTNode *pt; //指向找到的结点
int i; //在节点中关键字序号,1...m
int tag; //1表示查找成功,0表示查找失败。
};
int InitDSTable(BTree &DT)
{
DT=NULL;
return OK;
}//InitDSTable
void DestroyDSTable(BTree &DT)
{
int i;
if(DT) //非空树
{
for(i=0;i<=DT->keynum;i++)
DestroyDSTable(DT->node[i].ptr);
free(DT);
DT=NULL;
}//if
}//DestroyDSTable
int Search(BTree p,KeyType K)
{//在p->node[1...keytype].key中查找i,使得p->node[i].key<=K<
//p->node[i+1].key
int i=0,j;
for(j=1;j<=p->keynum;j++)
if(p->node[j].key<=K)
i=j;
return i;
}//Search
void Insert(BTree &q,int i,Record *r,BTree ap)
{//将r->key、r和ap分别插入到q->key[i+1]、
//q->recptr[ i+1]和q->ptr[i+1]中
int j;
for(j=q->keynum;j>i;j--) //空出q->node[i+1]
q->node[j+1]=q->node[j];
q->node[i+1].key=r->key;
q->node[i+1].ptr=ap; //前加入的结点,还没有儿子结点
q->node[i+1].recptr=r;
q->keynum++;
}//Insert
void NewRoot(BTree &T,Record *r,BTree ap)
{// 生成含信息(T,r,ap)的新的根结点*T,原T和ap为子树指针
BTree p;
p=(BTree)malloc(sizeof(BTNode));
p->node[0].ptr=T;
T=p;
if(T->node[0].ptr)
T->node[0].ptr->parent=T;
T->parent=NULL;
T->keynum=1;
T->node[1].key=r->key;
T->node[1].recptr=r;
T->node[1].ptr=ap;
if(T->node[1].ptr)
T->node[1].ptr->parent=T;
}//NewRoot
void split(BTree &q,BTree &ap)
{// 将结点q分裂成两个结点,前一半保留,后一半移入新生结点ap
int i,s=(m+1)/2;
ap=(BTree)malloc(sizeof(BTNode));//生成新结点ap
ap->node[0].ptr=q->node[s].ptr;//原来结点中间位置关键字相应指针指向的子树放到
//新生成结点的0棵子树中去
for(i=s+1;i<=m;i++) //后一半移入ap
{
ap->node[i-s]=q->node[i];
if(ap->node[i-s].ptr)
ap->node[i-s].ptr->parent=ap;
}//for
ap->keynum=m-s;
ap->parent=q->parent;
q->keynum=s-1; // q的前一半保留,修改keynum
}//split
void InsertBTree(BTree &T,Record *r,BTree q,int i)
{//在m阶B树T上结点*q的key[i]与key[i+1]之间插入关键字K的指针r。若引起
// 结点过大,则沿双亲链进行必要的结点分裂调整,使T仍是m阶B树。
BTree ap=NULL;
int finished=false;
int s;
Record *rx;
rx=r;
while(q&&!finished)
{
Insert(q,i,rx,ap);//将r->key、r和ap分别插入到q->key[i+1]、
//q->recptr[i+1]和q->ptr[i+1]中
if(q->keynum<m)
finished=true;
else
{//分裂结点*q
s=(m+1)/2;
rx=q->node[s].recptr;
split(q,ap);//将q->key[s+1..m],q->ptr[s..m]和q->recptr[s+1..m]
//移入新结点*ap
q=q->parent;
if(q)
i=Search(q,rx->key);//在双亲结点*q中查找rx->key的插入位置
}//else
}//while
if(!finished) //T是空树(参数q初值为NULL)或根结点已分裂为
//结点*q和*ap
NewRoot(T,rx,ap);
}//InsertBTree
Result SearchBTree(BTree T,KeyType K)
{// 在m阶B树T上查找关键字K,返回结果(pt,i,tag)。若查找成功,则特征值
// tag=1,指针pt所指结点中第i个关键字等于K;否则特征值tag=0,等于K的
// 关键字应插入在指针Pt所指结点中第i和第i+1个关键字之间。
BTree p=T,q=NULL; //初始化,p指向待查结点,q指向p的双亲
int found=false;
int i=0;
Result r;
while(p&&!found)
{
i=Search(p,K);//p->node[i].key≤K<p->node[i+1].key
if(i>0&&p->node[i].key==K)
found=true;
else
{
q=p;
p=p->node[i].ptr;//在子树中继续查找
}//else
}//while
r.i=i;
if(found)
{
r.pt=p;
r.tag=1;
}//if
else
{
r.pt=q;
r.tag=0;
}//else
return r;
}//SearchBTree
void print(BTNode c,int i) // TraverseDSTable()调用的函数
{
printf("(%d,%s)",c.node[i].key,c.node[i].recptr->others.info);
}//print
void TraverseDSTable(BTree DT,void(*Visit)(BTNode,int))
{// 初始条件: 动态查找表DT存在,Visit是对结点操作的应用函数
// 操作结果: 按关键字的顺序对DT的每个结点调用函数Visit()一次且至多一次
int i;
if(DT) //非空树
{
if(DT->node[0].ptr) // 有第0棵子树
TraverseDSTable(DT->node[0].ptr,Visit);
for(i=1;i<=DT->keynum;i++)
{
Visit(*DT,i);
if(DT->node[i].ptr) // 有第i棵子树
TraverseDSTable(DT->node[i].ptr,Visit);
}//for
}//if
}//TraverseDSTable
void InputBR(BTree &t,Record r[])
{
Result s;
for(int i=0;i<N;i++)
{
s=SearchBTree(t,r[i].key);
if(!s.tag)
InsertBTree(t,&r[i],s.pt,s.i);
}
}//InputBR
void UserSearch(BTree t)
{
int i;
Result s;
printf("\n请输入待查找记录的关键字: ");
scanf("%d",&i);
s=SearchBTree(t,i);
if(s.tag)
print(*(s.pt),s.i);
else
printf("没找到");
printf("\n");
}//UserSearch
void DeleteIt(BTree t,BTNode *dnode,int id)
{
if(dnode->keynum>=ceil(m/2))
{
dnode->keynum--;
dnode->node[id].ptr=NULL;
}//if被删关键字Ki所在结点的关键字数目不小于ceil(m/2),则只需从结点中删除Ki和相应指针Ai,树的其它部分不变。
else if((dnode->keynum==(ceil(m/2)-1))&&((id+1)<(m-1))&&dnode->parent->node[id+1].ptr->keynum>(ceil(m/2)-1))
{
for(int i=1;i<m&&dnode->parent->node[i].key < dnode->parent->node[id+1].ptr->node[1].key;i++)
dnode->node[i].key=dnode->parent->node[i].key;
dnode->parent->node[1].key=dnode->parent->node[id+1].ptr->node[1].key;
(dnode->parent->node[id+1].ptr->keynum)--;
}//else if 被删关键字Ki所在结点的关键字数目等于ceil(m/2)-1,则需调整。本次为与右兄弟调整
else if((dnode->keynum==(ceil(m/2)-1))&&((id-1)>0 )&&dnode->parent->node[id-1].ptr->keynum>(ceil(m/2)-1))
{
for(int i=1;i<m&&dnode->parent->node[i].key > dnode->parent->node[id-1].ptr->node[dnode->parent->node[id-1].ptr->keynum].key;i++)
dnode->node[i].key=dnode->parent->node[i].key;
dnode->parent->node[1].key=dnode->parent->node[id-1].ptr->node[dnode->parent->node[id-1].ptr->keynum].key;
(dnode->parent->node[id-1].ptr->keynum)--;
}//2-else if被删关键字Ki所在结点的关键字数目等于ceil(m/2)-1,则需调整。本次为与左兄弟调整
else if((dnode->keynum==(ceil(m/2)-1))&&((id+1)<(m-1))&&dnode->parent->node[id+1].ptr->keynum==(ceil(m/2)-1))
{
do
{
BTree tmp;
tmp=dnode;
dnode->parent->node[id+1].ptr->node[2]=dnode->parent->node[id+1].ptr->node[1];
dnode->parent->node[id+1].ptr->node[1]=dnode->parent->node[1];
dnode->parent->node[id+1].ptr->keynum++;
dnode->parent->node[id+1].ptr->node[0].ptr=dnode->node[1].ptr;
dnode->parent->keynum--;
dnode->parent->node[id].ptr=NULL;
tmp=dnode;
if(dnode->parent->keynum>=(ceil(m/2)-1))
dnode->parent->node[1]=dnode->parent->node[2];
dnode=dnode->parent;
free(tmp);
}while(dnode->keynum<(ceil(m/2)-1)); //双亲中keynum<
}//3-else if被删关键字Ki所在结点和其相邻兄弟结点中的的关键字数目均等于ceil(m/2)-1,本次假设右兄弟存在
else if((dnode->keynum==(ceil(m/2)-1))&&(id-1)>0 &&dnode->parent->node[id-1].ptr->keynum==(ceil(m/2)-1))
{
do
{
BTree tmp;
tmp=dnode;
dnode->parent->node[id-1].ptr->node[2]=dnode->parent->node[id-1].ptr->node[1];
dnode->parent->node[id-1].ptr->node[1]=dnode->parent->node[1];
dnode->parent->node[id-1].ptr->keynum++;
dnode->parent->node[id-1].ptr->node[0].ptr=dnode->node[1].ptr;
dnode->parent->keynum--;
dnode->parent->node[id].ptr=NULL;
tmp=dnode;
if(dnode->parent->keynum>=(ceil(m/2)-1))
dnode->parent->node[1]=dnode->parent->node[2];
dnode=dnode->parent;
free(tmp);
}while(dnode->keynum<(ceil(m/2)-1)); //双亲中keynum<
}//4-else if被删关键字Ki所在结点和其相邻兄弟结点中的的关键字数目均等于ceil(m/2)-1,本次假设左兄弟存在
else printf("Error!"); //出现异常
}//DeleteIt
void UserDelete(BTree t)
{
KeyType date;
Result s;
printf("Please input the date you want to delete:\n");
scanf("%d",&date);
s=SearchBTree(t,date);
if(!s.tag) printf("Search failed,no such date\n");
else DeleteIt(t,s.pt,s.i);
}//UserDelete
int main()
{
Record r[N]={{24,"1"},{45,"2"},{53,"3"},{12,"4"},{37,"5"},
{50,"6"},{61,"7"},{90,"8"},{100,"9"},{70,"10"},
{3,"11"},{30,"12"},{26,"13"},{85,"14"},{3,"15"},
{7,"16"}};
BTree t;
InitDSTable(t);
InputBR(t,r);
printf("按关键字的顺序遍历B_树:\n");
TraverseDSTable(t,print);
UserSearch(t);
UserDelete(t);
TraverseDSTable(t,print);
DestroyDSTable(t);
return 1;
}
七、复杂度分析
B-树查找包含两种基本动作:
●在B-树上查找结点
●在结点中找关键字
前一操作在磁盘上进行,后一操作在内存进行。因此查找效率主要由前一操作决定。在磁盘上查找的次数取决于关键字结点在B-树上的层次数。
定理:若n≥1,m≥3,则对任意一棵具有n个关键字的m阶B-树,其树高度h至多为logt((n+1)/2)+1,t=
ceil(m/2)。也就是说根结点到关键字所在结点的路径上涉及的结点数不超过logt((n+1)/2)+1。推理如下:
B+树
B+ 树是一种树数据结构,是一个n叉树,每个节点通常有多个孩子,一颗B+树包含根节点、内部节点和叶子节点。根节点可能是一个叶子节点,也可能是一个包含两个或两个以上孩子节点的节点。
一、定义
B+树是应文件系统所需而出的一种B-树的变型树。一棵m阶的B+树和m阶的B-树的差异在于:
1.有n棵子树的结点中含有n个关键字,每个关键字不保存数据,只用来索引,所有数据都保存在叶子节点。
2.所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。
3.所有的非终端结点可以看成是索引部分,结点中仅含其子树(根结点)中的最大(或最小)关键字。
通常在B+树上有两个头指针,一个指向根结点,一个指向关键字最小的叶子结点。
二、B+树的查找、插入和删除
B+树的查找
对B+树可以进行两种查找运算:
2.从根结点开始,进行随机查找。
在查找时,若非终端结点上的关键值等于给定值,并不终止,而是继续向下直到叶子结点。因此,在B+树中,不管查找成功与否,每次查找都是走了一条从根到叶子结点的路径。其余同B-树的查找类似。
以下是从根节点查找叶子节点k的伪代码:
1
2
3
4
5
6
7
8
9
10
|
Function: search (k)
return tree_search (k, root); Function: tree_search (k, node)
if node is a leaf then return node;
switch k do case k < k_0
return tree_search(k, p_0);
case k_i ≤ k < k_{i+ 1 }
return tree_search(k, p_{i+ 1 });
case k_d ≤ k
return tree_search(k, p_{d+ 1 });
|
B+树的插入
m阶B树的插入操作在叶子结点上进行,假设要插入关键值a,找到叶子结点后插入a,做如下算法判别:
①如果当前结点是根结点并且插入后结点关键字数目小于等于m,则算法结束;
②如果当前结点是非根结点并且插入后结点关键字数目小于等于m,则判断若a是新索引值时转步骤④后结束,若a不是新索引值则直接结束;
③如果插入后关键字数目大于m(阶数),则结点先分裂成两个结点X和Y,并且他们各自所含的关键字个数分别为:u=大于(m+1)/2的最小整数,v=小于(m+1)/2的最大整数;
由于索引值位于结点的最左端或者最右端,不妨假设索引值位于结点最右端,有如下操作:
如果当前分裂成的X和Y结点原来所属的结点是根结点,则从X和Y中取出索引的关键字,将这两个关键字组成新的根结点,并且这个根结点指向X和Y,算法结束;
如果当前分裂成的X和Y结点原来所属的结点是非根结点,依据假设条件判断,如果a成为Y的新索引值,则转步骤④得到Y的双亲结点P,如果a不是Y结点的新索引值,则求出X和Y结点的双亲结点P;然后提取X结点中的新索引值a’,在P中插入关键字a’,从P开始,继续进行插入算法;
④提取结点原来的索引值b,自顶向下,先判断根是否含有b,是则需要先将b替换为a,然后从根结点开始,记录结点地址P,判断P的孩子是否含有索引值b而不含有索引值a,是则先将孩子结点中的b替换为a,然后将P的孩子的地址赋值给P,继续搜索,直到发现P的孩子中已经含有a值时,停止搜索,返回地址P。
B+树的删除
B+树的删除也仅在叶子结点进行,当叶子结点中的最大关键字被删除时,其在非终端结点中的值可以作为一个“分界关键字”存在。若因删除而使结点中关键字的个数少于m/2 (m/2结果取上界,如5/2结果为3)时,其和兄弟结点的合并过程亦和B-树类似。
三、B+树与B树的区别
一棵m阶的B+树和m阶的B树的异同点在于:
四、B+树与操作系统的文件索引和数据库索引
为什么说B+树比B 树更适合实际应用中操作系统的文件索引和数据库索引?
B+树的磁盘读写代价更低
-
B+树的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。
举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶
B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。
-
B+树的查询效率更加稳定
由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。
红黑树
红黑树(Red Black Tree) 是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。
它是在1972年由Rudolf Bayer发明的,当时被称为平衡二叉B树(symmetric binary B-trees)。后来,在1978年被 Leo J. Guibas 和 Robert Sedgewick 修改为如今的“红黑树”。
红黑树和AVL树类似,都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n 是树中元素的数目。
一、性质
红黑树是每个节点都带有颜色属性的二叉查找树,颜色或红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:
性质1. 节点是红色或黑色。
性质2. 根节点是黑色。
性质3 每个叶节点(NIL节点,空节点)是黑色的。
性质4 每个红色节点的两个子节点都是黑色。(从每个叶子到根的所有路径上不能有两个连续的红色节点)
性质5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。
这些约束强制了红黑树的关键性质: 从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。
要知道为什么这些特性确保了这个结果,注意到性质4导致了路径不能有两个毗连的红色节点就足够了。最短的可能路径都是黑色节点,最长的可能路径有交替的红色和黑色节点。因为根据性质5所有最长的路径都有相同数目的黑色节点,这就表明了没有路径能多于任何其他路径的两倍长。
在很多树数据结构的表示中,一个节点有可能只有一个子节点,而叶子节点不包含数据。用这种范例表示红黑树是可能的,但是这会改变一些属性并使算法复杂。为此,本ul文中我们使用
"nil 叶子" 或"空(nl)叶子",如上图所示,它不包含数据而只充当树在此结束的指示。这些节点在绘图中经常被省略,导致了这些树好象同上述原则相矛盾,而实际上不是这样。与此有关的结论是所有节点都有两个子节点,尽管其中的一个或两个可能是空叶子。
注意:由于红黑树也是二叉查找树,它们当中每一个节点的比较值都必须大于或等于在它的左子树中的所有节点,并且小于或等于在它的右子树中的所有节点。这确保红黑树运作时能够快速的在树中查找给定的值。
二. 数据结构定义
RBT数据结构在基本二叉树数据结构之上增加一个color和parent,color用于保存节点颜色,parent指向父节点。
- #define COLOR_RED 0
- #define COLOR_BLACK 1
-
- typedef int keyType;
-
-
- struct BinaryTreeNode {
- keyType key;
- int color;
- BinaryTreeNode* parent;
- BinaryTreeNode* left;
- BinaryTreeNode* right;
- };
-
-
- typedef BinaryTreeNode rbnode;
-
-
- typedef BinaryTreeNode rbtree;
三、 插入Key
无论怎么样操作,性质1和性质3是始终能够保持的。新插入节点的时候,新节点的初始颜色为红色,这样可以不直接破坏性质5,这个时候可能性质4受到威胁,需要调整节点颜色或这左一些旋转等操作。假设新插入的节点为N,其父节点为P,祖父节点为G,叔父节点为U,下面具体分析一下插入新节点的各种情况。
情形1 、空树
当树为空的时候,直接将N节点设为黑色作为树的根节点返回。
情形2 、P为黑色节点
图中所示为N插入到P左孩子节点中,这个过程完全满足性质 1 - 5 的要求,并没有破坏 RBT 的规则,因此,此时即可停止检查,插入过程结束。
同理,若P为黑色节点,N插入后作为P的右孩子节点也不会破坏 RBT的规则。
(下面开始讨论 P 为红色 的情形,由 性质2 推导出 G 一定存在,根据性质 4,G一定是黑色)
情形3 、P为红色节点,U存在且为红色节点
这种情况下,
将G变为红色,P和U变为黑色,这样维持了G为Root的子树内性质4和5,
然后以G作为新的插入节点,从情形1开始迭代。
情形4 、P为红色节点,U不存在或者U为黑色
|
(a) 若P在G的左侧,插入点N也在P的左侧 ------ LL 型
假设G的右侧有 路径有 x个黑色节点,则c有x个黑色节点,G为root的子树路径有x+1个黑色节点。 此时, 只需要以P为中心右旋,将P变为黑色,G变为红色,G的左子树替换为c,这样就可以继续保证以P为root的子树有x+1个黑色节点,检查停止。 |
|
(b)若P在G的左侧,N在P的右侧 -----LR型
这时候先将节点P和节点N做调整,进行左旋,变化成(a)的形态,然后做一次右旋。到此,调整完毕。 |
图略 |
(c)若P在G的右侧,N在P的右侧 ----------RR型
情形和(a)正好相反,做一次左旋即可。 |
图略 |
(d)若P在G的右侧,N在P的左侧 ----------RL型
情形和(b)正好相反,先做做一次右旋,后进行一次左旋即可。 |
RBT插入算法过程代码如下:
四、删除Key
从RBT中删除指定的Key时,需要重新调整树的形态使之满足红黑树的特性。下面来具体分析一下删除Key的过程。
(1)根据key找到要删除的节点Node:如果没有找到,直接返回,否则,进行下一步操作;
(2)如果Node有两个孩子节点,那么删除Node之后该如何放置其孩子节点呢?
这个时候需要进行预处理,转化为删除只有一个孩子的节点的情形。
找到Node的中序前驱(后继)节点,将前驱(后继)节点的值复制到Node中,Node指向前驱(后继)节点;
(3)到此步骤,Node确切的指示为待删除的节点,且Node最多只有一个孩子节点。
删除Node节点,将Node的孩子节点顶替Node的位置.(注意Node为Root的情形)
(4)调整RBT树使其满足规定的5大特性。
假设上一步中顶替上来的节点为 N ,其父节点为 P ,其兄弟节点为 S ,Sl 和 Sr 分别为 S 的左右孩子节点 ,假设 N 在 P 的左侧, 调整过程如下:
(右侧与左侧对称,这里分析一种即可)
情形1 、N节点为红色
|
当N节点为红色的时候,由于左侧缺少一个黑色的节点,可以将N节点的颜色修改为黑色,这样即可从新满足性质5.
调整完毕。 |
情形2、S节点为红色
|
当S节点为红色节点时,则可以将P节点向左旋转,旋转之后P为红色,S为黑色,这个时候S-Sl这条简单路径黑色节点数目合法,S-P-Sl节点数目也合法,S-P-N路径黑色节点数目少一个。
相当于,P的左侧删除了一个黑色节点,应当重新调整 P,S,Sl,Sr所指向的节点,进行后续操作。
后续可能的情形为:3,5,6 |
情形3、P节点为红色,S,Sl,Sr为黑色
|
当P为红色,S为黑色,Sl和Sr均为黑色的时候,则可以简单的交换P节点和S节点的颜色,这样即可使各条简单路径的黑色节点数目和删除节点前相等。
调整完毕。 |
情形4、P,S,Sl,Sr均为黑色
|
当P、S、Sl、Sr均为黑色节点的时候,只需要简单的将S节点标记为红色,这样以P节点为根的个简单路径黑色节点数目比删除之前少一个。
因此,P相当与N的位置,从P的父节点开始递归进行调整。
(如果此时P为树的根节点,即可停止调整) |
情形5、Sl为红色节点并且Sr为黑色
|
这种情况下,可以将Sl进行右旋操作,右旋之后,Sl为黑色,S为红色,Sr不变,这样保持P节点右子树中各简单路径黑色节点数目和旋转前一样。
这个时候,原来的S相当于Sl的位置,Sl相当与a,Sr相当与S。
更新S,Sl,Sr新指向的位置,进行下一步操作。
后续情形为:6. |
情形6、Sr为红色
|
这时,将P节点做一次左旋操作,将Sr的颜色设置为黑色,P和S交换颜色,调整之后,各简单路径的黑色节点数目和删除节点之前一样。
此时调整结束。 |
版权声明:本文为博主原创文章,未经博主允许不得转载。
B-树、B+树、红黑树
标签:b树 b+树 红黑树 数据结构
原文地址:http://blog.csdn.net/lingling_1/article/details/46806733