查找及查找代码

查找

一、查找概论

各位都用过搜索引擎。搜索引擎的大概工作原理就是利用网络“爬虫”抓取并复制网页,并且可以通过该网页的链接来抓取更多的网页。

那么,搜索引擎的是通过什么来抓取网页的呢?就是通过“关键字”来识别网页并抓取网页的。

 

查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)。

查找表(Search Table)是由同一类型的数据元素(或记录)构成的集合。

关键字(Key)是数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素,也可以标识一个记录的某个数据项(字段)。

若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(Primary Key)。对于那些可以识别多个数据元素(或记录)的关键字,我们称为次关键字(Secondary Key)。

例如,若将一张成绩单作为查找表,则学号或姓名都可以作为主关键字,因为它们可以唯一确定一个查找记录。而成绩则只能作为次关键字,因为不能唯一确定一个查找记录。

 

二、顺序表查找

顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。

1、顺序表查找算法

//代码见附录

这段代码比较简单。需要注意的是待查表的数据是从下标1开始的。

2、顺序表查找算法优化

上文所给的算法并非完美,因为每次循环时都需要对i是否越界(即i<=n)进行判断。事实上,我们有更优化的算法。设置一个哨兵,可以解决不需要每次让i与n进行比较。

//代码见附录

此时代码是从尾部开始查找,由于a[0]等于key,也就是说,一定会找到key值的位置,要么是在某个非0位置(表示成功),要么是0(表示失败)。

这种方式是在查找数据的尽头设置“哨兵”免去了每次比较数据后都需要判断数据是否越界,看似差别与原先不大,但在数据量较大时,效率提高很大,是非常常用的技巧。

 

三、有序表查找

我们可以做一个游戏:给定一个100以内正整数让你猜,我只说“大了”或“小了”。对于这个游戏,从1开始依次数到100当然可以,但是效率太低。我们可以先猜50,然后针对“大了/小了”再决定下次猜的数字(25或75)。我们把这种每次取中间记录的查找方法叫做折半查找

折半查找(Binary Search),又称为二分查找。它的前提是线性表中的记录必须是关键字有序,而且线性表必须采用顺序存储。

折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录关键字,则在中间记录的左半区进行查找;若给定值大于中间记录的关键字,则在中间记录的右半区进行查找。不断重复上述过程直至查找成功(或失败)为止。

//代码见附录

由于折半查找的前提是需要有序表存储,因此如果某个数据表需要频繁执行插入或删除操作,维护其有序需要不小的工作量,这时不推荐使用折半查找。

 

四、散列表(哈希表)查找

上文中介绍的查找方法,都需要“比较”。我们发现,为了查找结果,“比较”是不可避免的,但是否真的有必要呢?

1、散列表查找定义

试想这样的场景:你来到某所不熟悉的学校,想要找一个叫“盖伦”的同学。有两种方法:

1)询问教务处的老师,老师会拿出学生名册,从上到下查找,最终告诉你:“盖伦”同学在德玛西亚班

2)去操场打听认识“盖伦”的同学,他会告诉你:“盖伦”同学在德玛西亚班

以上两种方法都可以查到“盖伦”同学的位置,第一种方法就是常规的查找方法,而第二种方法就是散列表查找方法。

也就是说,我们只需要得到某个函数f(),使得:

存储位置=f(关键字);

那样我们就可以不用通过比较关键字来查找存储位置,而是直接通过该函数算出存储位置。这就是另一种存储技术——散列技术。

对应函数f称为散列函数,又称为哈希(Hash)函数。使用散列技术将记录存储在一块存储空间中,这块存储空间就称为散列表或哈希表(Hash Table)。关键字所对应的记录存储位置就称为散列地址。

2、散列表查找步骤

整个散列过程其实只有两步:

1)在存储时,通过散列函数计算记录的散列位置,构造散列表。

2)当查找记录时,通过散列函数计算记录的散列位置。

例如,我们可以构造一种散列函数f,得到下面这张散列表:

编号 姓名 地址

1    盖伦 德玛西亚

2    卡特琳娜 诺克萨斯

3       均衡教派

4    蒙多 祖安

5    提莫 班德尔城

当我们进行查找时,如果查找“慎”这条记录,则我们可以

f(慎)

就会得到“均衡教派”的查找结果。

因此,散列技术既是一种存储方法,也是一种查找方法。

散列技术最适合的问题是查找与给定值相等的记录。对于其他查找方法来说,散列技术不需要比较,而是直接计算其位置,这样就大大提高了效率。

但是,有利就有弊。散列技术的最大问题是冲突。在理想状态下,每一个关键字,通过散列函数计算出来的位置都是不一样的。但现实中我们经常会碰到两个关键字key1!=key2,而计算后f(key1)=f(key2)的情况,这种现象我们称为冲突(collision),并把key1和key2称为这个散列函数的同义词(synonym)。

3、散列函数的构造方法

由上文我们可以看出,散列技术的最关键技术就是构造一个好的散列函数。构造散列函数需要考虑2条原则:

⒈计算简单

⒉散列地址分布均匀(即尽量减少冲突)

1)直接定址法

直接定址法就是取关键字的某个线性函数值作为散列地址。即

f(key)=a*key+b(a、b为常数)

例如,现在要对0~100岁的人口统计,则我们可以直接使用年龄的数字作为地址。此时f(key)=key。

地址 年龄 人数

00   0    500万

01   1    600万

……

20   20   1500万

……

这样的散列函数的优点是简单、均匀,不会产生冲突。缺点是需要事先知道关键字的分步情况。适合查找表较小且连续的情况。在现实应用中,此方法虽简单但并不常用。

2)平方取中法

这种计算方法也很简单。例如关键字是1234,它的平方是1522756,我们抽取其中间的三位数227作为散列地址。再比如关键字是4321,它的平方就是18671041,抽取中间的三位就是671(或710)作为散列地址。

平方取中法适合于不知道关键字分布,而位数又不是很大的情况。

3)除留余数法

此方法是最常用的构造散列函数的方法。对于散列表长度为m的散列函数公式为:

f(key)=key mod p(p<=m)

mod是取模(求余数)的意思。实际上不仅可以直接对关键字取模,也可以在平方取中后再取模。

很显然,除留余数法的关键就在于选取合适的p,p如果选的不好,就很容易产生同义词。

例如:现在有以下关键字列表:

12 15 16 21 22 25 29 38 47 56 67 78

我们选取P为12,则我们可以生成散列表如下:

下标  0  1  2  3  4  5  6  7  8  9  10  11

关键字12 25 38 15 16 29 78 67 56 21 22  47

选取p不合理的话,产生同义词的几率就大很多。例如,如果有以下关键字表

12  24  36  48  60  72  84  96  108  120  132  144

如果我们选取p=12的话,那么求得的地址就都是0。而如果选取p=11的话就好很多

下标  1   2   3   4   5   6   7   8   9    10   0    1

关键字12  24  36  48  60  72  84  96  108  120  132  144

此时只有12和144有冲突,相对于p=12来说就好很多。

根据经验来说,若散列表长度为m,通常p为小于或等于表长(尽量接近m)的质数。

4、处理散列冲突的方法

即使设计再好的散列函数,在实际应用中也不可能完全没有冲突。既然冲突不可避免,就要考虑如何处理它。

1)开放定址法

开放定址法就是一旦发生了冲突,就去寻找下一个空的散列位置,只要散列表足够大,空的散列地址就一定能找到,并将其存入散列表。

开放定址法又称线性探测法。

2)再散列函数法

我们事先准备多个散列函数,如果发生了冲突,就使用另外的散列函数计算,直至消除冲突为止。这种方法能使得关键字不产生冲突,当然相应地计算量也增加了。

3)链地址法

如果我们对散列表稍加修改,将普通的散列表变成一个存储链表的表,当发生了冲突时,直接将同义词都存在同一个子表中。这样的话就没有什么冲突问题了,无论有多少个冲突,只要在当前位置给链表再增加节点即可。

链地址法对于可能会造成很多冲突的散列函数来说提供了绝不会出现冲突的保障,但是也带来了查找时需要遍历链表的额外损耗。

4)公共溢出区法

在散列表外,额外建立一块与原散列表一样大小的缓冲区(溢出表)。如果发生了冲突,冲突的关键字就按顺序存入缓冲区。计算散列地址时,先与基本表进行比对,如果发现查找不成功,则在溢出表内再进行查找。如果有冲突的数据很少,公共溢出区法查找的性能相当高。

 

常见查找算法时间复杂度对比


代码

#include <stdio.h>

#include <stdlib.h>

int number[11]={0,12,16,24,35,47,59,62,73,88,99};//待查表,第一个元素是哨兵位而不是待查数据,待查数据从下标1(第二个元素)开始

int Sequential_Search(int *a,int n,int key)

{

    int i;

    for(i=1;i<=n;i++)

    {

        if(a[i]==key)

            return i;//返回查找到的下标

    }

    return 0;//返回0代表查找失败

}

int Sequential_Search2(int *a,int n,int key)

{

    int i;

    a[0]=key;//设置哨兵

    i=n;

    while(a[i]!=key)

    {

        i--;

    }

    return i;//返回0则查找失败

}

int Binary_Search(int *a,int n,int key)

{

    int low,high,mid;

    low=1;

    high=n;

    while(low<=high)

    {

        mid=(low+high)/2;

        if(key<a[mid])

            high=mid-1;

        else if(key>a[mid])

            low=mid+1;

        else

            return mid;

    }

    return 0;

}

int Interpolation_Search(int *a,int n,int key)//插值查找

{

    int low,high,mid;

    low=1;

    high=n;

    while(low<=high)

    {

        mid=low+(high-low)*(key-a[low])/(a[high]-a[low]);

        if(key<a[mid])

            high=mid-1;

        else if(key>a[mid])

            low=mid+1;

        else

            return mid;

    }

    return 0;

}

int main()

{

    int position;

    //position = Binary_Search(number,10,47);

    //printf("Position is %d\n",position);

    return 0;

}


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值