哈希概念:
![](https://i-blog.csdnimg.cn/blog_migrate/f3799f9011ef3ade89a50f4dbc50bb7f.png)
但是当我们想插入11的时候要怎么办呢?
哈希冲突
上述想要插入11的时候 但却发现11的位置被占用了,这样的情况我们成为哈希冲突。即不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞。
那么我们该怎么处理这个问题?
哈希函数
会引起哈希冲突的原因可能是因为hash函数写的有问题,没有考虑到hash冲突的事情.
hash函数的设计原则有以下几条
- 哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有m个地址时,其值 域必须在0到m-1之间。
- 哈希函数计算出来的地址能均匀分布在整个空间中。
- 哈希函数应该比较简单。
![](https://i-blog.csdnimg.cn/blog_migrate/e74cff94913de610cea09b56d1040939.png)
当我们要删除一个值的时候,为了不影响其他值的查找,我们选择标记删除。
enum state
{
EMPTY,
EXIST,
DELET
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
state _st = EMPTY;
};
线性探测如何扩容:
我们试想一下如果所有点都是冲突的,也就是说他们没有一个在hash函数划定的位置那我们查找的时候的会发生冲突进而向后不断查找,这样就有可能让空间复杂度退化。
我们引入负载因子概念:
a = 表中数据个数/表容量
a越大表明插入的数据越多冲突的可能性越大,反之越小,对于开放地址发来说应该严格控制a在0.7-0.8之间 超过后cpu缓存不命中几率大大增加 所以一旦超出了就该扩容。
#pragma once
#include <iostream>
#include <utility>
#include <vector>
using namespace std;
namespace HashTable
{
enum state
{
EMPTY,
EXIST,
DELET
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
state _st = EMPTY;
};
template<class K>
struct _hash // 对类型进行探测地址时使用
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct _hash<string> // 对特殊类型进行探测地址时使用
{
size_t operator()(const string& str)
{
size_t i = 0;
for (auto it : str)
{
i += it;
}
return i;
}
};
template<class K, class V, class hash = _hash<K>>
class Hash
{
public:
bool insert(const pair<K, V> kv)
{
if (Find(kv.first))
{
return false;
}
if (_size == 0 || _size * 10 / _tables.size() >= 7) // 控制负载因子小于0.7 否则扩容
{
size_t newSize = 0;
newSize = _tables.size() == 0 ? 4 : _tables.size() * 2;
// 扩容后的映射关系需要调整
Hash<K, V> newHash;
newHash._tables.resize(newSize);
for (auto it : _tables)
{
if (it._st == EXIST)
{
newHash.insert(it._kv);
}
}
_tables.swap(newHash._tables);
}
hash hs;
size_t hashi = hs(kv.first) % _tables.size();
// 线性探测
while (_tables[hashi]._st == EXIST) // 判断当前位置是否冲突
{
hashi++;
hashi = hashi % _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._st = EXIST;
_size++;
return true;
}
HashData<K, V>* Find(const K& key)
{
if (_size == 0)
return nullptr;
hash hs;
size_t start = 0;
size_t hashi = hs(key) % _tables.size();
start = hashi;
while (_tables[hashi]._st != EMPTY)
{
if (_tables[hashi]._kv.first == key && _tables[hashi]._st != DELET)
{
return &(_tables[hashi]);
}
hashi++;
hashi %= _tables.size();
if (start == hashi) // 可能会边删除边插入导致全是DELET 进入死循环 要保证只查找一次
return nullptr;
}
return nullptr;
}
bool erase(const K& key)
{
if (!Find(key))
{
return false;
}
(Find(key))->_st = DELET;
return true;
}
private:
vector<HashData<K, V>> _tables;
size_t _size = 0;
};
}
再简单讲一下二次探测
二次探测是根据2的n次幂得来的
和线性探测对比来看 线性探测在发现冲突后会一次一步的向后找空位置,二二次探测会向后2^n-1(n是第几次冲突)查找空位置
研究:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任 何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在 搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5,如果超出 必须考虑增容。
![](https://i-blog.csdnimg.cn/blog_migrate/1a24e17d8a224dbdb156cbe9f927cd3d.png)
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, 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;
}
}
inline size_t __stl_next_prime(size_t n)
{
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 nullptr;
}
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 (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
++num;
}
}
return num;
}
size_t MaxBucketLenth()
{
size_t maxLen = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
size_t len = 0;
Node* cur = _tables[i];
while (cur)
{
++len;
cur = cur->_next;
}
//if (len > 0)
//printf("[%d]号桶长度:%d\n", i, len);
if (len > maxLen)
{
maxLen = len;
}
}
return maxLen;
}
private:
vector<Node*> _tables;
size_t _size = 0; // 存储有效数据个数
};