目录
- 1.哈希概念
- 2.哈希冲突
- 3.哈希函数
- 4.哈希冲突解决
- 5.闭散列
- 1.何时扩容?如何扩容?
- 2.线性探测
- 3.二次探测
- 6.开散列(哈希桶)
- 1.概念
- 2.开散列增容
- 3.开散列思考
- 只能存储key为整形的元素,其他类型怎么解决?
- 除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
- 4.开散列与闭散列比较
1.哈希概念
- 顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应的关系,因此在查找一个元素时,必须要经过关键码的多次比较。顺序查找时间复杂度为O(N),平衡树中为树的高度,即 O(logN),搜索的效率取决于搜索过程中元素的比较次数
- 理想的搜索方法:可以不经过任何比较,一次直接从表中得到要搜索的元素
- 如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素
- 当向该结构中:
- 插入元素
- 根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
- 搜索元素
- 对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
- 该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
- 插入元素
2.哈希冲突
- 对于两个数据元素的关键字k_i和k_j(i != j),有k_i != k_j,但有:Hash(k_i) == Hash(k_j)
- 即:不同关键字通过相同哈希函数计算出相同的哈希地址,该种现象称为 哈希冲突 或 哈希碰撞
- 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”
3.哈希函数
- 引起哈希冲突的一个原因可能是:哈希函数设计不够合理
- 哈希函数设计原则:
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值域必须在0到m-1之间
- 哈希函数计算出来的地址能均匀分布在整个空间中
- 哈希函数应该比较简单
- 常见哈希函数:
- 直接定址法 – (常用) --> 不存在哈希冲突
- 取关键字的某个线性函数为散列地址: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为随机数函数
- 数学分析法 – (了解) – 懒得介绍
- 直接定址法 – (常用) --> 不存在哈希冲突
- 注意:哈希函数设计的越精妙,产生哈希冲突的可能性就越低,但是无法避免哈希冲突
4.哈希冲突解决
- 解决哈希冲突两种常见的方法是:闭散列和开散列
5.闭散列
- 闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去。那如何寻找下一个空位置呢?
1.何时扩容?如何扩容?
- 散列表的载荷因子定义为:α = 填入表中的元素个数 / 散列表的长度
- α越大,表中元素越多,产生冲突概率越大
- α越小,表明元素越少,产生冲突概率越小
- 一般不要超过0.7~0.8
- 什么时候扩容? --> 负载因子到一个基准值就扩容
- 基准值越大,冲突越多,效率越低,空间利用率越高
- 基准值越小,冲突越少,效率越高,空间利用率越低
2.线性探测
-
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
- 插入
-
通过哈希函数获取待插入元素在哈希表中的位置
-
如果该位置中没有元素则直接插入新元素
-
如果该位置中有元素发生哈希冲突, 使用线性探测找到下一个空位置,插入新元素
-
- 插入
-
删除
- 采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素,会影响其他元素的搜索
- 比如删除元素4,如果直接删除掉,44查找起来可能会受影响
- 因此线性探测采用标记的伪删除法来删除一个元素
3.二次探测
- 线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找
- 因此二次探测为了避免该问题,找下一个空位置的方法为:
- H_i = (H_0 + i^2 ) % m 或者 H_i = (H_0 - i^2 ) % m (i = 1,2,3**…)**
- H_0是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小
- 研究表明:
- 当表的长度为质数且表载荷因子a不超过0.5时,新的表项一定能够插入,而且任何一个位置都不会被探查两次
- 因此只要表中有一半的空位置,就不会存在表满的问题。在搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出必须考虑增容
- 因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷
enum State
{
EMPTY,
EXIST,
DELETE
};
template <class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template <class K>
struct HashFunc
{
size_t operator()(const K &key)
{
return (size_t)key;
}
};
template <> // 特化
struct HashFunc<string>
{
size_t operator()(const string &key)
{
size_t val = 0;
for (auto &ch : key)
{
val *= 131; // BKDR
val += ch;
}
return val;
}
};
template <class K, class V, class Hash = HashFunc<K>> // Hash允许用户自己提供HashFunc
class HashTable
{
public:
bool Insert(const pair<K, V> &kv)
{
if (Find(kv.first)) // 元素已存在则不插入
{
return false;
}
// 负载因子到了就扩容
if (_tables.size() == 0 || 10 * _size / _tables.size() >= 7) // 将载荷因子α定为 0.7
{
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable<K, V> newHT; // 构建一个新的HashTable对象,来进行映射逻辑
newHT._tables.resize(newsize);
// 旧表的数据映射到新表
for (auto &e : _tables)
{
if (e._state == EXIST) // 状态为存在则进行映射
{
newHT.Insert(e._kv);
}
}
_tables.swap(newHT._tables);
}
// 线性探测
Hash hash;
size_t hashi = hash(kv.first) % _tables.size(); // 哈希地址计算
while (_tables[hashi]._state == EXIST)
{
++hashi;
hashi %= _tables.size(); // 防止已经遍历完vector,若遍历完,则回头
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
++_size;
// 二次探测
// Hash hash;
// size_t start = hash(kv.first) % _tables.size();
// size_t i = 0;
// size_t hashi = start;
// while (_tables[hashi]._state == EXIST)
//{
// ++i;
// hashi = start + i * i; // 二次探测的哈希地址跳跃
// hashi %= _tables.size(); // 防止已经遍历完vector,若遍历完,则回头
// }
//_tables[hashi]._kv = kv;
//_tables[hashi]._state = EXIST;
//++_size;
return true;
}
HashData<K, V> *Find(const K &key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
size_t hashi = hash(key) % _tables.size();
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._state == EXIST && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
++hashi;
hashi %= _tables.size();
}
return nullptr;
}
bool Erase(const K &key)
{
HashData<K, V> *ret = Find(key);
if (ret)
{
ret->_state = DELETE; // 标记删除即可
--_size;
return true;
}
else
{
return false;
}
}
private:
vector<HashData<K, V>> _tables;
size_t _size = 0; // 存储有效数据的个数
};
6.开散列(哈希桶)
1.概念
-
开散列法又叫链地址法(开链法),首先对关键码集合用散列函数计算散列地址,具有相同地址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中
-
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素
2.开散列增容
- 桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?
- 开散列最好的情况是:每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容
3.开散列思考
-
只能存储key为整形的元素,其他类型怎么解决?
-
哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为整形的方法
- 利用仿函数
-
除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
4.开散列与闭散列比较
- 应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销
- 事实上:
- 由于开放定址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子a <= 0.7
- 而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间
template <class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V> *_next;
HashNode(const pair<K, V> &kv)
: _kv(kv)
, _next(nullptr)
{}
};
template <class K>
struct HashFunc
{
size_t operator()(const K &key)
{
return (size_t)key;
}
};
template <> // 特化
struct HashFunc<string>
{
size_t operator()(const string &key)
{
size_t val = 0;
for (auto &ch : key)
{
val *= 131; // BKDR
val += ch;
}
return val;
}
};
template <class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node *cur = _tables[i];
while (cur)
{
Node *next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
// vector本身不需要手动析构,析构函数会去自动调用所有成员变量的析构函数
}
inline size_t __stl_next_prime(size_t n) // STL中素数空间优化
{
static const size_t __stl_num_primes = 28;
static const size_t __stl_prime_list[__stl_num_primes] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291};
for (size_t i = 0; i < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return -1;
}
bool Insert(const pair<K, V> &kv)
{
// 去重
if (Find(kv.first))
{
return false;
}
Hash hash;
// 负载因子到1就扩容
if (_size == _tables.size())
{
// size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node *> newTables;
// newTables.resize(newsize, nullptr);
newTables.resize(__stl_next_prime(_tables.size()), nullptr);
// 旧表中节点移动映射到新表
for (size_t i = 0; i < _tables.size(); ++i)
{
Node *cur = _tables[i];
while (cur)
{
Node *next = cur->_next;
size_t hashi = hash(cur->_kv.first) % newTables.size();
cur->_next = newTables[hashi]; // 头插逻辑
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
// 头插
size_t hashi = hash(kv.first) % _tables.size();
Node *newnode = new Node(kv);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return true;
}
Node *Find(const K &key)
{
if (_tables.size() == 0)
{
return nullptr;
}
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node *cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool Erase(const K &key)
{
if (_tables.size() == 0)
{
return true;
}
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node *prev = nullptr;
Node *cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
// 1.头删
// 2.中间删
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
size_t Size()
{
return _size;
}
// 表的长度
size_t TablesSize()
{
return _tables.size();
}
// 桶的个数
size_t BucketNum()
{
size_t num = 0;
for (auto &hashNode : _tables)
{
if (hashNode)
{
++num;
}
}
return num;
}
size_t MaxBucketLength()
{
size_t maxLen = 0;
for (auto &hashNode : _tables)
{
size_t len = 0;
Node *cur = hashNode;
while (cur)
{
++len;
cur = cur->_next;
}
if (len > maxLen)
{
maxLen = len;
}
}
return maxLen;
}
private:
vector<Node *> _tables;
size_t _size = 0; // 存储有效数据个数
};