数据结构复习之散列表查找(哈希表)

一、散列表相关概念

散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。公式如下:

存储位置 = f(关键字)

这里把这种对应关系f称为散列函数,又称为哈希(Hash)函数。按这个思想,采用散列技术将记录存在在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。那么,关键字对应的记录存储位置称为散列地址。

  散列技术既是一种存储方法也是一种查找方法。散列技术的记录之间不存在什么逻辑关系,它只与关键字有关,因此,散列主要是面向查找的存储结构。

散列技术最适合的求解问题是查找与给定值相等的记录。对于查找来说,简化了比较过程,效率会大大提高。

  但是,散列技术不具备很多常规数据结构的能力,比如

  •     同样的关键字,对应很多记录的情况,不适合用散列技术;
  •     散列表也不适合范围查找等等。

在理想的情况下,每一个关键字,通过散列函数计算出来的地址都是不一样的,可现实中,这只是一个理想。市场会碰到两个关键字key1 !=
key2,但是却有f(key1) =
f(key2),这种现象称为冲突。出现冲突将会造成查找错误,因此可以通过精心设计散列函数让冲突尽可能的少,但是不能完全避免。

二、散列函数的构造方法

2.1 直接定址法

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

优点:简单、均匀,也不会产生冲突。

缺点:需要事先知道关键字的分布情况,适合查找表较小且连续的情况。

由于这样的限制,在现实应用中,此方法虽然简单,但却并不常用。

2.2 数字分析法

如果关键字时位数较多的数字,比如11位的手机号"130****1234",其中前三位是接入号;中间四位是HLR识别号,表示用户号的归属地;后四为才是真正的用户号。如下图所示。

如果现在要存储某家公司的登记表,若用手机号作为关键字,极有可能前7位都是相同的,选择后四位成为散列地址就是不错的选择。若容易出现冲突,对抽取出来
的数字再进行反转、右环位移等。总的目的就是为了提供一个散列函数,能够合理地将关键字分配到散列表的各个位置。

数字分析法通过适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布比较均匀,就可以考虑用这个方法。

2.3 平方取中法

这个方法计算很简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做散列地址。

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

2.4 折叠法

折叠法是将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

比如关键字是9876543210,散列表表长为三位,将它分为四组,987|654|321|0,然后将它们叠加求和987 + 654 + 321 + 0 = 1962,再求后3位得到散列地址962。

折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。

2.5 除留余数法

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

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

很显然,本方法的关键在于选择合适的p,p如果选不好,就可能会容易产生冲突。

根据前辈们的经验,若散列表的表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。

2.6 随机数法

选择一个随机数,取关键字的随机函数值为它的散列地址。也就是f(key) = random(key)。这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。

总之,现实中,应该视不同的情况采用不同的散列函数,这里只能给出一些考虑的因素来提供参考:

(1)计算散列地址所需的时间

(2)关键字的长度;

(3)散列表的长度;

(4)关键字的分布情况;

(5)记录查找的频率。

综合以上等因素,才能决策选择哪种散列函数更合适。

三、处理散列冲突的方法

3.1 开放定址法

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

它的公式为:

比如说,关键字集合为{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},表长为12。散列函数f(key) = key mod 12。

当计算前5个数{12, 67, 56, 16, 25}时,都是没有冲突的散列地址,直接存入,如下表所示。

计算key = 37时,发现f(37) = 1,此时就与25所在的位置冲突。于是应用上面的公式f(37) = (f(37) + 1) mod 12 =2,。于是将37存入下标为2的位置。如下表所示。

接下来22,29,15,47都没有冲突,正常的存入,如下标所示。

到了48,计算得到f(48) = 0,与12所在的0位置冲突了,不要紧,我们f(48) = (f(48) + 1) mod 12 = 1,此时又与25所在的位置冲突。于是f(48) = (f(48) + 2) mod 12 = 2,还是冲突......一直到f(48) = (f(48) + 6) mod 12 = 6时,才有空位,如下表所示。

把这种解决冲突的开放定址法称为线性探测法

考虑深一步,如果发生这样的情况,当最后一个key = 34,f(key) = 10,与22所在的位置冲突,可是22后面没有空位置了,反而它的前面有一个空位置,尽管可以不断地求余后得到结果,但效率很差。因此可以改进di=12, -12, 22, -22.........q2, -q2(q<= m/2),这样就等于是可以双向寻找到可能的空位置。对于34来说,取di = -1即可找到空位置了。另外,增加平方运算的目的是为了不让关键字都聚集在某一块区域。称这种方法为二次探测法。

还有一种方法,在冲突时,对于位移量di采用随机函数计算得到,称之为随机探测法

既然是随机,那么查找的时候不也随机生成di 吗?如何取得相同的地址呢?这里的随机其实是伪随机数。伪随机数就是说,如果设置随机种子相同,则不断调用随机函数可以生成不会重复的数列,在查找时,用同样的随机种子,它每次得到的数列是想通的,相同的di 当然可以得到相同的散列地址。

总之,开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,是常用的解决冲突的方法。

3.2 再散列函数法

对于散列表来说,可以事先准备多个散列函数。

这里RH就是不同的散列函数,可以把前面说的除留余数、折叠、平方取中全部用上。每当发生散列地址冲突时,就换一个散列函数计算。

这种方法能够使得关键字不产生聚集,但相应地也增加了计算的时间。

3.3 链地址法

将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表前面的指针。对于关键字集合{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},用前面同样的12为余数,进行除留余数法,可以得到下图结构。

此时,已经不存在什么冲突换地址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。

链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保证。当然,这也就带来了查找时需要遍历单链表的性能损耗。

3.4 公共溢出区法

这个方法其实更好理解,你冲突是吧?那重新给你找个地址。为所有冲突的关键字建立一个公共的溢出区来存放。

就前面的例子而言,共有三个关键字37、48、34与之前的关键字位置有冲突,那就将它们存储到溢出表中。如下图所示。

在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表中进行顺序查找。如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。

四、散列表查找实现

#include <stdio.h>
#include <stdlib.h>
#define OK 1
#define ERROR 0
#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 //定义散列表表未数组的长度
#define NULLKEY -32768

typedef struct
{
    int *elem;  //数据元素存储基地址,动态分配数组
    int count;  //当前数据元素个数
}HashTable;
 int m = 0;     //散列表长,全局变量

 //初始化散列表
 int InitHashTable(HashTable *h)
 {
     int i;
     m = HASHSIZE;
     h->elem = (int *)malloc(sizeof(int) * m );
     if(h->elem == NULL)
     {

         fprintf(stderr, "malloc() error.\n");
         return ERROR;
     }
     for(i = 0; i < m; i++)
     {
         h->elem[i] = NULLKEY;
     }

     return OK;
 }

 //散列函数
 int Hash(int key)
 {
     return key % m;    //除留余数法
 }

 //插入关键字进散列表
 void InsertHash(HashTable *h, int key)
 {
     int addr = Hash(key);              //求散列地址
     while(h->elem[addr] != NULLKEY) //如果不为空,则冲突
     {
         addr = (addr + 1) % m;         //开放地址法的线性探测
     }

     h->elem[addr] = key;                //直到有空位后插入关键字
 }

 //散列表查找关键字
 int  SearchHash(HashTable h, int key)
 {
     int addr = Hash(key);                  //求散列地址
     while(h.elem[addr] != key)     //如果不为空,则冲突
     {
         addr = (addr + 1) % m;     //开放地址法的线性探测
         if(h.elem[addr] == NULLKEY || addr == Hash(key))
         {
             //如果循环回原点
             printf("查找失败, %d 不在Hash表中.\n", key);
             return UNSUCCESS;
         }
     }
     printf("查找成功,%d 在Hash表第 %d 个位置.\n", key, addr);
     return SUCCESS;
 }

 int main(int argc, char **argv)
 {
      int i = 0;
      int num = 0;
      HashTable h;

      //初始化Hash表
      InitHashTable(&h);

      //未插入数据之前,打印Hash表
      printf("未插入数据之前,Hash表中内容为:\n");
      for(i = 0; i < HASHSIZE; i++)
      {
          printf("%d  ", h.elem[i]);
      }
      printf("\\n");

      //插入数据
      printf("现在插入数据,请输入(A代表结束哦).\n");
      while(scanf("%d", &i) == 1 && num < HASHSIZE)
      {
          if(i == ‘a‘)
          {
              break;
          }
          num++;
          InsertHash(&h,i); 

          if(num > HASHSIZE)
          {
              printf("插入数据超过Hash表大小\n");
              return ERROR;
          }
      } 

      //打印插入数据后Hash表的内容
      printf("插入数据后Hash表的内容为:\n");
      for(i = 0; i < HASHSIZE; i++)
      {
          printf("%d  ", h.elem[i]);
      }
      printf("\n");

     printf("现在进行查询.\n");
     SearchHash(h, 12);
     SearchHash(h, 100); 

     return 0;
 } 

五、散列表的性能分析

如果没有冲突,散列查找是所介绍过的查找中效率最高的。因为它的时间复杂度为O(1)。但是,没有冲突的散列只是一种理想,在实际应用中,冲突是不可避免的。

那散列查找的平均查找长度取决于哪些因素呢?

(1)散列函数是否均匀

散列函数的好坏直接影响着出现冲突的频繁程度,但是,不同的散列函数对同一组随机的关键字,产生冲突的可能性是相同的(为什么??),因此,可以不考虑它对平均查找长度的影响。

(2)处理冲突的方法

相同的关键字、相同的散列函数,但处理冲突的方法不同,会使得平均查找长度不同。如线性探测处理冲突可能会产生堆积,显然就没有二次探测好,而链地址法处理冲突不会产生任何堆积,因而具有更好的平均查找性能。

(3)散列表的装填因子

所谓的装填因子a = 填入表中的记录个数/散列表长度。a标志着散列表的装满的程度。当填入的记录越多,a就越大,产生冲突的可能性就越大。也就说,散列表的平均查找长度取决于装填因子,而不是取决于查找集合中的记录个数。

不管记录个数n有多大,总可以选择一个合适的装填因子以便将平均查找长度限定在一个范围之内,此时散列表的查找时间复杂度就是O(1)了。为了这个目标,通常将散列表的空间设置的比查找表集合大。

引用:http://blog.chinaunix.net/uid-26548237-id-3480645.html

时间: 2024-09-29 16:32:17

数据结构复习之散列表查找(哈希表)的相关文章

[数据结构] 散列表(哈希表)

散列表(哈希表) 比较难理解的官方定义:散列表/哈希表(Hash table),是根据关键码值(Key value)而直接进行访问的数据结构.它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列函数,存放记录的数组叫做散列表. 举个例子,我们在查找中文字典时.假设我们要查找"翁weng",我们根据weng找到了对应的页码233,这个过程就是根据关键码值映射得到了表中的位置.然后我们在字典这个散列表中,根据我们刚才得到的位置 233页,直接访问了"

数据结构—散列表查找(哈希)

顺序表查找某个关键字的记录时,要从表头开始,挨个的比较a[i]与key的值时"="还是"≠",直到相等才返回i,表示查找成功,例如我们可以通过a[i]与key相比结果的 大或者小来进行折半查找到序列的下标:再通过顺序存储的存储位置计算法:LOC (ai)=LOC(a1)+(i-1)×c,得到内存地址,此时发现为了查找到结果,""比较"都是不可避免的,但是真的有必要吗?能否直接通过关键字Key找到记录的内存地址呢?答案是有的! 散列表概念

漫画 | 什么是散列表(哈希表)?

创建与输入数组相等长度的新数组,作为直接寻址表.两数之和的期望是Target,将Target依次减输入数组的元素,得到的值和直接寻址表比较,如果寻址表存在这个值则返回:如果不存在这个值则将输入数组中的元素插入寻址表,再进行输入数组中的下一个元素. 再进一步优化可以将输入数组直接作为直接寻址表,控制对应的下标就好,代码如下: Code:直接寻址表 class Solution { public int[] twoSum(int[] nums, int target) { for (int i =

十三、散列表(哈希表)

散列表 散列表插入分两步: 1. 根据散列函数找到索引 2. 处理索引冲突情况:拉链法和线性探测法 散列表是时间上和空间上作出权衡的一个例子.散列表采用函数映射找索引,查找很快,但是键的顺序信息不会保存(HashSet HashMap的本质) 散列函数 对于每种类型的键我们都学要一个与之对应的散列函数 正整数散列: 常用取余散列:k%M 浮点数散列: 例如0-1之间可以乘以一个M得到0-M-1之前的索引值,但是高位影响比低位大(0.12的1比2的影响更大,不符合均匀性),所以可以将键表示为二进制

散列表(哈希表)的实现

散列函数直接用key%size的形式,size为散列表的大小. 冲突处理采用平方探测法,为保证可以探测到整个散列表空间,散列表大小设置为4k+3形式的素数. 当散列表中的元素过多时会造成性能下降,这时应该倍增散列表的大小,重新计算原来散列表中每个元素在新的散列表中的位置. 散列表的实现 <span style="font-size:18px;">// HashTable.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h"

散列表(哈希表)

1.按先后顺序存储在A[i]中,查找需要O(n),如果用二分查找,需要O(logn) 2.定义一个一维数组A[1..1353],使得A[key]=key,这样,查找只需O(1)就可以了,但空间开销比较大 思考:有什么办法使得查找时间快,占用空间小 哈希表基本原理 哈希表的基本原理是使用一个下标范围比较大的数组A来存储元素,设计一个函数h,对于要存储的线性表的每个元素node,取一个关键字key,算出一个函数值h(key),把h(key)作为数组下标,用A[h(key)]这个数组单元来存储node

大话数据结构—散列表查找(哈希表)

一.基本概念 散列技术:在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key). f:散列函数/哈希函数: 采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表. 关键字对应的记录存储位置称为散列地址. 散列技术既是一种存储方法,也是一种查找方法. 散列技术适合求解问题是查找与给定值相等的记录.查找速度快. 散列技术不适合范围查找,不适合查找同样关键字的记录,不适合获取记录的排序,最值. 冲突:关键字key1不等于k

数据结构之散列表查找

数据结构之--散列表查找 定义:通过某个函数f,使得 ?    ?    ?存储位置=f(关键字) ?    ?    ?这样我们可以通过查找关键字不需要比较久可以获得需要记录的存储位置.这就是一种新的存储技术--散列技术. ?    ?    ?散列技术在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key).查找时根据这个确定的对应关系找到给定值key的映射f(key),若查找集合中存在这个记录,则必定在f(key)的位置上. ?    ? 

查找 之 散列表查找(哈希表)

基础概念 散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key).这里对应关系f称为散列函数,又称为哈希(Hash)函数. 采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(Hash table). 散列技术既是一种存储方法,也是一种查找方法. 散列技术最适合的求解问题是查找与给定值相等的记录.不适合一对多的查找,也不适合范围查找. 散列技术中的两个关键问题: 设计一个简单.均匀.存储利用率高的散列函数