文章目录
- 1.何为哈希?
- 1.1百度搜索
- 1.2自身理解
- 1.3哈希方法/散列方法
- 1.4哈希冲突/哈希碰撞
- 1.5如何解决?
- 哈希函数的设计
- 2.闭散列和开散列
- 2.1闭散列/开放定址法
- 2.2开散列/链地址法/开链法
- 1.概念
- 2.容量问题
- 3.代码实现[配备详细注释]
- 3.1闭散列
- 3.2开散列
1.何为哈希?
1.1百度搜索
1.2自身理解
顺序结构以及平衡树中,元素关键码与其存储位置之间没有对应关系
在查找一个元素时,必须要经过key的多次比较。
顺序查找时间复杂度为O(N),平衡树中为树的高度,即O(logN).
有没有这样一种方法 不经过任何比较 直接从表中得到要查找的元素。
大佬神作: 构造一种存储结构,通过某种函数使元素的存储位置与key之间建立一一映射的关系,在查找时通过该函数找到该元素.
1.3哈希方法/散列方法
插入元素:
将待插入元素的key,以某个函数[哈希函数/散列函数]计算出该元素的存储位置并按此位置存放,构造出一个结构[哈希表/散列表]
搜索元素:
对元素的key进行同样的计算,求得的函数值即为元素的存储位置,在结构中按此位置取元素比较,若关键码相等,则搜索成功
1.4哈希冲突/哈希碰撞
- 不同关键码通过哈希函数计算出相同的哈希地址,该种现象称为哈希冲突或哈希碰撞.
- 把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”。
1.5如何解决?
哈希函数的设计
设计一个合理的哈希函数
哈希函数设计原则:
- 简单方便
- 哈希函数要使得关键码均分分布
- 定义域为所有key 值域为[0, n)
常见哈希函数
- 直接定址法–(常用)
取关键字的某个线性函数为散列地址:Hashi(Key)= A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况 否则导致—数据量小 但是需要的空间极大 例如 :数据-1 2 3 9999下标: 1 2 3 9999
使用场景:数值小且分布集中 - 除留余数法–(常用)
设散列表中允许地址数为n,除数p的取值规则: 小于等于n 接近/等于n的质数
哈希函数:Hashi(key) = key % p(p <= n) - 平方取中法–(了解)
假设关键码为6392,它的平方为40857664,抽取中间的3位857或576作为hashi;
使用场景:不知道关键码的分布 位数不是很大 - 折叠法–(了解)
将关键码从左到右分割成位数近似相等的几部分 将这几部分叠加求和
按散列表表长 取后几位作为hashi
使用场景:无所谓关键码的分布 位数较大 - 随机数法–(了解)
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即Hashi(key) = random(key)
使用场景:关键字长度不等 - 数学分析法–(了解)
假设关键字为某一地区的手机号,大部分前几位都相同的 取后面的四位作为hashi
还出现冲突–对抽取数字进行反转(如1234改成4321)、右环移位(如1234改成4123)、左环移位、前两数与后两数叠加(如1234改成12+34=46)等.
使用场景:关键字位数比较大 事先知道关键字的分布 关键字的若干位分布较均匀
== 注意:哈希冲突只可缓解 不可避免 ==
2.闭散列和开散列
2.1闭散列/开放定址法
当发生哈希冲突时,如果哈希表未被装满,把key存放到冲突位置的==“下一个” ==空位置
寻找空位置:
- 线性探测
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置
插入:
- 通过哈希函数获取待插入元素在哈希表中的位置
- 该位置没有元素直接插入新元素
- 该位置中有元素 使用线性探测找到下一个空位置 插入新元素
删除:
- 线性探测采用标记的伪删除法来删除一个元素
- 若直接删除 会影响其他元素的搜索
例如上图: 删除对象是33 直接删除 hash6 这个位置应该怎么办? 需要搞一个东西让哈希表的使用者知道这里原来有元素 现在被删除了 置成null? 如果置成空 当想要查找 1002/43即33后面的元素时 遇到hash6 使用者被告知这里是空 停止查找 此时使用者得到的信息是 哈希表中并不存在此元素 这与现实违背 那怎么办? 答案是置成"删除"状态 使得使用者知道何为空何为删除 这就是伪标记删除法
负载因子
哈希表的负载因子定义为: α = 表中的元素个数[ _n ] / 哈希表长[ _tables.size() ]
负载因子a: 哈希表装满程度的标志因子
表长是定值,α与_n成正比
α越大 填入表中的元素越多 哈希冲突可能性越大
α越小 填入表中的元素越少 哈希冲突可能性越小
哈希表的平均查找长度是负载因子α的函数 处理冲突方法不同函数不同
负载因子越大,冲突的概率越高,查找效率越低,空间利用率越高
负载因子越小,冲突的概率越低,查找效率越高,空间利用率越低
容量问题: 1.size是实际能够访问数据的范围 2.capacity是存储数据的空间大小
优点:
实现简单
缺点;
x占据y的位置 y就得放到y+1的位置
冲突累计 产生数据堆积
本意是要减缓哈希冲突
虽然使得有相同hashi的不同数据有位置存放
但是数据堆积时 会使得寻找某关键码的位置需要许多次比较
导致搜索效率降低。
- 二次探测
线性探测寻找空位置的方法[逐个后移]导致线性探测的缺陷[产生冲突的数据堆积]
修改探测的方法:
2.2开散列/链地址法/开链法
1.概念
对关键码用哈希函数计算哈希地址,具有相同地址的不同关键码归于同一子集合,每一个子集合称为一个桶各个桶中的元素通过一个单链表链接起来,各链表的头结点存储在哈希表中。
2.容量问题
桶的个数有限[即哈希表的表长有限]
随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容
某种情况下 每个哈希桶中刚好挂一个节点, 再继续插入元素时,每一次都会发生哈希冲突,我们假定在元素个数刚好等于桶的个数时,进行扩容 即
需要了解到是 我们最然控制α为1时进行扩容 并不代表此时哈希桶都挂了一个结点 更为普遍的情况是 一些桶为空 一些桶有许多结点 只不过结点总个数为哈希表长度大小
3.代码实现[配备详细注释]
3.1闭散列
//闭散列/开放地址法
namespace OpenAddressing
{
//状态枚举
enum State
{
EMPTY,
EXIST,
DELETE
};
//哈希数据元素
template<class K, class V>
struct HashData
{
pair<K, V> _pair;
State _state = EMPTY;
};
//哈希表
template<class K, class V>
class HashTable
{
public:
//插入函数
bool Insert(const pair<K, V>& pair)
{
//值已存在 插入错误
if (Find(pair.first))
return false;
//负载因子/荷载系数 -- Load_Factor = _n / _tables.size();
//(double)_n / (double)_tables.size() >= 0.7
//_n * 10 / _tables.size() >= 7
//使得扩容发生的条件: size为0 负载因子达到阈值
if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
{
/ 低级代码 /
/*
//先更新size 由size作为参数扩容 解决只改容量 不更新访问范围的问题
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
//调用vector的有参构造[有参构造里调用reserve] 创建一个新表
vector<HashData<K,V>> newtables(newsize);
//遍历旧表 由哈希函数更新数据位置
for (auto& e : _tables)
{
if (e._state == EXIST)
{
//哈希函数计算出的下标
size_t hashi = pair.first % newtables.size();
//重新线性探测
size_t index = hashi;//index代替hashi进行操作 保留原始hashi的值不变
for (size_t i = 1; newtables[index]._state == EXIST; ++i)
{
index = hashi + i; //从原始下标不断 +1 +2 +3 ...
index %= newtables.size();//防止越界 只在表内定位index
}
//将数据放入合适位置
newtables[index]._pair = e._pair;
newtables[index]._state = EXIST;
}
}
//新表的数据才是我们想要的数据 交换后 newtables中存放的变为旧数据
//newtables是个局部变量 让其"自生自灭"
_tables.swap(newtables);
*/
// 高级代码 //
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable<K, V> other;
other._tables.resize(newsize);
for (auto& e : _tables)
{
if (e._state == EXIST)
other.Insert(e._pair);
}
_tables.swap(other._tables);
/
//以上高级代码实际是对下面的线性探测进行了复用
}
//哈希函数计算出的下标
size_t hashi = pair.first % _tables.size();
// 线性探测
size_t index = hashi;//index代替hashi进行操作 保留原始hashi的值不变
for (size_t i = 1; _tables[index]._state == EXIST; ++i)
{
index = hashi + i; //从原始下标不断 +1 +2 +3 ...
index %= _tables.size();//防止越界 只在表内定位index
}
//将数据放入合适位置
_tables[index]._pair = pair;
_tables[index]._state = EXIST;
_n++; //数据个数++
return true;
}
//查找函数
HashData<K, V>* Find(const K& key)
{
//哈希表为空 返回nullptr
if (_tables.size() == 0)
return nullptr;
//哈希函数计算出的下标
size_t hashi = key % _tables.size();
// 线性探测
size_t index = hashi;
for (size_t i = 1; _tables[index]._state != EMPTY; ++i)
{
//obj是key的前提是obj存在
if (_tables[index]._state == EXIST
&& _tables[index]._pair.first == key)
{
return &_tables[index];
}
index = hashi + i;
index %= _tables.size();
//当表中元素状态非exist即delete时
//for循环判断条件一直为真 死循环
//解决: 当找了一圈还未找到
//表中无此key 返回false
if (index == hashi)
break;
}
return nullptr;
}
//删除函数
bool Erase(const K& key)
{
HashData<K, V>* pos = Find(key);
if (pos)
{
pos->_state = DELETE;
--_n; //虽然已标记删除 仍然要使数据个数减减 防止有用数据未达到阈值就执行扩容
return true;
}
else
return false;
}
private:
vector<HashData<K, V>> _tables;
size_t _n = 0;//存储的数据个数
};
// 测试函数 ///
void TestHashTable()
{
int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
HashTable<int, int> ht;
//插入
for (auto& e : a)
ht.Insert(make_pair(e, e));
//插入第8个数据 达到阈值 测试扩容
ht.Insert(make_pair(15, 15));
//查找 + 删除
int tmp = 12;
if (ht.Find(tmp))
cout << tmp << "在" << endl;
else
cout << tmp << "不在" << endl;
ht.Erase(tmp);
if (ht.Find(tmp))
cout << tmp << "在" << endl;
else
cout << tmp << "不在" << endl;
}
}
3.2开散列
//开散列/链地址法
namespace ChainAddressing
{
//结点类
template<class K, class V>
struct HashNode
{
HashNode<K, V>* _next;
pair<K, V> _pair;
HashNode(const pair<K, V>& pair)
:_next(nullptr)
, _pair(pair)
{
}
};
//哈希表类
template<class K, class V>
class HashTable
{
typedef HashNode<K, V> Node;
public:
//析构函数
~HashTable()
{
for (auto& ptr : _tables)
{
while (ptr)
{
//记录下一个结点
Node* next = ptr->_next;
//释放当前结点
delete ptr;
//更新ptr
ptr = next;
}
ptr = nullptr;
}
}
//查找函数
Node* Find(const K& key)
{
//为空不查找 返回nullptr
if (_tables.size() == 0)
return nullptr;
//哈希函数计算的下标
size_t hashi = key % _tables.size();
//首先得到表里的指针 即相当于每一个桶的头指针
//[实际上 每一个桶就是一个链表 表中的ptr是每一个链表的哨兵指针]
Node* ptr = _tables[hashi];
while (ptr)
{
if (ptr->_pair.first == key)
return ptr;
ptr = ptr->_next;
}
return nullptr;
}
//删除函数
bool Erase(const K& key)
{
//哈希函数计算的下标
size_t hashi = key % _tables.size();
//首先得到表里的指针 即相当于每一个桶的头指针
//[实际上 每一个桶就是一个链表 表中的ptr是每一个链表的哨兵指针]
Node* ptr = _tables[hashi];
Node* prv = nullptr;
while (ptr)
{
//当前值为目标值 执行删除操作
if (ptr->_pair.first == key)
{
if (prv == nullptr)
_tables[hashi] = ptr->_next;
else
prv->_next = ptr->_next;
delete ptr;
return true;
}
//当前值不为目标值 继续向下遍历
else
{
prv = ptr;
ptr = ptr->_next;
}
}
return false;
}
//插入函数
bool Insert(const pair<K, V>& pair)
{
//表中已有 返回false
if (Find(pair.first))
return false;
//负载因子/荷载系数 -- Load_Factor = _n / _tables.size();
//负载因子 == 1时扩容
if (_n == _tables.size())
{
/// 高级代码1.0 /
/*
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable<K, V> newht;
newht.resize(newsize);
for (auto& ptr : _tables)
{
while (ptr)
{
newht.Insert(ptr->_pair);
ptr = ptr->_next;
}
}
_tables.swap(newht._tables);
*/
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newtables(newsize, nullptr);
//遍历旧表 取出旧表里每一个指针
for (auto& ptr : _tables)
{
//ptr是旧表里存储的每一个指针
//它指向当前哈希桶的首结点 即他存储的是首结点的地址
while (ptr)
{
//算出 当前结点根据新哈希函数计算的新下标
size_t hashi = ptr->_pair.first % newtables.size();
//ptr是首结点的地址 ptr->_next为第二个结点的地址
Node* next = ptr->_next;
// 头插到新表
ptr->_next = newtables[hashi];
newtables[hashi] = ptr;
//更新ptr 即向下遍历
ptr = next;
}
}
_tables.swap(newtables);
}
//哈希函数计算出的下标
size_t hashi = pair.first % _tables.size();
//链表头插
Node* newnode = new Node(pair);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
private:
vector<Node*> _tables; // 指针数组
size_t _n = 0; // 存储有效数据个数
};
测试函数 //
void TestHashTable1()
{
int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
HashTable<int, int> ht;
for (auto& e : a)
{
ht.Insert(make_pair(e, e));
}
ht.Insert(make_pair(15, 15));
ht.Insert(make_pair(25, 25));
ht.Insert(make_pair(35, 35));
ht.Insert(make_pair(45, 45));
}
void TestHashTable2()
{
int a[] = { 3, 33, 2, 13, 5, 12, 1002 };
HashTable<int, int> ht;
for (auto& e : a)
{
ht.Insert(make_pair(e, e));
}
ht.Erase(12);
ht.Erase(3);
ht.Erase(33);
}
}