数据结构之动态查找表
数据结构是程序设计的重要基础,它所讨论的内容和技术对从事软件项目的开发有重要作用。学习数据结构要达到的目标是学会从问题出发,分析和研究计算机加工的数据的特性,以便为应用所涉及的数据选择适当的逻辑结构、存储结构及其相应的操作方法,为提高利用计算机解决问题的效率服务。
动态查找表的特点是表结构本身是在查找过程中动态生成的,即对于给定值 key,若表中存在关键字等于 key 的记录,则查找成功返回;否则插入关键字为 key 的记录。
1、二叉排序树
1.1、二排序树的定义
二叉排序树又称二叉查找树,它或者是一棵空树,或者是具有以下性质的二叉树。
(1)若它的左子树非空,则左子树上所有结点的值均小于根结点的值。
(2)若它的右子树非空,则右子树上所有结点的值均大于根结点的值。
(3)左、右子树本身是二叉排序树。
下图为一棵二叉排序树。
1.2、二叉排序树的查找过程
因为二叉排序树的左子树上所有结点的关键字均小于根结点的关键字,右子树上所有结点的关键字均大于根结点的关键字,所以在二叉排序树上进行查找的过程为: 二叉排序树非空时,将给定值与根结点的关键字值相比较,若相等,则查找成功;若不相等,则当根结点的关键字值大于给定值时,下一步到根的左子树中进行查找,否则到根的右子树中进行查找。若查找成功,则查找过程是走了一条从树根到所找到结点的路径;否则,查找过程终止于一棵空的子树。
设二叉排序树采用二叉链表存储,结点的类型定义如下:
java
typedef struct Tnode{
int data; /*结点的关键字值*/
struct Tnode *lchild,*rchild; /*指向左、右子树的指针*/
}BSTnode,*BSTree;
【函数】二叉排序树的查找算法。
java
BSTree SearchBST(BSTree root, int key, BSTree *father)
/*在root 指向根的二叉排序树中查找键值为 key 的结点*/
/*若找到,返回该结点的指针;否则返回空指针 NULL*/
{
BSTree p = root;
*father = NULL;
while (p && p->data!=key){
*father = p;
if( key < p->data ) p = p->lchild;
else p = p->rchild;
}/*while*/
return p;
}/*SearchBST*/
1.3、在二叉排序树中插入结点的操作
二叉排序树是通过依次输入数据元素并把它们插入到二叉树的适当位置构造起来的,具体的过程是:每读入一个元素,建立一个新结点。若二叉排序树非空,则将新结点的值与根结点的值相比较,如果小于根结点的值,则插入到左子树中,否则插入到右子树中;若二叉排序树为空,则新结点作为二叉排序树的根结点。设关键字序列为{46,25,54,13,29,91},则整个二叉排序树的构造过程如下图 所示。
【函数】二叉排序树的插入算法。
java
int InsertBST(BSTree *root, int newkey)
/*在*root 指向根的二叉排序树中插入一个键值为 newkey 的结点,插入成功则返回0,否则返回-1*/
{
BSTree s,p,f;
s =(BSTree)malloc(sizeof(BSTnode));
if (!s) return -1;
s->data = newkey;
s->lchild = NULL;
s->rchild = NULL;
p = SearchBST(*root, newkey, &f); /*寻找插入位置*/
if(p) return -1; /*键值为 newkey 的结点已在树中,不再插入*/
if(!f) *root= s; /*若为空树,键值为newkey 的结点为树根*/
else if (newkey < f->data) /*作为父结点的左孩子插入*/
f->lchild = s;
else f->rchild = s; /*作为父结点的右孩子插入*/
return 0;
}/*InsertBST*/
从上面的插入过程还可以看到,每次插入的新结点都是二叉排序树上新的叶子结点,因此插入结点时不必移动其他结点,仅需改动某个结点的孩子指针。这就相当于在一个有序序列上插入一个记录而不需要移动其他记录。这表明在二叉排序树进行查找具有类似于折半查找的特性,二叉排序树可采用链表存储结构,因此是动态查找表的一种适宜表示。
另外,由于一棵二叉排序树的形态完全由输入序列决定,所以在输入序列已经有序的情况下,所构造的二叉排序树是一棵单枝树。例如,对于关键字序列 (12,18,23,45,60),建立的二叉排序树如下图所示,这种情况下的查找效率与顺序查找的效率相同。
1.4、在二叉排序树中删除结点的操作
在二叉排序树中删除一个结点,不能把以该结点为根的子树都删除,只能删除这个结点并仍旧保持二叉排序树的特性。也就是说,在二叉排序树上删除一个结点相当于在有序序列中删除一个元素。
假设要在二叉排序树种删除结点 *p(p 指向被删除结点),*f 为其双亲结点,则该操作可分为3 种情况:结点*p 为叶子结点; 结点*p 只有左子树或者只有右子树;结点*p 的左子树、右子树均存在。
(1)若结点 *p 为叶子结点且 *p 不是根结点,即 p->lchild 及 p->rchild 均为空,则由于删去叶子结点后不破坏整棵树的结构,因此只需修改 *p 的双亲结点 *f 的相应指针即可。
f->lchild(或 f->rchild)= NULL;
若结点 *p 只有左子树或者只有右子树且 *p 不是根结点,此时只要将 *p 的左子树或右子树接成其双亲结点 *f的左子树(或右子树),即令
f->lchild (或 f->rchild)= p->lchild;
或
f>lchild(或 f>rchild) = p->rchild:
(3)若 *p 结点的左、右子树均不空,则删除 *p 结点时应将其左子树、右子树连接到适当的位置,并保持二叉排序树的特性。可采用以下两种方法进行处理:一是令 *p 的左子树为 *f的左子树(若 *p 不是根结点且 *p 是 *f的左子树,否则为右子树),而将 *p 的右子树下接到中序遍历时 *p 的直接前驱结点 *s ( *s 结点是 *p 的左子树中最右下方的结点) 的右孩子指针上;是用 *p 的中序直接前驱(或后继)结点 *s 代替 *p 结点,然后删除 *s 结点,如下图所示
从二叉排序树的定义可知,中序遍历二叉排序树可得到一个关键字有序的序列。这也说明,一个无序序列可以通过构造一棵二叉排序树而得到一个有序序列,构造二叉排序树的过程就是对无序序列进行排序的过程。
2、平衡二叉树
平衡二叉树又称为 AVL 树,它或者是一棵空树,或者是具有下列性质的二叉树。它的左子树和右子树都是平衡二叉树,且左子树和右子树的高度之差的绝对值不超过 1。若将二叉树结点的平衡因子(Balance Factor,BF) 定义为该结点左子树的高度减去其右子树的高度,则平衡二叉树上所有结点的平衡因子只可能是-1、0 和 1。只要树上有一个结点的平衡因子的绝对值大于1,则该二又树就是不平衡的。
分析二叉排序树的查找过程可知,只有在树的形态比较均匀的情况下,查找效率才能达到最佳。因此,希望在构造二叉排序树的过程中,保持其为一棵平衡二叉树。
使二叉排序树保持平衡的基本思想是: 每当在二叉排序树中插入一个结点时,首先检查是否因插入破坏了平衡。若是,则找出其中的最小不平衡二叉树,在保持二叉排序树特性的情况下,调整最小不平衡子树中结点之间的关系,以达到新的平衡。所谓最小不平衡子树,是指离插入结点最近且以平衡因子的绝对值大于1 的结点作为根的子树。
2.1、平衡二叉树上的插入操作
一般情况下,假设由于在二叉排序树上插入结点而失去平衡的最小子树根结点的指针为a,也就是说,a 所指结点是离插入结点最近且平衡因子的绝对值超过1 的祖先结点,那么,失去平衡后进行调整的规律可归纳为以下4 种情况。
(1)LL型单向右旋平衡处理。如下图所示,由于在 *a(即结点A)的左子树的左子树上插入新结点,使 *a 的平衡因子由1增至2,导致以*a 为根的子树失去平衡,因此需进行一次向右的顺时针旋转操作。
(2)RR型单向左旋平衡处理。如下图所示,由于在 *a(即结点A)的右子树的右子树上插入新结点,使 *a 的平衡因子由 -1变为 -2,导致以 *a 为根的子树失去平衡,因此需进行一次向左的逆时针旋转操作。
(3)LR型先左后右双向旋转平衡处理。如下图所示,由于在 *a(即结点A)的左子树的右子树上插入新结点,使 *a 的平衡因子由 1 增至2,导致以 *a 为根结点的子树失去平衡,因此需进行两次旋转(先左旋后右旋)操作。
(4)RL型先右后左双向旋转平衡处理。如下图所示,由于在 *a(即结点A)的右子树的左子树上插入新结点,使 *a的平衡因子由 -1变为 -2,导致以 *a为根结点的子树失去平衡,因此需进行两次旋转(先右旋后左旋)操作。
2.2、平衡二叉树上的删除操作
在平衡二叉树上进行删除操作比插入操作更复杂。若待删结点的两个子树都不为空,就用该结点左子树上的中序遍历的最后一个结点(或其右子树上的第一个结点)替换该结点,将情况转化为待删除的结点只有一个子树后再进行处理。当一个结点被删除后,从被删结点到树根的路径上所有结点的平衡因子都需要更新。对于每一个位于该路径上的平衡因子为 ±2 的结点来说,都要进行平衡处理。
3、B_树
一棵m阶的B_树,或为空树,或为满足下列特性的m叉树。
(1)树中每个结点最多有m棵子树。
(2)若根结点不是叶子结点,则最少有两棵子树。
(3)除根之外的所有非终端结点最少有 m 2 \frac m2 2m棵子树。
(4)所有的非终端结点中包含下列数据信息
( n , A 0 , K 1 , A 1 , K 2 , A 2 , . . . , K n , A n ) (n,A_0,K_1,A_1,K_2,A_2,...,K_n,A_n) (n,A0,K1,A1,K2,A2,...,Kn,An)
其中,K~i~(i=1,2,...,n)为关键字,且K~i~<K~i+1~(i=1,2,...,n-1); A~i~(i=0,1,...,n)为指向子树根结点的指针,且指针 A~i-1~ 所指子树中所有结点的关键字均小于 K~i~(i=1,2,...,n),A~n~所指子树中所有结点的关键字均大于K~n~,n 为结点中关键字的个数且满足 { m 2 − 1 ≤ n ≤ m − 1 } {\huge\{}\frac m2 -1≤n≤m-1\huge\} {2m−1≤n≤m−1}。
(5)所有的叶子结点都出现在同一层次上,并且不带信息(可以看作是外部结点或查找失败的结点,实际上这些结点不存在,指向这些结点的指针为空)。
一棵4阶的B树如下图所示。
由 B树的定义可知,在 B树上进行查找的过程是:首先在根结点所包含的关键字中查找给定的关键字,若找到则成功返回;否则确定待查找的关键字所在的子树并继续进行查找,直到查找虚功或查找失败(指针为空) 时为止。
B树上的插入和删除运算较为复杂,因为要保证运算后结点中关键字的个数大子等于 m 2 − 1 \frac m2-1 2m−1,因此涉及结点的"分裂"及"合并"问题。
在B树中插入一个关键字时,不是在树中增加一个叶子结点,而是首先在低层的某个非终端结点中添加一个关键字,若该结点中关键字的个数不超过 m-1,则完成插入;否则,要进行结点的"分裂"处理。所谓"分裂",就是把结点中处于中间位置上的关键字取出来插入到其父结点中,并以该关键字为分界线,把原结点分成两个结点。"分裂"过程可能会一直持续到树根。
同样,在 B_树中删除一个结点时,首先找到关键字所在的结点,若该结点在含有信息的最后一层,且其中关键字的数目不少于 m 2 − 1 \frac m2-1 2m−1,则完成删除;否则需进行结点的"合并"运算。若待删除的关键字所在的结点不在含有信息的最后一层,则将该关键字用其在 B 树中的后继替代,然后删除其后继元素,即将需要处理的情况统一转化为在含有信息的最后一层再进行删除运算。