目录
一. 哈希的概念及性质
1. 基本概念
哈希(散列) : 是一种映射思想, 将键值(元素)通过哈希函数与哈希值(存储地址)建立映射关系.
哈希表 : 是以哈希思想实现的数据结构.
哈希函数 : 不是一个固定的函数, 只要符合哈希的映射思想, 可以建立映射关系的函数, 都可以被称为是哈希函数.
哈希冲突 : 不同的键值通过相同的哈希函数得到了相同的哈希地址.
哈希表在理想的情况下, 数据通过哈希函数直接查找到存储地址, 那么时间复杂度就可以达到 O(1).
2. 哈希函数
2.1 哈希函数的设计原则
- 哈希函数的定义域必须包括需要存储的全部键值, 且如果哈希表有 n 个地址, 其值域为 [0, n-1].(尽量使映射关系一一对应, 可以快速查找和减少冲突)
- 哈希函数计算出来的哈希值能均匀分布在整个哈希表中.(使数据映射到地址集合中任何一个地址的概率是相等的, 减少冲突)
- 哈希函数应该尽可能简单, 实用.
2.2 常见的哈希函数
直接定址法(常用) : 直接将键值或键值的某个线性函数值为哈希值
例: Hash(key) = A*key + B (A B 均为常数)
适用: 范围比较集中, 每个数据分配一个唯一位置.
优点: 简单, 均匀.
缺点: 需要提前知道键值的分布情况.
除留余数法(常用) : 取某个不大于哈希表表长 m 的数 p, 除键值后所得的余数为哈希值.
例: Hash(key) = key % p (p<=m)
适用: 范围不集中, 分布分散的数据.
优点: 简单易用, 性能均衡.
缺点: 容易出现哈希冲突, 需要借助特定方法解决.
平方取中法 : 将键值平方后的中间几位作为哈希值.
例: 假设键值为1111, 平方值为1234321, 将其中间的三位数 343 作为哈希值.
适用: 不知道键值的分布, 而位数又不是很大的情况.
折叠法 : 将键值分割成位数相同的几部分(最后一部分可以不同), 然后将这几部分叠加求和, 并按哈希表表长, 取后几位作为哈希值.
适用: 事先不需要知道键值的分布,且键值位数比较多.
随机数法 : 选择一随机函数, 键值的随机函数值作为哈希值.
例: Hash(key) = random(key)
适用: 键值长度不等时.
3. 哈希冲突的解决方法
3.1 闭散列
闭散列又叫开放定址法, 哈希表的每个存储地址只存放一个的元素.
闭散列的基础结构
简单的使用顺序表模拟下闭散列
template<class K, class V>
class HashTable
{
//每个存储位置的状态
enum State { EMPTY, EXIST, DELETE };
//每个存储位置存放的元素
struct Elem
{
pair<K, V> _val;
State _state = EMPTY; //每个存储位置默认为空状态
};
public:
HashTable(size_t capacity = 6)
: _ht(capacity), _size(0), _totalSize(0)
{}
//删除
bool Erase(const K& key)
{
size_t pos = HashFunc(key); //哈希函数获取映射地址
while (_ht[pos]._state != EMPTY)
{
//查找数据存在时跳过删除状态的存储地址
if (_ht[pos]._state == EXIST && _ht[pos]._val.first == key) //若存在, 且状态也是存在
{
_ht[pos]._state = DELETE; //存储位置状态设置为删除, 若设为空, 可能会影响其他键值的线性查找
_size--;
return true;
}
pos = (pos + 1) % _ht.capacity();
}
return false;
}
private:
size_t HashFunc(const K& key) //哈希函数
{ return key % _ht.capacity(); }
private:
vector<Elem> _ht; //使用顺序表存放
size_t _size; //有效数据个数 负载因子
};
在插入发生哈希冲突时, 若哈希表的负载因子达到一定值, 就扩容空间并在新表重新映射所有数据;
若没有, 就从发生冲突的位置开始, 遍历哈希表, 直到找到空位置存储数据.
//插入
bool Insert(const pair<K, V>& val)
{
//是否扩容
if (_size * 10 / _ht.capacity() >= 7) //若哈希表中实际存储的数据数量达到了70%, 就扩容
{
HashTable<K, V> new_table(_ht.capacity() * 2); //创建新表
for (int i = 0; i < _ht.capacity(); i++) //依次将原表数据映射到新表中
{
if (_ht[i]._state == EXIST)
new_table.Insert(_ht[i]._val);
}
Swap(new_table);
}
//查找当前键值是否存在, 保证键值唯一
size_t pos = HashFunc(val.first); //哈希函数获取映射地址
while (_ht[pos]._state != EMPTY)
{
//查找数据存在时跳过删除状态的存储地址
if (_ht[pos]._state == EXIST && _ht[pos]._val.first == val.first) //重复则不插入
return false;
pos = (pos + 1) % _ht.capacity();
}
//键值不存在, 插入数据
pos = HashFunc(val.first); //重新获取映射地址, 插入时不跳过删除状态的存储地址
while (_ht[pos]._state == EXIST)
pos = (pos + 1) % _ht.capacity();
_ht[pos]._val = val;
_ht[pos]._state = EXIST; //存储地址状态设置为存在
_size++;
return true;
}
但是这种线性探测当数据分布不均匀时, 可能会导致越来越多的数据都需要线性探测, 平均查找长度可能会越来越长, 插入, 查找都会越来越慢.
虽然可以使用二次探测(每次向后探测 i ^ 2 步)或者左右探测(当前位置的左右同时探测)来优化, 但实际上更推荐使用开散列.
3.2 开散列
开散列又叫链地址法(开链法, 哈希桶), 即存储位置存放的是单链表, 多个键值可以映射相同的哈希值, 数据直接插入链表中即可, 查找时遍历链表查找.
开散列中每个链表中存放的都是发生哈希冲突的元素, 但是开散列的不同冲突之间不会互相影响, 所以效率和适用情况会比闭散列更好.(结构下面单独实现)
在一般情况下, 单链表的长度不会太长的, 查找效率基本为 O(1);
若长度过长, 可以将链表转换为红黑树, 可以大幅度的减少高度, 保证效率至少为 O(log n).
二. 哈希表模拟实现
1. 哈希表的基本结构
注:
class V: 单链表的节点中存储数据的类型. 若是键值结构, 则为 pair 类型; 若为键结构, 则为 K 类型.
class KeyOfValue: 仿函数, 作用是 从需要存储的数据中得到键(key)值. 若是键值结构, 则需返回 pair.first 的; 若为键结构, 则直接返回 key.
class HF: 仿函数, 作用是 将键值映射为无符号整型, 方便映射存储地址, 不同的类型会有不同的哈希函数.
- 哈希表的单链表节点结构
//单链表的节点
template<class V>
struct HashBucketNode
{
typedef HashBucketNode<V> Node;
HashBucketNode(const V& data)
: _pNext(nullptr), _data(data)
{}
Node* _pNext; //下一个节点
V _data; //节点的数据
};
- 哈希表的迭代器结构
template <class V, class Ref, class Ptr, class KeyOfValue, class HF>
struct HBIterator
{
typedef HashBucket<V, KeyOfValue, HF> HashBucket;
typedef HashBucketNode<V>* PNode;
typedef HBIterator<V, Ref, Ptr, KeyOfValue, HF> Self;
typedef HBIterator<V, V&, V*, KeyOfValue, HF> It;
//构造函数
HBIterator(PNode pNode = nullptr, HashBucket* pHt = nullptr)
: _pNode(pNode) , _pHt(pHt) {}
//针对 const_iterator 的构造函数
HBIterator(const It& it);
//前置++
Self& operator++();
//后置++
Self operator++(int);
//解引用
Ref operator*();
Ptr operator->();
bool operator==(const Self& it)
{ return _pNode == it._pNode; }
bool operator!=(const Self& it)
{ return _pNode != it._pNode; }
PNode _pNode; // 当前迭代器关联的节点
HashBucket* _pHt; // 哈希表对象的指针--方便迭代器寻找下一个节点
};
- 哈希表的主体
//哈希桶
template<class V, class KeyOfValue, class HF = HashFunc<V>>
class HashBucket
{
template <class V, class Ref, class Ptr, class KeyOfValue, class HF>
friend struct HBIterator;
typedef HashBucketNode<V> Node;
typedef Node* PNode;
public:
typedef HBIterator<V, V&, V*, KeyOfValue, HF> iterator;
typedef HBIterator<V, const V&, const V*, KeyOfValue, HF> const_iterator;
//构造函数
HashBucket(size_t capacity = 16)
: _table(capacity), _size(0) {}
//析构函数
~HashBucket()
{ clear(); }
//插入函数
pair<iterator, bool> insert(const V& data);
//删除函数
iterator erase(const V& data);
//查找函数
iterator find(const V& data);
//迭代器
iterator begin();
iterator end();
//清除函数, 释放所有的单链表
void clear();
//哈希表中哈希桶的数量
size_t bucket_count()// const
{ return _table.capacity(); }
//内置交换函数
void swap(Self& ht)
{
_table.swap(ht._table);
std::swap(_size, ht._size);
}
private:
//哈希函数
size_t HashFunc(const V& data)// const
{
return HF()(KeyOfValue()(data)) % _table.capacity();
}
//检查扩容
void CheckCapacity();
private:
vector<Node*> _table;
size_t _size; //哈希表中有效元素的个数
};
2. 哈希表迭代器
2.1 构造函数
在 unordered_set 中所使用的 iterator 迭代器和 const_iterator 迭代器都是哈希表的 const_iterator 迭代器;
为了 unordered_set 能够正常返回 begin() 和 end() 迭代器类型, 所以需要使用 iterator 类型能够构造创建 const_iterator 类型.
typedef HBIterator<V, V&, V*, KeyOfValue, HF> It;
当 iterator 迭代器参数传进来时, It的类型依旧是普通迭代器 HBIterator<V, V&, V*, KeyOfValue, HF>;
当 const_iterator 迭代器参数传进来时, It的类型就会变成普通迭代器 HBIterator<V, V&, V*, KeyOfValue, HF>, 就可以达成使用 iterator 迭代器类型构造 const_iterator 迭代器类型.
HBIterator(const It& it)
: _pNode(it._pNode)
, _pHt(it._pHt)
{}
哈希表中的迭代器的 begin() 则定义为哈希桶中的第一个元素, end() 则定义为空.
iterator begin()
{
for (int i = 0; i < _table.capacity(); i++) //查找第一个元素
if (_table[i])
return {_table[i], this}; //this 为哈希表对象的指针
return {nullptr, this};
}
iterator end()
{
return {nullptr, this}; //this 为哈希表对象的指针
}
2.2 operator++
前置++的移动逻辑:
若当前节点的 next 为真, 代表当前哈希桶还有数据, 直接移动至 next 节点;
若当前节点的 next 为空, 代表当前节点为此哈希桶中的最后一个节点, 应该移动至下一个非空哈希桶中的第一个节点.
第二种情况下需要当前节点在哈希表的位置, 所以迭代器需要包含哈希表对象的指针和哈希函数, 并且迭代器类需为哈希表类的友元类.
//前置++
Self& operator++()
{
if (!_pNode) //当前节点为空或为 end()
return *this;
if (_pNode->_pNext) //当前节点不为哈希桶的最后一个节点时
_pNode = _pNode->_pNext;
else
{
//获取当前哈希桶在哈希表中的下标后 +1 找到下一个哈希桶
size_t bucketNo = _pHt->HashFunc(_pNode->_data) + 1;
//若当前哈希桶为最后一个哈希桶
_pNode = nullptr;
//寻找下一个非空哈希桶, 返回第一个节点
for (; bucketNo < _pHt->bucket_count(); ++bucketNo)
{
if (_pNode = _pHt->_table[bucketNo])
break;
}
}
return *this;
}
//后置++
Self operator++(int)
{
Self tmp = *this;
operator++();
return tmp;
}
2.3 解引用
避免空指针解引用即可.
//解引用
V& operator*()
{
if (!_pNode)
assert(0);
return _pNode->_data;
}
V* operator->()
{
if (!_pNode)
assert(0);
return &(operator*());
}
3. 哈希表函数
3.1 insert()
插入函数为了方便 unordered_map/unordered_set 使用, 返回值使用 pair 类型.
若键值存在, 返回节点的迭代器和 false;
若键值不存在, 则直接头插在单链表中, 返回节点的迭代器和 true;
//插入函数
pair<iterator, bool> insert(const V& data)
{
//是否扩容
CheckCapacity();
//查找是否存在相同键值数据存在
KeyOfValue kov;
size_t pos = HashFunc(data);
Node* cur = _table[pos];
while (cur)
{
if (kov(cur->_data) == kov(data))
return { {cur,this}, false }; //若键值存在, 返回节点的迭代器和 false;
cur = cur->_pNext;
}
//头插节点
Node* new_node = new Node(data);
new_node->_pNext = _table[pos];
_table[pos] = new_node;
_size++;
return { {new_node,this}, true };
}
//是否扩容
void CheckCapacity()
{
if (_size == _table.capacity()) //若哈希表中的有效元素等于哈希桶的数量就扩容
{
KeyOfValue kov;
vector<Node*> new_table(_size * 2); //创建新表
for (int i = 0; i < _table.capacity(); i++) //依次从旧表中转移节点至新表
{
Node* cur = _table[i];
while (cur)
{
_table[i] = cur->_pNext;
//旧表节点转移至新表
size_t pos = HF()(kov(cur->_data)) % new_table.capacity();
cur->_pNext = new_table[pos];
new_table[pos] = cur;
cur = _table[i];
}
}
_table.swap(new_table);
}
}
3.2 erase()
若删除成功, 返回删除节点的下一个节点的迭代器;
若失败, 返回 end() 的迭代器;
//删除函数
iterator erase(const V& data)
{
KeyOfValue kov;
size_t pos = HashFunc(data); //找到键值的映射地址
Node* cur = _table[pos]; //获取哈希桶的第一个节点
Node* prev = nullptr;
while (cur)
{
if (kov(cur->_data) == kov(data)) //若哈希桶中存在需要删除的节点
{
iterator ret(cur, this);
++ret; //这里偷懒直接调用的迭代器的 operator++
if (!prev) //若删除节点为哈希桶的第一个节点
_table[pos] = cur->_pNext;
else //若不为第一个节点
prev->_pNext = cur->_pNext;
delete cur; //释放节点
_size--;
return ret;
}
prev = cur;
cur = cur->_pNext;
}
return { nullptr, this }; //若没有需要删除的节点, 返回 end() 的迭代器
}
3.3 find()
若查找成功, 返回查找节点的迭代器;
若失败, 返回 end() 迭代器.
//查找函数
iterator find(const V& data)
{
KeyOfValue kov;
size_t pos = HashFunc(data);
Node* cur = _table[pos];
while (cur)
{
if (kov(cur->_data) == kov(data))
return {cur, this};
cur = cur->_pNext;
}
return { nullptr, this };
}
3.4 clear()
//清除函数, 释放所有的哈希桶
void clear()
{
//复用的迭代器和 erase()
for (auto it = begin(); it != end();)
it = erase(*it);
_size = 0;
}
三. 完整代码
Hash.h
#pragma once
#include <iostream>
#include <vector>
#include <utility>
#include <cassert>
using namespace std;
//声明
template<class V, class KeyOfValue, class HF>
class HashBucket;
template<class T>
class HashFunc
{
public:
size_t operator()(const T& val)
{
return val;
}
};
//特化
template<>
class HashFunc<string>
{
public:
size_t operator()(const string& s)
{
const char* str = s.c_str();
unsigned int seed = 131; // 31 131 1313 13131 131313
unsigned int hash = 0;
while (*str)
{
hash = hash * seed + (*str++);
}
return hash;
}
};
template<class V>
struct HashBucketNode
{
typedef HashBucketNode<V> Node;
HashBucketNode(const V& data)
: _pNext(nullptr), _data(data)
{}
Node* _pNext;
V _data;
};
template <class V, class Ref, class Ptr, class KeyOfValue, class HF>
struct HBIterator
{
typedef HashBucket<V, KeyOfValue, HF> HashBucket;
typedef HashBucketNode<V>* PNode;
typedef HBIterator<V, Ref, Ptr, KeyOfValue, HF> Self;
typedef HBIterator<V, V&, V*, KeyOfValue, HF> It;
HBIterator(PNode pNode = nullptr, HashBucket* pHt = nullptr)
: _pNode(pNode)
, _pHt(pHt)
{}
HBIterator(const It& it)
: _pNode(it._pNode)
, _pHt(it._pHt)
{}
//前置++
Self& operator++()
{
if (!_pNode) //当前节点为空或为 end()
return *this;
if (_pNode->_pNext) //当前节点不为哈希桶的最后一个节点时
_pNode = _pNode->_pNext;
else
{
//获取当前哈希桶在哈希表中的下标后 +1 找到下一个哈希桶
size_t bucketNo = _pHt->HashFunc(_pNode->_data) + 1;
//若当前哈希桶为最后一个哈希桶
_pNode = nullptr;
//寻找下一个非空哈希桶, 返回第一个节点
for (; bucketNo < _pHt->bucket_count(); ++bucketNo)
{
if (_pNode = _pHt->_table[bucketNo])
break;
}
}
return *this;
}
//后置++
Self operator++(int)
{
Self tmp = *this;
operator++();
return tmp;
}
Ref operator*()
{
if (!_pNode)
assert(0);
return _pNode->_data;
}
Ptr operator->()
{
if (!_pNode)
assert(0);
return &(operator*());
}
bool operator==(const Self& it)
{
return _pNode == it._pNode;
}
bool operator!=(const Self& it)
{
return _pNode != it._pNode;
}
PNode _pNode; // 当前迭代器关联的节点
HashBucket* _pHt; // 哈希桶--主要是为了找下一个空桶时候方便
};
template<class V, class KeyOfValue, class HF = HashFunc<V>>
class HashBucket
{
template <class V, class Ref, class Ptr, class KeyOfValue, class HF>
friend struct HBIterator;
typedef HashBucketNode<V> Node;
typedef Node* PNode;
typedef HashBucket<V, HF> Self;
public:
typedef HBIterator<V, V&, V*, KeyOfValue, HF> iterator;
typedef HBIterator<V, const V&, const V*, KeyOfValue, HF> const_iterator;
HashBucket(size_t capacity = 16)
: _table(capacity)
, _size(0)
{}
~HashBucket()
{
clear();
}
//插入函数
pair<iterator, bool> insert(const V& data)
{
//是否扩容
CheckCapacity();
//查找是否存在相同键值数据存在
KeyOfValue kov;
size_t pos = HashFunc(data);
Node* cur = _table[pos];
while (cur)
{
if (kov(cur->_data) == kov(data))
return { {cur,this}, false }; //若键值存在, 返回节点的迭代器和 false;
cur = cur->_pNext;
}
//头插节点
Node* new_node = new Node(data);
new_node->_pNext = _table[pos];
_table[pos] = new_node;
_size++;
return { {new_node,this}, true };
}
//删除函数
iterator erase(const V& data)
{
KeyOfValue kov;
size_t pos = HashFunc(data); //找到键值的映射地址
Node* cur = _table[pos]; //获取哈希桶的第一个节点
Node* prev = nullptr;
while (cur)
{
if (kov(cur->_data) == kov(data)) //若哈希桶中存在需要删除的节点
{
iterator ret(cur, this);
++ret; //这里偷懒直接调用的迭代器的 operator++
if (!prev) //若删除节点为哈希桶的第一个节点
_table[pos] = cur->_pNext;
else //若不为第一个节点
prev->_pNext = cur->_pNext;
delete cur; //释放节点
_size--;
return ret;
}
prev = cur;
cur = cur->_pNext;
}
return { nullptr, this }; //若没有需要删除的节点, 返回 end() 的迭代器
}
//查找函数
iterator find(const V& data)
{
KeyOfValue kov;
size_t pos = HashFunc(data);
Node* cur = _table[pos];
while (cur)
{
if (kov(cur->_data) == kov(data))
return {cur, this};
cur = cur->_pNext;
}
return { nullptr, this };
}
//迭代器
iterator begin()
{
for (int i = 0; i < _table.capacity(); i++) //查找第一个元素
if (_table[i])
return {_table[i], this}; //this 为哈希表对象的指针
return {nullptr, this};
}
iterator end()
{
return {nullptr, this}; //this 为哈希表对象的指针
}
void clear()
{
for (auto it = begin(); it != end();)
it = erase(*it);
_size = 0;
}
size_t size()//const
{
return _size;
}
bool empty()//const
{
return 0 == _size;
}
size_t bucket_count()// const
{
return _table.capacity();
}
size_t bucket_size(const V& data)// const
{
size_t pos = HashFunc(data);
Node* cur = _table[pos];
size_t count = 0;
while (cur) cur = cur->_pNext, count++;
return count;
}
void swap(Self& ht)
{
_table.swap(ht._table);
std::swap(_size, ht._size);
}
private:
size_t HashFunc(const V& data)// const
{
return HF()(KeyOfValue()(data)) % _table.capacity();
}
//是否扩容
void CheckCapacity()
{
if (_size == _table.capacity()) //若哈希表中的有效元素等于哈希桶的数量就扩容
{
KeyOfValue kov;
vector<Node*> new_table(_size * 2); //创建新表
for (int i = 0; i < _table.capacity(); i++) //依次从旧表中转移节点至新表
{
Node* cur = _table[i];
while (cur)
{
_table[i] = cur->_pNext;
//旧表节点转移至新表
size_t pos = HF()(kov(cur->_data)) % new_table.capacity();
cur->_pNext = new_table[pos];
new_table[pos] = cur;
cur = _table[i];
}
}
_table.swap(new_table);
}
}
private:
vector<Node*> _table;
size_t _size; // 哈希表中有效元素的个数
};