🌈前言
本篇文章进行数据结构中哈希表的学习!!!
🌅哈希表
🚁1、哈希概念
概念:
理想的搜索方法:
当向该结构进行:
-
插入元素:根据待插入元素的关键码,再使用一个函数计算出该元素的存储位置并按此位置进行存放 -
搜索元素:对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功 -
该方式即为哈希(散列)方法,哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
- 例如:我们有一组数据 {1,3,6,8,9}
- 哈希函数设置为:hash(key) = key % capacity,capacity为存储元素底层空间总的大小
总结:使用该方法,不用对关键码进行多次比较,所以搜索速度很快!!!
🚂2、哈希冲突
哈希冲突:
-
对于两个数据元素的关键字Ki和Kj(i != j),有Ki != Kj,但也有:Hash(Ki) == Hash(Kj),即: -
不同关键字通过相同的哈希函数计算出相同的映射地址,该现象成为哈希冲突或哈希碰撞 -
把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”
🚃3、哈希函数
引起哈希冲突的原因可能是:哈希函数设计不够合理。哈希函数设计原则:
常见哈希函数:
- 直接定址法(常用):
- 除留余数法(常用)
- 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数:Hash(Key) = Key % p(p <= m),将关键码转换成哈希地址
- 平方取中法(了解)
- 折叠法(了解)
- 折叠法是将关键字从左到右分割成位数相等的几部分(最后一部分的位数可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址
- 随机数法(了解)
- 选择一个随机函数,取关键字的随机函数值作为它的哈希地址,即:Hash(Key)= random(Key),其中random为随机数函数
🚄4、哈希冲突解决方法
🌈前言:解决哈希冲突的两种常用方法:闭散列和开散列
🚅4.1、闭散列
- 闭散列:也叫开放定址法,当发生哈希冲突时,如果哈希表没有被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置的”下一个“空位置中去“
🚆4.1.1、线性探测
- 线性探测:从发生冲突的位置开始,依次向后探测,直到找到下一个空位置为止
插入:
删除:
enum State
{
EMPTY,
EXIST,
DELETE
}
闭散列的扩容:
关于关键值(key)自定义类型问题:
- 如果关键值是自定义类型时,我们就不能将他隐式的转换成为无符号整数,需要实现一个仿函数来进行转换,一般只需要转换string,后面增加直接特化即可
template <typename K>
struct HashDF
{
size_t operator()(const string& str)
{
size_t hash_sum = 0;
for (auto& e : str)
{
hash_sum = hash_sum * 131 + e;
}
return hash_sum;
}
};
template <>
struct HashDF<...>
{};
线性探测的实现:
namespace CloseHash
{
template <typename K>
struct HashDF
{
size_t operator()(const string& str)
{
size_t hash_sum = 0;
for (auto& e : str)
{
hash_sum = hash_sum * 131 + e;
}
return hash_sum;
}
};
enum State
{
EMPTY,
EXIST,
DELETE
};
template<typename K, typename V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template<typename K, typename V, typename Function = HashDF<K>>
class HashTable
{
private:
typedef HashData<K, V> HD;
public:
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first) != nullptr)
return false;
if (_tables.size() == 0 || _n * 100 / _tables.size() >= 75)
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable<K, V> newHT;
newHT._tables.resize(newSize);
for (auto& e : _tables)
{
if (e._state == EXIST)
{
newHT.Insert(e._kv);
}
}
newHT._tables.swap(_tables);
}
size_t starti = df(kv.first);
starti %= _tables.size();
size_t hashi = starti;
size_t i = 1;
while (_tables[hashi]._state == EXIST)
{
hashi = starti + i;
++i;
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._state = EXIST;
_n++;
return true;
}
bool erase(const K& key)
{
HD* p = Find(key);
if (p != nullptr)
{
p->_state = DELETE;
--_n;
return true;
}
return false;
}
HD* Find(const K& key)
{
if (_tables.size() == 0)
return nullptr;
size_t starti = df(key);
starti %= _tables.size();
size_t hashi = starti;
size_t i = 1;
while (_tables[hashi]._state != EMPTY)
{
if (_tables[hashi]._state != DELETE && _tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
hashi = starti + i;
++i;
hashi %= _tables.size();
}
return nullptr;
}
private:
vector<HD> _tables;
size_t _n = 0;
Function df;
};
总结:
🚇4.1.2、二次探测
- 二次探测:线性探测的缺陷是产生冲突的数据堆积在一块,这与其找下一个空位置有关系,因为找空位置的方式就是挨着往后逐个去找,因此二次探测为了避免该问题
插入:
- 找下一个空位置的方法为:H = (H0 + i2) % m,或者:H = (H0 - i2) % m,。其中:i = 1,2,3…, 是通过散列函数Hash(x)对元素的关键码 key 进行计算得到的位置,m是表的大小
注意:二次探测就不实现了,跟线性探测基本一致,就是hashi每次走i2步
🚈4.2、开散列
🚉4.2,1、概念及结构
开散列的概念:
template <typename K, typename V>
struct HashNode
{
public:
HashNode(const pair<K, V>& kv)
: _kv(kv)
, _next(nullptr)
{}
public:
pair<K, V> _kv;
HashNode<K, V>* _next;
};
template <typename K, typename V, typename hashF = HashDF<K>>
class HashTable
{
private:
vector<Node*> _tables;
size_t _n = 0;
hashF HashDF;
};
🚐4.2,2、插入
- 开散列的插入:
- 哈希桶的结构:哈希桶其实是一个数组指针,每个桶(数组中的元素)里面存储的是一个链表
- 链表里面有指向下一个节点的指针和值域
- 插入数据就相当于对链表进行头插就行了,将关键码映射的地址直接头插到对应的桶中
注意:从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素,冲突的值在链表中进行头插
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
size_t hashi = kv.first;
hashi %= _tables.size();
Node* newNode = new Node(kv);
newNode->_next = _tables[hashi];
_tables[hashi] = newNode;
++_n;
return true;
}
插入操作:
🚑4.2,3、扩容
- 开散列的扩容
void _CheckCapacity()
{
if (_tables.size() == 0 || _tables.size() == _n)
{
size_t NewSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable newHT;
newHT._tables.resize(NewSize, nullptr);
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur != nullptr)
{
Node* next = cur->_next;
size_t hashi = HashDF(cur->_kv.first) % NewSize;
cur->_next = newHT._tables[hashi];
newHT._tables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
newHT._tables.swap(_tables);
}
}
🚒4.2,4、删除
- 开散列的删除:
bool erase(const K& key)
{
size_t hashi = key;
hashi %= _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur != nullptr)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
}
🚓4.2,5、思考
- 开散列的思考:
只能存储key为整形的元素,那么自定义类型要怎么解决呢???
template <typename K>
struct HashDF
{
size_t operator()(const K& key)
{
return static_cast<size_t>(key);
}
};
template <>
struct HashDF<string>
{
size_t operator()(const string& str)
{
size_t HDS = 0;
for (auto& e : str)
{
HDS = HDS * 131 + static_cast<size_t>(e);
}
return HDS;
}
};
template <typename K, typename V, typename hashF = HashDF<K>>
class HashTable
{
private:
vector<Node*> _tables;
size_t _n = 0;
hashF HashDF;
}
🚔5、哈希桶完整代码
#pragma once
#include <iostream>
#include <vector>
using namespace std;
namespace BUCKetHash
{
template <typename K>
struct HashDF
{
size_t operator()(const K& key)
{
return static_cast<size_t>(key);
}
};
template <>
struct HashDF<string>
{
size_t operator()(const string& str)
{
size_t HDS = 0;
for (auto& e : str)
{
HDS = HDS * 131 + static_cast<size_t>(e);
}
return HDS;
}
};
template <typename K, typename V>
struct HashNode
{
public:
HashNode(const pair<K, V>& kv)
: _kv(kv)
, _next(nullptr)
{}
public:
pair<K, V> _kv;
HashNode<K, V>* _next;
};
template <typename K, typename V, typename hashF = HashDF<K>>
class HashTable
{
private:
typedef HashNode<K, V> Node;
public:
HashTable() = default;
HashTable(const HashTable& ht)
{
_tables.resize(ht._tables.size(), nullptr);
for (size_t i = 0; i < ht._tables.size(); ++i)
{
Node* cur = ht._tables[i];
while (cur != nullptr)
{
this->Insert(cur->_kv);
cur = cur->_next;
}
}
}
HashTable& operator=(const HashTable ht)
{
HashTable tmp(ht);
this->_n = tmp._n;
tmp._tables.swap(_tables);
return *this;
}
~HashTable()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur != nullptr)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
if (_tables.size() == 0 || _tables.size() == _n)
{
size_t NewSize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable newHT;
newHT._tables.resize(NewSize, nullptr);
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur != nullptr)
{
Node* next = cur->_next;
size_t hashi = HashDF(cur->_kv.first) % NewSize;
cur->_next = newHT._tables[hashi];
newHT._tables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
newHT._tables.swap(_tables);
}
size_t hashi = HashDF(kv.first);
hashi %= _tables.size();
Node* newNode = new Node(kv);
newNode->_next = _tables[hashi];
_tables[hashi] = newNode;
++_n;
return true;
}
bool erase(const K& key)
{
size_t hashi = HashDF(key);
hashi %= _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur != nullptr)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_n;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
return nullptr;
size_t hashi = HashDF(key);
hashi %= _tables.size();
Node* cur = _tables[hashi];
while (cur != nullptr)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
private:
vector<Node*> _tables;
size_t _n = 0;
hashF HashDF;
};
}
总结:开散列与闭散列比较
|