王道视频-数据结构-笔记7:查找


0 笔记说明

来源于2020 王道考研 数据结构,博客内容是对自己笔记的书面整理,根据自身学习需要,我可能会增加必要内容。


1 查找的基本概念

1)查找。在数据集合中寻找满足某种条件的数据元素的过程称为查找。查找结果分为两种:一是查找成功,即在数据集合中找到了满足条件的数据元素;二是查找失败。

2)查找表。用于查找的数据集合称为查找表,由同一类型的数据元素组成,可以是数组、链表等数据类型。对查找表经常进行的操作一般有4种:①查询某个特定的数据元素是否在查找表中;②检索满足条件的某个特定的数据元素的各种属性;③在查找表中插入一个数据元素;④从查找表中删除某个数据元素。

3)静态查找表。若一个查找表的操作只涉及上述操作①和②,则无须动态地修改查找表,此类查找表称为静态查找表。与此对应,需要动态地插入或删除的查找表称为动态查找表。适合静态查找表的查找方法有顺序查找、折半查找、哈希查找等;适合动态查找表的查找方法有二叉排序树的查找、哈希查找等。

4)关键字。数据元素中唯一标识该元素的某个数据项的值,使用基于关键字的查找,查找结果应该是唯一的。例如“学号”这一数据项唯一地标识一名学生。

5)平均查找长度。在查找过程中,一次查找的长度是指需要比较的关键字次数,而平均查找长度则是所有查找过程中进行关键字的比较次数的平均值,其数学定义为:
在这里插入图片描述
式中,n是查找表的长度;Pi是查找第i个数据元素的概率,一般认为每个数据元素的查找概率相等,即Pi=l/n;Ci是找到第i个数据元素所需进行的比较次数。

平均查找长度是衡量查找算法效率的最主要的指标


2 顺序查找和折半查找

2.1 顺序查找

顺序查找又称线性查找,主要用于在线性表中进行查找。顺序查找通常分为:

1、对一般的无序线性表的顺序查找;

2、对按关键字有序的顺序表的顺序查找。

2.1.1 一般线性表的顺序查找

作为一种最直观的查找方法,其基本思想是从线性表的一端开始,逐个检查关键字是否满足给定的条件。若查找到某个元素的关键字满足给定条件,则查找成功,返回该元素在线性表中的位置;若已经查找到表的另一端,但还没有查找到符合给定条件的元素,则返回查找失败的信息。下面给出其算法:

typedef struct{ //查找表的数据结构
    ElemType *elem; //元素存储空间基址,建表时按实际长度分配,从1号位置开始存储
    int TableLen; //表的长度
}SSTable;

int Search_Seq(SSTable ST,ElemType key){
    ST.elem[0]=key; //哨兵
    for(i=ST.TableLen;ST.elem[i]!=key;--i); //从后向前查找
    return i; //若表中不存在关键字为key的元素,将查找到i为0时退出for循环
}

在上述算法中,将ST.elem[0]称为“哨兵”。引入它的目的是使得Search_Seq内的循环不必判断数组是否会越界,因为满足i==0时,循环一定会跳出。

对于有n个元素的表,给定值key与表中第i个元素相等,即定位第i个元素时,需进行n-i+1次关键字的比较,即Ci=n-i+1。查找成功时,顺序查找的平均长度为:
在这里插入图片描述
当每个元素的查找概率相等,即Pi=1/n时,有:
在这里插入图片描述
查找不成功时,与表中各关键字的比较次数显然是n+1次,从而顺序查找不成功的平均查找长度为ASL不成功=n+1。

通常,查找表中记录的查找概率并不相等。若能预先得知每个记录的查找概率,则应先对记录的查找概率进行排序,使表中记录按查找概率由小至大重新排列。顺序查找的缺点是当n较大时,平均查找长度较大,效率低;优点是对数据元素的存储没有要求,顺序存储或链式存储皆可。对表中记录的有序性也没有要求,无论记录是否按关键字有序,均可应用。

对线性的链表只能进行顺序查找

2.1.2 有序表的顺序查找

若在查找之前就已经知道表是关键字有序的,则查找失败时可以不用再比较到表的另一端就能返回查找失败的信息,从而降低顺序查找失败的平均查找长度。

假设表L是按关键字从小到大排列的,查找的顺序是从前往后,待查找元素的关键字为key,当查找到第i个元素时,发现第i个元素对应的关键字小于key,但第i+1个元素对应的关键字大于key,这时就可返回查找失败的信息,因为第i个元素之后的元素的关键字均大于key,所以表中不存在关键字为key的元素。

可以用如图7.1所示的判定树来描述有序顺序表的查找过程:
在这里插入图片描述
树中的圆形结点表示有序顺序表中存在的元素;树中的矩形结点称为失败结点(若有n个结点,则相应地有n+1个查找失败结点),它描述的是那些不在表中的数据值的集合。若查找到失败结点,则说明查找不成功

在有序表的顺序查找中,查找成功的平均查找长度和一般线性表的顺序查找一样。查找失败时,查找指针一定走到了某个失败结点。这些失败结点是我们虚构的空结点,实际上是不存在的,所以到达失败结点时所查找的长度等于它上面的一个圆形结点的所在层数。查找不成功的平均查找长度在相等查找概率的情形下为:
在这里插入图片描述
上式中,qj是到达第j个失败结点的概率,在相等查找概率的情形下,它为1/(n+1);lj是第j个失败结点所在的层数。

有序表的顺序查找和下面的折半查找的思想是不一样的,且有序表的顺序查找中的线性表可以是链式存储结构

2.2 折半查找

折半查找又称二分查找,它仅适用于有序的顺序表,其基本思想如下:

首先将给定值key与表中中间位置的元素比较,若相等,则查找成功,返回该元素的存储位置;若不等,则所需查找的元素只能在中间元素以外的前半部分或后半部分。例如,在查找表升序排列时,若给定值key大于中间元素,则所查找的元素只可能在后半部分。然后在缩小的范围内继续进行同样的查找,如此重复,直到找到为止,或确定表中没有所需要查找的元素,则查找不成功,返回查找失败的信息。

算法如下:

int Binary_Search(SeqList L,ElemType key){
    int low=0,high=L.TableLen-1,mid;
    while(low<=high){
        mid=(low+high)/2; //取中间位置
        if(L.elem[mid]==key)
            return mid; //查找成功则返回所在位置
        else if(L.elem[mid]>key)
            high=mid-1; //从前半部分继续查找
        else
            low=mid+1; //从后半部分继续查找
    }
    return -1; //查找失败,返回-1
}

例如,已知11个元素的有序表{7,10,13,16,19,29,32,33,37,41,43},要查找值为11的元素,指针low和high分别指向表的下界和上界,mid则指向表的中间位置⌊(low+high)/2⌋。

以下是查找11的过程:
在这里插入图片描述
折半查找的过程可用图7.2所示的二叉树来描述,称为判定树:
在这里插入图片描述
树中每个圆形结点表示一个记录,结点中的值为该记录的关键字值;树中最下面的叶结点都是方形的,它表示查找不成功的情况。

如果当前low和high之间有奇数个元素,则mid分隔后,左右两部分元素个数相等;如果当前low和high之间有偶数个元素,则mid分隔后,左半部分比右半部分少一个元素。

从判定树可以看出,查找成功时的查找长度为从根结点到目的结点的路径上的结点数,而查找不成功时的查找长度为从根结点到对应失败结点的父结点的路径上的结点数;每个结点值均大于其左子结点值,且均小于其右子结点值。若有序序列有n个元素,则对应的判定树有n个圆形的非叶结点和n+1个方形的叶结点。

判定树是一棵平衡二叉树。用折半查找法查找到给定值的比较次数最多不会超过树的高度。在等概率查找时,查找成功的平均查找长度为:
在这里插入图片描述
式中,h是树的高度,并且元素个数为n时树高h=⌈log2(n+1)⌉。所以折半查找的时间复杂度为O(log2n),平均情况下比顺序查找的效率高。

在上面的图7.2所示的判定树中,在等概率情况下,查找成功(圆形结点)的ASL=(1×1+2×2+3×4+4×4)/11=3,查找不成功(方形结点)的ASL=(3×4+4×8)/12=11/3。

折半查找法仅适合于顺序存储结构,不适合于链式存储结构,且要求元素按关键字有序排列

2.3 分块查找

分块查找又称索引顺序查找,基本思想是:将查找表分为若干子块。块内的元素可以无序,但块之间是有序的,即第一个块中的最大关键字小于第二个块中的所有记录的关键字,第二个块中的最大关键字小于第三个块中的所有记录的关键字,以此类推。再建立一个索引表,索引表中的每个元素含有各块的最大关键字和各块中的第一个元素的地址,索引表按关键字有序排列。

分块查找的过程分为两步:

1、在索引表中确定待查记录所在的块,可以顺序查找或折半查找索引表。注意,若索引表中不包含目标关键字,则折半查找索引表最终停在low>high,要在low所指分块中查找。

2、在块内顺序查找。

例如关键码集合为{88,24,72,61,21,6,32,11,8,31,22,83,78,54},按照关键码值24,54,78,88,分为4个块和索引表,如图7.3所示:
在这里插入图片描述
分块查找的平均查找长度为索引查找和块内查找的平均长度之和,设索引查找和块内查找的平均查找长度分别为LI,LS,则分块查找的平均查找长度为:
在这里插入图片描述
将长度为n的查找表均匀地分为b块,每块有s个记录,在等概率情况下,若在块内和索引表中均采用顺序查找,则平均查找长度为:
在这里插入图片描述
此时,若s=√n,则平均查找长度取最小值√n+1;若对索引表采用折半查找时,则平均查找长度为:
在这里插入图片描述


3 B树和B+树

3.1 B树及其基本操作

不整理

3.2 B+树的基本概念

不整理


4 哈希表

4.1 哈希表的基本概念

在前面介绍的线性表和树表的查找中,记录在表中的位置与记录的关键字之间不存在确定关系,因此,在这些表中查找记录时需进行一系列的关键字比较。这类查找方法建立在“比较”的基础上,查找的效率取决于比较的次数。

哈希函数:一个把查找表中的关键字映射成该关键字对应的地址的函数,记为Hash(key)=Addr,这里的地址可以是数组下标、索引或内存地址等。哈希函数可能会把两个或两个以上的不同关键字映射到同一地址,称这种情况为冲突,这些发生碰撞的不同关键字称为同义词。一方面,设计得好的哈希函数应尽量减少这样的冲突;另一方面,由于这样的冲突总是不可避免的,所以还要设计好处理冲突的方法。

哈希表:根据关键字而直接进行访问的数据结构,哈希表建立了关键字和存储地址之间的一种直接映射关系

下面分别介绍常用的哈希函数和处理冲突的方法。

4.2 哈希函数的构造方法

在构造哈希函数时,必须注意以下几点:

1)哈希函数的定义域必须包含全部需要存储的关键字,而值域的范围则依赖于哈希表的大小或地址范围。

2)哈希函数计算出来的地址应该能等概率、均匀地分布在整个地址空间中,从而减少冲突的发生。

3)哈希函数应尽量简单,能够在较短的时间内计算出任一关键字对应的哈希地址。

下面介绍常用的4个哈希函数:

1、直接定址法:直接取关键字的某个线性函数值为哈希地址,哈希函数为H(key)=key或H(key)=a×key+b,其中,a和b是常数。这种方法计算最简单,且不会产生冲突。它适合关键字的分布基本连续的情况,若关键字分布不连续,则会造成存储空间的浪费。

2、除留余数法:这是一种最简单、最常用的方法,假定哈希表表长为m,取一个不大于m但最接近或等于m的质数p,利用哈希函数H(key)=key%p,把关键字转换成哈希地址。除留余数法的关键是选好p,使得每个关键字通过该函数转换后等概率地映射到哈希空间上的任一地址,从而尽可能减少冲突的可能性。

3、数字分析法:设关键字是r进制数(如十进制数),而r个数码在各位上出现的频率不一定相同,可能在某些位上分布均匀一些,每种数码出现的机会均等;而在某些位上分布不均匀,只有某几种数码经常出现,此时应选取数码分布较为均匀的若干位作为哈希地址。这种方法适合于已知的关键字集合,若更换了关键字,则需要重新构造新的哈希函数。

4、平方取中法:顾名思义,这种方法取关键字的平方值的中间几位作为哈希地址。具体取多少位要视实际情况而定。这种方法得到的哈希地址与关键字的每位都有关系,因此使得哈希地址分布比较均匀,适用于关键字的每位取值都不够均匀或均小于哈希地址所需的位数。

在不同的情况下,不同的哈希函数具有不同的性能,因此不能笼统地说哪种哈希函数最好。在实际选择中,采用何种构造哈希函数的方法取决于关键字集合的情况,但目标是为了尽量降低产生冲突的可能性。

4.3 处理冲突的方法

任何设计出来的哈希函数都不可能绝对地避免冲突。为此,必须考虑在发生冲突时应该如何处理,即为产生冲突的关键字寻找下一个“空”的Hash地址。用Hi表示处理冲突中第i次探测得到的哈希地址,假设得到的另一个哈希地址H1仍然发生冲突,只得继续求下一个地址H2,以此类推,直到Hk不发生冲突为止,则Hk为关键字在表中的地址。

4.3.1 开放定址法

所谓开放定址法,是指可存放新表项的空闲地址既向它的同义词表项开放,又向它的非同义词表项开放。其数学递推公式为:
在这里插入图片描述
式中,H(key)为哈希函数;i=0,1,2,…,k,其中k≤m-1,m表示哈希表表长,di为增量序列。

取定某一增量序列后,对应的处理方法就是确定的,其中增量序列有以下4种取法:

1、线性探测法。当di=0,1,2,…,m-1时,称为线性探测法。这种方法的特点是:冲突发生时,顺序查看表中下一个单元(探测到表尾地址m-1时,下一个探测地址是表首地址0),直到找出一个空闲单元(当表未填满时一定能找到一个空闲单元)或查遍全表。线性探测法可能使第i个哈希地址的同义词存入第i+1个哈希地址,这样本应存入第i+1个哈希地址的元素就争夺第i+2个哈希地址的元素的地址……从而造成大量元素在相邻的哈希地址上“聚集”起来,大大降低了查找效率。

2、平方探测法。当d=02,12,-12,22,-22,…,k2,-k2时,称为平方探测法,其中k≤m/2,哈希表长度m必须是一个可以表示成4k+3的素数,又称二次探测法。平方探测法是一种较好的处理冲突的方法,可以避免出现“聚集”问题,它的缺点是不能探测到哈希表上的所有单元,但至少能探测到一半单元。

3、再哈希法。当di=Hash2(key)时,称为再哈希法,又称双哈希法。需要使用两个哈希函数,当通过第一个哈希函数H(key)得到的地址发生冲突时,则利用第二个哈希函数Hash2(key)计算该关键字的地址增量。它的具体哈希函数形式如下:
在这里插入图片描述
初始探测位置H0=H(key)%m。i是冲突的次数,初始为0。在再哈希法中,最多经过m-1次探测就会遍历表中所有位置,回到H0位置。

4、伪随机序列法。当di=伪随机数序列时,称为伪随机序列法。

注意:在开放定址的情形下,不能随便物理删除表中的已有元素,因为若删除元素,则会截断其他具有相同哈希地址的元素的查找地址。因此,要删除一个元素时,可给它做一个删除标记,进行逻辑删除。但这样做的副作用是:执行多次删除后,表面上看起来哈希表很满,实际上有许多位置未利用,因此需要定期维护哈希表,要把删除标记的元素物理删除。

4.3.2 拉链法

显然,对于不同的关键字可能会通过哈希函数映射到同一地址,为了避免非同义词发生冲突,可以把所有的同义词存储在一个线性链表中,这个线性链表由其哈希地址唯一标识。假设哈希地址为i的同义词链表的头指针存放在哈希表的第i个单元中,因而查找、插入和删除操作主要在同义词链中进行。拉链法适用于经常进行插入和删除的情况

例如,关键字序列为{19,14,23,01,68,20,84,27,55,11,10,79},哈希函数H(key)=key%13,用拉链法处理冲突,建立的表如图7.9所示:
在这里插入图片描述

4.4 哈希查找及性能分析

哈希表的查找过程与构造哈希表的过程基本一致。对于一个给定的关键字key,根据哈希函数可以计算出其哈希地址Addr=Hash(key),然后执行步骤如下:

①检测查找表中地址为Addr的位置上是否有记录,若无记录,返回查找失败;若有记录,比较它与key的值,若相等,则返回查找成功标志,否则执行步骤②。

②用给定的处理冲突方法计算“下一个哈希地址”,并把Addr置为此地址,转入步骤①。

例如,关键字序列{19,14,23,01,68,20,84,27,55,11,10,79}按哈希函数H(key)=key%13和线性探测处理冲突构造所得的哈希表L如图7.10所示:
在这里插入图片描述
给定值84的查找过程为:首先求得哈希地址H(84)=6,因L[6]不空且L[6]≠84,则找第一次冲突处理后的地址H1=(6+1)%16=7,而L[7]不空且L[7]≠84,则找第二次冲突处理后的地址H2=(6+2)%16=8,L[8]不空且L[8]=84,查找成功,返回记录在表中的序号8。

给定值38的查找过程为:先求哈希地址H(38)=12,L[12]不空且L[12]≠38,则找下一地址H1=(12+1)%16=13,由于L[13]是空记录,故表中不存在关键字为38的记录。

查找各关键字的比较次数如图7.11所示:
在这里插入图片描述
平均查找长度ASL为:ASL=(1×6+2+3×3+4+9)/12=2.5

对同一组关键字,设定相同的哈希函数,则不同的处理冲突的方法得到的哈希表不同,它们的平均查找长度也不同,本例与上节采用拉链法的平均查找长度不同。

从哈希表的查找过程可见:

(1)虽然哈希表在关键字与记录的存储位置之间建立了直接映像,但由于“冲突”的产生,使得哈希表的查找过程仍然是一个给定值和关键字进行比较的过程。因此,仍需要以平均查找长度作为衡量哈希表的查找效率的度量。

(2)哈希表的查找效率取决于三个因素:哈希函数、处理冲突的方法和装填因子。哈希表的装填因子一般记为α,定义为一个表的装满程度,即:
在这里插入图片描述
哈希表的平均查找长度依赖于哈希表的装填因子α,而不直接依赖于n或m。直观地看,α越大,表示装填的记录越“满”,发生冲突的可能性越大,反之发生冲突的可能性越小。


END

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值