查找
一、查找概论
- 查找表:由同一类型的数据元素(或记录)构成的集合
- 关键字:数据元素中某个数据项的值,又称键值,用它可以标识一个数据元素,也可以标识一个记录的某个数据项(字段),称为关键码
- 主关键字:此关键字可以唯一地标识一个记录
- 次关键字:可以识别多个数据元素(或记录)的关键字
- 查找:根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)
查找表按照操作方式可分为:静态查找表与动态查找表
- 静态查找表:只作查找操作的查找表
- 动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素
二、顺序表查找
顺序查找(静态查找):从表中第一个或最后一个记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值都不等时,则表中没有所查的记录,查找不成功。
顺序表查找算法
//顺序查找,a为数组,n为要查找的数组长度,key为要查找的关键字
int Sequential_Search(int *a,int n,int key)
{
int i;
for(i=1;i<=n;i++)
{
if(a[i]==key)
{
return i;
}
}
return 0;
}
顺序查找表优化
//增加哨兵顺序查找
int Sequential_Search2(int *a,int n,int key)
{
int i;
a[0]=key;//设置a[0]为关键字值,称为哨兵
i=n;//循环从数组尾部开始
while(a[i]!=key)
{
i--;
}
return i;
}
三、有序表查找
(1)折半查找
折半查找(二分查找):前提是线性表中的记录必须是关键码有序,线性表必须采用顺序存储。
基本思想:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所查找区域无记录,查找失败。
假设有序表数组{0,1,16,24,35,47,,59,62,73,88,99},查找是否存在62
int Binary_Search(int *a,int n,int key)
{
int low,high,mid;
low=1;//定义最低下标为记录首位
high=n;//定义最高下标为记录末位
while(low<=hihg)
{
mid=(low+high)/2;
if(key<a[mid])
{
high=mid-1;
}
else if(key>a[mid])
{
low=mid+1;
}
else
{
return mid;
}
}
return 0;
}
(2)插值查找
插值查找:根据要查找的关键字key与查找表中最大最小记录的关键字比较后的查找方法
核心公式:(key-a[low])/(a[high]-a[low])
int Binary_Search(int *a,int n,int key)
{
int low,high,mid;
low=1;//定义最低下标为记录首位
high=n;//定义最高下标为记录末位
while(low<=hihg)
{
mid=low+(high-low)*(key-a[low])/(a[high]-a[low]);//插值
if(key<a[mid])
{
high=mid-1;
}
else if(key>a[mid])
{
low=mid+1;
}
else
{
return mid;
}
}
return 0;
}
(3)斐波那契查找
int Fibonacci_Search(int *a,int n,int key)
{
int low,high,mid,i,k;
low=1;//定义最低下标为记录首位
high=n;//定义最高下标为记录末位
k=0;
while(n>F[k]-1)//计算n位于斐波那契数列的位置
{
k++;
}
for(i=n;i<F[k]-1;i++)//将不满的数值补全
{
a[i]=a[n];
}
while(low<=high)
{
mid=low+F[k-1]-1;//计算当前分隔的下标
if(key<a[mid])//若查找记录小于当前分割记录
{
high=mid-1;//最高下标调整到分割下标mid-1处
k=k-1;//斐波那契数列下标减一位
}
else if(key>a[mid])//若查找记录大于当前分割记录
{
low=mid+1;//最低下标调整到分割下标mid+1处
k=k-2;//斐波那契数列下标减两位
}
else
{
if(mid<=n)
{
return mid;//若相等则说明mid即为查找到的位置
}
else
{
return n;//若mid>n说明是补全数值,返回n
}
}
}
return 0;
}
四、线性索引查找
索引:把一个关键字与它对应的记录相关联的过程
索引按结构可分为线性索引、树形索引与多级索引
线性索引:将索引项集合组织为线性结构,也称为索引表
三种线性索引:稠密索引、分块索引、倒排索引
(1)稠密索引
稠密索引:在线性索引中,将数据集中的每个记录对应一个索引项,索引项一定是按照关键码有序的排列
(2)分块索引
分块索引:是把数据集的记录分成了若干块,并且这些快需要满足两个条件
- 块内无序:即每一块的记录不要求有序。
- 块间有序
定义的分块索引的索引项结构分三个数据项
- 最大关键码存储每一块中的最大关键字
- 存储了块中的记录个数,以便于循环时使用
- 用于指向块首数据元素的指针,便于开始对这一块中记录进行遍历
(3)倒排索引
索引项的通用结构:
- 次关键码
- 记录号表:存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者该记录的主关键字)
五、二叉排序树
二叉排序树(二叉查找树),是一棵空树或者是具有下列性质的二叉树
- 若其左子树不空,则左子树上所有结点的值均小于它的根结构的值
- 若其右子树不空,则右子树上所有结点的值均大于它的根结点的值
- 其左右子树也分别为二叉排序树
(1)二叉排序树查找操作
二叉树结构
//二叉树的二叉链表结点结构定义
typedef struct BiTNode//结点结构
{
int data;//结点数据
struct BiTNode *lchild,*rchild;//左右孩子指针
}BiTNode,*BiTree;
二叉排序树的查找
//递归查找二叉排序树T中是否存在key
//指针f指向T的双亲,其初始调用值为NULL
//若查找成功,则指针p指向该数据元素结点,并返回TRUE
//否则指针p指向查找路径上访问的最后一个结点并返回FALSE
Status SearchBST(BiTree T,int key,BiTree f,BiTree *p)
{
if(!T)//查找不成功
{
*p=f;
return FALSE;
}
else if(key==T->data)//查找成功
{
*p=T;
return TRUE;
}
else if(key<T->data)
{
return SearchBST(T->lchild,key,T,p);//在左子树继续查找
}
else
{
return SearchBST(T->rchild,key,T,p);//在右子树继续查找
}
}
(2)二叉排序树插入操作
//当二叉排序树T中不存在关键字等于key的数据元素时
//插入key并返回TRUE,否则返回FALSE
Status InsertBST(BiTree *T,int key)
{
BiTree p,s;
if(!SearchBST(*T,key,NULL,&p))//查找不成功
{
s=(BiTree)malloc(sizeof(BiTNode));
s->data=key;
s->lchild=s->rchild=NULL;
if(!p)
{
*T=s;//插入s为新的根结点
}
else if(key<p->data)
{
p->lchild=s;//插入s为左孩子
}
else
{
p->rchild=s;//插入s为右孩子
}
return TRUE;
}
else
{
return FALSE;//树中已有关键字相同的结点,不再插入
}
}
(3)二叉排序树删除操作
//若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点
//并返回TRUE,否则返回FALSE
Status DeleteBST(BiTree *T,int key)
{
if(!*T)//不存在关键字等于key的数据元素
{
return FALSE;
}
else
{
if(key==(*T)->data)//找到关键字等于key的数据元素
{
return Delete(T);
}
else if(key<(*T)->data)
{
return DeleteBST(&(*T)->lchild,key);
}
else
{
return DeleteBST(&(*T)->rchild,key);
}
}
}
Delete代码
//从二叉排序树中删除结点p,并重接它的左或右子树
Status Delete(BiTree *p)
{
BiTree q,s;
if((*p)->rchild==NULL)//右子树空则需要重接它的左子树
{
q=*p;
*p=(*p)->lchild;
free(q);
}
else if((*p)->lchild==NULL)//只需要重接其右子树
{
q=*p;
*p=(*p)->rchild;
free(q);
}
else//左右子树均不空
{
q=*p;
s=(*p)->lchild;
while(s->rchild)//转左,然后向右到尽头(找待删结点的前驱)
{
q=s;
s=s->rchild;
}
(*p)->data=s->data;//s指向被删结点的直接前驱
if(q!=*p)
{
q->rchild=s->lchild;//重接q的右子树
}
else
{
q->lchild=s->lchild;//重接q的左子树
}
free(s);
}
return TRUE;
}
六、平衡二叉树(AVL树)
平衡二叉树:是一种二叉排列树,其中每一个节点的左子树和右子树的高度差至多等于1
平衡因子BF:二叉树上结点的左子树深度减去右子树深度的值
最小不平衡子树:距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树
(1)平衡二叉树实现算法
改进二叉排列树的结点结构,增加一个bf,用于存储平衡因子
//二叉树的二叉链表结点结构定义
typedef struct BiTNode//结点结构
{
int data;//结点数据
int bf;//结点的平衡因子
struct BiTNode *lchild,*rchild;//左右孩子指针
}BiTNode,*BiTNode;
右璇操作代码
//对以p为根的二叉排序树作右旋操作
//处理之后p指向新的树根结点,即旋转处理之前的左子树的根结点
void R_Rotate(BiTree *p)
{
BiTree L;
L=(*p)->lchild;//L指向p的左子树根结点
(*p)->lchild=L->rchild;//L的右子树挂接为p的左子树
L->rchild=(*p);
*p=L;//p指向新的根结点
}
左旋操作代码
//对以p为根的二叉排序树作左旋操作
//处理之后p指向新的树根结点,即旋转处理之前的右子树的根结点0
void L_Rotate(BiTree *p)
{
BiTree R;
R=(*p)->lchild;
(*p)->lchild=R->lchild;
R->lchild=(*p);
*p=R;
}
左平衡旋转处理的函数代码
#define LH+1//左高
#define EH 0;//等高
#define RH-1;//右高
//对以指针T所指结点为根的二叉树作左平衡旋转处理
//本算法结束时,指针T指向新的根结点
void LeftBalance(BiTree *T)
{
BiTree L,Lr;
L=(*T)->rchild;//L指向T的左子树根结点
switch(L->bf)
{
//检查T的左子树的平衡度,并作相应平衡处理
case LH;//新结点插入在T的左孩子的左子树上,要作单右旋处理
{
(*T)->bf=L=>bf=EH;
R_Rotata(T);
break;
}
case R
Lr=L->rchild;//Lr指向T的左孩子的右子树根
switch(Lr->bf)//修改T及其左孩子的平衡因子
{
case LH:(*T)->bf=RH;
{
L->bf=EH;
break;
}
case EH:(*T)->bf=L->bf=EH;
{
break;
}
case RH:(*T)->bf=EH;
{
L->bf=LH;
break;
}
}
Lr->bf=EH;
L_Rotate(&(*T)->lchild);//对T的左子树作左旋平衡处理
R_Rotate(T);//对T做右旋平衡处理
}
}
}
主函数
//若在平衡的二叉排序树T中不存在和e有相同关键字的结点,则插入一个
//数据元素为e的新结点并返回1,否则返回0.若因插入而使二叉排序树失去平衡,则作平衡旋转处理,布尔变量taller反映T长高与否
Status InsertAVL(BiTree *T,int e,Status *taller)
{
if(!*T)
{
//插入新结点,树“长高”,置taller为TRUE
*T=(BiTree)malloc(sizeof(BiTNode));
(*T)->data=e;
(*T)->data=e;
(*T)->lchild=(*T)->rchild=NULL;
(*T)->bf=EH;
&taller=TRUE;
}
else
{
if(e==(*T)->data)
{
//树中已存在和e有相同关键字的结点则不再插入
*taller=FALSE;
return FALSE;
}
if(e<(*T)->data)
{
//应继续在T的左子树中进行搜索
if(!InsertAVL(&(*T)->lchild,e,taller))//为插入
{
return FALSE;
}
if(*taller)//已插入到T的左子树中且左子树“长高”
{
switch((*T)->bf)//检查T的平衡度
{
case LH://原本左子树比右子树高,需要作左平衡处理
{
LeftBalance(T);
*taller=FALSE;
break;
}
case EH://原本左右子树等高,现因左子树增高而树增高、
{
(*T)->bf=LH;
*taller=TRUE;
break;
}
case RH://原本右子树比左子树高。现左右子树等高
{
(*T)->bf=EH;
*taller=FALSE;
break;
}
}
}
else
{
//应继续在T的右子树中进行搜索
if(!InsertAVL(&(*T)->rchild,e,taller))//为插入
{
return FALSE;
}
if(*taller)//已插入到T的右子树“长高”
{
switch((*T)->bf)//检查T的平衡度
{
case LH://原本左子树比右子树高,现左右子树等高
{
(*T)->bf=EH;
*taller=FALSE;
break;
}
case EH://原本左右子树等高,现因右子树增高而树增高、
{
(*T)->bf=RH;
*taller=TRUE;
break;
}
case RH://原本右子树比左子树高。需要作右平衡处理
{
RightBalance(T);
*taller=FALSE;
break;
}
}
}
}
}
return TRUE;
}
七、多路查找树(B树)
多路查找树其每个结点的孩子树可以多于两个,且每个结点处可以存储多个元素
(1)2-3树
2-3树:其中的每个结点都具有两个孩子(2结点)或三个孩子(3结点)
- 一个2结点包含一个元素和两个孩子(或没有孩子)
- 一个3结点包含一小一大两个元素和三个孩子(或没有孩子)
2-3树的插入与删除实现
(2)2-3-4树
一个4结点包含小中大三个元素和四个孩子(或没有孩子)
(3)B树
B树是一种平衡的多路查找树。结点最大的孩子数目称为B树的阶。
(4)B+树
八、散列表查找(哈希表)概述
(1)散列表查找定义
散列技术:在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。对应关系f称为散列函数,又称哈希函数。
散列表(哈希表):采用散列技术将记录存储在一块连续的存储空间中
(2)散列表查找步骤
- 在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录
- 当查找记录时,通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录
冲突:两个关键字可key1不等于key2,但是f(key1)-f(key2)
key1与key2称为此散列函数的同义词
(3)散列函数的构造方法
-
直接定址法:取关键字的某个线性函数值为散列地址,即f(key)=a*key+b
-
数字分析法
-
平方取中法
-
折叠法
-
除留余数法
-
随机数法
(4)处理散列冲突的方法
- 开放定址法:一旦发生冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入
- 公式:Fi(key)=(F(key)+di)MODm(di=1,2,3,…,m-1)
- 再散列函数法
- 链地址法
- 公共溢出区法
(5)散列表查找实现
定义散列表结构及相关常数
HashTable为散列表结构,elem为一个动态数组
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12//定义散列表为数组的长度
#define NULLKEY -32768
typedef struct
{
int *elem;//数据元素存储基质,动态分配数组
int count;//当前数组元素个数
}HashTable;
int m=0;//散列表表长,全局变量
散列表初始化
//初始化散列表
Status InitHashTable(HashTable *H)
{
int i;
m=HASHSIZE;
H->count=m;
H->elem=(int*)malloc(m*sizeof(int));
for(i=0;i<m;i++)
{
H->elem[i]=NULLKEY;
}
return OK;
}
定义散列函数
//散列函数
int Hash(int key)
{
return key&m;//除留余数法
}
对散列表进行插入
//插入关键字进散列表
void InsertHash(HashTable *H,int key)
{
int addr=Hash(key);//求散列地址
while(H->elem[addr]!=NULLKEY)//如果不为空,则冲突
{
addr=(addr+1)%m;//开放定地址的线性探测
}
H->elem[addr]=key;//直到有空位后插入关键字
}
散列表查找关键字
Status SearchHash(HashTable H,int key,int *addr)
{
*addr=Hash(key);//求散列地址
while(H.elem[*addr]!=key)//如果不为空,则冲突
{
*addr=(*addr+1)%m;//开发定地址的线性探测
if(H.elem[*addr]==NULLKEY||*addr==Hash(key))
{
//如果循环回到原点
return UNSUCCESS;//则说明关键字不存在
}
}
return SUCCESS;
}