查找顺序查找是一种最基本和最简单的查找方法。它的思路是,从表中的第一个元素开始,将给定的值与表中逐个元素的关键字进行比较,直到两者相符,查到所要找的元素为止。否则就是表中没有要找的元素,查找不成功。对于表中记录的关键字是无序的表,只能采用这种方法。
描述顺序查找的算法见框图 8-1。其中 n是表 r的长度,k是要查的元素的关键字,i查到的元素的序号。
折半查找又称二分查找,是针对有序表进行查找的简单,
有效而又较常用的方法 。 所谓 有序表,即要求表中的各元素按关键字的值有序 ( 升序或降序 ) 存放 。
折半查找不像顺序查找那样,从第一个记录开始逐个顺序搜索,其基本思想是:首先选取表中间位置的记录,
将其关键字与给定关键字 k进行比较,若相等,则查找成功;否则,若 k值比该关键字值大,则要找的元素一定在表的后半部分 ( 或称右子表 ),则继续对右子表进行折半查找;若 k值比该关键字值小,则要找的元素一定在表的前半部分 ( 左子表 ),同样应继续对左子表进行折半查找 。 每进行一次比较,要么找到要查找的元素,要么将查找的范围缩小一半 。 如此递推,直到查找成功或把要查找的范围缩小为空 ( 查找失败 ) 。
设表的长度为 n,表的被查找部分的头为 low,尾为 high,
初始时,low=1,high=n,k为关键字的值。
( 2) 若 k=r[mid].key,成功,否则:
若 k<r[mid].key,则 high=mid― 1,重复 ( 1) ;
若 k>r[mid].key,则 low=mid+1,重复 ( 1) ;
直到成功或不成功 ( 此时 low>high) 。
具体算法如下:
Void binsrch(struct noder[ ],int n,int k)
{ int mid,low,high,find;
low=1; high=n;find=0; /*置区间初值 */
while ((low<=high) &&(!find))
{ mid=(low+high)/2; /*求中点 */
if (k= = r[mid].key)
find=1; /*已查到 */
else if(k>r[mid].key )
low=mid+1; /*在后半区间查找 */
else high=mid-1; /*在前半区间查找 */
}
if (find)
return (mid); /*查找成功,返回找到元素的位置 */
else return (0); /*查找不成功,返回 0标记 */
}
采用折半查找,当查找成功时,最少比较次数为一次,如在上例中查找关键字值为 18的结点,
只需一次比较 。 最多经过 log2n次比较之后,待查找子表要么为空,要么只剩下一个结点,所以要确定查找失败需要 log2n次或 log2n+1次比较 。 可以证明,折半查找的平均查找长度是:
从折半查找的平均查找长度 ASL来看,当表的长度 n很大时,该方法尤其能显示出其时间效率。
但由于折半查找的表仍是线性表,若经常要进行插入、删除操作,则元素排列费时太多,因此折半查找比较适合于一经建立就很少改动而又需要经常查找的线性表,较少查找而又经常需要改动的线性表可以采用链接存储,使用顺序查找。
分块查找在处理线性表时,如果既希望能够快速查找,又要经常动态变化,则可以采用分块查找方法 。 分块查找又称索引顺序查找,要求将待查的元素均匀的分成块,块间按大小排序,块内不排序 。 因此需要建立一个块的最大 ( 或最小 ) 关键字表,称之为,索引表,。
具体而言,假设我们按结点元素关键字升序方式组织表中各块,则要求第一块中任一结点的关键字值都小于第二块中所有结点的关键字值;第二块中任一结点的关键字值都小于第三块中所有结点的关键字值;如此类推。然后选择每块中的最大(或最小)关键字值组成索引表。换言之,索引表中的结点个数等于线性表被分割的块数。
例如要找关键字为 k的元素,则先用折半查找法由索引表查出 k所在的块,再用顺序查找法在对应的块中找出 k。 在图 8-2中若要找关键字值为 41的元素,则先由索引表查出 41在第二块中 ( 29<41<64),再在第二块中找到
41。
由此我们可以总结:分块查找分两步进行,第一步确定要查找结点在表中的哪一块,第二步在确定的块中找到该结点 。
分块查找的平均查找长度为:
ASL=ASLb+ASLe
其中 ASLb是折半查找的平均查找长度,ASLe是用顺序查找法查块内元素的平均查找长度 。 设每块中有 s个元素,可以近似的得到:
可以看出分块查找的平均查找长度位于顺序查找和折半查找之间 。
下面我们简单的对以上几种查找方法做出比较:
( 1) 平均查找长度:折半查找最小,分块查找次之,顺序查找最大;
( 2) 表的结构:顺序查找对有序表,无序表均适用;折半查找仅适用于有序表;分块查找要求表中元素是逐段有序的,就是块与块之间的记录按关键字有序;
( 3)存储结构:顺序查找和分块查找对向量和线性链表结构均适用;折半查找只适用于向量存储结构的表,因而要求表中元素基本不变,而在需要插入或删除运算时,要移动元素,才能保持表的有序性,所以影响了查找效率。
哈希法哈希法又名散列法,因其英文单词 Hash而得名,是一种完全不同的查找方法 。
前面我们所介绍的三种查找方法的共同特点在于:通过对关键字的一系列比较,逐步缩小查找范围,直到确定结点的存储位置或确定查找失败 。
而哈希法则希望不经过任何比较,一次存取就能得到所查元素,因此必须在记录的存储位置和它的关键字之间建立一个确定的对应关系,使得每个关键字和结构中一个唯一的存储位置相对应,这样查找时只需对结点的关键字进行某种运算就能确定结点在表中的位置 。 因此哈希法的平均比较次数和表中所含结点的个数无关 。
打个比方让我们更清楚的认识哈希法的不同。假定某教室有 35个座位,如果不加限定让学生任意就座,则要找某个学生时就要将待找学生与当前座位上的学生一一做比较,这就是我们前面所介绍的查找方法的大致思路。
而哈希法则要限定学生所坐的位置,比如可规定学生座位的编号应与其学号的末两位相同,则学号为 993605的学生应坐编号为 5的座位。这样我们要找某个学生时只需根据其学号的末两位到相应座位上去找即可,不必一一比较了。在这个例子里,学生好比记录,学号则为关键字,对关键字进行的操作则是取其末两位,用以确定记录的位置。
哈希函数的构造方法一个好的哈希函数应使函数值均匀的分布在存储空间的有效地址范围内,以尽可能减少冲突 。 但鉴于实际问题中关键字的不同,没法构造出统一的哈希函数 。 从而构造哈希函数的方法多种多样,这里只介绍一些比较常用的,计算较为简便的方法 。
1,自身函数关键字自身作为哈希函数,即 H( k) =k,也可自身加上一个常数作为哈希函数,即
H(k)=k+c。 例如上例中建立的哈希函数 H(学号 )=学号 -9900采用的就是这样的方法 。
但这种函数只适用于给定的一组关键字为关键字集合中的全体元素的情况,故不常用 。
2,除余法选择一个适当的正整数 m,用 m去除关键字,取其余数作为散列地址,即 H( key)
=key%m。 上例中后来所设定的函数 H(学号 )=学号 %10采用的就是除余法 。
但是在这种方法中,m的选择是值得研究的 。 如果选择关键字内部代码基数的幂次来除关键字,其结果必定是关键字的低位数字均匀性较差 。 若取 m为任意偶数,则当关键字内部代码为偶数时,得到的哈希函数值为偶数;若关键字内部代码为奇数,则哈希函数值为奇数 。 因此,m为偶数也是不好的 。 理论分析和试验结果均证明 m应取小于存储区容量的素数 。
查找,根据给定的某个值,在表中确定一个关键字等于给定值的记录或数据元素 。
关键字 (Keyword):一个或一组能唯一标识该记录的数据项称为该记录的关键字 。
平均查找长度 ( Average Search Length),为确定记录在表中的位置所进行的和关键字的比较的次数的期望值称之为查找算法的平均查找长度,简称 ASL。
有序表,表中的各元素按关键字的值有序 ( 升序或降序 ) 存放 。
哈希表,将结点的关键字 key作为自变量,通过一个确定的函数关系 H,计算出相应的函数值 H(key),然后以 H(key)作为该结点的存储单元地址 。 用这种方式建立起来的线性表称为哈希表或叫散列表哈希函数,把结点关键字转换为该结点存储单元地址的函数 H称为哈希函数或叫散列函数 。
在学习这些概念的基础上,我们先后学习了三种基于将待查元素的关键字和表中元素的关键字进行比较的查找算法,即顺序查找、折半查找和分块查找,并对它们做出比较。我们也学习了一种不同的查找算法,即哈希法,它的基本思路是:在记录的存储位置和它的关键字之间建立一个确定的对应关系,使得每个关键字和结构中一个唯一的存储位置相对应,
这样查找时只需对结点的关键字进行某种运算就能确定结点在表中的位置,其间我们知道了如何构造哈希函数和如何解决冲突问题。读者应熟悉各种查找算法的思路、算法及性能分析,以灵活应用于各种实际问题中。