[算法] 查找相关算法
查找:查询某个关键字是否在(数据元素集合)表中的过程。也称作检索。
主关键字:能够惟一区分各个不同数据元素的关键字
次关键字:通常不能惟一区分各个不同数据元素的关键字
查找成功:在数据元素集合中找到了要查找的数据元素
查找不成功:在数据元素集合中没有找到要查找的数据元素
静态查找:只查找,不改变数据元素集合内的数据元素
动态查找:既查找,又改变(增减)集合内的数据元素
静态查找表:静态查找时构造的存储结构
动态查找表:动态查找时构造的存储结构
平均检索长度(ASL,Average Search Length):检索过程中对关键码的平均比较次数,衡量检索算法优劣的时间标准。
·ASL是存储结构中对象总数n的函数
·Pi 为检索第 i 个元素的概率
·Ci 为找到第 i 个元素所需的关键码值与给定值的比较次数
查找分类:
·静态查找: 顺序查找 、二分法查找 、分块查找
1.顺序查找:
针对线性表里的所有记录,逐个进行关键码和给定值的比较。若某个记录的关键码和给定值比较相等,则检索成功;否则检索失败(找遍了仍找不到)。
存储:可以顺序、链接
排序要求:无
性能分析:
·ASL(平均查找长度):假设查找成功的概率为p,查找失败的概率为q = (1 - p),
则平均查找长度为(n + 1)(1 - p / 2)。
·(n + 1) / 2 < ASL < (n + 1)
优缺点:
优点:插入元素可以直接加在表尾Θ(1)
缺点:查找时间太长Θ(n)
2.二分查找:
先给数据排序(例如按升序排好),形成有序表,然后再将key与正中元素相比,若key小,则缩小至前半部内查找;再取其中值比较,每次缩小1/2的范围,直到查找成功或失败为止。反之,如果key大,则缩小至后半部内查找。
下面是二分查找的代码:
1 int BinSearch(vector<int> s,int key) { 2 int low = 0,high = s.size() - 1,middle; 3 while(low <= high) { 4 middle = low + (high - low) / 2; 5 if(s[middle] == key) { 6 return middle; 7 } 8 else if(s[middle] < key) { 9 low = middle + 1; 10 } 11 else { 12 high = middle - 1; 13 } 14 } 15 return -1; 16 }
性能分析:
·最大检索长度:
·失败的检索长度:或
·ASL(平均检索长度):约为(n > 50)
优缺点:
优点:平均检索长度与最大检索长度相近,检索速度快
缺点:要排序、顺序存储,不易更新(插/删)
3.分块查找(顺序与二分法的折衷)
"按块有序",设线性表中共有n个数据元素,将表分成b块,不需要均匀,每一块可能不满,每一块中的关键码不一定有序,但前一块中的最大关键码必须小于后一块中的最小关键码。
索引表:
存储各块中的最大关键码及各块起始位置,可能还需要块中元素个数(每一块可能不满)。由于表是分块有序的,所以索引表是一个递增有序表。
分块查找分两个阶段:
(1)确定待查元素所在的块 (2)在块内查找待查的元素
性能分析:
分块查找为两级查找:
·先在索引表中确定待查元素所在的块;设在索引表中确定块号的时间开销是ASLb
·然后在块内查找待查的元素。 在块中查找记录的时间开销为ASLw
ASL(n) = ASLb + ASLw
假设索引表的长度为b,主表中每个子表的长度为s,并假设在索引表上和在主表上均采取顺序查找算法,则索引顺序表上查找算法的平均查找长度为:
ASL = (b + 1) / 2 + (s + 1) / 2 = (b + s) / 2 + 1
若采用二分法查找确定记录所在的子表,则查找成功时的平均检索长度为
ASL = ASLb + ASLw,约为 log2(b + 1) - 1 + (s + 1) / 2
当s = n^(1/2)时,ASL取得最小值 n^(1/2) + 1
优缺点:
优点:(1)插入、删除相对较易; (2)没有大量记录移动
缺点:(1)增加一个辅助数组的存储空间; (2)初始线性表分块排序,当大量插入/删除时,或结点分布不均匀时,速度下降
·动态查找
·散列查找
查找某一条记录需要进行一系列的"比较"。查找的效率依赖于比较的次数。能否在记录的关键字和存储地址之间构造这样一种关系 f ,使得关键字和存储地址一一对应 ?此对应关系 f 称为散列函数。
几个重要概念:
·负载因子:α=n/m(散列表的空间大小为m,填入表中的结点数为n)
·冲突:某个散列函数对于不相等的关键码计算出了相同的散列地址,在实际应用中,不产生冲突的散列函数极少存在。
·同义词:发生冲突的两个关键码
·散列函数:把关键码值映射到存储位置的函数,通常用 h 来表示:Address = Hash ( key )
构造散列函数时的几点要求:
(1)散列函数的定义域必须包括需要存储的全部关键码,如果散列表允许有 m 个地址时,其值域必须在 0 到 m-1 之间。
(2)散列函数计算出来的地址应能均匀分布在整个地址空间中:若 key 是从关键码集合中随机抽取的一个关键码,散列函数应能以同等概率取 0 到 m-1 中的每一个值。
(3)散列函数应是简单的,能在较短的时间内计算出结果。
常用散列函数选取方法:
1. 除余法
H(key) = key % p 或 H(key) = key % p + c 这里 p < m;余数总在 0 ~ p-1 之间。
示例:有一个关键码 key = 962148,散列表大小 m = 25,即 HT[25]。取质数 p= 23。散列函数 hash ( key ) = key % p。则散列地址为 hash ( 962148 ) = 962148 % 23 = 12。可以按计算出的地址存放记录。需要注意的是,使用上面的散列函数计算出来的地址范围是 0到 22,因此,从23到24这几个散列地址实际上在一开始是不可能用散列函数计算出来的,只可能在处理冲突时达到这些地址。
选取 p 为质数的理由:
设 key 值都为奇数,选 p 为偶数;则 H(key) = key % p ,结果为奇数,一半单元被浪费掉。
设 key 值都为 5 的倍数,选 p 为 95;则 H(key) = key % p ,结果为: 0、5、10、15、…… 90 。4/5 的单元被浪费掉。
2. 折叠法
将关键字从左到右分割成位数相等的几部分,最后一部分位数不够时可以短些,然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。比如关键字是9876543210,散列表表长是3位,将其分为四组,然后叠加求和:987 + 654 + 321 + 0 = 1962,取后3位962作为散列地址。
折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。
3. 平方取中法
e.g: (4731)^2 = 223 82 361 ;选取 82 (在 m =100 情况下)。
此方法在词典处理中使用十分广泛。它先计算构成关键码的标识符的内码的平方,然后按照散列表的大小取中间的若干位作为散列地址。
设标识符可以用一个计算机字长的内码表示。因为内码平方数的中间几位一般是由标识符所有字符决定,所以对不同的标识符计算出的散列地址大多不相同,即使其中有些字符相同。
4. 基数转换法
将关键字k转换为另外一种数字基数,再对表的大小取模。如:k=(345)10 地址为(423)9 % 表的大小
5. 直接定址法
H(key) = key 或 H(key) = a ×key + b 如:k1, k2 分别有值 10 、1000;选10 、1000 作为存放地址。
以上介绍了几种常用的散列函数。在实际工作中应根据关键码的特点,选用适当的方法。有人曾用“轮盘赌”的统计分析方法对它们进行了模拟分析,结论是平方取中法最接近于“随机化”。
哈希冲突:
对于不同的关键字可能得到同一哈希地址,即 key1≠ key2 ,而 f(key1) = f(key2) ,这种现象称为哈希冲突。
哈希冲突解决方法
1、开放定址法
用开放定址法解决冲突的做法是:当冲突发生时,使用某种探查(亦称探测)技术在散列表中形成一个探查(测)序列。沿此序列逐个单元地查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止(若要插入,在探查到开放的地址,则可将待插入的新结点存人该地址单元)。查找时探查到开放的地址则表明表中无待查的关键字,即查找失败。
注意:
①用开放定址法建立散列表时,建表前须将表中所有单元(更严格地说,是指单元中存储的关键字)置空。
②空单元的表示与具体的应用相关。
按照形成探查序列的方法不同,可将开放定址法区分为线性探查法、线性补偿探测法、随机探测等。
(1)线性探查法(Linear Probing)
该方法的基本思想是:
将散列表T[0..m-1]看成是一个循环向量,若初始探查的地址为d(即h(key)=d),则最长的探查序列为:
d,d+l,d+2,…,m-1,0,1,…,d-1
即:探查时从地址d开始,首先探查T[d],然后依次探查T[d+1],…,直到T[m-1],此后又循环到T[0],T[1],…,直到探查到T[d-1]为止。
探查过程终止于三种情况:
(1)若当前探查的单元为空,则表示查找失败(若是插入则将key写入其中);
(2)若当前探查的单元中含有key,则查找成功,但对于插入意味着失败;
(3)若探查到T[d-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。
利用开放地址法的一般形式,线性探查法的探查序列为:
hi=(h(key)+i)%m 0≤i≤m-1 //即di=i
用线性探测法处理冲突,思路清晰,算法简单,但存在下列缺点:
① 处理溢出需另编程序。一般可另外设立一个溢出表,专门用来存放上述哈希表中放不下的记录。此溢出表最简单的结构是顺序表,查找方法可用顺序查找。
② 按上述算法建立起来的哈希表,删除工作非常困难。假如要从哈希表 HT 中删除一个记录,按理应将这个记录所在位置置为空,但我们不能这样做,而只能标上已被删除的标记,否则,将会影响以后的查找。
③ 线性探测法很容易产生堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。按照线性探测法处理冲突,如果生成哈希地址的连续序列愈长 ( 即不同关键字值的哈希地址相邻在一起愈长 ) ,则当新的记录加入该表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味着,一旦出现堆聚 ( 伴随着冲突 ) ,就将引起进一步的堆聚。
(2)线性补偿探测法
线性补偿探测法的基本思想是:
将线性探测的步长从 1 改为 Q ,即将上述算法中的 j = (j + 1) % m 改为: j = (j + Q) % m ,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
【例】 PDP-11 小型计算机中的汇编程序所用的符合表,就采用此方法来解决冲突,所用表长 m = 1321 ,选用 Q = 25 。
(3)随机探测
随机探测的基本思想是:
将线性探测的步长从常数改为随机数,即令: j = (j + RN) % m ,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。
这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。
2、拉链法
拉链法解决冲突的做法是:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,负载因子α可以大于 1,但一般均取α≤1。
【例】设有 m = 5 , H(K) = K mod 5 ,关键字值序例 5 , 21 , 17 , 9 , 15 , 36 , 41 , 24 ,按外链地址法所建立的哈希表如下图所
与开放定址法相比,拉链法有如下几个优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求负载因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放地址法构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放地址法中,空地址单元(即开放地址)都是查找失败的条件。因此在 用开放地址法处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。
拉链法的缺点是:指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。