文章分为下面5个方面做介绍。
1. 基本思想
2. 散列函数
3. 分离链接法
4. 开放定址法
5. 再散列
6. 小结
散列是一种用于以常数时间执行插入、删除和查找的技术。但是那些需要元素间任何排序信息的操作将不会得到有效的支持。
1. 基本思想
散列表数据结构是包括一些项的具有一定大小的数组。查找一般是对项的某个部分进行,这部分称为键(key)。
将每个键映射到从0到TableSize-1这个范围中的某个数,并且将其放到适当的单元。这种映射就称为“散列函数”(hash function),理想情况下它应该计算简单并且应该使任何两个不同的键映射到不同的内存单元。实际中我们选定的散列函数应该尽可能在单元之间均匀地分配键。
这就是散列的基本思想。剩下的问题则是要选择一个函数(冲突解决函数),决定当两个键散列到同一个单元的时候(称为冲突(collision)),应该怎么处理。
2.散列函数
如果输入的键是整数,则一般合理的方法就是直接返回“Key mod TableSize”。如果输入的键是字符串,我们也可以把字符串中字符的ASCII码值加起来。这些散列函数实现简单但是容易造成冲突。如果表很大,假设TableSize = 10007,并假设所有键至多有8个字符。由于ASCII字符的值最大为127,因此这个散列函数只能在0-1016之间取值,显然不合理。
另一种散列函数如下图所示。
当散列表足够大的时候,上面这个函数还是不够合适。
散列函数的第三种尝试涉及键中的所有字符。
程序根据Horner法则计算一个37的多项式函数。
再好的散列函数也难以避免,一个新的元素在插入时,可能与一个已经插入的元素散列到相同的值,那么就产生一个冲突。解决冲突的方法有几种,最简单的两种是:分离链接法和开放定址法。
3. 分离链接法
其做法是将散列到同一个值的所有元素保留到一个链表中。考虑一个简单的情形,我们建立0-9的完全平方数,选取hash(x) = x mod 10。下图对其进行的描述。
我们用C++里面的vector和list来实现分离链接法。
类框架。
<span style="font-size:12px;">class HashTable
{
public:
HashTable();
~HashTable();
//explicit HashTable(int size = 101);
bool contains( const int& x) const;
void make_empty();
bool insert( const int& x);
bool remove( const int& x);
void print();
private:
vector< list< int> > the_lists;
int current_size;
void rehash();
int myhash( const int& x) const;
int hash( const int& x) const;
};</span>
Hash函数,这里选取最简单的形式,hash(x) = x mod TableSize。
myhash函数,这里是确保映射的值没有超出范围。
make_empty用于清空哈希表,contains检查元素是否被包含,remove移除一个元素。
<span style="font-size:12px;">int HashTable ::hash(const int & x ) const
{
int r = x % the_lists.size();
return r;
}
int HashTable ::myhash(const int & x ) const
{
int hash_val = hash( x);
hash_val %= the_lists.size();
if (hash_val < 0) {
hash_val += the_lists.size();
}
return hash_val;
}
bool HashTable ::contains(const int& x) const
{
const list< int> which_list = the_lists [myhash( x) ];
return (find(which_list.begin(), which_list.end(), x) != which_list.end());
}
void HashTable ::make_empty()
{
for ( int i = 0; i < the_lists.size(); i++) {
the_lists [i ].clear();
}
}
</span>
<span style="font-size:12px;">bool HashTable::remove(const int& x)
{
<span style="white-space:pre"> </span>list<int>& which_list = the_lists[myhash(x)];
<span style="white-space:pre"> </span>auto itr = find(which_list.begin(), which_list.end(), x);
<span style="white-space:pre"> </span>if ( itr == which_list.end())
<span style="white-space:pre"> </span>return false;
<span style="white-space:pre"> </span>which_list.erase(itr);
<span style="white-space:pre"> </span>--current_size;
<span style="white-space:pre"> </span>return true;
}
</span>
对于插入操作,为了实现简单,这里假设哈希表不允许出现重复的元素。如果要插入的项已经存在,那么什么都不错。否则将其放至表的前端。
<span style="font-size:12px;">bool HashTable ::insert(const int & x )
{
list< int>& which_list = the_lists [myhash( x) ];
if (find(which_list.begin(), which_list.end(), x) != which_list.end())
return false;
which_list.push_back( x);
// rehash
if (++current_size > the_lists.size())
rehash();
return true;
}</span>
我们定义散列表的装填因子(load factor)λ为散列表的元素个数与散列表大小的比值。分离链接法的一般方法是使得装填因子约等于1。
在插入操作中,我们需要检查表的大小是否已经超出范围(装填因子超过1)。需要当前表已经容纳不下所有元素,我们需要执行再散列操作。其基本思想是把表的空间扩大(这里我们把表的空间扩大到原空间的两倍大后的第一个质数),同时所有元素需要重新散列到新建立的哈希表空间。(因为TableSize发生了变化,哈希函数hash(x) = x mod TableSize 的值也发生了变化)
<span style="font-size:12px;">void HashTable ::rehash()
{
vector< list< int> > old_lists = the_lists;
the_lists.resize(next_prime(2 * the_lists.size()));
for ( int i = 0; i < the_lists.size(); i++) {
the_lists [i ].clear();
}
current_size = 0;
for ( int i = 0; i < old_lists.size(); i++) {
auto itr = old_lists [i ].begin();
while (itr != old_lists [i ].end())
insert( *itr ++);
}
}
int HashTable ::next_prime(const int& x)
{
int t = x;
while (t != INT_MAX) {
++t;
int i = 2;
for (; i <= sqrt(t); ++i) {
if (t % i == 0)
break;
}
if (i > sqrt(t))
return t;
}
}</span><span style="font-size:18px;">
</span>
4. 开放定址法
(1)线性探测
(2)平方探测
<span style="font-size:12px;"> struct HashEntry
{
int element;
EntryType info;
HashEntry( int ele = 0, EntryType i = EMPTY) : element( ele), info( i) {}
};
</span>
散列表的数据成员
<span style="font-size:12px;"> std:: vector< HashEntry> the_array;</span>
比较困难的是查找操作。这里介绍一种进行平方探测的快速方法。由平方解法函数的定义可知,f(i)=f(i-1)+2i-1,因此下一个要探测的单元位置由上一个探测所使用的距离加上自增2后的偏移距离所得的和来确定。
<span style="font-size:12px;">int HashTableProbing ::find_pos(const int& x) const
{
int offset = 1;
int current_pos = myhash( x);
while (the_array [current_pos ].info != EMPTY && the_array[current_pos] .element != x) {
current_pos += offset;
offset = offset + 2;
if (current_pos >= the_array.size())
current_pos -= the_array.size();
}
return current_pos;
}
</span>
插入和删除
<span style="font-size:12px;">bool HashTableProbing ::insert(const int & x )
{
int current_pos = find_pos( x);
if (is_active(current_pos))
return false;
the_array [current_pos ] = HashEntry( x, ACTIVE);
if (++current_size > the_array.size() / 2)
rehash();
return true;
}
</span>
<span style="font-size:12px;">bool HashTableProbing ::remove(const int & x )
{
int current_pos = find_pos( x);
if (is_active(current_pos))
the_array [current_pos ].info = DELETED;
else
return false;
return true;
}
</span>
(3)双散列
这个公式是说,将第二个散列函数应用到x并在距离
等处探测。第二个散列函数的选择很重要。
一般应该保证改值和TableSize互素。一个简单的做法就是保证TableSize的大小为一个素数。