一、查找的基本概念
被查找的对象是由一组记录组成的表或文件,而每个记录则由若干个数据项组成,并假设每个记录都有一个能唯一标识该记录的关键字。
在这种条件下,查找的定义是:给定一个值k,在含有n个记录的表中找出关键字等于k的记录。
若找到,则查找成功,返回该记录的信息或该记录在表中的位置;否则查找失败,返回相关的指示信息。
若在查找的同时对表做修改运算(如插入和删除),则相应的表称之为动态查找表,否则称之为静态查找表。
由于查找运算的主要运算是关键字的比较,所以通常把查找过程中对关键字需要执行的平均比较次数(也称为平均查找长度)作为衡量一个查找算法效率优劣的标准。
平均查找长度ASL(Average Search Length)定义为:
其中,n是查找表中记录的个数。pi是查找第i个记录的概率,一般地,均认为每个记录的查找概率相等,即pi=1/n(1≤i≤n),ci是找到第i个记录所需进行的比较次数。
平均查找长度分为成功情况下的平均查找长度和不成功情况下的平均查找长度。
二、线性表的查找
在表的组织方式中,线性表是最简单的一种。三种在线性表上进行查找的方法:
(1)顺序查找(2) 二分查找(3)分块查找
因为不考虑在查找的同时对表做修改,故上述三种查找操作都是在静态查找表上实现的。
查找与数据的存储结构有关,线性表有顺序和链式两种存储结构。本节只介绍以顺序表作为存储结构时实现的顺序查找算法。
定义被查找的顺序表类型定义如下:
#define MAXL <表中最多记录个数>typedef struct
{ KeyType key; //KeyType为关键字的数据类型
InfoType data; //其他数据
} NodeType;
typedef NodeType SeqList[MAXL]; //顺序表类型
(1) 顺序查找
顺序查找是一种最简单的查找方法。
思路:从表的一端开始,顺序扫描线性表,依次将扫描到的关键字和给定值k相比较,
若当前扫描到的关键字与k相等,则查找成功;若扫描结束后,仍未找到关键字等于k的记录,则查找失败。
顺序查找的算法如下(在顺序表R[0..n-1]中查找关键字为k的元素,成功时返回找到的元素的逻辑序号,失败时返回0):
int SeqSearch(SeqList R,int n,KeyType k)
{ int i=0;
while (i<n && R[i].key!=k) //从表头往后找
i++;
if (i>=n) //未找到返回0
return 0;
else
return i+1; //找到返回逻辑序号i+1
}
从顺序查找过程可以看到(不考虑越界比较i<n),ci取决于所查记录在表中的位置。如查找表中第1个记录R[0]时,仅需比较一次;而查找表中最后一个记录R[n-1]时,需比较n次,即ci=i。因此,成功时的顺序查找的平均查找长度为:
查找成功时的平均比较次数约为表长的一半 。
二、二分查找
二分查找也称为折半查找,要求线性表中的节点必须己按关键字值的递增或递减顺序排列,即有序表。
思路:首先用要查找的关键字k与中间位置的节点的关键字相比较,这个中间节点把线性表分成了两个子表。
若比较结果相等则查找完成;若不相等,再根据k与该中间节点关键字的比较大小确定下一步查找哪个子表,这样递归进行下去,直到找到满足条件的节点或者该线性表中没有这样的节点。
其算法如下(在有序表R[0..n-1]中进行折半查找,成功时返回元素的逻辑序号,失败时返回0):
int BinSearch(SeqList R,int n,KeyType k)
{ int low=0,high=n-1,mid;
while (low<=high) //当前区间存在元素时循环
{ mid=(low+high)/2;
if (R[mid].key==k)//查找成功返回其逻辑序号mid+1
return mid+1;
if (R[mid].key>k) //继续在R[low..mid-1]中查找
high=mid-1;
else
low=mid+1; //继续在R[mid+1..high]中查找
}
return 0;
}
二分查找过程可用二叉树来描述,称为描述二分查找的判定树或比较树。
方法:把当前查找区间的中间位置上的记录作为根;左子表和右子表中的记录分别作为根的左子树和右子树。
例,对于给定11个数据元素的有序表{2,3,10,15,20,25,28,29,30,35,40},采用二分查找,试问:
(1)若查找给定值为20的元素,将依次与表中哪些元素比较?
(2)若查找给定值为26的元素,将依次与哪些元素比较?
(3)假设查找表中每个元素的概率相同,求查找成功时的平均查找长度和查找不成功时的平均查找长度。
解:
(1)若查找给定值为20的元素,依次与表中25,10,15,20元素比较,共比较4次。
(2)若查找给定值为26的元素,依次与25,30,28元素比较,共比较3次。
(3)在查找成功时,会找到图中某个圆形节点,则成功时的平均查找长度:
在查找不成功时,会找到图中某个方形节点,则不成功时的平均查找长度:
三、索引存储结构和分块查找
1.索引存储结构
索引存储结构=数据表+索引表
索引表中的每一项称为索引项,索引项的一般形式是:(关键字,地址)
关键字唯一标识一个节点,地址作为指向该关键字对应节点的指针,也可以是相对地址。
2. 分块查找
性能:介于顺序查找和二分查找之间的查找方法。
思路:
(1)将数据表R[0..n-1]均分为b块,前b-1块中记录个数为s=n/b,最后一块即第b块的记录数小于等于s;
(2)每一块中的关键字不一定有序,但前一块中的最大关键字必须小于后一块中的最小关键字,即要求表是“分块有序”的;
(3)抽取各块中的最大关键字及其起始位置构成一个索引表IDX[0..b-1],即IDX[i](0≤i≤b-1)中存放着第i块的最大关键字及该块在表R中的起始位置。由于表R是分块有序的,所以索引表是一个递增有序表。
索引表的数据类型定义如下:
#define MAXI <索引表的最大长度>typedef struct
{ KeyType key; //KeyType为关键字的类型
int link; //指向对应块的起始下标
} IdxType;
typedef IdxType IDX[MAXI]; //索引表类型
采用二分查找索引表的分块查找算法如下(索引表I的长度为m):
int IdxSearch(IDX I,int b,SeqList R,int n,KeyType k)
{ int low=0,high=m-1,mid,i;
int s=n/b; //s为每块的元素个数,应为n/b的向上取整
while (low<=high) //在索引表中进行折半查找,找到位置high+1
{ mid=(low+high)/2;
if (I[mid].key>=k)
high=mid-1;
else
low=mid+1;
} //应在索引表的high+1块中,再在线性表中进行顺序查找
i=I[high+1].link;
while (i<=I[high+1].link+s-1 && R[i].key!=k)
i++;
if (i<=I[high+1].link+s-1)
return i+1; //查找成功,返回该元素的逻辑序号
else
return 0; //查找失败,返回0
}
若以二分查找来确定块,则分块查找成功时的平均查找长度为:
若以顺序查找确定块,则分块查找成功时的平均查找长度为:
例,对于具有10000个元素的文件。
(1)若采用分块查找法查找,并顺序查找来确定元素所在的块,则分成几块最好?每块的最佳长度为多少?此时的平均查找长度为多少?
(2)若采用分块查找法查找,假定每块长度为s=20,此时的平均查找长度是多少?
(3)若直接采用顺序查找和折半查找,其平均查找长度各是多少?
解:(1)对于具有10000个元素的文件,若采用分块查找法查找,并顺序查找来确定元素所在的块。
每块中最佳元素个数:s=10000的平方根=100,总的块数b=n/s=100。
此时有:ASL=100+1=101
若采用分块查找法查找,并用折半查找确定块,平均查找长度为:
ASL=log2101+50=57
(2)s=20,则b=n/s=10000/20=500。
在进行分块查找时,若用顺序查找确定块,则有:ASL=260+1=261
在进行分块查找时,若用折半查找确定块,则有:ASLlog2501+10=19
(3)若直接采用顺序查找,则有:ASL=(10000+1)/2=5000.5;若直接采用折半查找,则有:ASL=log210001-1=13
由此可见,分块查找算法的效率介于顺序查找和折半查找之间。