我们在平时使用的顺序结构(顺序表等)和平衡树中,元素的关键码和其存储位置之间没有对应关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序表的时间复杂度为O(N),平衡树的时间复杂度为为树的高度,即O(log2N),搜索的效率取决于搜索过程中元素的比较次数。
一种理想的搜索方法:可以不经过比较,一次直接得到要搜索的元素,如果构造一种存储结构,通过某种函数,使元素的存储位置和关键码能够建立一一映射的关系,那么在查找时,只需要通过该函数便可以很快得到该元素。
也就是说,在我们向该结构中插入或者搜索元素时,根据元素的关键码,通过某种函数去计算得到一个存储位置(哈希地址),然后直接用得到的位置来进行插入或者搜索等操作。这种方法就叫做哈希 Hash(散列),这个函数叫做哈希函数(HashFunc)。
假如现在有两个不同的关键码,通过一种哈希函数计算之后,得到了相同的哈希地址,这种现象叫做哈希冲突。
这种情况说明哈希函数设计的不够合理,哈希函数的设计原则如下:
1. 哈希函数的定义域必须包含全部要存储的关键码,如果哈希结构有m的地址时,其值域必须在0到m-1之间。
2. 哈希函数计算出来的地址能够均匀的分布在整个空间中。
3. 哈希函数要简单。
常见的几种哈希函数:
1. 直接定制法:
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
2. 除留余数法:
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址。
3. 平方取中法:
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址; 再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址。
开放定址法也叫闭散列,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
我们来看一个例子:假设现在有一个关键码集合{1,4,5,6,7,9},哈希结构的容量为10,哈希函数为 Hash(key)=key%10。将所有关键码插入到该哈希结构中,如图。
假如现在有一关键码24要插入该结构中,使用哈希函数求得哈希地址为4,但是该地址处已经存放了元素,此时发生哈希冲突。
线性探测:从发生哈希冲突位置开始,依次向后探测,直到找到下一个空位置为止。例如上面的例子,插入关键码24时,进行线性探测,插入后如下图。
接下来我们采用除留余数法来实现开放定址法的哈希,在实现之前我们先搞清楚几个问题
1. 用该方法需要关键码必须为整型才能被模,所以我们需要实现将非整型转化为整型。
2. 模的数值最好为素数,需要我们创建一个素数表。
3. 增容问题。
散列表的载荷因子定义为: α=填入表中的元素个数/散列表的长度。
α是散列表装满程度的标志因子。由于表长是定值,α与“填入表中的元素个数”成正比,所以,α越大,表明填入表中的元素越多,产生冲突的可能性就越大:反之,α越小,标明填入表中的元素越少,产生冲突的可能性就越小。实际上,散表的平均查找长度是载荷因子α的函数。只是不同处理冲突的方法有不同的函数。
对于开放定址法,荷载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了荷载因子为0.75,超过此值将resize散列表。
HashFunc.h:
#pragma once
#include
#include
using namespace std;
template
class HashFunc //使用仿函数实现
{
public:
size_t operator()(const K& key)
{
return key;
}
};
template<>
class HashFunc //特化处理string类型关键码
{
public:
size_t operator()(const string& str)
{
return BKDRHash(str.c_str());
}
size_t BKDRHash(const char * str)
{
unsigned int seed = 131; // 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return (hash & 0x7FFFFFFF);
}
};
// 素数表的定义
const int PRIMECOUNT = 28;
const size_t primeList[PRIMECOUNT] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
在哈希表中的每一个存储位置,应该包含两个因子,一个是要存储的数据,一个是存储状态。存储状态分为,空EMPTY,已存储EXIT,已删除DELETE。因为我们在删除数据时,不能直接删除,否则会影响其他元素的搜索(因为使用线性探测来处理哈希冲突的元素,例如上面的例子:删除元素4之后可能会影响元素24的搜索)
HashTable.h:
#include
#include
#include"HashFunc.h"
using namespace std;
enum State //三种状态
{
EMPTY,
EXIT,
DELETE
};
template //每个存储位置的结构体
struct Elem
{
pair kv;
State state;
};
template>
class HashTable
{
public:
HashTable(size_t size)
: _size(0)
{
ht.resize(size);
for (size_t i = 0; i < size; i++) //初始化给每个存储位置设置空状态
{
ht[i].state = EMPTY;
}
}
bool insert(const pair& _kv)
{
CheckCapacity(); //检测容量是否需要增容
size_t index = _HashFunc(_kv.first);//通过哈希函数获取哈希地址
while (ht[index].state != EMPTY)
{
if (ht[index].state == EXIT&&ht[index].kv.first == _kv.first)
{
return false;
}
index++;
if (index == ht.size()) //如果哈希地址为最后一个存储位置时还不为空,从头开始寻找
{
index = 0;
}
}
ht[index].kv = _kv;
ht[index].state = EXIT;
_size++;
return true;
}
int find(const K& key)
{
size_t index = _HashFunc(key);
while (ht[index].state != EMPTY)
{
if (ht[index].state == EXIT&&ht[index].kv.first == key)
{
return index;
}
index++;
if (index == ht.size())
{
index = 0;
}
}
return -1;
}
bool erase(const K& key)
{
int index = find(key);
if (-1 == index)
{
return false;
}
ht[index].state = DELETE;
_size--;
return true;
}
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
private:
size_t _HashFunc(const K& key) //哈希函数
{
return HashFunc()(key) % ht.size(); //通过仿函数来获得整型关键码。
} //然后取余数作为哈希地址
void Swap(HashTable& _ht)
{
swap(ht, _ht.ht);
swap(_size, _ht._size);
}
//增容时需要用到,获取下一个比当前容量大的素数作为新的容量
size_t GetNextPrime(size_t prime)
{
size_t i = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
void CheckCapacity()
{
if (_size * 10 / ht.size() > 7)
{
HashTable newht(GetNextPrime(ht.size())); //新建一个容量大的哈希表
for (size_t i = 0; i < ht.size(); i++)
{
if (ht[i].state == EXIT)
{
newht.insert(ht[i].kv); //将原来的哈希表中的元素插入到新哈希表中
}
}
Swap(newht); //交换两个哈希表
}
}
vector> ht;
size_t _size;
};
void testHashTable()
{
HashTable HT(3);
HT.insert(make_pair(1, 1));
HT.insert(make_pair(5, 5));
HT.insert(make_pair(2, 2));
HT.insert(make_pair(4, 4));
HT.insert(make_pair(3, 3));
HT.find(2);
HT.erase(1);
}
拉链法又叫开散列,首先将关键码根据哈希函数来计算出哈希地址,对相同的哈希地址放在某一子集合中,每个子集合叫做一个桶,每个通放的都是哈希冲突元素,每个桶中的元素通过单链表连接,每个链表的头节点存放在哈希表中,这种结构叫做哈希桶。
HashFunc.h
#pragma once
#include
#include
using namespace std;
template
class HashFunc
{
public:
size_t operator()(const K& key)
{
return key;
}
};
template<>
class HashFunc
{
public:
size_t operator()(const string& str)
{
return BKDRHash(str.c_str());
}
size_t BKDRHash(const char * str)
{
unsigned int seed = 131; // 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return (hash & 0x7FFFFFFF);
}
};
const int PRIMECOUNT = 28;
const size_t primeList[PRIMECOUNT] =
{
53ul, 97ul, 193ul, 389ul, 769ul,
1543ul, 3079ul, 6151ul, 12289ul, 24593ul,
49157ul, 98317ul, 196613ul, 393241ul, 786433ul,
1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,
50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,
1610612741ul, 3221225473ul, 4294967291ul
};
HashBucket.h
#include
#include
#include"HashFunc.h"
using namespace std;
template
struct Node
{
pair kv;
Node* next = nullptr;
};
template>
class HashBucket
{
public:
HashBucket(size_t size = 10)
:_size(0)
{
hb.resize(size, nullptr); //初始化给每个桶设置为空
}
Node* insert(const pair& _kv)
{
CheckCapacity();
size_t bucket = _HashFunc(_kv.first);
Node* cur = hb[bucket];
while (cur)
{
if (cur->kv.first == _kv.first)
{
return cur;
}
cur = cur->next;
}
cur = new Node; //插入的时候选择头插,减少遍历的时间
cur->kv = _kv;
cur->next = hb[bucket];
hb[bucket] = cur;
_size++;
return cur;
}
Node* find(const K& key)
{
size_t bucket = _HashFunc(key);
Node* cur = hb[bucket];
while (cur)
{
if (cur->kv.first == key)
{
return cur;
}
cur = cur->next;
}
return nullptr;
}
Node* erase(const K& key)
{
size_t bucket = _HashFunc(key);
Node* prev = nullptr;
Node* cur = hb[bucket];
Node* result = nullptr;
while (cur)
{
if (cur->kv.first == key)
{
if (hb[bucket] == cur)
{
hb[bucket] = cur->next;
}
else
{
prev->next = cur->next;
}
result = cur->next;
delete cur;
_size--;
return result;
}
prev = cur;
cur = cur->next;
}
return nullptr;
}
size_t size() const
{
return _size;
}
bool empty() const
{
return _size == 0;
}
~HashBucket()
{
clear();
}
private:
size_t _HashFunc(const K& key)
{
//通过仿函数获取整型key值,然后去余数作为哈希地址
return HashFunc()(key) % hb.size();
}
void clear()
{
for (size_t i = 0; i < hb.size(); i++)
{
Node* cur = hb[i];
while (cur)
{
hb[i] = cur->next;
delete cur;
cur = hb[i];
}
}
_size = 0;
}
void Swap(HashBucket& _hb)
{
swap(hb, _hb.hb);
swap(_size, _hb._size);
}
size_t GetNextPrime(size_t prime)
{
size_t i = 0;
for (; i < PRIMECOUNT; ++i)
{
if (primeList[i] > prime)
return primeList[i];
}
return primeList[i];
}
void CheckCapacity()
{
if (_size == hb.size())
{
//增容的时候是新建一个哈希桶,然后将原来的哈希桶的节点拆下来放到新的哈希桶中,即不建
//立新的节点。
HashBucket newhb(GetNextPrime(hb.size()));
for (size_t i = 0; i < hb.size(); i++)
{
Node* cur = hb[i];
while (cur)
{
hb[i] = cur->next;
//计算该元素在新的哈希桶中的位置
size_t bucket = newhb._HashFunc(cur->kv.first);
Node* pos = newhb.hb[bucket];
while (pos)
{
if (pos->kv.first == cur->kv.first)
{
break;
}
pos = pos->next;
}
if (nullptr == pos)
{
cur->next = newhb.hb[bucket];
newhb.hb[bucket] = cur;
}
else
{
cur->next = pos->next;
pos->next = cur;
}
cur = hb[i];
}
}
newhb._size = _size;
Swap(newhb);
}
}
private:
vector*> hb;
size_t _size;
};
void testHashBucket()
{
HashBucket HB(3);
HB.insert(make_pair(2, 2));
HB.insert(make_pair(4, 4));
HB.insert(make_pair(5, 5));
HB.insert(make_pair(1, 1));
HB.insert(make_pair(3, 3));
HB.find(5);
HB.erase(3);
}