查找
1、查找的基本概念
查找表
查找表是由同―类型的数据元素(或记录)构成的集合。
关键字
关键字是数据元素(或记录)中某个数据项的值,用它可以标识一个数据元素(或记录)。若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(对不同的记录,其主关键字均不同)。反之,称用以识别若干记录的关键字为次关键字。当数据元素只有一个数据项时,其关键字即为该数据元素的值。
查找
查找是指根据给定的某个值,在查找表中确定一个其关键字等于给定值的记录或数据元素。若表中存在这样的一个记录,则称查找成功,此时查找的结果可给出整个记录的信息,或指示该记录在查找表中的位置;若表中不存在关键字等于给定值的记录,则称查找不成功,此时查找的结果可给出一个“空”记录或“空”指针。
动态查找表和静态查找表
若在查找的同时对表做修改操作(如插入和删除),则相应的表称之为动态查找表,否则称之为静态查找表。换句话说,动态查找表的表结构本身是在查找过程中动态生成的,即在创建表时,对于给定值,若表中存在其关键字等于给定值的记录,则查找成功返回;否则插入关键字等于给定值的记录。
平均查找长度
为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法,在查找成功时的平均查找长度。( Average Search Length,ASL )。
2、线性表的查找
①.顺序查找
顺序查找( Sequential Search )的查找过程为:从表的一端开始,依次将记录的关键字和给定值进行比较,若某个记录的关键字和给定值相等,则查找成功;反之,若扫描整个表后,仍未找到关键字和给定值相等的记录,则查找失败。
顺序查找方法既适用于线性表的顺序存储结构,又适用于线性表的链式存储结构。
//顺序查找
#include<iostream>
using namespace std;
#define MAXSIZE 100
#define OK 1;
typedef struct{
int key;//关键字域
}ElemType;
typedef struct{
ElemType *R;
int length;
}SSTable;
int InitList_SSTable(SSTable &L)
{
L.R=new ElemType[MAXSIZE];
if (!L.R)
{
cout<<"初始化错误";
return 0;
}
L.length=0;
return OK;
}
int Insert_SSTable(SSTable &L)
{
int j=0;
for(int i=0;i<MAXSIZE;i++)
{
L.R[i].key=j;
L.length++;
j++;
}
return 1;
}
int Search_Seq(SSTable ST, int key){
//在顺序表ST中顺序查找其关键字等于key的数据元素。若找到,则函数值为
//该元素在表中的位置,否则为0
for (int i=ST.length; i>=1; --i)
if (ST.R[i].key==key) return i; //从后往前找
return 0;
}// Search_Seq
void Show_End(int result,int testkey)
{
if(result==0)
cout<<"未找到"<<testkey<<endl;
else
cout<<"找到"<<testkey<<"位置为"<<result<<endl;
return;
}
void main()
{
SSTable ST;
InitList_SSTable(ST);
Insert_SSTable(ST);
int testkey1=7,testkey2=200;
int result;
result=Search_Seq(ST, testkey1);
Show_End(result,testkey1);
result=Search_Seq(ST, testkey2);
Show_End(result,testkey2);
}
改进:把待查关键字key存入表头(“哨兵”),从后向前逐个比较,可免去查找过程中每一步都要检测是否查找完毕,加快速度。
//设置监视哨的顺序查找
#include<iostream>
using namespace std;
#define MAXSIZE 100
#define OK 1;
typedef struct{
int key;//关键字域
}ElemType;
typedef struct{
ElemType *R;
int length;
}SSTable;
int InitList_SSTable(SSTable &L)
{
L.R=new ElemType[MAXSIZE];
if (!L.R)
{
cout<<"初始化错误";
return 0;
}
L.length=0;
return OK;
}
int Insert_SSTable(SSTable &L)
{
int j=1;//空出ST.R[0]的位置
for(int i=1;i<MAXSIZE;i++)
{
L.R[i].key=j;
L.length++;
j++;
}
return 1;
}
int Search_Seq(SSTable ST, int key){
//在顺序表ST中顺序查找其关键字等于key的数据元素。若找到,则函数值为
//该元素在表中的位置,否则为0
ST.R[0].key = key; //“哨兵”
for(int i = ST.length; ST.R[i].key!=key; --i) ; //从后往前找
return i;
}// Search_Seq
void Show_End(int result,int testkey)
{
if(result==0)
cout<<"未找到"<<testkey<<endl;
else
cout<<"找到"<<testkey<<"位置为"<<result<<endl;
return;
}
void main()
{
SSTable ST;
InitList_SSTable(ST);
Insert_SSTable(ST);
int testkey1=7,testkey2=200;
int result;
result=Search_Seq(ST, testkey1);
Show_End(result,testkey1);
result=Search_Seq(ST, testkey2);
Show_End(result,testkey2);
}
顺序查找的时间复杂度为O(n)。
顺序查找的优点是:算法简单,对表结构无任何要求,既适用于顺序结构,也适用于链式结构,无论记录是否按关键字有序均可应用。
其缺点是:平均查找长度较大,查找效率较低,所以当n很大时,不官采用顺序查找。
②.折半查找
折半查找(Binary Search )也称二分查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列(顺序有序)。
折半查找的查找过程为:从表的中间记录开始,如果给定值和中间记录的关键字相等,则查找成功;如果给定值大于或者小于中间记录的关键字,则在表中大于或小于中间记录的那一半中查找,这样重复操作,直到查找成功,或者在某一步中查找区间为空,则代表查找失败。
设表长为n, low、high和mid分别指向待查元素所在区间的上界、下界和中点,key为要查找的值
- 初始时,令low=1 , high=n, mid=⌊(1ow+high)/2⌋
- 让key与mid指向的记录比较
- 若key==R[ mid].key,查找成功
- 若key<R[mid].key,则high=mid-1
- 若key>R[mid].key,则low=mid+1 - 重复上述操作,直至low>high时,查找失败
//折半查找
#include<iostream>
using namespace std;
#define MAXSIZE 100
#define OK 1;
typedef struct{
int key;//关键字域
}ElemType;
typedef struct{
ElemType *R;
int length;
}SSTable;
int InitList_SSTable(SSTable &L)
{
L.R=new ElemType[MAXSIZE];
if (!L.R)
{
cout<<"初始化错误";
return 0;
}
L.length=0;
return OK;
}
int Insert_SSTable(SSTable &L)
{
int j=1;
for(int i=1;i<MAXSIZE;i++)
{
L.R[i].key=j;
L.length++;
j++;
}
return 1;
}
int Search_Bin(SSTable ST,int key) {
// 在有序表ST中折半查找其关键字等于key的数据元素。若找到,则函数值为
// 该元素在表中的位置,否则为0
int low=1,high=ST.length; //置查找区间初值
int mid;
while(low<=high) {
mid=(low+high) / 2;
if (key==ST.R[mid].key) return mid; //找到待查元素
else if (key<ST.R[mid].key) high = mid -1; //继续在前一子表进行查找
else low =mid +1; //继续在后一子表进行查找
}//while
return 0; //表中不存在待查元素
}// Search_Bin
void Show_End(int result,int testkey)
{
if(result==0)
cout<<"未找到"<<testkey<<endl;
else
cout<<"找到"<<testkey<<"位置为"<<result<<endl;
return;
}
void main()
{
SSTable ST;
InitList_SSTable(ST);
Insert_SSTable(ST);
int testkey1=7,testkey2=200;
int result;
result=Search_Bin(ST, testkey1);
Show_End(result,testkey1);
result=Search_Bin(ST, testkey2);
Show_End(result,testkey2);
}
折半查找的时间复杂度为O(log2n )。可见,折半查找的效率比顺序查找高,但折半查找只适用于有序表,且限于顺序存储结构。
折半查找的优点是:比较次数少,查找效率高。
其缺点是:对表结构要求高,只能用于顺序存储的有序表。查找前需要排序,而排序本身是一种费时的运算。同时为了保持顺序表的有序性,对有序表进行插人和删除时,平均比较和移动表中一半元素,这也是一种费时的运算。因此,折半查找不适用于数据元素经常变动的线性表。
③.分块查找
- 分块有序,即分成若干子表,要求每个子表中的数值都比后一块中数值小(但子表内部未必有序)。
- 建立“索引表”,索引表包括:
- 关键字项:各子表中的最大关键字。
- 指针项:指示该子表的起始地址。
- 先确定待查记录所在块
- 对索引表使用折半查找法(因为索引表是有序表); - 确定了待查关键字所在的子表后,在子表内采用顺序查找法(因为各子表内部是无序表);
分块查找的优点是:在表中插人和删除数据元素时,只要找到该元素对应的块,就可以在该块内进行插人和删除运算。由于块内是无序的,故插人和删除比较容易,无需进行大量移动。如果线性表既要快速查找又经常动态变化,则可采用分块查找。
其缺点是:要增加一个索引表的存储空间并对初始索引表进行排序运算。
3、树表的查找
①.二叉排序树
二叉排序树(Binary Sort Tree)又称二叉查找树,它是一种对排序和查找都很有用的特殊二叉树。
二叉排序树的定义
二叉排序树或者是一棵空树,或者是具有下列性质的二叉树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树。
二叉排序树是递归定义的。由定义可以得出二叉排序树的一个重要性质:中序遍历一棵二叉树时可以得到一个结点值递增的有序序列。
二叉树排序树的操作-查找
- 若二叉排序树为空,则查找失败,返回空指针。
- 若二叉排序树非空,将给定值key与根结点的关键字T->data.key进行比较:
- 若key==T->data.key,则查找成功,返回根结点地址;
- 若keydata.key,则进一步查找左子树;
- 若key>T->data.key,则进一步查找右子树。
二叉树排序树的操作-插入
- 若二叉排序树为空,则待插入结点*S作为根结点插入到空树中。
- 若二叉排序树非空,则将key与根结点的关键字T>data.key进行比较:
- 若key小于T->data.key,则将S插入左子树;
- 若key大于T>data,key,则将S插入右子树。
二叉树排序树的操作-创建
- 将二叉排序树T初始化为空树。
- 读入一个关键字为key的结点。
- 如果读入的关键字key不是输入结束标志,则循环执行以下操作:
- 将此结点插入二叉排序树T中;
- 读人一个关键字为key的结点。
例:设关键字的输入次序为:45,24,53,45,12,24,90,写出二叉排序树的创建过程。
二叉树排序树的操作-删除
原则:删除后保持所有节点的中序遍历顺序不变
删除一个结点,不能把以该结点为根的子树都删掉,将因删除结点而断开的二叉链表重新链接起来
假设被删结点为p(即p指针指向的结点),其双亲为f,p是f的左孩子(或右孩子),分三种情况︰
- 被删结点*p为叶子结点
2. 被删结点*p只有左子树或右子树
3. 被删结点*p既有左子树又有右子树
//二叉排序树的综合操作
#include<iostream>
using namespace std;
#define ENDFLAG '#'
//char a[10]={'5','6','7','2','1','9','8','10','3','4','#'};//全局变量
typedef struct ElemType{
char key;
}ElemType;
typedef struct BSTNode{
ElemType data; //结点数据域
BSTNode *lchild,*rchild; //左右孩子指针
}BSTNode,*BSTree;
//二叉排序树的递归查找
BSTree SearchBST(BSTree T,char key) {
//在根指针T所指二叉排序树中递归地查找某关键字等于key的数据元素
//若查找成功,则返回指向该数据元素结点的指针,否则返回空指针
if((!T)|| key==T->data.key) return T; //查找结束
else if (key<T->data.key) return SearchBST(T->lchild,key); //在左子树中继续查找
else return SearchBST(T->rchild,key); //在右子树中继续查找
} // SearchBST
//二叉排序树的插入
void InsertBST(BSTree &T,ElemType e ) {
//当二叉排序树T中不存在关键字等于e.key的数据元素时,则插入该元素
if(!T) { //找到插入位置,递归结束
BSTree S = new BSTNode; //生成新结点*S
S->data = e; //新结点*S的数据域置为e
S->lchild = S->rchild = NULL; //新结点*S作为叶子结点
T =S; //把新结点*S链接到已找到的插入位置
}
else if (e.key< T->data.key)
InsertBST(T->lchild, e ); //将*S插入左子树
else if (e.key> T->data.key)
InsertBST(T->rchild, e); //将*S插入右子树
}// InsertBST
//二叉排序树的创建
void CreateBST(BSTree &T ) {
//依次读入一个关键字为key的结点,将此结点插入二叉排序树T中
T=NULL;
ElemType e;
cin>>e.key; //???
while(e.key!=ENDFLAG){ //ENDFLAG为自定义常量,作为输入结束标志
InsertBST(T, e); //将此结点插入二叉排序树T中
cin>>e.key; //???
}//while
}//CreatBST
void DeleteBST(BSTree &T,char key) {
//从二叉排序树T中删除关键字等于key的结点
BSTree p=T;BSTree f=NULL; //初始化
BSTree q;
BSTree s;
/*------------下面的while循环从根开始查找关键字等于key的结点*p-------------*/
while(p){
if (p->data.key == key) break; //找到关键字等于key的结点*p,结束循环
f=p; //*f为*p的双亲结点
if (p->data.key> key) p=p->lchild; //在*p的左子树中继续查找
else p=p->rchild; //在*p的右子树中继续查找
}//while
if(!p) return; //找不到被删结点则返回
/*―考虑三种情况实现p所指子树内部的处理:*p左右子树均不空、无右子树、无左子树―*/
if ((p->lchild)&& (p->rchild)) { //被删结点*p左右子树均不空
q = p;
s = p->lchild;
while (s->rchild) //在*p的左子树中继续查找其前驱结点,即最右下结点
{q = s; s = s->rchild;} //向右到尽头
p->data = s->data; //s指向被删结点的“前驱”
if(q!=p){
q->rchild = s->lchild; //重接*q的右子树
}
else q->lchild = s->lchild; //重接*q的左子树
delete s;
}//if
else{
if(!p->rchild) { //被删结点*p无右子树,只需重接其左子树
q = p; p = p->lchild;
}//else if
else if(!p->lchild) { //被删结点*p无左子树,只需重接其右子树
q = p; p = p->rchild;
}//else if
/*――――――――――将p所指的子树挂接到其双亲结点*f相应的位置――――――――*/
if(!f) T=p; //被删结点为根结点
else if (q==f->lchild) f->lchild = p; //挂接到*f的左子树位置
else f->rchild = p; //挂接到*f的右子树位置
delete q;
}
}//DeleteBST
//二叉排序树的删除
//中序遍历
void InOrderTraverse(BSTree &T)
{
if(T)
{
InOrderTraverse(T->lchild);
cout<<T->data.key;
InOrderTraverse(T->rchild);
}
}
void main()
{
BSTree T;
cout<<"请输入若干字符,用回车区分,以#结束输入"<<endl;
CreateBST(T);
cout<<"当前有序二叉树中序遍历结果为"<<endl;
InOrderTraverse(T);
char key;//待查找或待删除内容
cout<<"请输入待查找字符"<<endl;
cin>>key;
BSTree result=SearchBST(T,key);
if(result)
{cout<<"找到字符"<<key<<endl;}
else
{cout<<"未找到"<<key<<endl;}
cout<<"请输入待删除的字符"<<endl;
cin>>key;
DeleteBST(T,key);
cout<<"当前有序二叉树中序遍历结果为"<<endl;
InOrderTraverse(T);
}
②.平衡二叉树
平衡二叉树又称AVL树
- 一棵AVL树或者是空树,或者是具有下列性质的二叉排序树:
- 它的左子树和右子树都是AVL树,且左子树和右子树的深度之差的绝对值不超过1。
- 左子树和右子树也是AVL树。
每个结点附加一个数字,给出该结点平衡因子BF。
BF:该结点的左子树深度和右子树深度之差。
AVL树任一结点平衡因子只能取-1,0,1。
平衡二叉树的平衡调整方法
如果在一棵AVL树中插入一个新结点,就有可能造成失衡,此时必须重新调整树的结构,使之恢复平衡。
调整方法:找到离插入点最近且平衡因子绝对值超过1的祖先结点,以该结点为根的子树称为最小不平衡子树,可将重新平衡的范围局限于这颗子树。
平衡二叉树的插入
在向一棵本来是AVL树中插入一个新结点时,如果树中某个结点的平衡因子的绝对值|balance| > 1,则出现了不平衡,需要做平衡化处理。
算法︰从一棵空树开始,通过输入一系列关键字,逐步建立AVL树。在插入新结点时使用平衡旋转方法进行平衡化处理。
例:
③.B-树
B-树的概念
B-树是由R.Bayer和E.Maccreight于1970年提出的,是一种特殊的多叉树,是一种在外存文件系统中常用的动态索引技术,是大型数据库文件的一种组织结构。B-树中的每个结点大小都相同。
B-树的结点结构
- m称为B-树的阶,m≥3
- par为指向父亲结点的指针域
- K1、K2、…Kn为n个按从小到大顺序排列的关键字
- 对非根结点⌈m/2⌉-1≤n≤m-1
- P0、P1、P2,、…Pn为n+1个指针,分别指向该结点的n+1棵子树
每个结点关键字最少=⌈m/2⌉-1=2-1=1;最多=m-1=3,每个结点的子树数目最少为⌈m/2⌉=2,最多为m=4。不管每个结点实际使用了多少个关键字域和指针域,它都包含4个关键字域、4个指向记录存储位置的指针域、5个指向孩子结点的指针域、一个指向父亲结点的指针域和一个保存关键字个数的n域。
B-树的特点
- 一棵m阶B-树(m叉树)是一棵平衡的m路搜索树,它或者是空树,或者是满足下列性质的树:
- 根结点至少有2个子树。
- 除根结点以外的所有结点(不包括失败结点)至少有⌈m/2⌉个子树。
- 所有的失败结点(叶子结点)都位于同一层。
- 每个结点最多有m棵子树。 - 在B-树中的“失败”结点是当搜索值x不在树中时才能到达的结点。
- 关键字的插入次序不同,将生成不同结构的B-树。
- 一棵B-树是平衡的m路搜索树,但一棵平衡的m路搜索树不一定是B-树。
B-树的搜索算法
- B-树的搜索过程是一个在结点内搜索和循某一条路径向下一层搜索交替进行的过程。
- B-树的搜索时间与B-树的阶数m和B-树的高度h直接有关,必须加以权衡。
- 在B-树上进行搜索,搜索成功所需的时间取决于关键码所在的层次;搜索不成功所需的时间取决于树的高度。
高度h与关键码个数N之间的关系
设在m阶B-树中每层结点个数达到最少,则B-树的高度可能达到最大。设树中关键码个数为N,从B-树的定义知:h-1层至少有2⌈m/2⌉h-2个结点。
m值的选择
如果提高B-树的阶数m,可以减少树的高度,从而减少读入结点的次数,因而可减少读磁盘的次数。
事实上,m受到内存可使用空间的限制。当m很大超出内存工作区容量时,结点不能一次读入到内存,增加了读盘次数,也增加了结点内搜索的难度。
m值的选择:应使得在B-树中找到关键码×的时间总量达到最小。
这个时间由两部分组成:从磁盘中读入结点所用时间+在结点中搜索x所用时间。
B-树的插入
- 在B-树中查找给定关键字的记录,若查找成功,则插入操作失败;否则将新记录作为空指针p插入到查找失败的叶子结点的上一层结点(由q指向)中。
- 若插人新记录和空指针后,q指向的结点的关键字个数未超过m-l,则插人操作成功,否则转入步骤③。
- 以该结点的第⌈m/2⌉个关键字K⌈m/2⌉为拆分点,将该结点分成3个部分:K⌈m/2⌉左边部分、K⌈m/2⌉、K⌈m/2⌉右边部分。K⌈m/2⌉左边部分仍然保留在原结点中;K⌈m/2⌉右边部分存放在一个新创建的结点(由p指向)中;关键字值为K⌈m/2⌉的记录和指针p插人到q的双亲结点中。因q的双亲结点增加一个新的记录,所以必须对q的双亲结点重复②和③的操作,依次类推,直至由q指向的结点是根结点,转入步骤④。
- ④由于根结点无双亲,则由其分裂产生的两个结点的指针p和q,以及关键字为K⌈m/2⌉的记录构成一个新的根结点。此时,B-的高度增加1。
B- 树的删除
在B-树上删除一个关键码时,首先需要找到这个关键码所在的结点,从中删去这个关键码。
若该结点不是叶结点,且被删关键码为Ki;,1 ≤i≤n,则在删去该关键码之后,应以该结点Pi;所指示子树中的最小关键码×来代替被删关键码Ki;所在的位置,然后在×所在的叶结点中删除x。
//B-树的查找
//B-树的插入
#include<iostream>
using namespace std;
#define FALSE 0
#define TRUE 1
#define OK 1
#define m 3 //B-树的阶,暂设为3
typedef struct BTNode{
int keynum; //结点中关键字的个数,即结点的大小
BTNode *parent; //指向双亲结点
int key[m+1]; //关键字矢量,0号单元未用
BTNode *ptr[m+1]; //子树指针矢量
}BTNode,*BTree;
//- - - - - B-树的查找结果类型定义- - - - -
struct Result{
BTNode *pt; //指向找到的结点
int i; //1..m,在结点中的关键字序号
int tag; //1:查找成功,0:查找失败
};
int Search(BTree T,int key)
{
BTree p=T;
int endnum;
if(p) //树不为空时
{
endnum=p->keynum; //获得首节点包含的记录个数
}
else
{
return 0; //返回没找到
}
int i=0;
if(endnum==0)
{
return i; //树存在,但仅有一个为空根节点
}
else if(key>=p->key[endnum])//节点不为空,但当前值比最大的key还大
{
i=endnum;
return i;
}
else if(key<=p->key[1]) //节点不为空,但当前值比最小的key还小
{
return i;}
else
{
for(i=1;i<endnum;i++) //有合适的位置,即处于当前结点的最大和最小值之间,或找到了
{
if(p->key[i]<=key && key<p->key[i+1])
return i;
}
}
}
void Insert(BTree &q,int i,int x,BTree &ap)
{//将x插入q结点的i+1位置中
int j;
for(j=m-1;j>i;j--)
{
//将插入位置之后的key全部后移一位
q->key[j+1]=q->key[j];
}
for(j=m;j>i;j--)
{
//相应地也移动其后ptr的位置
q->ptr[j]=q->ptr[j-1];
}
q->key[i+1]=x;//插入x到该位置
q->ptr[i+1]=ap;
q->keynum++;
}
void split(BTree &q,int s,BTree &ap)
{ //将q->key[s+1,..,m], q->ptr[s+1,..,m]移入新结点*ap作为右结点
//原结点作为新的左侧结点
//中间值被保存在ap[0]->key中,等待找到跳转回InsertBTree()寻找到到合适的插入位置插入
int i;
ap=new BTNode;
for(i=s+1;i<=m;i++)
{ //将q->key[s+1,..,m]保存到ap->key[0,..,m-s+1]中
//将q->ptr[s+1,..,m]保存到ap->ptr[0,..,m-s+1]中
ap->key[i-s-1]=q->key[i];
ap->ptr[i-s-1]=q->ptr[i];
}
if(ap->ptr[0])
{
//当ap有子树的时候
for(i=0;i<=1;i++)
{
//将ap的子树的父亲改为ap自己
ap->ptr[i]->parent=ap;
}
}
ap->keynum=(m-s)-1;
ap->parent=q->parent;//将ap的父亲改为q的父亲
q->keynum=q->keynum-(m-s);//修改q的记录个数
}
void NewRoot(BTree &T,BTree q,int x,BTree &ap)//生成含信息(T, x, ap)的新的根结点*T,原T和ap为子树指针
{
BTree newT=new BTNode;//新建一个结点作为新的根
newT->key[1]=x;//写入新根的key[1]
newT->ptr[0]=T;//将原来的树根作为新根的左子树
newT->ptr[1]=ap;//ap作为新根的右子树
newT->keynum=1;
newT->parent=NULL;//新根的父亲为空
ap->parent=newT;//ap的父亲为新根
T->parent=newT;//T的父亲为新根
T=newT;//树改成新根引导的
}
//B-树的插入
int InsertBTree(BTree &T,int K,BTree q,int i){
int x=K;
BTree ap=NULL;
int finished=FALSE;//x表示新插入的关键字,ap为一个空指针
while(q&&!finished){
Insert(q,i,x,ap); //将x和ap分别插入到q->key[i+1]和q->ptr[i+1]
if (q->keynum<m)
finished=TRUE; //插入完成
else{ //分裂结点*q
int s= m/2;
split(q,s,ap);
x=ap->key[0];// x=q->key[s];
//将q->key[s+1..m], q->ptr[s..m]和q->recptr[s+1..m] 移入新结点*ap
q=q->parent;
if(q)
{
i=Search(q,x);
} //在双亲结点*q中查找x的插入位置
} //else
} //while
if(!finished) //T是空树(参数q初值为NULL)或者根结点已分裂为结点*q和*ap
NewRoot(T,q,x,ap); //生成含信息(T, x, ap)的新的根结点*T,原T和ap为子树指针
return OK;
} //InsertBTree //InsertBTree
//B-树的查找
Result SearchBTree(BTree &T, int key){
/*在m阶B-树T上查找关键字key,返回结果(pt,i,tag)。若查找成功,则特征值tag=1,指针pt所指结点中第i个关键字等于key;否则特征值tag=0,等于key的关键字应插入在指针pt所指结点中第i和第i+1个关键字之间*/
BTree p=T;
BTree q=NULL;
int found=FALSE;
int i=0; //初始化,p指向待查结点,q指向p的双亲
while(p&&!found){
i=Search(p,key);
//在p->key[1..keynum]中查找i,使得:p->key[i]<=key<p->key[i+1]
if(i>0&&p->key[i]==key)
found=TRUE; //找到待查关键字
else
{
q=p;
p=p->ptr[i];
}
}
Result result;
if(found)
{
result.pt=p;
result.i=i;
result.tag=1;
return result;
} //查找成功
else
{
result.pt=q;
result.i=i;
result.tag=0;
return result;
} //查找不成功,返回K的插入位置信息
}//SearchBTree
void InitialBTree(BTree &T)
{
//初始化一个空的根
T->keynum=0;
T->parent=NULL;
for(int i=0;i<m+1;i++)
{
T->ptr[i]=NULL;
}
}
void main()
{
BTree T=new BTNode;
InitialBTree(T);
//先用SearchBTree()找到要插入的位置,得到一个Result结构体
//再用InsertBTree()插入数据
Result result;
int a[11]={45,24,53,90,3,12,50,61,70,100};
for(int i=0;i<10;i++)
{
result=SearchBTree(T,a[i]);
if(result.tag==0)
{
InsertBTree(T,a[i],result.pt,result.i);
}
}
cout<<"OK";
}
④.B+树
m阶B+树的定义
- 树中每个非叶结点最多有m棵子树;
- 根结点(非叶结点)至少有2棵子树。除根结点外,其它的非叶结点至少有⌈m/2⌉棵子树;
- 所有叶结点都处于同一层次上,包含了全部关键码及指向相应数据对象存放地址的指针,且叶结点本身按关键码从小到大顺序链接。
- 在B+树中有两个头指针:一个指向B+树的根结点,一个指向关键码最小的叶结点。
可对B+树进行两种搜索运算:
循叶结点链顺序搜索
另一种是从根结点开始,进行自顶向下,直至叶结点的随机搜索。
在B+树上进行随机搜索、插入和删除的过程基本上与B-树类似。只是在搜索过程中,如果非叶结点上的关键码等于给定值,搜索并不停止,而是继续沿右指针向下,一直查到叶结点上的这个关键码。
B+树的搜索分析
B+树的插入仅在叶结点上进行。每插入一个(关键码-指针)索引项后都要判断结点中的子树棵数是否超出范围。
当插入后结点中的子树棵数n > m1 时,需要将叶结点分裂为两个结点,它们的关键码分别为⌈(m1+1)/2⌉和⌊(m1+1)/2⌋.
它们的双亲结点中应同时包含这两个结点的最大关键码和结点地址。此后,问题归于在非叶结点中的插入了。
B+树的删除
仅在叶结点上进行。当在叶结点上删除一个(关键码-指针)索引项后,结点中的子树棵数仍然不少于⌈m1/2⌉,这属于简单删除,其上层索引可以不改变。
如果删除的关键码是该结点的最小关键码,但因在其上层的副本只是起了一个引导搜索的“分界关键码”的作用,所以上层的副本仍然可以保留。
如果在叶结点中删除一个(关键码-指针)索引项后,该结点中的子树棵数n小于结点子树棵数的下限⌈m1/2⌉,必须做结点的调整或合并工作。
如果右兄弟结点的子树棵数已达到下限⌈m1/2⌉ ,没有多余的关键码可以移入被删关键码所在的结点,必须进行两个结点的合并。将右兄弟结点中的所有(关键码-指针)索引项移入被删关键码所在结点,再将右兄弟结点删去。
结点的合并将导致双亲结点中“分界关键码”的减少,有可能减到叶结点中子树棵数的下限⌈m/2⌉以下。这样将引起非叶结点的调整或合并。如果根结点的最后两个子女结点合并,树的层数就会减少一层。
B-树与B+树的差异
B树分为B-树和B+树,它们的树结构大致相同。一个m阶的B+树与B-树的差异是:
- 在B-树中,每个结点含有n个关键字和n+1棵子树,而在B+树中,每个结点含有n个关键字和n棵子树;
- 在B-树中,每个结点(除根结点外)中的关键字个数n的取值范围是:⌈m/2⌉-1≤n≤m-1
;在B+树中,每个结点(除根结点外)中的关键字个数n的取值范围是:⌈m/2⌉ ≤n≤m,树根结点的取值范围是:1≤n≤m。 - B+树中的所有叶子结点包含了全部关键字及指向对应记录的指针,且所有叶子结点按关键字从小到大的顺序依次链接;
- B+树中所有非叶子结点仅起到索引的作用,即结点中的每个索引项只含有对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
4、散列表的查找
①.散列表的基本概念
散列表:是一个有限连续的地址空间,是一种存储结构。
散列地址:数据元素(记录)的存储位置。
通常情况下︰散列表是一维数组,散列地址是数组的下标。
散列函数:在数据元素(记录)的存储位置p和关键字key之间建立一个确定的对应关系H,使p=H(key),称这个对应关系H为散列函数,p为散列地址。
散列方法:选取某个函数,依该函数按关键字计算元素的存储位置,并按此存放。查找时,由同一个函数对给定值k计算地址,将k与地址单元中元素关键字进行比较,确定查找是否成功。
冲突:不同的关键字映射到同一个散列地址,即key 1≠key2,而H(key1)=H(key2),这种现象称为冲突,key1和key2互称为同义词。
②.散列函数的构造方法
直接定址法
即H(key)=a·key + b (a、b为常数)。
- 优点:这种散列函数计算简单,并且不可能有冲突发生。
- 缺点:当关键字的分布基本连续时,可用直接定址法的散列函数;否则,若关键字分布不连续将造成内存单元的大量浪费。
- 如:H(学号)=学号-201816010101
除留余数法
设散列表长度为m,用关键字key除以一个不大于m的数p,所得的余数作为散列地址的方法。
除留余数法的散列函数H(key)为:H(key)=key mod p (mod为求余运算,p≤m)
这个方法的关键是选取适当的p,一般情况下,可以选p为小于表长的最大质数。例如,表长m = 100,可取p =97。
③.处理冲突的方法
“处理冲突”的实际含义是:为产生冲突的地址寻找下一个散列地址。
开放地址法
基本方法:当冲突发生时,形成某个探测序列;按此序列逐个探测散列表中的其他地址,直到找到给定的关键字或一个空地址(开放的地址)为止,将发生冲突的记录放到该地址中。
新的散列地址的计算公式是:
Hi(key)=(H(key)+di)MOD m, i=1,2,…, k(k≤m-1)
其中:
- H(key):散列函数;
- m:散列表长度;
- di:第i次探测时的增量序列;
- Hi(key) :经第i次探测后得到的散列地址。
探测:是指寻找“下一个”空位的过程。
对增量di有三种取法:
- 线性探测法:依次循环探测d的下一个地址,即一旦冲突,就找附近(下一个)空地址存入。di = 1,2,3,… . …m-1
- 二次探测法:发生冲突时前后查找空位置 di=12,-12,22,-23,32,…±k2 (k ≤ m/2)
- 随机探测法:di是一组伪随机数列或者 di = i×H2(key)(又称双散列函数探测)
链地址法
基本思想:将具有相同散列地址的记录链成一个单链表,m个散列地址就设m个单链表,然后用一个数组将m个单链表的表头指针存储起来,形成一个动态的结构
单链表中存放的是同义词,称为同义词链表。在这种方法中,散列表每个单元中存放的不再是记录本身,而是相应同义词单链表的头指针
④.散列表的查找
哈希表的查找过程和建表过程一致,以开放定址法为例:
- 求出k的哈希地址
- 若表中此位置上为空记录,则查找失败,返回;也可将关键字等于k的记录填入
- 如果该分量不空且关键字=k,则成功返回;否则按设定的处理冲突的方法找下一地址
- 重复前两步
#include<stdio.h>
#include<malloc.h>
#include<string.h>
#define MaxSize 1000
typedef struct node
{
int key;
struct node *next;
}NodeType;
typedef struct
{
NodeType*first;
}HashTable;
HashTable ha[MaxSize];//哈希表
int keys[MaxSize];//存键值
void Insert(HashTable ha[],int m,int key)
{
int adr;
adr=key%m;//关键字%哈希表长度
NodeType *q;
q=(NodeType*)malloc(sizeof(NodeType));
q->key=key;
q->next=NULL;
//通过链接形成某地址的哈希表
if(ha[adr].first==NULL)
{
ha[adr].first=q;
}else//头插法
{
q->next=ha[adr].first;
ha[adr].first=q;
}
}
void Seek(HashTable ha[],int m,int k)
{
int i=0,adr;
adr=k%m;
//判断是哪一个地址的哈希表
NodeType *q;
q=ha[adr].first;
//q为当前地址的哈希表的头指针
while(q!=NULL)
{
i++;
if(q->key==k) break;
q=q->next;
}
if(q!=NULL)
printf("%d,%d",adr,i);
else
printf("-1");
}
int main()
{
int m,n,k;
scanf("%d",&m);//哈希表长度
scanf("%d",&n);//关键字个数
for(int i=0;i<n;i++)
{
scanf("%d",&keys[i]);//关键字集合
}
for(int i=0;i<m;i++)
{
ha[i].first=NULL;
}
for(int i=0;i<n;i++)
{
Insert(ha,m,keys[i]);///插入哈希表
}
scanf("%d",&k);
Seek(ha,m,k);
}
例:
5、总结
查找是数据处理中经常使用的一种操作。本章主要介绍了对查找表的查找,查找表实际上仅仅是一个集合,为了提高查找效率,将查找表组织成不同的数据结构,主要包括3种不同结构的查找表:线性表、树表和散列表。
( 1 )线性表的查找。主要包括顺序查找、折半查找和分块查找。
(2)树表的查找。树表的结构主要包括二叉排序树、平衡二叉树、B-树和B+树。
①二叉排序树的查找过程与折半查找过程类似。
②二叉排序树在形态均匀时性能最好,而形态为单支树时其查找性能则退化为与顺序查找相同,因此,二叉排序树最好是一棵平衡二叉树。平衡二叉树的平衡调整方法就是确保二叉排序树在任何情况下的深度均为O(logzn ),平衡调整方法分为4种:LL型、RR型、LR型和RL型。
③B-树是一种平衡的多叉查找树,是一种在外存文件系统中常用的动态索引技术。在B-树上进行查找的过程和二叉排序树类似,是一个顺指针查找结点和在结点内的关键字中查找交叉进行的过程。为了确保B-树的定义,在B-树中插人一个关键字,可能产生结点的“分裂”,而删除一个关键字,可能产生结点的“合并”。
④B+树是一种B-树的变型树,更适合做文件系统的索引。在B+树上进行随机查找、插人和删除的过程基本上与B-树类似,但具体实现细节又有所区别。
(3)散列表的查找。散列表也属线性结构,但它和线性表的查找有着本质的区别。它不是以关键字比较为基础进行查找的,而是通过一种散列函数把记录的关键字和它在表中的位置建立起对应关系,并在存储记录发生冲突时采用专门的处理冲突的方法。这种方式构造的散列表,不仅平均查找长度和记录总数无关,而且可以通过调节装填因子,把平均查找长度控制在所需的范围内。
散列查找法主要研究两方面的问题:如何构造散列函数,以及如何处理冲突。
①构造散列函数的方法很多,除留余数法是最常用的构造散列函数的方法。它不仅可以对关键字直接取模,也可在折叠、平方取中等运算之后取模。
②处理冲突的方法通常分为两大类:开放地址法和链地址法,二者之间的差别类似于顺序表和单链表的差别。
6、例题与应用
采用除留余数法实现哈希表的创建,任意采用一种处理冲突的方法解决冲突,计算哈希表的平均查找长度。编程实现以下功能:
已知一组关键字(19,14,23,1,68,20,84,27,55,11,10,79),哈希函数定义为:H(key)=key MOD 13, 哈希表长为m=16。实现该哈希表的散列,并计算平均查找长度(设每个记录的查找概率相等)。
(1)哈希表定义为定长的数组结构;
(2)使用线性探测再散列或链地址法解决冲突;
(3)散列完成后在屏幕上输出数组内容或链表;
(4)输出等概率查找下的平均查找长度;
(5)完成散列后,输入关键字完成查找操作,要分别测试查找成功与查找不成功两种情况。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 13
#define Hashsize 16
int sign = 2;
typedef struct Hash
{
int date;
int sign;
} HashNode;
//线性冲突处理
void compare(HashNode H[], int p, int i, int key[])
{
p++;
if (H[p].sign != 0)
{
sign++;
compare(H, p, i, key);
}
else
{
H[p].date = key[i];
H[p].sign = sign;
sign = 2;
}
}
void Hashlist(HashNode H[], int key[])
{
int p;
for (int i = 0; i < 12; i++)
{
p = key[i] % N;
if (H[p].sign == 0)
{
H[p].date = key[i];
H[p].sign = 1;
}
else
compare(H, p, i, key);
}
}
//查找冲突处理
int judge(HashNode H[], int num, int n)
{
n++;
if (n >= Hashsize)
return 0;
if (H[n].date == num)
{
printf("位置\t数据\n");
printf("%d\t %d\n\n", n, H[n].date);
return 1;
}
else
{
judge(H, num, n);
}
}
//查找
int search(char num, HashNode H[])
{
int n;
n = num % N;
if (H[n].sign == 0)
{
printf("查找失败!");
return 0;
}
if (H[n].sign != 0 && H[n].date == num)
{
printf("位置\t数据\n");
printf("%d\t %d\n", n, H[n].date);
}
else if (H[n].sign != 0 && H[n].date != num)
{
if (judge(H, num, n) == 0)
return 0;
}
return 1;
}
int main(void)
{
int key[N] = {19, 14, 23, 1, 68, 20, 84, 27, 55, 11, 10, 79};
float a = 0;
HashNode H[Hashsize];
for (int i = 0; i < Hashsize; i++)
H[i].sign = 0;
Hashlist(H, key);
printf("建立好的哈希表如下所示:\n位置\t\t数据\n");
for (int i = 0; i < Hashsize; i++)
{
if (H[i].sign != 0)
{
printf("%d\t-->\t%d\n", i, H[i].date);
}
else
{
H[i].date = 0;
printf("%d\t-->\t%d\n", i, H[i].date);
}
}
int num;
printf("输入查找数值(输入-1退出):\n");
for (int i = 0;; i++)
{
scanf("%d", &num);
if (num == -1)
break;
if (search(num, H) == 0)
printf("该数值不存在!\n");
}
for (int i = 0; i < Hashsize; i++)
{
a = a + H[i].sign;
}
printf("平均查找长度:%0.2f\n", a / 12);
return 0;
}