哈希概念:
构造一种存储结构,通过某种函数使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素。当向该结构中插入元素时,根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放。
当先结构中搜索元素时,对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功。 该方式即为哈希方法,哈希方法中使用的转换函数称为哈希函数,构造出来的结构称为哈希表。
哈希冲突:不同的关键字通过相同哈希函数计算出相同的哈希地址。
哈希冲突解决方法:
(1)闭散列:
也叫开放地址法,当发生哈希表未被装满,说明在哈希表中必然还有空位置,可以把key存放到表中下一个空位中去。 那如何寻找下一个空余位置呢?
(a)线性探测:线性探测要做的就是把发生冲突的元素插入到从当前位置开始后移动到第一个空位置。
eg:假设哈希表的长度为10,我们有5个数据分别为74,29,39,44,4.用除留余数法插入到哈希表:
采用线性探测,实现起来很简单,缺陷是一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低。
调试结果:
(b)二次探测 二次探测和线性探测差不多,只是发生冲突后后移的大小为后移次数的平方。
调试结果:
完整代码:
负载因子: 散列表负载因子定义为:a = 填入表中的元素个数/散列表的长度
a是散列表装满程度的标志因子。表长为定值,a与“填入表中的元素个数”成正比,所以a越大,表明填入表中的元素越多,产生冲突的可能性越大。
开放地址法中,载荷因子是特别重要因素,应严格控制在0.1-0.8.
代码实现:
#include<iostream>
#include<vector>
using namespace std;
enum State
{
EXIST,//存在
EMPTY,//空
DELETE,//删除
};
template<class K,class V>
struct HashNode
{
K _key;
V _value;
State _state;
HashNode(const K& key=K(),const V& value=V())
:_key(key)
,_value(value)
,_state(EMPTY)
{}
};
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return key;
}
};
//模板的特化
template<>
struct Hash<string>
{
static size_t BKDRHash(const char*str)
{
unsigned int seed = 131;
unsigned int hash = 0;
while(*str)
{
hash=hash*seed+(*str++);
}
return(hash &0x7FFFFFF);
}
size_t operator()(const string&s)
{
return BKDRHash(s.c_str());
}
};
template<class K,class V,class __HashFunc = Hash<K>>
class HashTable
{
typedef HashNode<K,V> Node;
public:
HashTable()
:_size(0)
{}
bool Insert(const K&key,const V&value)
{
CheckCapacity();
if(Find(key))//滤重
{
return false;
}
size_t i = 1;
size_t index = HashFunc(key);//生成下标值
size_t start = index;
while(_tables[index]._state == EXIST)
{
//二次探测
//index = start+i*i;
//index %= _tables.size();
//++i;
++index;
if(index == _tables.size())
{
index =0;//当到最后一个位置还没有找到,从第一个位置开始遍历
}
}
_tables[index]._key = key;
_tables[index]._value = value;
_tables[index]._state = EXIST;
_size++;
return true;
}
bool Earse(const K& key)
{
Node *tmp = Find(key);
if(tmp)
{
tmp->_state = DELETE;
--_size;
return true;
}
else
{
return false;
}
}
Node* Find(const K& key)
{
size_t index = HashFunc(key);
size_t start = index;
size_t i = 1;
while(_tables[index]._state != EMPTY)
{
if(_tables[index]._key == key)
{
if(_tables[index]._state == EXIST)
{
return &_tables[index];
}
else
{
return NULL;
}
}
//二次探测
index = start + i*i;
index %= _tables.size();
++i;
if(index == _tables.size())
{
index = 0;
}
}
return NULL;
}
void CheckCapacity()
{
if(_tables.size()==0 || _size*10/_tables.size() >= 7)
{
size_t newsize = _tables.size()*2;//扩容两倍
if(newsize == 0)
{
newsize = 10;
}
HashTable<K,V,__HashFunc>newht;
newht._tables.resize(newsize);
for(size_t i = 0;i < _tables.size();++i)
{
if(_tables[i]._state == EXIST)
{
newht.Insert(_tables[i]._key,_tables[i]._value);//将数据插入新空间
}
}
_tables.swap(newht._tables);
}
}
size_t HashFunc(const K&key)
{
__HashFunc hf;
return hf(key)%_tables.size();
}
protected:
vector<Node>_tables;
size_t _size;//有效字段
};
int main()
{
HashTable<int,int> ht;
int a[]= {74,29,39,44,4};
for(size_t i = 0;i<sizeof(a)/sizeof(a[0]);i++)
{
ht.Insert(a[i],i);
}
ht.Earse(29);
}
(2)开散列:
又叫开链法或拉链法, 首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,每个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
通常,每个桶对应的链表节点都很少,将n个关键码通过某一个散列函数,存放到散列表的m个桶中,那么每一个桶中链表的平均长度为n/m,以搜索平均长度为n/m的链表代替了搜索长度为n的顺序表,搜索效率快得多。 应用拉链法处理溢出,需要增设链接指针,似乎增加了存储开销,事实上由于开放地址法必须保证大量的空闲空间以确保搜索效率,如二次探测法要求装载因子a<=0.7,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。
哈系桶的实现:顺序表+(挂)链表
代码实现:
#include<iostream>
#include<vector>
using namespace std;
namespace HASH_BUCKET
{
template<class V>
struct HashNode
{
V _v;
HashNode<V>* _next;
//构造函数
HashNode(const V& v)
:_v(v)
,_next(NULL)
{}
};
template<class K,class V,class KeyOfValue>
class HashTable
{
typedef HashNode<V> Node;
public:
//表的初始化
HashTable()
:_size(0)
{}
bool Insert(const V& v)
{
CheckCapacity();
KeyOfValue kov;
size_t index = HashFunc(kov(v),_tables.size());
Node* cur = _tables[index];
while(cur)
{
if(kov(cur->_v) == kov(v))//滤重
{
return false;
}
cur = cur->_next;
}
Node* node = new Node(v);
node->_next = _tables[index];
_tables[index] = node;
++_size;
return true;
}
bool Earse(const K& key)
{
KeyOfValue kov;
size_t index = HashFunc(key,_tables.size());
Node* cur = _tables[index];
if(cur == NULL)
{
return NULL;
}
if(kov(cur->_v) == key)
{
_tables[index] = cur->_next;
delete cur;
return true;
}
else//不是头的情况
{
Node* prev = cur;
cur = cur ->_next;
while(cur)
{
if(kov(cur->_v) == key)
{
prev->_next = cur->_next;
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
}
Node* Find(const K& key)
{
size_t index = HashFunc(kov(v),_tables.size());
Node* cur = _tables[index];
KeyOfValue kov;
whiel(cur)
{
if(kov(cur->_v)==key)
{
return cur;
}
else
{
cur = cur->_next;
}
}
return NULL;
}
//扩容
void CheckCapacity()
{
if (_tables.size() == 0)
{
_tables.resize(10, NULL);
}
//哈希表满的情况(平均每个下面挂了一个)
else if (_size == _tables.size())
{
//创建一个新表,将旧表的数据依次拷贝到新表
size_t newsize = _tables.size() * 2;
vector<Node*> newtables;
newtables.resize(newsize,NULL);
KeyOfValue kov;
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur)
{
//对节点cur在新表中重新定位
size_t index = HashFunc(kov(cur->_v),newsize);
Node* next = cur->_next;
cur->_next = _tables[i];
newtables[index] = cur;
//对旧表中下一个数据进行插入
cur = next;
}
_tables[i] = NULL;
}
//将新表和旧表进行交换
_tables.swap(newtables);
}
}
//计算下标位置
size_t HashFunc(const K& key,size_t size)
{
return key%size;
}
private:
vector<Node*> _tables;
size_t _size;
};
template<class K>
struct SetKeyOfValue
{
const K& operator()(const K& key)
{
return key;
}
};
template<class K,class V>
struct MapKeyOfValue
{
const V& operator()(const pair<K,V>& kv)
{
return kv.first;
}
};
void TestHashTable()
{
HashTable<int,int,SetKeyOfValue<int>> ht;
ht.Insert(4);
ht.Insert(15);
ht.Insert(14);
ht.Insert(24);
ht.Insert(33);
ht.Insert(55);
ht.Earse(33);
}
}
int main()
{
HASH_BUCKET::TestHashTable();
return 0;
}