哈希表初篇

来源:互联网 发布:小海淘宝助手1089 编辑:程序博客网 时间:2024/05/21 01:30

一、哈希表

    哈希表又称散列表。哈希表存储的基本思想是:以数据表中的每个记录的关键字k为自变量,通过一种函数H(k)计算出函数值。把这个值解释为一块连续存储空间(即数组空间)的单元地址(即下标),将该记录存储到这个单元中。在此称该函数H为哈希函数或散列函数。按这种方法建立的表称为哈希表或散列表。

        我们知道:哈希表是一个固定大小的数组,数组的每个元素是一个链表(单向或双向)的头指针。哈希表的查询是飞快的。因为它不需要从头搜索,它利用Key的“哈希算法”直接定位,查找非常快,各种数据库中的数据结构基本都是它。但带来的问题是,哈希表的尺寸、哈希算法。

        哈希表的数组是定长的,如果太大,则浪费,如果太小,体现不出效率。合适的数组大小是哈希表的性能的关键。哈希表的尺寸最好是一个质数,最小的质数尺寸是17。

         当然,根据不同的数据量,会有不同的哈希表的大小。对于数据量时多时少的应用,最好的设计是使用动态可变尺寸的哈希表,那么如果你发现哈希表尺寸太小了,比如其中的元素是哈希表尺寸的2倍时,我们就需要扩大哈希表尺寸,一般是扩大一倍。

        理想情况下,哈希函数在关键字和地址之间建立了一个一一对应关系,从而使得查找只需一次计算即可完成。由于关键字值的某种随机性,使得这种一一对应关系难以发现或构造。因而可能会出现不同的关键字对应一个存储地址。即k1≠k2H(k1)=H(k2),这种现象称为冲突把这种具有不同关键字值而具有相同哈希地址的对象称同义词

在大多数情况下,冲突是不能完全避免的。这是因为所有可能的关键字的集合可能比较大,而对应的地址数则可能比较少。

 对于哈希技术,主要研究两个问题:

(1)如何设计哈希函数以使冲突尽可能少地发生。

(2)发生冲突后如何解决

二、哈希函数

构造好的哈希函数的方法,应能使冲突尽可能地少,因而应具有较好的随机性。这样可使一组关键字的散列地址均匀地分布在整个地址空间。根据关键字的结构和分布的不同,可构造出许多不同的哈希函数。

1.直接定址法

   直接定址法是以关键字k本身或关键字加上某个数值常量c作为哈希地址的方法。该哈希函数H(k)为:

     H(k)=k+c  (c≥0) c也可以小于0 吧???

  这种哈希函数计算简单,并且不可能有冲突发生。当关键字的分布基本连续时,可使用直接定址法的哈希函数。否则,若关键字分布不连续将造成内存单元的大量浪费。

2.除留余数法(Division Method)

   取关键字k除以哈希表长度m所得余数作为哈希函数地址的方法。即:

     H(k)=k%m   

   这是一种较简单、也是较常见的构造方法。这种方法的关键是选择好哈希表的长度m。使得数据集合中的每一个关键字通过该函数转化后映射到哈希表的任意地址上的概率相等

  通常不能m不能选择太小的数和偶数。

  ex1:当所有的数都是偶数时,如果m是较小的偶数,那么映射的结果永远为偶数,所以导致奇数的槽永远不会被使用

  ex2:如果m = 2^r, 那么哈希值依靠的不是数中所有的位数,而仅仅只是低阶r位。如1 0 1 0 11 0 1 1 1 0对2^6求余,如果是对于大于11 0 1 1 1 0 的数, 余数都为红色标记部分,对于小于11 0 1 1 1 0的数, 余数为a-1位数(最高位数)。

  所以,在m取值为素数(质数prime)时,冲突可能性相对较少。 并且m不能太接近2或者10这种常见进制的幂。

3.随机数法

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

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

4.平方取中法

   取关键字平方后的中间几位作为哈希函数地址(若超出范围时,可再取模)。

5.折叠法

   这种方法适合在关键字的位数较多,而地址区间较小的情况

   将关键字分隔成位数相同的几部分。然后将这几部分的叠加和作为哈希地址(若超出范围,可再取模)。

   例如,假设关键字为某人身份证号码430104681015355,则可以用4位为一组进行叠加。即5355+8101+1046+430=14932,舍去高位。则有H(430104681015355)=4932为该身份证关键字的哈希函数地址。

6.(multiplication method) > Division Method

  m = 2^r, 假设计算机有wbits位数

  h(k)=((A*K) % 2^w) rsb(w-r)(right shift bitsite, 右移w-r比特位), 并且w,r都是已知的

其中,A为奇数整数,2^(w-1)<A<2^w,但是A不能太接近其中一个,它的速度快于求余法,是因为移位操作会更快

ex:m = 2^3=8, w=7, A = 1 0 1 1 0 0 1

                  *k = 1 1 0 1 0 1 1

         1 0 0 1 0 1 0 0 1 1 0 0 1 1  % 2^7 只要低7位

                       01 1 0 0 1 1  rsb 4

                                                        0 1 1 = 3

三、冲突处理方法

假设哈希表的地址范围为0m-l当对给定的关键字k,由哈希函数H(k)算出的哈希地址为i(0≤i≤m-1)的位置上已存有记录,这种情况就是冲突现象处理冲突就是为该关键字的记录找到另一个的哈希地址。即通过一个新的哈希函数得到一个新的哈希地址。如果仍然发生冲突,则再求下一个,依次类推。直至新的哈希地址不再发生冲突为止。

常用的处理冲突的方法有开放地址法open addressing链地址法storage of links两大类

1.开放定址法

  用开放定址法处理冲突就是当冲突发生时,形成一个地址序列。沿着这个序列逐个探测,直到找出一个的开放地址。将发生冲突的关键字值存放到该地址中去。工作方式和搜索操作是一样的。

  如 Hi=(H(k)+d(i)) % m, i=1,2,k  (k<m-1)

  其中H(k)为哈希函数,m为哈希表长,d为增量函数,d(i)=dl,d2dn-l

  增量序列的取法不同,可得到不同的开放地址处理冲突探测方法。

(1)线性探测法Linear Probe

   线性探测法是从发生冲突的地址(设为d)开始,依次探查d+l,d+2,m-1(当达到表尾m-1时,又从0开始探查)等地址,直到找到一个空闲位置来存放冲突处的关键字。

   若整个地址都找遍仍无空地址,则产生溢出。

   线性探查法的数学递推描述公式为:

          d0=H(k, 0)

                      di=(di-1+1)% m (1≤i≤m-1)     h(k, i) = (h(k,0) + i ) mod m

    但是这种方法会出现primary clustering, long unused slots???

(2)平方探查法

  设发生冲突的地址为d,则平方探查法的探查序列为:d+12,d+22直到找到一个空闲位置为止。

平方探查法的数学描述公式为:

     d0=H(k)

     di=(d0+i*i) % m (1≤i≤m-1)

(3)double-hashing 双哈希函数

     h(k, i) = (h1(k) + i*h2(k) ) mod m

    一般情况h1(k)为偶,h2(k)为奇数,在m = 2^r时算法表现非常好 

   开放式寻址法的分析:需要截图加上。。。

(2)伪随机法

    设发生哈希冲突的地址为d,则伪随机数法的探查序列为d+Ri。Ri为一伪随机数序列的第i个数值。伪随机数法的数学递推描述公式为:

2.链地址法

用链地址法解决冲突的方法是:把所有关键字为同义词的记录存储在一个线性链表中,这个链表称为同义词链表。并将这些链表的表头指针放在数组中(下标从0到m-1)。这类似于图中的邻接表和树中孩子链表的结构。

第一种方法是为发生哈希冲突的不同的同义词建立不同的单链表,
第二种方法是为所有发生哈希冲突的数据元素建立一个单链表。

3.再哈希法:

方法:构造若干个哈希函数,当发生冲突时,根据另一个哈希函数计算下一个哈希地址,直到冲突不再发生。缺点:计算时间增加。

4.建立一个公共溢出区

假设哈希函数的值域为[0,m-1],则设向量HashTable[0..m-1]为基本表,另外设立存储空间向量OverTable[0..v]用以存储发生冲突的记录。。 

四.  Hash查找过程

 对于给定值 K,计算哈希地址 i = H(K),若 r[i] = NULL  则查找不成功,若 r[i].key = K  则查找成功,否则“求下一地址 Hi”,直至r[Hi] =NULL  (查找不成功)  或r[Hi].key = K  (查找成功) 为止。

查找及性能分析

第一:与装填因子(loading factor)有关

  所谓装填因子是指哈希表中己存入的元素个数n与哈希表的大小m的比值,即=n/m。

 当越小时,发生冲突的可能性越小,越大(最大为1)时,发生冲突的可能性就越大。

第二:与所构造的哈希函数有关

   若哈希函数选择得当,就可使哈希地址尽可能均匀地分布在哈希地址空间上,从而减少冲突的发生。否则,若哈希函数选择不当,就可能使哈希地址集中于某些区域,从而加大冲突的发生。

第三:与解决冲突的哈希冲突函数有关

  哈希冲突函数选择的好坏也将减少或增加发生冲突的可能性。 

四.  给出一个哈希表C++实现的例子

#include<iostream>   using namespace std;  const int size=151; //it should be prime, the smallest prime that can be used as size is 17 typedef struct element  {      int key;      int state;  }Element;  class HashTable  {  private:      Element * m_element;      int m_size;      int m_nodenum;  static int m_collision; static int no_collision;public:      HashTable(int a):m_size(size) //init an  array with m_size, with integer 0 {    m_nodenum=0;   m_element=new Element[m_size];   //new an array  for(int i=0;i<m_size;i++)    {     m_element[i].key=0;              m_element[i].state=0;    }   }      ~HashTable()   {          delete []m_element; //should delete this space  }  int HashFunc(int key)   {    return (key)%(m_size);     }  int HashCollisionFunc(int index, int collision_time) //collision time from 1 to ....  {    //collision_time = (collision_time*3)%m_size;  return (index + collision_time) % (m_size);    //collision time = (collision_time*3)%m_size) }    bool Insert(int key);      bool Search(int item,int &pos);      int GetSize()   {          return m_nodenum;   }      void Display()   {    if(m_nodenum==0)    {     cout<<"the table is empty!"<<endl;              return ;    }          for(int i=0;i<m_size;i++)    {              if(this->m_element[i].state==1)     {                  cout.width(4);                  cout<<m_element[i].key;                  cout<<"                         ";                  cout.width(3);                  cout<<i<<endl;     }    }   cout<<"total collison time"<<m_collision<<"collision rate"<<((float)(m_nodenum-no_collision)/(float)m_nodenum)<<"collision rate2 "<<(((float)m_collision)/((float)(m_collision+no_collision)))<<endl; }    };  int HashTable::m_collision = 0;int HashTable::no_collision = 0;  bool HashTable::Insert(int key)  {   if(m_size == m_nodenum) {  cout<<"table is full, insert failed!"<<endl;  return false; } int index = HashFunc(key); int oldindex = 0; int collision_time = 0;    while(collision_time<m_size)  {          if(m_element[index].state!=1)    {     m_element[index].state = 1;   m_element[index].key = key;   if(collision_time == 0)   {    no_collision++;//used for statistic   }   m_nodenum++;     cout<<"value"<<key<<"index"<<index<<endl;   return true;    }          else  //deal with collision  {   collision_time++;   m_collision++;//statistic   oldindex = index;            index = HashCollisionFunc(index,collision_time);     cout<<"deal collision time"<<collision_time<<"old index"<<oldindex<<"new index"<<index<<endl;  } } cout<<"insert"<<key<<" failed!"<<endl; return false;      }  bool HashTable::Search(int item, int &pos)  {      int index = HashFunc(item);  int collision_time = 0;    while(collision_time<m_size)  {          if(m_element[index].state==1&&m_element[index].key==item)    {              cout<<"find the element "<<item<<" and the position is "<<index<<endl;              pos=index;              return true;    }          else  //deal with collision  {   collision_time++;            index = HashCollisionFunc(index,collision_time);    } }      cout<<"could not find element"<<item<<endl;      pos=-1;      return false;  }  int main()  {   HashTable htable(size); for(int i = 0; i < size-20; i++) {  htable.Insert(rand()%1000); }      htable.Display();      int pos;      //htable.Search(41,pos);      //cout<<"the pos is "<<pos<<endl;  system("pause");    return 0;  }

 可以通过相关的统计数据来分析算法的优越性,这个实例在151空间插入1000之内的131个随机数, 冲突率为39%, 可以看出算法效率并不高。

0 0
原创粉丝点击