概念
散列是一种以常数平均时间执行插入,删除和查找的技术。散列的实现通过散列表和Key-Value对实现。关键字Key通过一定的规则映射到散列表的某一个单元,这个映射的规则就是散列函数。不同的关键字可能会映射到同一个单元,这种情况成为冲突或者哈希碰撞。
哈希函数
hash函数的构造准则:简单、均匀
1. 散列函数的计算简单,快速;
2. 散列函数能将关键字集合K均匀地分布在地址集{0,1,…,m-1}上,使冲突最小。
常见的散列函数(哈希函数)有:
取模
将关键值对哈希表的长度值取模,是一种简单快捷的哈希策略。为了使分布更均匀,也可以在平方后取余,或者多项式函数的值后取余。有一种称为Horner法则的方法。随机数法
H(Key) = random(Key),通过随机函数生成随机数进行取模。相乘取整法
该方法包括两个步骤:首先用关键字key乘上某个常数A ,并抽取出key.A的小数部分;然后用m乘以该小数后取整。即:H(key) = m * (Key - Key * A)。
该方法最大的优点是m的选取比除余法要求更低。比如,完全可选择它是2的整数次幂。虽然该方法对任何A的值都适用,但对某些值效果会更好。Knuth建议选取A=sqrt(5) - 1。哈希函数在密码学中的应用常见的哈希函数有MD5和SHA-1。能将任意长度的字符串生成同样长度的数值。
为了使分布更加均匀,散列函数的长度取为素数更合理。很多时候关键字是一个字符串,可以计算其ASCII值的和,或者选取字符串中的部分进行计算。
解决冲突
解决冲突的方法有拉链法和分离地址法两种。
拉链法
拉链法就是将所有映射到同一个哈希表格的元素用一个链表保存下来,对于每个哈希表的表格都维护一张链表,有新的数据插入就在链表插入。
用C++实现拉链法的哈希表
/************************************************************************/
/* 文件说明:
/* 采用分离链接法实现哈希表,采用C++标准库中的vector和list实现
/*
/************************************************************************/
#include <vector>
#include <list>
#include <string>
class Vocabulary;
int hash(const std::string & key, const int &tableSize);
int hash(const int & key, const int &tableSize);
//int hash(const Vocabulary & e, const int &tableSize);
namespace stl
{
template <typename HashedObj>
class HashTable
{
public:
//typedef vector<std::list<HashedObj> >::size_type SIZE;
HashTable(int size = 101); //初始化哈希表的大小
~HashTable(){}
bool contains(const HashedObj & obj);
bool insert(const HashedObj & obj);
bool remove(const HashedObj & obj);
private:
std::vector<std::list<HashedObj> > theList; //哈希表
int myhash(const HashedObj & obj) const; //哈希函数
};
//函数定义
template <typename HashedObj>
HashTable<HashedObj>::HashTable(int size /*= 101*/)
{
/*根据哈希表的大小分配vector的空间*/
theList.reserve(size);
theList.resize(size);
}
template <typename HashedObj>
int HashTable<HashedObj>::myhash(const HashedObj & obj) const
{
//根据object不同调用不同版本的hash重载函数
return hash(obj, theList.size());
}
/************************************************************************/
/* 函数名称:contains
/* 函数功能:查找指定对象是否在哈希表中
/* 返回值:存在返回true,不存在返回false
/************************************************************************/
template <typename HashedObj>
bool HashTable<HashedObj>::contains(const HashedObj & obj)
{
int iHashValue = myhash(obj);
const std::list<HashedObj> & tempList = theList[iHashValue];
std::list<HashedObj>::const_iterator it = tempList.cbegin();
for (; it != tempList.cend() && *it != obj; ++it);
if(it != tempList.cend())
return true;
else
return false;
}
/************************************************************************/
/* 函数名称:insert
/* 函数功能:向hash表中插入元素,如果元素已经存在则返回false,不存在则在链表的最前面插入
/* 返回值:成功返回true,失败返回返回false
/************************************************************************/
template <typename HashedObj>
bool HashTable<HashedObj>::insert(const HashedObj & obj)
{
int iHashValue = myhash(obj);
std::list<HashedObj> & tempList = theList[iHashValue];
if (contains(obj))
{
return false; //已经存在返回false
}
tempList.push_back(obj);
return true;
}
/************************************************************************/
/* 函数名称:remove
/* 函数功能:从哈希表中删除指定元素,如果元素不存在则返回false
/* 返回值:成功返回true,失败返回返回false
/************************************************************************/
template <typename HashedObj>
bool HashTable<HashedObj>::remove(const HashedObj & obj)
{
list<HashedObj> & tempList = theList[myhash(obj)];
auto it = find(tempList.begin(), tempList.end(), obj);
if (it == tempList.end())
{
return false;
}
tempList.erase(it);
return true;
}
}
//hash表中存入单词表类,一个对象表示对应一个单词
class Vocabulary
{
public:
Vocabulary(){ }
~Vocabulary(){ }
void SetWordName(std::string name)
{
m_sWord = name;
}
void SetWordExplain(std::string explain)
{
m_sExplain = explain;
}
const std::string getName() const
{
return m_sWord;
}
Vocabulary(const Vocabulary &vc){
}
Vocabulary& operator= (const Vocabulary & v)
{
m_sWord = v.m_sWord;
m_sExplain = v.m_sExplain;
return *this;
}
private:
std::string m_sWord;
std::string m_sExplain;
};
bool operator== (const Vocabulary & word1, const Vocabulary & word2)
{
if (word1.getName() == word2.getName())
{
return true;
}
else
{
return false;
}
}
bool operator!= (const Vocabulary & word1, const Vocabulary & word2)
{
return !(word1 == word2);
}
int hash(const Vocabulary & e,const int &tableSize)
{
return hash(e.getName(), tableSize);
}
int hash(const std::string & key, const int &tableSize)
{
//采用公式:h = (k1*32 + k2) * 32 + k3,将其扩展到n次多项式
long long int hashVal = 0;
int count = 0;
for (auto it = key.begin(); it != key.end(); ++it)
{
if (count++ % 2 == 0)
{
hashVal += (hashVal << 5) + *it;
}
}
return hashVal %= tableSize;
}
int hash(const int & key, const int &tableSize)
{
return key % tableSize;
}
开放地址法
拉链法由于插入元素时,需要分配内存,导致效率下降,因此,出现了开放地址法来解决冲突。开放地址法分为线性探测和平方探测。
s线性探测发生冲突后,找后面一个元素,直到找到一个为空的位置。但是线性探测容易发生聚集,导致效率低下。平方探测,发生冲突后找后面i的平方的位置元素,比如第一冲突,考察后面第1个位置,第二次冲突,考察后面第4个位置,第三次冲突考察后面第9个位置,用函数表示为F(i) = F(i - 1) * 2 - 1。
开放地址法状态因子不能太高,否则冲突次数太多,查找和插入效率都很低。而且删除不能真正删除,因为会影响哈希的结果,采用懒惰删除的方法。
用C语言实现开放地址法的哈希表
/*
** 用C语言实现平方探测的哈希表
*/
#include <stdlib.h>
#include <stdio.h>
typedef int Element;
typedef int position;
typedef struct HashTB* HashTable;
HashTable initTable(int size);
void destroyTable(HashTable H);
void insert(Element key, HashTable H);
position find(Element key, HashTable H);
void delete(position p, HashTable H);
HashTable rehash(HashTable H);
position hash(int k, int tableSize);
enum kindOfElement {FULL, EMPTY, DELETE};
typedef struct Entry
{
Element key; //元素值
enum kindOfElement info; //元素状态
};
typedef struct Entry Cell;
typedef struct HashTB
{
int tableSize;
Cell *cellTable;
};
/*初始化哈希表*/
HashTable initTable(int size)
{
HashTable H;
H = (HashTable)malloc(sizeof(struct HashTB));
if(H == NULL)
{
fprintf(stderr, "out of space.\n");
}
H->tableSize = size;
H->cellTable = malloc(sizeof(Cell) * size);
if(H->cellTable == NULL)
{
fprintf(stderr, "out of space.\n");
}
for(int i = 0; i < H->tableSize; ++i)
{
H->cellTable[i].info = EMPTY;
}
return H;
}
/*删除哈希表*/
void destroyTable(HashTable H)
{
if(H == NULL)
return;
free(H->cellTable);
free(H);
}
/*哈希函数*/
position hash(int k, int tableSize)
{
return k % tableSize;
}
/*如果找到指定元素返回该元素位置,如果找不到返回一个空位置*/
position find(Element key, HashTable H)
{
if(H == NULL)
return -1;
position pos = hash(key, H->tableSize);
int collisionNum = 0;
while(H->cellTable[pos].info != EMPTY && H->cellTable[pos].key != key)
{
pos = pos + ++collisionNum - 1;
if(pos > H->tableSize)
pos -= H->tableSize;
}
return pos;
}
/*插入元素*/
void insert(Element key, HashTable H)
{
if(H == NULL)
return;
position p = find(key, H);
if(H->cellTable[p].info != FULL)
{
H->cellTable[p].key = key;
H->cellTable[p].info = FULL;
}
}
/*删除元素*/
void delete(Element key, HashTable H)
{
if(H == NULL)
return;
position p = find(key, H);
if(H->cellTable[p].key == key && H->cellTable[p].info == FULL)
{
H->cellTable[p].info = DELETE;
}
}
/*扩容函数*/
HashTable rehash(HashTable H)
{
HashTable newH;
newH = initTable(H->tableSize * 2);
for(int i = 0; i < H->tableSize; ++i)
{
if(H->cellTable[i].info == FULL)
{
insert(H->cellTable[i].key, newH);
}
}
destroyTable(H);
return newH;
}
哈希表扩容
在hash表的实现中,我们一般会控制一个装载因子。装载因子定义为,元素个数与哈希表长度的比值。当装载因子过大的时候,会考虑扩容(resize)。扩容的方法一般来说是类似C++中Vector的方法扩大哈希两倍。扩大后,将所有的元素重新哈希插入。引用一篇博客中的文字如下,采用两个哈希表,避免扩容后重新一次插入数据的速度过慢。
当我们添加一个新元素时,一旦loadFactor大于等于1了,我们不能单纯的往hash表里边添加元素。因为添加完之后,loadFactor将大于1,这样也就不能保证查找的期望时间复杂度为常数级了。这时,我们应该对桶数组进行一次容量扩张,让size增大 。这样就能保证添加元素后 used / size 仍然小于等于1 , 从而保证查找的期望时间复杂度为O(1).但是,如何进行容量扩张呢? C++中的vector的容量扩张是一种好方法。于是有了如下思路 : Hash表中每次发现loadFactor==1时,就开辟一个原来桶数组的两倍空间(称为新桶数组),然后把原来的桶数组中元素全部转移过来到新的桶数组中。注意这里转移是需要元素一个个重新哈希到新桶中的,原因后面会讲到。
这种方法的缺点是,容量扩张是一次完成的,期间要花很长时间一次转移hash表中的所有元素。这样在hash表中loadFactor==1时,往里边插入一个元素将会等候很长的时间。
redis中的dict.c中的设计思路是用两个hash表来进行进行扩容和转移的工作:当从第一个hash表的loadFactor=1时,如果要往字典里插入一个元素,首先为第二个hash表开辟2倍第一个hash表的容量,同时将第一个hash表的一个非空桶中元素全部转移到第二个hash表中,然后把待插入元素存储到第二个hash表里。继续往字典里插入第二个元素,又会将第一个hash表的一个非空桶中元素全部转移到第二个hash表中,然后把元素存储到第二个hash表里……直到第一个hash表为空。
这种策略就把第一个hash表所有元素的转移分摊为多次转移,而且每次转移的期望时间复杂度为O(1)。这样就不会出现某一次往字典中插入元素要等候很长时间的情况了。