目录
哈希概念
哈希的思想是通过哈希函数将元素的存储位置和该元素关键码建立映射关系,这样就可以不经过比较,直接从哈希表(又称散列表)中查找我们所需要的元素,因此哈希表的查找效率为O(1)。
- 插入:通过Hash(key)的值算出位置,然后插入
- 查找:通过Hash(key)算出位置,看该位置是否存在该元素。
哈希冲突
通过上面的哈希函数我们也可以发现问题:不同的关键码可能映射到相同的位置,这种情况就称作哈希冲突。
那么,如何解决呢?
出现哈希冲突的原因可能是哈希函数设置的不合理。
哈希函数
下面介绍两种常用的哈希函数。
- 1.直接定址法
取关键字的线性函数作为地址: Hash(key)=A*key+B
优点:简单,均匀
缺点:需要提前直到关键字的分布情况
使用场景:关键字少且连续
- 2.除留余数法
哈希表种允许的地址为m个,设置一个接近m或者等于m的质数n作为除数
Hash(key)=key%n
哈希函数的选的好可以减少哈希冲突,但是却无法避免,下面介绍两种解决哈希冲突的办法。
闭散列
本篇哈希函数采用除留余数法。
闭散列也叫开放定址法,它的思想是如果发生哈希冲突,就把当前key存放到冲突位置后的下一个空位置中,也就是去占别人的位置。
线性探测
线性探测:从冲突位置开始,依次向后探测一个位置,直到找到空位置。
插入:
- 通过哈希函数获取位于哈希表当中的位置
- 如果该位置为空,直接插入新元素,不为空,则发生冲突,此生采用线性探测寻找下一个空位置。
删除 :
- 闭散列处理哈希冲突,无法直接删除元素,所以我们可以采用状态标记,空标记为EMPTY,
存在标记为EXIST,删除标记为DELETE。
enum Status
{
EXIST,
DELETE,
EMPTY
};
线性探测优点:非常简单
线性探测缺点:发生哈希冲突,数据容易堆积在一起,使得查找效率降低。
扩容
负载因子:a=表中数据个数/表容量
表中数据越多,产生冲突的可能性就越大,因此我们为了防止效率降低,应该根据负载因子进行扩容。
对于开放定址法,负载因子应该严格限制在0.7~0.8以下。
二次探测
二次探测的思想与线性探测区别不大,它们都是依次向后探测找空位置。
但是二次探测向后探测,依次探测i^2个位置,i=0,1,2,3……
扩容
研究表明:当表的长度为质数且表装载因子a不超过0.5时,新的表项一定能够插入,而且任
何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在
搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子a不超过0.5。
闭散列哈希表结构
底层采用vector进行存储,要对key值进行取模计算位置,但我们不保证存储的元素key都是整数,因此我们可以增加一个HashFunc模板参数,将不同的key类型都能转换为可以取模的整数。
以string类型为例:
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return key;
}
};
template<>
struct Hash<string>
{
size_t operator()(const string& s)
{
size_t ret = 0;
for (auto e : s)
{
ret *= 31;
ret += e;
}
return ret;
}
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
Status _status=EMPTY;
};
template<class K, class V,class HashFunc=Hash<K>>
class HashTable
{
private:
vector<HashData<K, V>> _table;
size_t _n=0;//有效数据个数
};
闭散列代码实现
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
Status _status=EMPTY;
};
template<class K, class V,class HashFunc=Hash<K>>
class HashTable
{
public:
bool Erase(const K& key)
{
auto ret = Find(key);
if (ret)
{
ret->_status = DELETE;
--_n;
return true;
}
return false;
}
HashData<K, V>* Find(const K& key)
{
if (_table.empty())
{
return nullptr;
}
HashFunc hf;
size_t start = hf(key) % _table.size();
size_t i = 0;
size_t index = start + i;
while (_table[index]._status !=EMPTY)
{
if (_table[index]._kv.first == key && _table[index]._status == EXIST)
{
return &_table[index];
}
++i;
index = start + i;
//index = start + i * i;
index %= _table.size();
}
return nullptr;
}
bool insert(const pair<K, V>& kv)
{
auto ret = Find(kv.first);
if (ret)
{
return false;
}
if (_table.size() == 0 || (_n * 10) / _table.size() >= 7)
{
//负载因子大于0.7扩容
size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
HashTable<K, V, HashFunc> newHashTable;
newHashTable._table.resize(newsize);
for (size_t i = 0; i < _table.size(); ++i)
{
if (_table[i]._status == EXIST)
{
newHashTable.insert(_table[i]._kv);
}
}
_table.swap(newHashTable._table);
}
HashFunc hf;
size_t start = hf(kv.first) % _table.size();
size_t i = 0;
size_t index = start+i;
while (_table[index]._status == EXIST)
{
++i;
//线性探测
index = start + i;
//二次探测
//index = start + i * i;
index %= _table.size();
}
_table[index]._kv = kv;
_table[index]._status = EXIST;
++_n;
return true;
}
private:
vector<HashData<K, V>> _table;
size_t _n=0;//有效数据个数
};
开散列
开散列又叫链地址法,先对key使用哈希函数计算地址,具有相同地址的key归为一个桶,桶内元素通过单链表进行链接。
插入
- 先用哈希函数计算地址,找到桶
- 在该桶内进行头插
删除
- 先用哈希函数计算地址,找到桶
- 依次比较桶内元素key值,找到要删除的key
开散列结构
底层采用vector存储节点指针
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 HashFunc=Hash<K>>
class HashTable
{
typedef HashNode<K, V> Node;
private:
vector<Node*> _table;//存储节点指针
size_t _n=0;//有效数据个数
};
扩容
桶的个数是一定的,随着元素不断插入,桶内元素增多,极端情况会导致一个桶内存储很多数据,此时查找效率会降低。
开散列理想情况:每个桶内一个元素,再插入元素,每一次都发生哈希冲突,因此规定元素个数等于桶个数时就进行扩容。
开散列实现
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 HashFunc=Hash<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
bool erase(const K& key)
{
if (_table.empty())
{
return false;
}
HashFunc hf;
size_t index = hf(key) % _table.size();
Node* cur = _table[index];
Node* prev = nullptr;
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_table[index] = cur->_next;
}
else
{
Node* next = cur->_next;
prev->_next = next;
}
--_n;
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
}
Node* Find(const K& key)
{
if (_table.empty())
{
return nullptr;
}
HashFunc hf;
size_t index = hf(key) % _table.size();
Node* cur = _table[index];
while (cur)
{
if (cur->_kv.first == key)
{
return cur;
}
cur = cur->_next;
}
return nullptr;
}
bool insert(const pair<K, V>& kv)
{
auto ret = Find(kv.first);
if (ret)
{
return false;
}
HashFunc hf;
if (_n==_table.size())
{
//扩容
size_t newsize = _table.size() == 0 ? 10 : _table.size() * 2;
vector<Node*> newTable;
newTable.resize(newsize);
for (size_t i = 0; i < _table.size(); ++i)
{
Node* cur = _table[i];
while (cur)
{
Node* next = cur->_next;
size_t index = hf(cur->_kv.first) % newsize;
cur->_next = newTable[index];
newTable[index] = cur;
cur = next;
}
_table[i] = nullptr;
}
_table.swap(newTable);
}
size_t index = hf(kv.first) % _table.size();
Node* newnode = new Node(kv);
newnode->_next = _table[index];
_table[index] = newnode;
++_n;
return true;
}
private:
vector<Node*> _table;//存储节点指针
size_t _n=0;//有效数据个数
};
总结
哈希表缺陷:空间浪费多。
闭散列与开散列比较:开散列空间效率高。
表面看开散列存储节点指针,需要增设链接指针,似乎增加了存储开销,但是其实闭散列表项所占空间比指针大得多,所以开散列比闭散列节省空间。