静态查找表与计算平均查找长度详解程序员

查找是在大量的信息中寻找一个特定的信息元素,在计算机应用中,查找是常用的基本运算,例如编译程序中符号表的查找。

     顺序查找,二分查找,hash查找是所有查找算法中较为基础的查找算法,较为容易理解,实现也较为简单。

NO1.顺序查找编辑

顺序查找过程:从表中的最后一个记录开始,逐个进行记录的关键字与给定值进行比较,若某个记录的关键字与给定值相等,则查找成功,找到所查的记录;反之,若直到第一个记录,其关键字和给定值比较都不相等,则表明表中没有所查的记录,查找失败。

  算法描述为

  

int Search(int d,int a[],int n) 
  { 
  /*在数组a[]中查找等于d的元素,若找到,则函数返回d在数组中的位置,否则为0。其中n为数组长度*/ 
  int i ; 
  /*从后往前查找*/ 
  for(i=0;i<n-1;i++){ 
    if(a[i] ==  d) 
    return i; 
//如果找到则返回d在数组的位置 
} 
  return 0; 
  /*如果找不到,则返回0*/ 
  }


NO2.二分查找编辑

二分查找又称折半查找,它是一种效率较高的查找方法。
  【二分查找要求】:1.必须采用顺序存储方式      2.必须按关键字大小有序排列。
【优缺点】
折半查找法的优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。  【算法思想】首先,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。  重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。  【算法
复杂度】假设其数组长度为n,其算法复杂度为o(log(n))
对一个有序的数组,
二分法就是最好的方法。
int binary_find(int *arr,int num,int value)   
{   
    if(NULL == arr || 0 == num)   
        return -1;  //如果要查找的数组为空,则返回-1 
       
    int start = 0;   
    int end = num - 1;   
   
    while(start <= end){   
        int middle = start +((end - start)  >> 1);   
        if(value == arr[middle])   
            return middle;   
        else if(value > arr[middle])   
            start = middle + 1;   
        else   
            end = middle - 1;   
    }   
    return -1;  //未找到值,返回-1 
}  


  折半查找法也称为二分查找法,它充分利用了元素间的次序关系,采用分治策略,可在最坏的情况下用O(log n)完成搜索任务。

NO3.hash查找

我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做
散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素”分类”,然后将这个元素存储在相应”类”所对应的地方。
但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了”冲突”,换句话说,就是把不同的元素分在了相同的”类”之中。后面我们将看到一种解决”冲突”的简便做法。
总的来说,”直接定址”与”解决冲突”是哈希表的两大特点。
哈希表的定义如下:1)每个数据按照某种聚类运算归到某一大类,然后所有数据链成一个链表;2)所有链表的头指针形成一个指针数组。这种方法因为不需要完整排序,所以在处理中等规模数据的时候很有效。其中节点的定义如下:

typedef struct _NODE   
{   
    int data;   
    struct _NODE* next;   
}NODE;  

    

查找代码:

NODE* hash_find(NODE* arr[],int mod,int value)   
{  //arr为要查找的序列,mod为模值,value为要查找的值 
    int index= data % mod;   
    if(NULL == arr[index])   
        return NULL;   
       
    NODE* pNode = arr[index];   
    while(pNode){   
        if(value == pNode->data)   
            return pNode;  //返回地址 
        pNode = pNode->next;   
    }   
    return pNode;   
}  

分析



      hash表因为不需要排序,只进行简单的归类,在数据查找的时候特别方便。查找时间的大小取决于mod的大小。mod越小,那么hash查找就越接近于普通查找;那么hash越大呢,那么hash一次查找成功的概率就大大增加。

Hash函数

Hash函数设计的好坏直接影响到对Hash表的操作效率。下面举例说明:

假如对上述的联系人信息进行存储时,采用的Hash函数为:姓名的每个字的拼音开头大写字母的ASCII码之和。

          address(张三)=ASCII(Z)+ASCII(S)=90+83=173;

          address(李四)=ASCII(L)+ASCII(S)=76+83=159;

          address(王五)=ASCII(W)+ASCII(W)=87+87=174;

          address(张帅)=ASCII(Z)+ASCII(S)=90+83=173;

假如只有这4个联系人信息需要进行存储,这个Hash函数设计的很糟糕。首先,它浪费了大量的存储空间,假如采用char型数组存储联系人信息的话,则至少需要开辟174*12字节的空间,空间利用率只有4/174,不到5%;另外,根据Hash函数计算结果之后,address(张三)和address(李四)具有相同的地址,这种现象称作冲突,对于174个存储空间中只需要存储4条记录就发生了冲突,这样的Hash函数设计是很不合理的。所以在构造Hash函数时应尽量考虑关键字的分布特点来设计函数使得Hash地址随机均匀地分布在整个地址空间当中。通常有以下几种构造Hash函数的方法:

1.直接定址法

取关键字或者关键字的某个线性函数为Hash地址,即address(key)=a*key+b;如知道学生的学号从2000开始,最大为4000,则可以将address(key)=key-2000作为Hash地址。

2.平方取中法

对关键字进行平方运算,然后取结果的中间几位作为Hash地址。假如有以下关键字序列{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取{72,89,00}作为Hash地址。

3.折叠法

将关键字拆分成几部分,然后将这几部分组合在一起,以特定的方式进行转化形成Hash地址。假如知道图书的ISBN号为8903-241-23,可以将address(key)=89+03+24+12+3作为Hash地址。

4.除留取余法

如果知道Hash表的最大长度为m,可以取不大于m的最大质数p,然后对关键字进行取余运算,address(key)=key%p。在这里p的选取非常关键,p选择的好的话,能够最大程度地减少冲突,p一般取不大于m的最大质数。

5.数字分析法

假设关键字是以r为基的数,并且哈希表中可能出现的关键字都是事先知道的,则可取关键字的若干数位组成哈希地址。       

例如有某些人的生日数据如下:

          年. 月. 日

          75.10.03
                85.11.23
                86.03.02
                86.07.12
                85.04.21
                96.02.15

经分析,第一位,第二位,第三位重复的可能性大,取这三位造成冲突的机会增加,所以尽量不取前三位,取后三位比较好

6.随机数法

选择一个随机函数,取关键字的随机函数值为它的哈希地址,即

H(key)=random(key) ,其中random为随机函数。通常用于关键字长度不等时采用此法。


平均查找长度
为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值称为查找算法在查找
成功时的
平均查找长度(),ASL成功。
对于含有n个数据元素的查找表,查找成功的平均查找长度为:ASL=∑PiCi (i=1,2,3,…,n),可以简单以数学上的期望来这么理解。其中:Pi 为查找表中第i个数据元素的概率,Ci为找到第i个数据元素时已经比较过的次数。
在查找表中查找不到待查元素,但是找到待查元素应该在表中存在的位置的平均查找次数称为查找
不成功时的
平均查找长度,不成功。
下面是查找算法的平均查找长度的分析:

1.顺序查找:

从表的一端开始,顺序扫描线性表,依次将扫描到的节点关键字和给定值k相比较。

等概率条件下…平均查找长度:ASL = (n+….+2+1)/n= (n+1)/2;

2.二分法查找:

前提是线性表是有序表。假设数据是按升序排序的,对于给定值x,从序列的中间位置开始比较,如果当前位置值等于x,则查找成功;若x小于当前位置值,则在数列的前半段中查找;若x大于当前位置值则在数列的后半段中继续查找,直到找到为止。

在等概率条件下…平均查找长度:ASL = log2(n+1)-1= log2(n+1) 

原因:设内部结点的总数为n=2^n-1,则判定树是深度为h=log2(n+1)的满二叉树(深度为h不计内部结点)。树中第k层上的结点个数为2^(k-1),查找他们所需的比较次数是k.因此在等概率假设下,二分查找成功时的平均查找长度为:ASL约等于log2(n+1)-1

 一有序表的关键字序列是(11,16,37,51,55,76,88,90,101,105)采用折半查找求其等概率情况下,查找成功的平均查找长度。

 按照折半查找算法,对序列中10个元素的查找过程如下:

⑴mid =(1+10)/2 查找到元素55。
⑵mid =(1+4)/2 查找到元素16。
或者mid =(6+10)/2 查找到元素90。
⑶mid =(1+1)/2 查找到元素16。
或者mid =(3+4)/2 查找到元素37。
或者mid =(6+7)/2 查找到元素76。
或者mid =(9+10)/2 查找到元素101。
⑷mid =(1+10)/2 查找到元素51。
或者mid =(7+7)/2 查找到元素105。


以上过程可以用一棵二叉树来描述,如图所示。

静态查找表与计算平均查找长度详解程序员


这棵二叉树又称为折半查找判定树

从判定树上可知,查找某一个元素所要进行的比较次数等于该元素结点在判定树中的层数。故:
ASL = (3+2+3+4+1+3+4+2+3+4) /10 = 2.9
从上例可以看出,折半查找成功的平均查找长度与序列中的具体元素无关,只取决于序列中元素的数目。所以,折半查找判定树只与查找表中元素的数目有关。

3.hash表

下面就用常用的hash表处理冲突的两种方法来举例,虽然hash表处理冲突的方法有很多种,但是这两种是较为常用的方式:

  首先,你要明白的是平均查找长度求的是期望,那么你就按照求期望的方法来求平均查找长度吧,千万记着期望怎么求平均查找长度就怎么求啊。

  题目:

在地址空间为0~16的散列区中,对以下关键字序列构造两个哈希表:
{Jan, Feb, Mar, Apr, May,  June, July, Aug, Sep, Oct, Nov, Dec}
(1) 用线性探测开放地址法处理冲突;
(2) 用链地址法(开散列存储)处理冲突 
并分别求这两个哈希表在等概率情况下查找成功和查找不成功时的平均查找长度。设哈希函数为 
H(key) = i/2,其中i为关键字中第一个字母在字母表中的序号,如下:
A B C D E F G H I  J    K   L  M  N  O   P   Q   R  S  T   U   V   W  X  Y   Z
1 2 3 4  5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

  解决如下:

(1) 线性探测进入散列区的次序如下,X 代表冲突,要找下一个空格
Jan -> 5
Feb -> 3
Mar -> 6
Apr -> 0
May -> 6X -> 7
June -> 5X -> 6X -> 7X -> 8
July -> 5X -> 6X -> 7X -> 8X -> 9
Aug -> 0X -> 1
Sep -> 9X -> 10
Oct -> 7X -> 8X -> 9X -> 10X -> 11
Nov -> 7X -> 8X -> 9X -> 10X -> 11X -> 12
Dec -> 2

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

Apr

Aug

Dec

Feb

 

Jan

Mar

May

Jun

July

Sep

OCt

Nov

 

 

 



  很明显,查找成功时,查找Jan、Feb、Mar等仅需要一次,其余的也可以由上面看出来
  所以查找成功时平均查找长度 (ASL) = (1 + 1 + 1 + 1 + 2 + 4 + 5 + 2 + 2 + 5 + 6 + 1) / 12 = 31/12 = 2.58 为什么是除以12呢?因为查找成功的情况总共有12种啊

  查找不成功时呢?什么是查找不成功呢?查找不成功就是从查找位置开始直到一个位置为空需要比较的次数。

  首先,26/2=13,也就是说查找不成功的情况也只能出现在0~13之间,只有这14种情况。

  举个例子来说,查找Aay吧,根据hash表,与Apr比较不匹配,接着与Aug比较又是不匹配,接着与Dec比较又是不匹配,又与Feb比较又是不匹配,到了4位置的时候为空了,即4上内容与nullkey比较,结果为空,所以查找Aay失败,查找长度为5。同理也能计算其他的。

  最终平均查找失败时平均查找长度为(5+4+3+2+1+9+8+7+6+5+4+3+2+1)/14=60/14。注意啊,这里是除以14啊。(这是求期望的方法啊)

(2) 链地址法
0 之下有 Apr, Aug
2 之下有 Dec
3 之下有 Feb
5 之下有 Jan, June, July
6 之下有 Mar, May
7 之下有 Oct, Nov
9 之下有 Sep
查找成功时候,查 Apr, Dec, Feb, Jan, Mar, Oct, Sep 各需 1 次,查 Aug, June, May, Nov 各需 2 次,查 July 需 3 次。

所以查找成功时平均查找长度 (ASL) = (1 * 7 + 2 * 4 + 3 * 1) / 12 = 18/12 = 1.5

查找失败时平均查找长度:举个例子吧,查找Boy,2/2=1,而1的地方的指针为空,就不用比较就可以知道不存在,查找产度为0。查找Aay,与Apr比较不匹配,与Aug比较不匹配,同时,Aug指向下一个节点的指针为空,就可以知道查找失败,查找长度为2。

 所以查找失败的平均查找长度:(2+1+1+3+2+2+1)/14=12/14。


原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/7292.html

(0)
上一篇 2021年7月17日
下一篇 2021年7月17日

相关推荐

发表回复

登录后才能评论