算法导论---------------散列表(hash table)

摘要:

  本章介绍了散列表(hash table)的概念、散列函数的设计及散列冲突的处理。散列表类似与字典的目录,查找的元素都有一个key与之对应,在实践当中,散列技术的效率是很高的,合理的设计散函数和冲突处理方法,可以使得在散列表中查找一个元素的期望时间为O(1)。散列表是普通数组概念的推广,在散列表中,不是直接把关键字用作数组下标,而是根据关键字通过散列函数计算出来的。书中介绍散列表非常注重推理和证明,看的时候迷迷糊糊的,再次证明了数学真的很重要。在STL中map容器的功能就是散列表的功能,但是map采用的是红黑树实现的,后面接着学习,关于map的操作可以参考:http://www.cplusplus.com/reference/map/

1、直接寻址表

  当关键字的的全域(范围)U比较小的时,直接寻址是简单有效的技术,一般可以采用数组实现直接寻址表,数组下标对应的就是关键字的值,即具有关键字k的元素被放在直接寻址表的槽k中。直接寻址表的字典操作实现比较简单,直接操作数组即可以,只需O(1)的时间。

2、散列表

  直接寻址表的不足之处在于当关键字的范围U很大时,在计算机内存容量的限制下,构造一个存储|U|大小的表不太实际。当存储在字典中的关键字集合K比所有可能的关键字域U要小的多时,散列表需要的存储空间要比直接寻址表少的很多。散列表通过散列函数h计算出关键字k在槽的位置。散列函数h将关键字域U映射到散列表T[0....m-1]的槽位上。即h:U->{0,1...,m-1}。采用散列函数的目的在于缩小需要处理的小标范围,从而降低了空间的开销。

  散列表存在的问题:两个关键字可能映射到同一个槽上,即碰撞(collision)。需要找到有效的办法来解决碰撞。

3、散列函数

  好的散列函数的特点是每个关键字都等可能的散列到m个槽位上的任何一个中去,并与其他的关键字已被散列到哪一个槽位无关。多数散列函数都是假定关键字域为自然数N={0,1,2,....},如果给的关键字不是自然数,则必须有一种方法将它们解释为自然数。例如对关键字为字符串时,可以通过将字符串中每个字符的ASCII码相加,转换为自然数。书中介绍了三种设计方案:除法散列法、乘法散法和全域散列法。

(1)除法散列法

  通过取k除以m的余数,将关键字k映射到m个槽的某一个中去。散列函数为:h(k)=k mod m 。m不应是2的幂,通常m的值是与2的整数幂不太接近的质数。

(2)乘法散列法

  这个方法看的时候不是很明白,没有搞清楚什么意思,先将基本的思想记录下来,日后好好消化一下。乘法散列法构造散列函数需要两个步骤。第一步,用关键字k乘上常数A(0<A<1),并抽取kA的小数部分。然后,用m乘以这个值,再取结果的底。散列函数如下:h(k) = m(kA mod 1)。

(3)全域散列

  给定一组散列函数H,每次进行散列时候从H中随机的选择一个散列函数h,使得h独立于要存储的关键字。全域散列函数类的平均性能是比较好的。

4、碰撞处理

  通常有两类方法处理碰撞:开放寻址(Open Addressing)法和链接(Chaining)法。前者是将所有结点均存放在散列表T[0..m-1]中;后者通常是把散列到同一槽中的所有元素放在一个链表中,而将此链表的头指针放在散列表T[0..m-1]中。

(1)开放寻址法

  所有的元素都在散列表中,每一个表项或包含动态集合的一个元素,或包含NIL。这种方法中散列表可能被填满,以致于不能插入任何新的元素。在开放寻址法中,当要插入一个元素时,可以连续地检查或探测散列表的各项,直到有一个空槽来放置待插入的关键字为止。有三种技术用于开放寻址法:线性探测、二次探测以及双重探测。

<1>线性探测

  给定一个普通的散列函数h‘:U —>{0,1,.....,m-1},线性探测方法采用的散列函数为:h(k,i) = (h‘(k)+i)mod m,i=0,1,....,m-1  

探测时从i=0开始,首先探查T[h‘(k)],然后依次探测T[h‘(k)+1],…,直到T[h‘(k)+m-1],此后又循环到T[0],T[1],…,直到探测到T[h‘(k)-1]为止。探测过程终止于三种情况:

  (1)若当前探测的单元为空,则表示查找失败(若是插入则将key写入其中);

  (2)若当前探测的单元中含有key,则查找成功,但对于插入意味着失败;

  (3)若探测到T[h‘(k)-1]时仍未发现空单元也未找到key,则无论是查找还是插入均意味着失败(此时表满)。

线性探测方法较容易实现,但是存在一次群集问题,即连续被占用的槽的序列变的越来越长。采用例子进行说明线性探测过程,已知一组关键字为(26,36,41,38,44,15,68,12,6,51),用除余法构造散列函数,初始情况如下图所示:

散列过程如下图所示:

<2>二次探测

  二次探测法的探查序列是:h(k,i) =(h‘(k)+i*i)%m ,0≤i≤m-1 。初次的探测位置为T[h‘(k)],后序的探测位置在次基础上加一个偏移量,该偏移量以二次的方式依赖于i。该方法的缺陷是不易探查到整个散列空间。

<3>双重散列

  该方法是开放寻址的最好方法之一,因为其产生的排列具有随机选择的排列的许多特性。采用的散列函数为:h(k,i)=(h1(k)+ih2(k)) mod m。其中h1和h2为辅助散列函数。初始探测位置为T[h1(k)],后续的探测位置在此基础上加上偏移量h2(k)模m。

(2)链接法

  将所有关键字为同义词的结点链接在同一个链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于1,但一般均取α≤1。

  举例说明链接法的执行过程,设有一组关键字为(26,36,41,38,44,15,68,12,6,51),用除余法构造散列函数,初始情况如下图所示:

最终结果如下图所示:

5、字符串散列

  通常都是将元素的key转换为数字进行散列,如果key本身就是整数,那么散列函数可以采用keymod tablesize(要保证tablesize是质数)。而在实际工作中经常用字符串作为关键字,例如身姓名、职位等等。这个时候需要设计一个好的散列函数进程处理关键字为字符串的元素。参考《数据结构与算法分析》第5章,有以下几种处理方法:

方法1:将字符串的所有的字符的ASCII码值进行相加,将所得和作为元素的关键字。设计的散列函数如下所示:

1 int hash(const string& key,int tablesize)
2 {
3     int hashVal = 0;
4     for(int i=0;i<key.length();i++)
5            hashVal += key[i];
6     return hashVal % tableSize;
7 }

  此方法的缺点是不能有效的分布元素,例如假设关键字是有8个字母构成的字符串,散列表的长度为10007。字母最大的ASCII码为127,按照方法1可得到关键字对应的最大数值为127×8=1016,也就是说通过散列函数映射时只能映射到散列表的槽0-1016之间,这样导致大部分槽没有用到,分布不均匀,从而效率低下。

方法2:假设关键字至少有三个字母构成,散列函数只是取前三个字母进行散列。设计的散列函数如下所示:

1 int hash(const string& key,int tablesize)
2 {
3         //27 represents the number of letters plus the blank
4         return (key[0]+27*key[1]+729*key[2])%tablesize;
5 }

  该方法只是取字符串的前三个字符的ASCII码进行散列,最大的得到的数值是2851,如果散列的长度为10007,那么只有28%的空间被用到,大部分空间没有用到。因此如果散列表太大,就不太适用。

方法3:借助Horner‘s 规则,构造一个质数(通常是37)的多项式,(非常的巧妙,不知道为何是37)。计算公式为:key[keysize-i-1]37^i,0<=i<keysize求和。设计的散列函数如下所示:

 1 int hash(const string & key,int tablesize)
 2 {
 3         int hashVal = 0;
 4         for(int i =0;i<key.length();i++)
 5             hashVal = 37*hashVal + key[i];
 6         hashVal %= tableSize;
 7         if(hashVal<0)  //计算的hashVal溢出 8            hashVal += tableSize;
 9        return hashVal;
10 }

  该方法存在的问题是如果字符串关键字比较长,散列函数的计算过程就变长,有可能导致计算的hashVal溢出。针对这种情况可以采取字符串的部分字符进行计算,例如计算偶数或者奇数位的字符。

6、再散列(rehashing)

  如果散列表满了,再往散列表中插入新的元素时候就会失败。这个时候可以通过创建另外一个散列表,使得新的散列表的长度是当前散列表的2倍多一些,重新计算各个元素的hash值,插入到新的散列表中。再散列的问题是在什么时候进行最好,有三种情况可以判断是否该进行再散列:

(1)当散列表将快要满了,给定一个范围,例如散列被中已经被用到了80%,这个时候进行再散列。

(2)当插入一个新元素失败时候,进行再散列。

(3)根据装载因子(存放n个元素的、具有m个槽位的散列表T,装载因子α=n/m,即每个链子中的平均存储的元素数目)进行判断,当装载因子达到一定的阈值时候,进行在散列。

  在采用链接法处理碰撞问题时,采用第三种方法进行在散列效率最好。

7、实例练习

  看完书后,有一股想把hash表实现的冲动。在此设计的散列表针对的是关键字为字符串的元素,采用字符串散列函数方法3进行设计散列函数,采用链接方法处理碰撞,然后采用根据装载因子(指定为1,同时将n个元素映射到一个链表上,即n==m时候)进行再散列。采用C++,借助vector和list,设计的hash表框架如下:

 1 template <class T>
 2 class HashTable
 3 {
 4 public:
 5     HashTable(int size = 101);
 6     int insert(const T& x);
 7     int remove(const T& x);
 8     int contains(const T& x);
 9     void make_empty();
10     void display()const;
11 private:
12     vector<list<T> > lists;
13     int currentSize;//当前散列表中元素的个数
14     int hash(const string& key);
15     int myhash(const T& x);
16     void rehash();
17 };

实现的完整程序如下所示:

  1 #include <iostream>
  2 #include <vector>
  3 #include <list>
  4 #include <string>
  5 #include <cstdlib>
  6 #include <cmath>
  7 #include <algorithm>
  8 using namespace std;
  9
 10 int nextPrime(const int n);
 11
 12 template <class T>
 13 class HashTable
 14 {
 15 public:
 16     HashTable(int size = 101);
 17     int insert(const T& x);
 18     int remove(const T& x);
 19     int contains(const T& x);
 20     void make_empty();
 21     void display()const;
 22 private:
 23     vector<list<T> > lists;
 24     int currentSize;
 25     int hash(const string& key);
 26     int myhash(const T& x);
 27     void rehash();
 28 };
 29
 30 template <class T>
 31 HashTable<T>::HashTable(int size)
 32 {
 33     lists = vector<list<T> >(size);
 34     currentSize = 0;
 35 }
 36
 37 template <class T>
 38 int HashTable<T>::hash(const string& key)
 39 {
 40     int hashVal = 0;
 41     int tableSize = lists.size();
 42     for(int i=0;i<key.length();i++)
 43         hashVal = 37*hashVal+key[i];
 44     hashVal %= tableSize;
 45     if(hashVal < 0)
 46         hashVal += tableSize;
 47     return hashVal;
 48 }
 49
 50 template <class T>
 51 int HashTable<T>:: myhash(const T& x)
 52 {
 53     string key = x.getName();
 54     return hash(key);
 55 }
 56 template <class T>
 57 int HashTable<T>::insert(const T& x)
 58 {
 59     list<T> &whichlist = lists[myhash(x)];
 60     if(find(whichlist.begin(),whichlist.end(),x) != whichlist.end())
 61         return 0;
 62     whichlist.push_back(x);
 63     currentSize = currentSize + 1;
 64     if(currentSize > lists.size())
 65         rehash();
 66     return 1;
 67 }
 68
 69 template <class T>
 70 int HashTable<T>::remove(const T& x)
 71 {
 72
 73     typename std::list<T>::iterator iter;
 74     list<T> &whichlist = lists[myhash(x)];
 75     iter = find(whichlist.begin(),whichlist.end(),x);
 76     if( iter != whichlist.end())
 77     {
 78           whichlist.erase(iter);
 79           currentSize--;
 80           return 1;
 81     }
 82     return 0;
 83 }
 84
 85 template <class T>
 86 int HashTable<T>::contains(const T& x)
 87 {
 88     list<T> whichlist;
 89     typename std::list<T>::iterator iter;
 90     whichlist = lists[myhash(x)];
 91     iter = find(whichlist.begin(),whichlist.end(),x);
 92     if( iter != whichlist.end())
 93           return 1;
 94     return 0;
 95 }
 96
 97 template <class T>
 98 void HashTable<T>::make_empty()
 99 {
100     for(int i=0;i<lists.size();i++)
101         lists[i].clear();
102     currentSize = 0;
103     return 0;
104 }
105
106 template <class T>
107 void HashTable<T>::rehash()
108 {
109     vector<list<T> > oldLists = lists;
110     lists.resize(nextPrime(2*lists.size()));
111     for(int i=0;i<lists.size();i++)
112         lists[i].clear();
113     currentSize = 0;
114     for(int i=0;i<oldLists.size();i++)
115     {
116         typename std::list<T>::iterator iter = oldLists[i].begin();
117         while(iter != oldLists[i].end())
118             insert(*iter++);
119     }
120 }
121 template <class T>
122 void HashTable<T>::display()const
123 {
124     for(int i=0;i<lists.size();i++)
125     {
126         cout<<i<<": ";
127         typename std::list<T>::const_iterator iter = lists[i].begin();
128         while(iter != lists[i].end())
129         {
130             cout<<*iter<<" ";
131             ++iter;
132         }
133         cout<<endl;
134     }
135 }
136 int nextPrime(const int n)
137 {
138     int ret,i;
139     ret = n;
140     while(1)
141     {
142         int flag = 1;
143         for(i=2;i<sqrt(ret);i++)
144             if(ret % i == 0)
145             {
146                 flag = 0;
147                 break;
148             }
149         if(flag == 1)
150             break;
151         else
152         {
153             ret = ret +1;
154             continue;
155         }
156     }
157     return ret;
158 }
159
160 class Employee
161 {
162 public:
163     Employee(){}
164     Employee(const string n,int s=0):name(n),salary(s){ }
165     const string & getName()const  { return name; }
166     bool operator == (const Employee &rhs) const
167     {
168         return getName() == rhs.getName();
169     }
170     bool operator != (const Employee &rhs) const
171     {
172         return !(*this == rhs);
173     }
174     friend ostream& operator <<(ostream& out,const Employee& e)
175     {
176         out<<"("<<e.name<<","<<e.salary<<") ";
177         return out;
178     }
179 private:
180     string name;
181     int salary;
182 };
183
184 int main()
185 {
186     Employee e1("Tom",6000);
187     Employee e2("Anker",7000);
188     Employee e3("Jermey",8000);
189     Employee e4("Lucy",7500);
190     HashTable<Employee> emp_table(13);
191
192     emp_table.insert(e1);
193     emp_table.insert(e2);
194     emp_table.insert(e3);
195     emp_table.insert(e4);
196
197     cout<<"Hash table is: "<<endl;
198     emp_table.display();
199     if(emp_table.contains(e4) == 1)
200         cout<<"Tom is exist in hash table"<<endl;
201     if(emp_table.remove(e1) == 1)
202           cout<<"Removing Tom form the hash table successfully"<<endl;
203     if(emp_table.contains(e1) == 1)
204         cout<<"Tom is exist in hash table"<<endl;
205     else
206         cout<<"Tom is not exist in hash table"<<endl;
207     //emp_table.display();
208     exit(0);
209 }

程序测试结果如下所示:

时间: 2024-11-08 19:06:44

算法导论---------------散列表(hash table)的相关文章

算法导论-散列表(Hash Table)

目录 引言 直接寻址 散列寻址 散列函数 除法散列 乘法散列 全域散列 完全散列 碰撞处理方法 链表法 开放寻址法 线性探查 二次探查 双重散列 随机散列 再散列问题 完整源码(C++) 参考资料 内容 1.引言 如果想在一个n个元素的列表中,查询元素x是否存在于列表中,首先想到的就是从头到尾遍历一遍列表,逐个进行比较,这种方法效率是Θ(n):当然,如果列表是已经排好序的话,可以采用二分查找算法进行查找,这时效率提升到Θ(logn);  本文中,我们介绍散列表(HashTable),能使查找效率

Java 散列表 hash table

Java 散列表 hash table @author ixenos hash table, HashTable, HashMap, HashSet hash table 是一种数据结构 hash table 为每个对象计算一个整数,该整数被称为散列码 hash code hash code 是由对象的实例域产生的一个整数,具有不同的数据域的对象将产生不同的hash code 如果自定义类,就要负责实现这个类的hashCode方法,注意要与equals方法兼容,即如果a.equals(b)为tr

散列表(Hash table)及其构造

散列表(Hash table) 散列表,是根据关键码值(Key value)而直接进行访问的数据结构.它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度.这个映射函数叫做散列函数,存放记录的数组叫做散列表. 已知的查找方法: 1.顺序查找 O(N) 2.二分查找(静态查找) O(log2N) 3.二叉搜索树 O(h) h为二叉树的高度 平衡二叉树 O(log2N) Q:如何快速搜索到需要的关键字?如果关键字不方便比较怎么办? 查找的本质:已知对象找位置 有序安排对象:全序.半序 直接

散列表(hash table)&mdash;&mdash;算法导论(13)

1. 引言     许多应用都需要动态集合结构,它至少需要支持Insert,search和delete字典操作.散列表(hash table)是实现字典操作的一种有效的数据结构. 2. 直接寻址表     在介绍散列表之前,我们前介绍直接寻址表.     当关键字的全域U(关键字的范围)比较小时,直接寻址是一种简单而有效的技术.我们假设某应用要用到一个动态集合,其中每个元素的关键字都是取自于全域U={0,1,-,m-1},其中m不是一个很大的数.另外,假设每个元素的关键字都不同.    为表示动

非对称算法,散列(Hash)以及证书的那些事

转载请注明出处 http://blog.csdn.net/pony_maggie/article/details/35389657 作者:小马 这几个概念在金融电子支付领域用得比较多,我忽然觉得把它们串起来一起讲,层层引入,可能更好理解一些.希望能以最简单朴实的方式讲明白他们之间的关系. 一非对称算法 关于非对称算法,你只要知道下面这些就行了,密钥是一对,一个叫公钥,一个叫私钥,前者公开,后者保密.假设你有一对公私钥,给你一串数据,你可以用私钥加密,然后把密文和公钥都放出去,别人可以用这个公钥解

算法导论11.2散列表Hash tables链式法解决碰撞

/* * IA_11.2ChainedHash.cpp * * Created on: Feb 12, 2015 * Author: sunyj */ #include <stdint.h> #include <iostream> #include <string.h> // CHAINED-HASH-INSERT(T, x) // insert x at the head of list T[h(x.key)] // CHAINED-HASH-SEARCH(T, k)

算法_散列表

使用散列的查找算法分为两步,第一步用散列函数将被查找的键转化为数组的一个索引,理想情况下不同的键都被转化为不同的索引值.而当多个键散列到相同的索引值的情况下,就需要处理碰撞冲突,为此有两种方法,拉链法和线性探测法. 散列函数用于通过键来获取其对应的索引值.好的散列函数应该具有计算简便,等价的键必然产生相等的散列值,均匀的散列所有的键的条件. 一.基于拉链法的散列表. 拉链法对于碰撞处理的解决方法是将大小为M的数组中的每个元素都指向一个链表,链表中的每一个节点都存储了散列值为该元素的索引的键值对.

散列表(hash表)

1. hash表: 又称散列表,以key-value的形式存储数据,能够由key快速定位到其指定的value,而不经过查找.它采用了函数式的映射思想,将记录的存储位置与关键词相关联,从而快速定位进行查找,复杂度为O(1). 2. hash函数: key和value的映射关系称为HASH函数,通过该函数可以计算key所对应的存储位置(表中存储位置,不是实际物理地址),即HASH地址. 构造HASH地址的方法有: (1)直接定址法:取关键词或关键词的某个线性函数为hash地址. (2)平方取中法:关

数据结构和算法篇——散列表

之前讲过博主在某网买了一个数据结构与算法的课程.本篇散列表是其中的三节.散列表应该是 Java 程序员常用并且最先碰到的一个数据结构了吧?Java 的 HashMap 就是对散列表的实现.可以说散列表算是一个比较基础.比较好理解(抛开需要缜密设计的哈希函数不说).比较好用(查询时间复杂度O(1))的一种数据结构.本篇在此分享这三节的总结笔记. 1)散列表开篇介绍:https://www.cnblogs.com/christmad/p/11519055.html 2)如何打造一个工业级的散列表:h