注意:
本文中所有的代码你可以在这里:
https://github.com/qeesung/algorithm/tree/master/chapter11/11-4/openAddressing(这里会及时更新)
或者这里:
http://download.csdn.net/detail/ii1245712564/8891141
找到
在前面的文章中我们介绍过《散列表之链接法》,在链接法中,如果不同键值却将有相同的映射值,即有不同键值的元素却映射到散列表中的同一位置,那么就采用链表的方法,将映射到同一位置的元素插入到同一个链表之中,当需要删除, 查询元素时,只需要遍历该链表即可,链接法在最坏情况下删除和查询元素的时间代价为 O(n)
今天我们来讲散列表中另外一种解决冲突的方法,那就是开放定址法(open addressing)。
假如你在外面旅游时,吃坏东西,急需上厕所,当你好不容易找到一件洗手间的时候,发现排了好多人,这时你会怎么做?
没错,放弃已被占用的位置,寻找新的插入位置就是开放定址法的思想,开放定址法中的开放
二字指的是没有被占用的位置,定址
指的是确定位置。开放定址法中,所有的元素都放在散列表中(链接法放在链表中)。也就是说散列表中的每一个位置,要么有元素,要么没有元素。当需要删除,查询元素时,我们从某一个位置开始,按照某种特定的确定下一个位置的方法来检查所有表项,直到找到目标元素,或者没有找到。
散列表有三种最基本的操作:
下面我们给出开放定址法散列表的定义
template < class T >
class OpenAddressingTable
{
public:
typedef size_t (*Hash_Func_Type)( const T & , size_t , size_t);// [> 定义散列函数的指针 <]
/* 描述表的一个位置的状态 */
enum ELE_STATUS{
EMPTY=0,// 空的
DELETED=1,// 被删除的
FULL=2 //有元素的
};
// ==================== LIFECYCLE =======================================
OpenAddressingTable (size_t array_size ,Hash_Func_Type _hash_func=NULL) ; /** constructor */
~OpenAddressingTable () ; /** destructor */
/** ==================== MUTATORS ======================================= */
bool hash_insert(const T & t) ; /* 向散列表中插入新的元素 */
bool hash_search(const T & t , T & target) const ; /* 在散列表中查找键值 */
bool hash_delete(const T & t) ; /* 在散列表中删除键值对应的元素 */
void hash_clear();
private:
/** ==================== DATA MEMBERS ======================================= */
T * array_ptr ; /* 散列表的数组指针 */
const size_t array_size ; /* 散列表数组的大小 */
ELE_STATUS * status_array_ptr ; /* 用于存放各个位置的状态信息*/
Hash_Func_Type HASH ; /* 采用的散列函数,这个函数用户也可以自己指定 */
}; /** ----- end of template class OpenAddressingTable ----- */
#include "open_addressing.cc"
#endif /* ----- #ifndef OPEN_ADDRESSING_INC ----- */
只要我们找到的位置没有元素在里面,那么表示这个位置可以插入新的元素
/**
* 插入一个元素
* @param t 要插入的元素
*/
template < class T >
bool OpenAddressingTable::hash_insert (const T & t)
{
size_t i = 0;
while(i!= array_size)
{
size_t j = HASH(t , i , array_size); // 取出位置
if(status_array_ptr[j] != FULL) // 判别元素是否是开放为空的
{
array_ptr[j] = t; //插入元素
status_array_ptr[j] = FULL;//并将该位置状态设为已经被插入
return true;
}
++i;
}
std::cerr<<"hash table overflow"<<std::endl;
return false;
} /** ----- end of method OpenAddressingTable::hash_insert ----- */
查找操作和插入操作比较类似,查找操作是要找到有非空位置,并判断是否是目标元素,当遇到为EMPTY
的时候,说明之后元素也不必再检查,因为后面已经没有元素了
/*
* 查找元素
* @param t 要查找的键值
* @param target 返回找到的目标
*/
template < class T >
bool OpenAddressingTable::hash_search (const T & t , T & target) const
{
size_t i = 0;
while(i != array_size)
{
size_t j = HASH(t , i , array_size) ;
if(status_array_ptr[j] == EMPTY) // 之后就再也没有元素了
return false;
else if (status_array_ptr[j] == FULL)
{
if(array_ptr[j].key == t.key )
{
target = array_ptr[j];
return true;
}
}
++i;
}
target = T();
return false;
} /** ----- end of method OpenAddressingTable::hash_search ----- */
删除操作和上面两个操作也比较相似,但是有一点需要注意,那就是已删除元素的散列表位置不能标记为EMPTY
,而是要标记为DELETED
,因为在查找操作中是以EMPTY
作为探查的终止,如果标记为EMPTY
,那么在被删元素之后的那些元素就会丢失
template < class T >
bool OpenAddressingTable::hash_delete (const T & t)
{
size_t i = 0;
while(i !=array_size)
{
size_t j = HASH(t , i , array_size);
if(status_array_ptr[j] == EMPTY)
return false;
else if (status_array_ptr[j] == FULL)
{
if(array_ptr[j].key == t.key )
{
status_array_ptr[j] = DELETED;// 不应该是EMPTY
return true;
}
}
++i;
}
return false;
} /** ----- end of method OpenAddresingTable::hash_delete ----- */
上文提到,我们从某一个位置开始,按照某种特定的确定下一个位置的方法来检查所有表项
,而这种特定的确定下一个位置的方法就是我们这里的探查方法。
在进入正题前,我们首先来看两个问题:
问题一:为什么需要探查?
回答一:前面我们讲过,开放定址法在一个位置被占用时,就会去寻找下一个新的位置,那这个找的方法也不是乱找。那么就需要按照一定的查找规律去一个一个的探查。
问题二:什么样的探查才算是一个好的探查?
回答二:一个好的探查一定是可以等可能的探查散列表中的所有表项的,每个关键字的探查序列等可能的为 <0,1,2,...,m−1> 的 m! 种排列中的一种。但是这种探查只是理想化的,我们只能做到尽可能的趋近。
为了使用开放定址法插入一个元素,需要连续的检查散列表,直到找到一个新的空槽来放置带插入的元素为止。检查的顺序不一定是0
,1
,2
,…,m-1
的这种顺序,而是要依赖待插入的关键字key
,为了确定探查哪些槽,我们将散列函数加以扩充,使之包含探查号
作为第二个输入参数,于是散列函数就定义为:
在下面我们将介绍三种探查的方法,这三种探查的方法都能保证每一个关键字的探查序列都是 {0,1,2,3,...,m−1} 的一个排序,但是并不能做到等可能的为 {0,1,2,3,...,m−1}m! 排列中的一个。
给定一个辅助散列函数 _h:U→{0,1,2,...,m−1} ,那么线性探查的散列函数为:
首先我们探查 HashTable[_h(k)] modm 位置,如果发现已经被占用,那么就探查 HashTable[_h(k)+1] modm ,如果也被占用了,那么就探查 HashTable[_h(k)+2] modm ,以此类推,直到找到合适的位置,或者探查了 m 次以后到位置 HashTable[_h(k)−1] modm ,那么探查停止。
/**
* 辅助散列函数
* @param t 要插入的元素
* @param array_size 散列表的大小
*/
template < class T >
size_t _hash (const T & t , size_t array_size)
{
return t.key%array_size;
} /** ----- end of method OpenAddressingTable::_hash ----- */
/**
* 线性探查
* @param t 要插入的元素
* @param offset 探查号
* @param array_size 散列表大小
*/
template < class T >
size_t linear_probing (const T & t, size_t offset , size_t array_size)
{
return (_hash(t , array_size)+offset)%array_size;
} /** ----- end of method OpenAddresingTable::linear_probing ----- */
举个栗子:
假设现在有大小为 5 的散列表,现在要插入三个元素 {a1(key=0),a2(key=1),a3(key=5)} 到散列表中插入 a1 , h(0,0)=0 ,位置 0 为空,可以直接插入
插入 a2 , h(1,0)=1 ,位置 1 为空,可以直接插入
插入 a3 ,首先探查好 h(5,0)=0 ,发现位置 0 已有元素,那么接下来探查 h(5,1)=1 ,发现位置 1 也有元素了,于是继续探查 h(5,2)=2 , 2 位置为空,可以插入
可能你也发现上面的问题了,如果我们要查找 a3 ,那么就要经过一个 a1 , a2 的探查,其中 a1 和 a3 的辅助散列值相同,要经过 a1 的探查还算是情有可原,但是 a2 和 a3 基本算是无关元素,我要找 a3 ,还要探查一次 a2 ,这样未免也太慢了,而且随着插入的元素越来越多,将会探查的不相关的元素就越来越多,连续被占用的糟会越来越长,那么效率就越来越低,这种问题称为一次群集,线性探查是一次群集最为严重探查方法,我们可以使用下面两种更为巧妙的探查方法来避免一次群集
二次探查相对于线性探查来说,在散列函数中添加了一个二次项:
/**
* 二次探查
*/
template < class T >
size_t quadratic_probing (const T & t , size_t offset , size_t array_size)
{
return (_hash(t , array_size)+offset*offset)%array_size;
} /** ----- end of method OpenAddressingTable::quadratic_probing ----- */
双重散列是用于开放定址法的最好的探查方法之一,因为双重散列对于任何一个键值来说,得到的探查序列都是足够随机的,双重散列采用下面的散列函数:
有一个散列函数就已经够头疼的了,现在还多了一个新的辅助散列函数,为了能查找整个散列表,值 h2(k) 必须要与散列表的大小 m 互素。有一种简便的方法可以保证这个条件成立,就是取 m 为2的幂,并设计一个总产生奇数的散列函数 h2 。另一种方法是取 m 为素数,并设计一个总产生比 m 小d额正整数的函数 h2 。
比如在我们去 m 为一素数的时候,我们可以讲散列函数设计如下
/**
* 双重散列的第二个辅助函数
*/
template < class T >
size_t _hash1 (const T & t , size_t array_size)
{
return (1+t.key%(array_size-1));
} /** ----- end of method OpenAddressingTable::_hash1 ----- */
/**
* 双重散列
*/
template < class T >
size_t double_hashing (const T & t , size_t offset , size_t array_size)
{
return (_hash(t , array_size)+offset*_hash1(t , array_size))%array_size ;
} /** ----- end of method OpenAddressingTable::double_hash ----- */
开放定址法的所有元素都存在于散列表之内,每一个表项要么存在元素,要么就为空,当发生映射值冲突的时候我们可以探查新的位置。最好的探查方法是双重散列,因为双重散列产生的探查序列足够随机,不像线性探查和二次探查哪样存在较为严重的群集现象。
开放定址法相对于链接法来说,可以将存储链表指针的内存空出来存储更多的数据,直接跳过了指针的操作,而是用数组的直接访问来访问元素,但是如果探查散列函数设计差劲的话,将会严重拖慢散列表的速度!