查找

来源:互联网 发布:2016国产网络剧排行榜 编辑:程序博客网 时间:2024/04/29 18:49

一、查找的基本概念


          查找又称检索,是信息处理领域的一项十分重要的功能,它同人们的日常工作和生活有着密切的联系。

          对于一个查找算法的时间复杂度,既可以采用数量级的形式表示,也可以采用平均查找长度来表示。平均查找长度就是在整个查找过程中需要访问元素的平均个数,即用来衡量在查找路径上需要同多少元素进行比较后才能查找成功。平均查找长度计算公式为:

ASL=∑(下标是i=1,上面是n)PiCi

       其中,n为查找表的长度,即表中所含元素的个数,Pi为查找第i个元素的概率,查找全部n个元素的概率之和恒定值1,若不特别指明,均认为查找每个元素概率相同,即P1=P2=......=Pn=1/n,Ci是查找第i个元素时与给定值X所需比较元素的个数。若查找每个元素的概率相同,则平均查找长度的计算公式可简化为:

ASL=1/n∑(下标是i=1,上面是n)Ci


二、顺序表查找


1、顺序查找


        顺序查找是一种最简单和最基本的查找方法。它从顺序表的一端开始,依次将每个元素值同给定值x进行比较,若某个元素值等于给定值x,则表明查找成功,返回该元素所在的下标;若直到所有n个元素都比较完毕,仍找不到与x相等的元素,则表明查找失败,返回特定值,假定用-1表示。

         顺序查找的算法比较简单,用Java语言描述为:

  

public static int sequenceSearch(Object [] a, Object x,int n){//从数组a的前n个元素中顺序查找出值为x的元素int i;//从表头元素a[0]开始顺序向后查找,查找成功则退出循环for(i=0;i<n;i++){if(a[i].equals(x)){break;}}if(i<n){return i;}else{return -1;}}

        对顺序查找算法的一个改进,可在表的尾端设置一个“岗哨”,即在查找之前把给定值x赋给数组a中下标为n的元素位置,这样每循环一次只需要进行元素比较,不需要进行下标是否越界的检查,当比较到下标为n的元素位置时,由于a[n]等于x必然成立,将自动退出循环。改进后的算法描述为:

public static int sequenceSearch(Object [] a, Object x,int n){//从数组a的前n个元素中顺序查找出值为x的元素a[n]=x;int i;//从表头元素a[0]开始顺序向后查找,查找成功则退出循环for(i=0;i<n;i++){if(a[i].equals(x)){break;}}if(i<n){return i;}else{return -1;}}

        由于改进后的算法省略了对下标越界的检查,所以必定能够提高算法的实际执行速度。当然在改进后的算法中,数组a的长度a.length要大于等于n+1.

        顺序查找的缺点是速度较慢,查找成功最多需要比较全部n个元素,平均查找长度为(1+2+3+4+。。。+n)/n=(n+1)/2次,约为表长度n的一半,查找失败也需比较n+1次(即i从0取值到n),所以顺序查找的时间复杂度为O(n)。


2、二分查找


     二分查找又称折半查找。作为二分查找对象的数据表必须是顺序存储的有序表,通常假定有序表是按元素的关键字从小到大有序,即若关键字为数值,则按数值有序,若关键字为字符或字符串数据,则按照国际双字节编码有序。二分查找的过程是:首先取整个有序表a[0]~a[n-1]的中点元素a[min](其中min=(n-1)/2)同给定值x比较,若相等,则查找成功,返回该元素的下标mid;否则,若a[mid]>x成立,表明若存在对应的元素,则该元素只可能落在左子表a[0]~a[mid-1]中,接着只要在左子表中继续进行二分查找即可;若a[mid]<x成立,表明若存在对应的元素,则该元素只可能落在右子表a[mid+1]~a[n-1]中,接着只要在右子表中继续进行二分查找即可;这样,经过一次比较,就将缩小一半的查找空间,如此进行下去,直到找到与给定值x相等的元素,或者当前查找区间为空(即表明查找失败)为止。

        二分查找的算法描述为:

public static int binarySearch(Object [] a,Object x,int n){//从数组a的前n个元素中二分查找给定值为x的元素int low=0,high=n-1;                //给表示待查区间上界和下界的变量赋值while(low<=high){int mid=(low+high)/2;          //求出待查区间内中点元素的下标if(((Comparable)a[mid]).compareTo(x)==0){return mid;                //查找成功返回元素的下标}else if(((Comparable)a[mid]).compareTo(x)>0){high=mid-1;                //修改区间下界,将在右子表上继续查找}else {low=mid+1;                 //修改区间下界,将在右子表上继续查找}}return -1;                         //查找失败返回-1}

        二分查找过程可用一棵二叉树来描述,树中的每个根结点对应当前查找区间的中点元素a[mid],它的左子树和右子树分别对应该区间在左子表和右子表,通常把此二叉树称为二分查找的判定树。由于二分查找是在有序表上进行的,所以其对应的判定树必然是一棵二叉搜索树。

        进行二分查找的判定树不仅是一棵二叉搜索树,而且是一棵理性平衡树,因为除最后一层外,其余所有层的结点数都是满的,所以判定树的高度h和结点数n之间的关系为:

h=log2n(向下取整)+1或h=log2(n+1)(向上取整)

       这就告诉我们,二分查找成功时,同元素进行比较的次数最多为判定树的高度h,在查找每个元素等概率的情况下,同元素的平均比较次数要略低于h,所以二分查找算法的时间复杂度为O(log2n)。显然二分查找比顺序查找的速度要快得多。

        二分查找的优点是比较次数少,查找速度快,但在查找之前要为建立有序表付出代价,同时对有序表的插入和删除都需要平均比较和移动表中的一半元素,是很浪费时间的操作,所以,二分查找适用于数据相对稳定、很少进行插入和删除运算的情况。另外,二分查找只适用于顺序存储的有序表,不适用于链接存储的有序表。


三、索引查找


1、索引的概念

         索引查找又称分级查找。索引查找是在具有索引存储结构的数据表上进行的查找。索引存储结构的方法是:首先把一个集合或线性表按照一定的函数关系或条件划分成若干个不同的子表,为每个子表对应建立一个索引项,由所有这些索引构成项构成对主表(即原集合或线性表)的一个索引表,然后,可采用顺序或链接的方法来存储索引表和主表中的每个子表。索引表中的每个索引项通常包含3个域:一是索引值域,用来存储对应子表的索引值,它相当于记录元素的关键字,在索引表中由此索引值来唯一标识一个索引项,亦即唯一标识一个子表;二是子表的开始位置域,用来存储对应子表的第一个元素的存储位置;三是子表长度域,用来存储对应子表的长度,即所包含的元素个数,此域不是必须的,可以根据情况取舍。索引项的类型可定义为:

public class IndexItem {Object index;                     //索引值的定义int start;                        //子表中第一个元素所在的下标位置int length;                       //子表的长度域public IndexItem(Object ind,int sta,int len){index=ind;start=sta;length=len;}}

        这里假定所有子表(合称为主表)被顺序或链接存储在同一个数组中,每个元素的存储位置就是其元素的下标值,所以子表的开始位置域start的类型被定义为int。


2、索引的引用举例

        例如,一个学校的教师登记表如表10-1所示,若以每个教师记录的职工号作为关键字,则此线性表(假定用LA表示)可简记为:

LA=(JS001,JS002,JS003,JS004,DZ001,DZ002,DZ003,JJ001,JJ002,HG001,HG002,HG003)

     

        若按照单位数据项的值(或关键字中的前两位字符)对表LA进行划分,使得具有相同值的元素分类到同一个子表中,则得到的4个子表分别为:

JS=(JS001,JS002,JS003,JS004)

DZ=(DZ001,DZ002,DZ003)

JJ=(JJ001,JJ002)

HG=(HG001,HG002,HG003)

        若使用一维数组a来顺序存储这4个子表,在实际存储时,可以在每个子表的后面预留一些空闲位置,待向子表中插入新元素之用,在这里假定预留两个空闲位置,则JS,DZ,JJ,HG子表在数组a中的开始下标位置应依次为0,6,11和15。根据这种划分所建立的索引表如表10-2所示。


        还可以按照职称数据项的值进行划分,使得具有相同职称的教师记录被组织在同一个子表中,则得到的4个子表分别为:

   JSH=(JS001,HG001)

   FJS=(JS004,DZ003,HG002)

   JIA=(JS002,JS003,DZ002,JJ001,JJ002)

   ZHU=(DZ001,HG003)

   。。。。。。


       在索引存储结构中,若索引表中每个索引项对应主表中的多条记录,则称为稀疏索引;若每个索引项唯一对应主表中的一条记录,则称为稠密索引。

       在计算机存储系统中,若存储原始数据记录的主文件时无序的,即记录不是按照关键字有序排列的,则一级索引(即对主文件进行的索引)必须使用稠密索引,并且通常使索引表按关键字有序;若主文件是有序的,则一级索引应稀疏索引,每个索引项对应主表中若干条记录,每个索引项中的索引值要大于等于对应一组记录的最大关键字,同时要小于下一个索引项所对应一组记录的最小关键字,显然这种稀疏索引也是按索引值有序的。若在文件存储中使用二级或二级以上索引,则相应的索引表均应为稀疏索引。

       在访问一个采用索引存储结构的文件时,首先是把整个索引表读入到内存中,以便能够利用顺序或二分查找方法快速地查找出给定索引值对应的一组记录的开始存储位置,然后再从主文件的相应子表中查找出给定关键字的记录。


3、索引查找算法

        索引查找是在索引表和主表上进行的查找。索引查找的过程是:首先根据给定的索引值K1,在索引表上查找出索引值等于K1的索引项,以确定对应子表在主表中的开始位置和长度,然后再根据给定值K2,在对应的子表中查找出等于K2的元素(结点)。对索引表或子表进行查找时,若表示顺序存储的有序表,则既可进行顺序查找,也可进行二分查找,否则只能进行顺序查找。

       设数组a是一个索引存储结构中的主表,保存着所有子表中的元素,数组b是建立在主表a上的一个索引表,m为索引表的实际长度,即所含的索引项的个数,它要小于等于数组b的长度b.length,k1和k2分别为给定待查找的索引值和元素值,当然它们的实际类型分别为索引表中索引值域的类型和主表的类型,并假定每个子表采用顺序存储,则索引查找算法描述为:

public static int indexSearch(Object [] a, IndexItem[] b,int m,Object k1,Object k2){//利用主表a和大小为m的索引表b索引查找值为k1,元素值为k2的记录int i,j;//在索引表中顺序查找索引值为k1的索引项for(i=0;i<m;i++){if(k1.equals(b[i].index)){break;}}//若i等于m则表明查找失败,返回-1if(i==m){return -1;}//在已经查找到的第i个子表中顺序查找元素为k2的记录j=b[i].start;while(j<b[i].start+b[i].length){if(k2.equals(a[j])){break;}else{j++;}}//若查找成功则返回元素在主表中的下标位置,否则返回-1if(j<b[i].start+b[i].length){return j;}else{return -1;}}

       若每个子表在主表a中采用的是以元素下标为地址的链接存储,则只要把上面算法中的while循环和其后的if语句替换为如下的while循环和返回语句即可:


       如索引表b为稠密索引,则算法更为简单,只要在参数表中给出索引表参数b、索引表长度参数m和待查的元素值参数k即可,而在算法中只需要利用k的关键字值查找索引表b,并当查找成功时返回b[i].start的值,失败时返回-1即可。     

        索引查找的比较次数等于算法中查找索引表的比较次数和查找相应子表的比较次数之和。假定索引表的长度为m,每个子表的平均长度为s,则索引查找的平均长度为:

ASL=(1+m)/2+(1+s)/2=1+(m+s)/2

        索引查找的速度快于顺序查找,而慢于二分查找。在主表被等分为√n(根号n)个子表的条件下,其时间复杂度为O√n

        若在主表中的每个存储子表后都预留空闲位置,则索引存储也便于进行插入和删除运算,因为其运算过程只涉及索引表和相应的子表,只需要对相应子表中的元素进行比较和移动,与其他任何子表无关,不像顺序表那样需要涉及整个表中的所有元素,即牵一发而动全身。


4、分块查找

        分块查找属于索引查找。它要求主表中每个子表(子表又称为块)之间是递增有序的,即前块中的最大关键字必须小于后块中的最小关键字,或者说后块中的最小关键字必须大于前块中的最大关键字,但每个块中元素的排列次序可以是任意的;它还要求索引表中的每个索引项的索引值域用来存储对应块中的最大关键字。由分块查找对主表和索引表的要求可知:索引表是按索引值递增有序的,即索引表是一个有序表;主表中的关键字域和索引表中的索引值域具有相同的数据类型,即为关键字所属的类型。

       图10-4 就是一个分块查找的示例,主表被分为3块,每块都占有5个记录位置,第1块中含有4个记录,第2块中含有5个记录,第3块中含有3个记录。第1块中的最大关键字为34,它小于第2块中的最小关键字36,第2块中的最大关键字为72,它小于第3块中的最小关键字86,所以,主表中块与块之间是递增有序的。从图中的索引表可以看出:每个索引项中的索引值域保存着对应块中的最大关键字,索引表是按照索引值递增有序的。


        当进行分块查找时,应根据所给的关键字首先查找索引表,从中查找出刚好大于等于所给关键字的那个索引项,从而找到待查块,然后再查找这个块,从中顺序查找到相应的记录(若存在的话)。由于索引表是有序的,所以在索引表上既可以采用顺序查找,也可以采用二分查找,而每个块中的记录排列是任意的,所以在块内只能采用顺序查找。

         例如,根据图10-4 查找关键字为40的记录时,假定采用顺序的方法查找索引表,首先用40同第1项索引值34比较,因40>34,则接着同第2项索引值72比较,因40<=72,所以查找索引表结束,转而顺序查找主表中从下标5开始的块,因关键字为40的记录位于该块的第3个位置,所以经过3次比较后查找成功。

         分块查找的算法同上面已经给出的索引查找算法类似,其算法描述为:

//分块查找public static int blockSearch(Object [] a,IndexItem [] b,int m,Object k){//利用主表a和大小为m的索引表b分块查找元素值为k的记录int i=0,j=0;//在索引表中顺序查找索引值为k所对应的索引项for(i=0;i<m;i++){if(((Comparable)k).compareTo(b[i].index)<=0){break;}}//若i等于m,则表明查找失败,返回-1if(i==m){return -1;}//在已经查找到的第i个子表中顺序查找元素值为k的记录j=b[i].start;while(j<b[i].start+b[i].length){if(((Comparable)k).compareTo(a[j])==0){break;}}//若查找成功则返回元素的下标位置,否则返回-1if(j<b[i].start+b[i].length){return j;}else {return -1;}}

         若在索引表上不是顺序查找,而是二分查找相应的索引项,则需要把算法中的for循环语句更换为如下的程序段:

    

int  low=0,high=m-1;while(low<=high){int mid=(low+high)/2;if(((Comparable)k).compareTo(b[mid].index)==0){i=mid;break;}else if(((Comparable)k).compareTo(b[mid].index)<0){high=mid-1;}else{low=mid+1;}}if(low>high){i=low;}

         在这里当而二分查找失败时,应把low的值赋给i,此时b[i].index是刚大于k的索引值。当然如low的值为m,则表示真正的查找失败。