作者主页: 作者主页
本篇博客专栏:C++
创作时间 :2024年8月6日
前言:
在计算机科学的广袤世界中,数据结构犹如基石,支撑着各种高效算法的构建与运行。而哈希表(Hash Table),作为其中一颗璀璨的明珠,以其独特的魅力和卓越的性能,在众多数据存储和检索场景中大放异彩。
哈希表,这个看似神秘却又充满力量的概念,其实与我们的日常生活息息相关。想象一下,当您在搜索引擎中输入关键词,瞬间就能获取到海量相关信息;当您在电商平台上浏览商品,系统能够迅速为您推荐符合您喜好的物品。这背后,哈希表都在默默发挥着关键作用。
哈希表的神奇之处在于它能够在平均情况下以接近常数的时间复杂度完成数据的插入、查找和删除操作,这使得它在处理大规模数据时具有极高的效率。然而,哈希表并非完美无缺,其也面临着哈希冲突、负载因子调整等一系列挑战。
在接下来的博客中,我们将深入探索哈希表的内部原理,剖析其工作机制,探讨如何优化哈希函数以减少冲突,研究不同的冲突解决策略,以及了解哈希表在实际编程中的广泛应用。无论您是初涉编程领域的新手,还是经验丰富的开发者,相信通过对哈希表的深入了解,都将为您的编程技能增添新的利器,让您在解决实际问题时更加游刃有余。
让我们一同踏上这段充满挑战与惊喜的哈希表之旅,揭开其神秘的面纱,领略数据结构的无穷魅力!
一、unordered系列关联式容器
1.1unordered_map
unordered_map在线文档说明
1.2unordered_set
unordered_set在线文档说明
这里介绍的两个unordered系列的关联式容器和map和set还是有点相似的,我们再来几道题目来熟练掌握它们的使用
重复n次的元素
两个数组的交集
二、底层结构
哈希概念:
可以不经过任何比较,一次直接从表中得到要搜索的元素。如果构造一种存储结构,通过某种函数(hashFunc)使元素的存储位置与它的关键码之间能够建立一一映射的关系,那么在查找时通过该函数可以很快找到该元素,这就是最理想的搜索方法
注意:哈希方法中使用的转换函数称为哈希(散列)函数,构造出来的结构称为哈希表(Hash Table)(或者称散列表)
用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快,但是有成千上万的数,总会有几个数,取余后相等,那我们该怎么存放值呢?
hash(5) = 5 % 10 = 5;
hash(55) = 55 % 10 = 5;
这时就要引入一个新的概念 -> 哈希冲突
哈希冲突的概念:
我们把把具有不同关键码而具有相同哈希地址的数据元素称为“同义词”
注意:哈希函数的设计目标是尽量减少冲突,但完全避免冲突几乎是不可能的
哈希函数:
哈希函数设计原则
哈希冲突解决
闭散列:
闭散列: 也叫开放定址法,当发生哈希冲突时,如果哈希表未被装满,说明在哈希表中必然还有空位置,那么可以把key存放到冲突位置中的“下一个” 空位置中去
线性探测
如果和上面讲的一样,现在需要插入元素55,先通过哈希函数计算哈希地址,hashAddr为5,
因此55理论上应该插在该位置,但是该位置已经放了值为5的元素,即发生哈希冲突
线性探测的实现:
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
Status _s;
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
public:
HashTable()
{
_tables.resize(10);
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
{
return false;
}
// 负载因子 -> 哈希表扩容
if (_n * 10 / _tables.size() == 7)
{
size_t newSize = _tables.size() * 2;
HashTable<K, V, Hash> newHT;
newHT._tables.resize(newSize);
// 遍历旧表
for (size_t i = 0; i < _tables.size(); i++)
{
if(_tables[i]._s == EXIST)
{
// 复用Insert
newHT.Insert(_tables[i]._kv);
}
}
// 交换两个表的数据
_tables.swap(newHT._tables);
}
Hash hf;
// 线性探测
size_t hashi = hf(kv.first) % _tables.size();
while (_tables[hashi]._s == EXIST)
{
hashi++;
hashi %= _tables.size();
}
_tables[hashi]._kv = kv;
_tables[hashi]._s = EXIST;
++_n;
return true;
}
HashData<K, V>* Find(const K& key)
{
Hash hf;
size_t hashi = hf(key) % _tables.size();
while (_tables[hashi]._s != EMPTY)
{
if (_tables[hashi]._kv.first == key)
{
return &_tables[hashi];
}
hashi++;
hashi %= _tables.size();
}
return NULL;
}
// 伪删除法
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_s = DELETE;
--_n;
return true;
}
else
{
return false;
}
}
private:
vector < HashData<K, V>> _tables;
size_t _n = 0; // 储存的关键字数据的个数
};
关于哈希表的取余
当我们的key不是整形的时候(常见的是string
),我们该怎么计算它的hashi
? 这里又得依靠我们的仿函数HashFunc
,又因为我们string
也是很常见的,我们将模板特化一下
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct HashFunc<string>
{
size_t operator()(const string& key)
{
size_t hash = 0;
for (auto e : key)
{
// 避免因为顺序不一样而产生一样的值 BKDR
// 避免 abc,acb同值不同意
e *= 31;
hash += e;
}
return hash;
}
};
- 线性探测优点:实现非常简单
- 线性探测缺点:一旦发生哈希冲突,所有的冲突连在一起,容易产生数据“堆积”,即:不同关键码占据了可利用的空位置,使得寻找某关键码的位置需要许多次比较,导致搜索效率降低
开散列
注意:开散列中每个桶中放的都是发生哈希冲突的元素
开散列实现
template<class K, class V>
struct HashNode
{
HashNode* _next;
pair<K, V> _kv;
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()
{
_tables.resize(10);
}
~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;
}
}
bool Insert(const pair<K, V>& kv)
{
Hash hf;
if (Find(kv.first))
{
return false;
}
// 负载因子
if (_n == _tables.size())
{
vector<Node*> newTables;
newTables.resize(_tables.size() * 2);
// 遍历旧表
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
// 挪动到新表
size_t hashi = hf(cur->_data) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t hashi = hf(kv.first) % _tables.size();
Node* newnode = new Node(kv);
// 头插
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return true;
}
Node* Find(const K& key)
{
Hash hf;
size_t hashi = hf(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)
{
Hash hf;
size_t hashi = hf(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr; // 记录上一个节点
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _tables;
size_t _n = 0;
};
开散列增容:
if (_n == _tables.size())
{
vector<Node*> newTables;
newTables.resize(_tables.size() * 2);
// 遍历旧表
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
// 挪动到新表
size_t hashi = hf(cur->_data) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
开散列与闭散列比较:
最后:
十分感谢你可以耐着性子把它读完和我可以坚持写到这里,送几句话,对你,也对我:
1.一个冷知识:
屏蔽力是一个人最顶级的能力,任何消耗你的人和事,多看一眼都是你的不对。
2.你不用变得很外向,内向挺好的,但需要你发言的时候,一定要勇敢。
正所谓:君子可内敛不可懦弱,面不公可起而论之。
3.成年人的世界,只筛选,不教育。
4.自律不是6点起床,7点准时学习,而是不管别人怎么说怎么看,你也会坚持去做,绝不打乱自己的节奏,是一种自我的恒心。
5.你开始炫耀自己,往往都是灾难的开始,就像老子在《道德经》里写到:光而不耀,静水流深。
最后如果觉得我写的还不错,请不要忘记点赞✌,收藏✌,加关注✌哦(。・ω・。)
愿我们一起加油,奔向更美好的未来,愿我们从懵懵懂懂的一枚菜鸟逐渐成为大佬。加油,为自己点赞!