主要内容
前提
(可回顾“基本概念”)
若在查找的同时对表做插入、删除等修改操作,则被查找的表称为动态查找表,反之则称为静态查找表。
换句话说,动态查找表的表结构本身是在查找过程中动态生成的,即在创建表时,对于给定值,若表中存在其关键字等于给定值的记录,则返回“已存在此关键字”等信息;否则插入关键字等于给定值的记录。
线性表查找算法明显更适合静态查找表。
若要对动态查找表进行高效率的查找,可采用二叉树作为查找表的组织形式,这类查找表统称为树表。
(本篇只对二叉排序树和平衡二叉树作较为详细的介绍,对于B-树和B+树只作初步描述(先挖坑,后补))
二叉排序树(Binary Sort Tree)
二叉排序树又称二叉查找树,它是一种对排序和查找都很有用的特殊二叉树。
特征:
1)若左子树不为空,则左子树上所有结点的值一定小于根结点的值。在数据结构中,对应左子树上所有结点的关键字的值一定小于根结点的关键字的值,data.key > lchild->data.key。
2)若右子树不为空,则右子树上所有结点的值一定大于根结点的值。
3)左子树和右子树都为二叉排序树。(递归)
4)中序遍历二叉排序树可以得到一个结点值递增的有序线性表。
*判定树是特殊的二叉排序树。
数据结构
typedef struct /*定义数据元素结构体*/
{
KeyType key; /*关键字*/
OtherType other; /*其他数据项*/
} Element;
typedef struct /*以二叉树结构定义查询表*/
{
Element data;
struct Element *lchild, *rchild;
} BSTNode, *BSTree; /*BSTNode表示二叉树上的结点,BSTree表示指向二叉树的指针*/
查找算法
查找算法是创建、插入和删除操作的基础,也比较简单。
<思路>
1)若二叉树为空,查找失败,返回空指针。
2)若二叉树不为空,比较根结点的关键字的值data.key和给定值key:
1. 若 data.key = key,则查找成功,返回根结点的地址;
2. 若 data.key < key,则递归查找右子树;
3. 若 data.key > key,则递归查找左子树。
/*------------非递归写法-------------*/
BSTree Bin_Search(BSTree t, KeyType key)
{
while(t)
{
if(data.key == key) return t;
else if(data.key < key) t = t.rchild;
else t = t.lchild;
}
if(!t) return NULL;
}
/*-------------递归写法--------------*/
BSTree Bin_Search(BSTree t,KeyType key)
{
if(t)
{
if(data.key == key) return t;
else if(data.key < key) Bin_Search(t.rchild, key);
else Bin_Search(t.lchild, key);
}
if(!t) return NULL;
}
需要注意的是,含有n个结点的二叉排序树的平均查找长度与树的形态有关,即与二叉树的创建算法有关。
同一组结点,以不同的次序输入会得到不同的二叉排序树,其中最好的情况是二叉树的形态与折半查找的判定树相似,深度达到最小。(回顾“折半查找”)
二叉排序树的查找与折半查找类似。
但就维护查找表而言,二叉排序树更加有效,因为无需移动数据,只需要修改指针就可以完成对查找表的插入和删除。因此动态查找表更适合采用二叉树结构。
插入算法
<思路>
1)若二叉树为空,插入新结点NewNode(这里函数参数为KeyType nkey,OtherType nother);
2)若二叉树不为空,比较关键字的值,若新结点的关键字ndata.key等于根结点的关键字data.key,由于关键字唯一的原则,打印ERROR等信息;否则:
1. ndata.key < data.key,则递归插入左子树;
2. ndata.key > data.key,则递归插入右子树。
void Bin_Insert(BSTree &t, KeyType nkey, OtherType nother)
{
/*------------新结点初始化-----------*/
BSTNode *NewNode = new BSTNode;
NewNode->data.key = nkey;
NewNode->data.other = nother;
NewNode->lchild = NewNode->rchild = NULL;
while((t) && (nkey != t.data.key)) /*当结点不为空,且关键字不重复*/
{
/*----------------插入--------------*/
if(!t.lchild) t.lchild = NewNode;break;
if(!t.rchild) t.rchild = NewNode;break;
/*----------------遍历--------------*/
if(nkey < t.data.key) t = t.lchild;
if(nkey > t.data.key) t = t.rchild;
}
if(!t) t = NewNode; /*如果根结点为空,直接插入*/
if(nkey == t.data.key) cout<<"ERROR"; /*当关键字重复,打印“ERROR”*/
创建算法
创建算法的思路就如前提中说到的那样。
一个无序序列可以通过构造一棵二叉排序树而变成一个有序序列,构造过程相当于排序过程。
#define NoMore 2333 /*定义NoMore来控制结点数或表示输入结束的标识*/
void Bin_Create(BSTree &t)
{
t = NULL; /*初始化为空树*/
KeyType nkey;OtherType nother;
cin>>nkey>>nother;
while(nkey != NoMore) /*当未达到最大结点数或输入未结束时,循环输入数据*/
{
Insert(t, nkey, nother);
cin>>nkey>>nother;
}
}
删除算法
通过关键字查找待删除结点。若不存在待删除结点,返回“ERROR”等信息;若存在,删除结点并修改相连指针。
删除某个结点后,将有可能导致其他结点的相对位置发生变化,因此我们需给予相应处理。
删除结点有3种情况:
1)被删除结点既有左子树,又有右子树;
2)被删除结点无左子树;
3)被删除节点无右子树。
只含单子树的情况比较简单,将结点删除后用下一个结点填补就可以了。
而含有双子树的情况则需要找到左子树中关键字最大的结点(即小于被删除结点的关键字中最大的值),然后用这个结点填补被删除结点的位置。
为什么?
因为左子树上所有的关键字必须小于根结点的关键字,为了在删除结点后仍保持这一特性,必须用大于左子树上其余结点关键字的结点来填补被删除结点的位置。
虽然右子树上所有结点的关键字都大于被删除结点,但是我们并不能直接用右子树中的根结点(即被删除节点的右孩子)来填补它的位置,因为右孩子本身还可能含有左子树,这将给操作带来十分多不必要的麻烦。
而左子树中关键字最大的结点一定不含有右子树(否则它就不是最大的),所以不会给操作带来麻烦。
void Bin_Delete(BSTree &t, KeyType key)
{
BSTNode *p = new BSTNode; /*待删除结点*p*/
BSTNode *f = new BSTNode; /*待删除结点的父节点*f*/
p = Bin_Search(t, key);
f = Bin_SearchP(t, key); /*稍微修改Bin_Search函数为Bin_SearchP函数,用作查找父结点*/
BSTNode *q = p; /*临时结点*q,用以保存*p的位置*/
/*---------既有左子树又有右子树---------*/
if((p->lchild) && (p->rchild))
{
BSTNode *s = p->lchild; /*在左子树中...*/
while(s->rchild) /*...找关键字最大的结点以及该结点的直接前驱,直接前驱辅助后续判断*/
{
q = s;
s = s->rchild;
}
p->data = s->data; /*用上面找到的结点的信息覆盖待删除结点的信息*/
if(q != p) q->rchild = s->lchild;
else q->lchild = s->lchild; /*这种情况下待删除结点没有左孩子*/
delete s; /*s的任务完成,撤销存储空间*/
}
/*-------------不含右子树------------*/
else if(!p->rchild)
p = p->lchild;
/*-------------不含左子树------------*/
else if(!p->lchild)
p = p->rchild;
/*--------以上步骤仅完成了*p子树的连接,还需将其与父结点*f连接-------*/
if(!f) t = p; /*当没有父结点,即待删除结点为根结点时*/
else if(q == f->lchild) f->lchild = p; /*原本为左子树则挂回去左子树位置*/
else f->rchild = p; /*反之挂回右子树位置*/
delete q; /*q的任务完成,撤销存储空间*/
}
平衡二叉树(Balanced Binary Tree)
平衡二叉树是由一般的二叉排序树经过平衡调整得到的,每个结点的左右子树深度差小于等于1的特殊的二叉排序树。
上面已经提到,二叉排序树的平均查找长度与它的形态有关,其中平衡二叉树就是一种最好的形态。
特征:
1)左右子树深度差的绝对值小于等于1;
2)左右子树也是平衡二叉树。(递归)
为了表示左右子树的深度差,我们给每个结点增加一个属性——平衡因子(Balance Factor),记录每个结点左右子树的深度差,结点值 = 左子树深度 - 右子树深度。只要有一个结点的平衡因子的绝对值大于1,这棵二叉排序树就是不平衡的。
当平衡的二叉排序树因插入结点而失去平衡时,仅需对最小不平衡子树进行平衡调整即可,即 以 离插入结点最近的,结点平衡因子等于2或-2的结点 为根结点的 子树。
插入结点的情况分为四种,相应地,平衡调整的方法也分为四种:
(根结点只表示一个位置,其余颜色代表具体的结点)
1)在根结点的左子树根结点的左子树上插入结点(LL型):
左子树根结点的左子树相当于树的“外层”,对此情况只需要将根结点左移(不违反二叉排序树的特征),即以左子树根结点作为新的根结点。但是左子树根结点上可能存在右子树,如果只是简单地将根结点左移,可能会出现三叉树的情况。所以我们将左子树根结点上的右子树挂接到原来的根结点上。
2)在根结点的右子树根结点的右子树上插入结点(RR型):
这种情况与LL型的平衡调整对称。
3)在根结点的左子树根结点的右子树上插入结点(LR型):
左子树根结点的右子树相当于树的“内层”,对此情况需要新增一个操作结点,即根结点的左子树根结点的右子树根结点。
由于根结点左移并不能改变左右子树的深度差,所以我们应该设法将插入结点“抬上去”。
具体操作是将右子树根结点变成新的根结点。
若右子树根结点存在左子树(LRL型),则将其左子树挂接到左子树根结点上;若右子树根结点存在右子树(LRR型),则将其右子树挂接到根结点上。
4)在根结点的右子树根结点的左子树上插入结点(RL型):
这种情况与LL型的平衡调整对称。
新增一个操作结点——根结点的右子树根结点的左子树根结点,并将左子树根结点变成新的根结点。
若左子树根结点存在左子树(RLL型),则将其左子树挂接到根结点上;若左子树根结点存在右子树(RLR型),则将其右子树挂接到右子树根结点上。
<平衡调整算法的思路>
1)插入算法:在二叉排序树上插入新结点;
2)查找算法:判断插入结点的情况;
3)平衡调整算法:修改相关结点的指针。
B-树和B+树
前面介绍的查找方法均适用于存储在计算机内存中较小的文件,统称为内查找法。若文件很大且存放于外存进行查找时,这些查找方法就不适用了。
内查找法均已结点为单位进行查找,需要反复地进行内、外存的交换。
1970年,适用于外查找的平衡多叉树——B-树被提出来,磁盘管理系统中的目录管理,以及数据库系统中的索引组织多数采用B-树这种数据结构。
B+树是B-树的一种变形,更适用于文件索引系统。
路过的圈毛君:“
数据结构、操作系统和数据库,这三个科目又联系起来了_(:з」∠)_
B-树和B+树先挖个坑,以后我需要用到再仔细去研究它们_(:з」∠)_x2
”