赞
踩
学完了二叉树,我们要学当前阶段数据结构的最后一个内容了:哈希!!
先来介绍两个用哈希封装的两个容器:unordered_map unordered_set
与map和set的不同:
哈希就是散列,
就是这些值key和存储位置之间存在着映射的关联关系
哈希函数的内容也就是一个哈希是如何设计的
哈希函数设计原则:
直接定址法–(常用)
取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B
除留余数法–(常用)
设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,
按照哈希函数:Hash(key) = key% p(p<=m),将关键码转换成哈希地址
平方取中法–(了解)
假设关键字为1234,对它平方就是1522756,抽取中间的3位227作为哈希地址;
再比如关键字为4321,对它平方就是18671041,抽取中间的3位671(或710)作为哈希地址
平方取中法比较适合:不知道关键字的分布,而位数又不是很大的情况
折叠法–(了解)
折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。
折叠法适合事先不需要知道关键字的分布,适合关键字位数比较多的情况
随机数法–(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即H(key) = random(key),其中random为随机数函数。
通常应用于关键字长度不等时采用此法
数学分析法–(了解)
设有n个d位数,每一位可能有r种不同的符号,这r种不同的符号在各位上出现的频率不一定相同,可能在某些位上分布比较均匀,每种符号出现的机会均等,在某些位上分布不均匀只有某几种符号经常出现。可根据散列表的大小,选择其中各种符号分布均匀的若干位作为散列地址。
对于第一种方法的弊端很明显:
比如存两个值:1和1000,那我是不是要开1001个空间,空间太浪费了
我们用第二种方法,就能大大的节省空间
但还是有问题:
如果取余之后,有了同样的余数怎么办??
对于两个数据元素的关键字
k
i
k_i
ki和
k
j
k_j
kj(i != j),有
k
i
k_i
ki !=
k
j
k_j
kj,但有:Hash(
k
i
k_i
ki) == Hash(
k
j
k_j
kj),
即:**不同关键字通过相同哈希哈数计算出相同的哈希地址,**该种现象称为哈希冲突或哈希碰撞。
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
发生哈希冲突该如何处理呢?
补充:
哈希不会填满,到超过自己设定的负载因子的时候,会按照一定倍数扩容
负载因子/载荷因子 = 存进去的数据个数/表的大小
闭散列一般是0.7
闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。
那如何寻找下一个空位置呢?
enum State
{
EMPTY,//空
EXIST,//存在
DELETE//删除
};
template<class K, class V>
struct HashData
{
pair<K, V>_kv; //键值对
State _state = EMPTY; //初始为空
};
从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止。
bool Insert(const pair<K, V>& kv) //线性探测版本 { if (Find(kv.first) == nullptr) { return false; } if (_n*10 / _tables.size() >= 7) { //方法2:比较神奇的写法 HashTable<K, V>newHT(_tables.size() * 2); for (auto& e : _tables) { if (e._state == EXIST) { newHT.Insert(e._kv); } } _tables.swap(newHT._tables); //把新的交换给旧的 //旧的还能自动释放 } size_t hashi = kv.first % _tables.size(); while (_tables[hashi]._state == EXIST) { hashi++; hashi %= _tables.size(); } _tables[hashi]._kv = kv; _tables[hashi]._state = EXIST; ++_n; return true; }
HashData<const k, V>* Find(const K& key)
{
size_t hashi = key % _tables.size();
while (_tables[hashi]._state != EMPTY)
{
if (key == _tables[hashi]._kv.first
&& _tables[hashi]._state == EXIST)
{
return &_tables[hashi];
}
hashi++;
hashi %= _tables.size();
}
return nullptr;
}
这里的返回值给指针是为了方便删除
// 哈希表每个空间给个标记
// EMPTY此位置空, EXIST此位置已经有元素, DELETE元素已经删除
enum State{EMPTY, EXIST, DELETE};
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
return true;
}
return false;
}
扩容
当超过负载因子的时候,我们就要扩容,
但是,扩容之后,可能原来不冲突的冲突了。
所以建议是:开一个新的空间来重新一一映射
在此提供两种思路:
开一个新空间,遍历旧表,将值给新表
//方法1
size_t newSize = _tables.size() * 2;
vector<HashData>newTables(newSize);
_tables.swap(newTables);
这种方法比较普通
这种方法更巧妙,每次插入新的值都去调用一次Insert,但是,是用扩容后的新表调用,所以不用再扩容,直接找到合适的位置存入即可,
并且交换新表和旧表
//方法2:比较神奇的写法
HashTable<K, V>newHT(_tables.size() * 2);
for (auto& e : _tables)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
}
}
_tables.swap(newHT._tables);
//把新的交换给旧的
//旧的还能自动释放
我们要想一想:如果参数是string这种无法进行%运算的类型怎么办?
所以我们要增加一个模板参数,来讲他们转化成可以运算的整形
那么我们可以将每一位的ASCII码值分别乘以131,来作为关键码去取余,这个131来自BKDRHash函数,我们可以看这篇文章的分析
struct HashFuncString
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto e : s)
{
hash += e;
hash *= 131;
}
return hash;
}
};
因为string经常作为key,所以我们可以进行特化
// 特化
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto e : s)
{
hash += e;
hash *= 131;
}
return hash;
}
};
这样就不用再单独去写一个HashFuncString函数了
找下一个空位置的方法为:
H
i
H_i
Hi = (
H
0
H_0
H0 +
i
2
i^2
i2 )% m, 或者:
H
i
H_i
Hi = (
H
0
H_0
H0 -
i
2
i^2
i2 )% m。其中:i = 1,2,3…,
H
0
H_0
H0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小。
也就是说,先对关键码进行加减运算,然后再取余
但是闭散列这种方式还是不够好
闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。
下一篇文章将实现哈希桶。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。