哈希
哈希表(Hash Table),也称为散列表,是一种常用的数据结构,用于实现键值对的存储和查找。它通过将键映射到一个索引位置来快速地访问和操作数据。
哈希表的基本思想是使用一个哈希函数将键映射到一个固定范围的整数,然后将这个整数作为索引来访问数组中的元素。这个数组通常被称为哈希表或哈希桶。当多个键映射到相同的索引位置时,称为哈希冲突,常用的解决冲突的方法有链地址法和开放地址法。
哈希表的优点包括:
- 快速的查找和插入操作:通过哈希函数将键映射到索引位置,可以在常数时间内进行查找和插入操作。
- 空间效率高:哈希表只需要存储键和值,不需要额外的空间来维护顺序。
- 灵活性:哈希表可以存储任意类型的键值对,只要能够定义哈希函数和相等比较函数。
然而,哈希表也存在一些缺点:
- 哈希函数的选择:选择一个好的哈希函数对于哈希表的性能至关重要,一个不好的哈希函数可能导致哈希冲突增多,影响性能。
- 内存消耗:哈希表需要预先分配一定大小的数组来存储数据,如果数据量较大,可能会占用较多的内存空间。
- 迭代顺序不确定:哈希表中的元素存储位置是根据哈希函数计算得到的,因此迭代哈希表的顺序是不确定的。
在C++中,可以使用std::unordered_map和std::unordered_set来实现哈希表。它们提供了高效的查找、插入和删除操作,并且可以存储任意类型的键值对或唯一元素。
哈希表的对应存储方式
哈希表的存储方式通常是通过数组来实现的,这个数组通常被称为哈希表或哈希桶。每个桶存储一个链表或者其他数据结构,用于解决哈希冲突。
具体来说,哈希表的存储方式可以分为以下几种:
- 链地址法(Separate Chaining):每个桶存储一个链表,当多个键映射到相同的索引位置时,将它们存储在同一个链表中。通过链表的方式解决了哈希冲突,可以存储任意数量的键值对。当链表过长时,可以考虑将链表转换为其他数据结构,如红黑树,以提高查找效率。
- 开放地址法(Open Addressing):当发生哈希冲突时,通过一定的规则找到下一个可用的空桶来存储冲突的键值对。常见的开放地址法包括线性探测、二次探测和双重哈希等。开放地址法的优点是节省了链表的空间开销,但当哈希表装载因子较高时,可能会导致查找效率下降。
- 建立公共溢出区(Overflow Area):当发生哈希冲突时,将冲突的键值对存储在一个公共的溢出区中,而不是在桶中。这样可以避免链表的使用,但需要额外的空间来存储溢出区。
无论使用哪种存储方式,哈希表的基本原理是通过哈希函数将键映射到索引位置,然后根据存储方式来处理哈希冲突。这样可以实现快速的查找、插入和删除操作,并且具有较低的冲突率和高效的空间利用率。
开放地址法(Open Addressing)是一种解决哈希冲突的方法,在发生冲突时,通过一定的规则找到下一个可用的空桶来存储冲突的键值对。开放地址法的核心思想是,当发生冲突时,不使用链表等数据结构来存储冲突的元素,而是将其存储在其他的空桶中。具体的规则可以包括线性探测、二次探测、双重哈希等。例如,线性探测是指当发生冲突时,顺序地检查下一个桶,直到找到一个空桶来存储冲突的元素。
负载因子(Load Factor)是指哈希表中已经存储的键值对数量与哈希表容量的比值。负载因子可以用来衡量哈希表的装载程度。通常情况下,负载因子越大,表示哈希表中存储的键值对越多,装载程度越高。负载因子的计算公式为:
负载因子 = 已存储的键值对数量 / 哈希表容量
负载因子的选择对哈希表的性能有影响。当负载因子较小时,哈希表中的空桶较多,查找、插入和删除操作的效率较高。但随着负载因子的增加,哈希冲突的概率也会增加,可能导致查找、插入和删除操作的效率下降。因此,合适的负载因子选择是重要的,一般来说,负载因子的取值范围为0.7到0.8之间,可以在空间利用率和性能之间做出平衡。当负载因子超过某个阈值时,可以考虑进行哈希表的扩容,以保持较低的负载因子。
解决哈希冲突
闭散列(Closed Hashing)和开散列(Open Hashing)是两种不同的解决哈希冲突的方法。
闭散列,也称为封闭寻址法(Closed Addressing),是指当发生哈希冲突时,将冲突的键值对存储在哈希表的同一个桶中,通常使用链表或其他数据结构来存储冲突的元素。当需要查找、插入或删除一个键值对时,首先计算出它的哈希值,然后在对应的桶中查找或操作。闭散列的优点是可以存储任意数量的键值对,但当链表过长时,可能会导致查找效率下降。
开散列,也称为开放寻址法(Open Addressing),是指当发生哈希冲突时,通过一定的规则找到下一个可用的空桶来存储冲突的键值对。开散列的核心思想是,当发生冲突时,不使用链表等数据结构来存储冲突的元素,而是将其存储在其他的空桶中。具体的规则可以包括线性探测、二次探测、双重哈希等。开散列的优点是节省了链表的空间开销,但当哈希表装载因子较高时,可能会导致查找效率下降。
解决哈希冲突的常用方法包括:
-
链地址法(Chaining):当发生哈希冲突时,将冲突的键值对存储在同一个桶中,通过链表或其他数据结构连接冲突的元素。
-
开放地址法(Open Addressing):当发生哈希冲突时,通过一定的规则找到下一个可用的空桶来存储冲突的键值对。常见的开放地址法包括线性探测、二次探测、双重哈希等。
-
再哈希法(Rehashing):当发生哈希冲突时,使用另一个哈希函数计算出一个新的哈希值,然后将冲突的键值对存储在新的位置上。
-
建立公共溢出区(Overflow Area):当发生哈希冲突时,将冲突的键值对存储在一个公共的溢出区中,通过其他方式来记录溢出区的元素。
-
负载因子调整和扩容:通过调整负载因子的大小,或者在负载因子超过一定阈值时进行哈希表的扩容,以减少哈希冲突的概率。
这些方法各有优缺点,适用于不同的场景和需求。选择合适的解决方法需要考虑哈希表的装载因子、数据分布情况、性能要求等因素。
#pragma once
#include <vector>
namespace OpenAddress
{
enum State
{
EMPTY,
EXIST,
DELETE
};
template<class K, class V>
struct HashData
{
pair<K, V> _kv;
State _state = EMPTY;
};
template<class K, class V>
class HashTable
{
public:
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
return false;
// 负载因子超过0.7就扩容
//if ((double)_n / (double)_tables.size() >= 0.7)
if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
{
//size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
//vector<HashData> newtables(newsize);
遍历旧表,重新映射到新表
//for (auto& data : _tables)
//{
// if (data._state == EXIST)
// {
// // 重新算在新表的位置
// size_t i = 1;
// size_t index = hashi;
// while (newtables[index]._state == EXIST)
// {
// index = hashi + i;
// index %= newtables.size();
// ++i;
// }
// newtables[index]._kv = data._kv;
// newtables[index]._state = EXIST;
// }
//}
//_tables.swap(newtables);
// 10:34继续
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
HashTable<K, V> newht;
newht._tables.resize(newsize);
// 遍历旧表,重新映射到新表
for (auto& data : _tables)
{
if (data._state == EXIST)
{
newht.Insert(data._kv);
}
}
_tables.swap(newht._tables);
}
size_t hashi = kv.first % _tables.size();
// 线性探测
size_t i = 1;
size_t index = hashi;
while (_tables[index]._state == EXIST)
{
index = hashi + i;
index %= _tables.size();
++i;
}
_tables[index]._kv = kv;
_tables[index]._state = EXIST;
_n++;
return true;
}
HashData<K, V>* Find(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
size_t hashi = key % _tables.size();
// 线性探测
size_t i = 1;
size_t index = hashi;
while (_tables[index]._state != EMPTY)
{
if (_tables[index]._state == EXIST
&& _tables[index]._kv.first == key)
{
return &_tables[index];
}
index = hashi + i;
index %= _tables.size();
++i;
// 如果已经查找一圈,那么说明全是存在+删除
if (index == hashi)
{
break;
}
}
return nullptr;
}
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret)
{
ret->_state = DELETE;
--_n;
return true;
}
else
{
return false;
}
}
private:
vector<HashData<K, V>> _tables;
size_t _n = 0; // 存储的数据个数
//HashData* tables;
//size_t _size;
//size_t _capacity;
};
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));
if (ht.Find(13))
{
cout << "13在" << endl;
}
else
{
cout << "13不在" << endl;
}
ht.Erase(13);
if (ht.Find(13))
{
cout << "13在" << endl;
}
else
{
cout << "13不在" << endl;
}
}
}
namespace HashBucket
{
template<class K, class V>
struct HashNode
{
HashNode<K, V>* _next;
pair<K, V> _kv;
HashNode(const pair<K, V>& kv)
:_next(nullptr)
, _kv(kv)
{}
};
template<class K, class V>
class HashTable
{
typedef HashNode<K, V> Node;
public:
~HashTable()
{
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
cur = nullptr;
}
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
return nullptr;
size_t hashi = 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)
{
size_t hashi = key % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (cur->_kv.first == key)
{
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
return true;
}
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
bool Insert(const pair<K, V>& kv)
{
if (Find(kv.first))
{
return false;
}
// 负载因因子==1时扩容
if (_n == _tables.size())
{
/*size_t newsize = _tables.size() == 0 ? 10 : _tables.size()*2;
HashTable<K, V> newht;
newht.resize(newsize);
for (auto cur : _tables)
{
while (cur)
{
newht.Insert(cur->_kv);
cur = cur->_next;
}
}
_tables.swap(newht._tables);*/
size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
vector<Node*> newtables(newsize, nullptr);
//for (Node*& cur : _tables)
for (auto& cur : _tables)
{
while (cur)
{
Node* next = cur->_next;
size_t hashi = cur->_kv.first % newtables.size();
// 头插到新表
cur->_next = newtables[hashi];
newtables[hashi] = cur;
cur = next;
}
}
_tables.swap(newtables);
}
size_t hashi = kv.first % _tables.size();
// 头插
Node* newnode = new Node(kv);
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);
}
}
#include <iostream>
#include <string>
#include <unordered_set>
#include <unordered_map>
#include <map>
#include <set>
#include <vector>
#include <time.h>
using namespace std;
void test_unordered_set1()
{
unordered_set<int> s;
s.insert(1);
s.insert(3);
s.insert(2);
s.insert(7);
s.insert(2);
unordered_set<int>::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
++it;
}
cout << endl;
for (auto e : s)
{
cout << e << " ";
}
cout << endl;
}
void test_unordered_map()
{
string arr[] = { "", "", "ƻ", "", "ƻ", "ƻ", "", "ƻ", "㽶", "ƻ", "㽶", "" };
map<string, int> countMap;
for (auto& e : arr)
{
countMap[e]++;
}
for (auto& kv : countMap)
{
cout << kv.first << ":" << kv.second << endl;
}
}
//
//int main()
//{
// test_unordered_set1();
// test_unordered_map();
//
// return 0;
//}
//int main()
//{
// const size_t N = 1000000;
//
// unordered_set<int> us;
// set<int> s;
//
// vector<int> v;
// v.reserve(N);
// srand(time(0));
// for (size_t i = 0; i < N; ++i)
// {
// v.push_back(rand());
// //v.push_back(rand()+i);
// //v.push_back(i);
// }
//
// size_t begin1 = clock();
// for (auto e : v)
// {
// s.insert(e);
// }
// size_t end1 = clock();
// cout << "set insert:" << end1 - begin1 << endl;
//
// size_t begin2 = clock();
// for (auto e : v)
// {
// us.insert(e);
// }
// size_t end2 = clock();
// cout << "unordered_set insert:" << end2 - begin2 << endl;
//
//
// size_t begin3 = clock();
// for (auto e : v)
// {
// s.find(e);
// }
// size_t end3 = clock();
// cout << "set find:" << end3 - begin3 << endl;
//
// size_t begin4 = clock();
// for (auto e : v)
// {
// us.find(e);
// }
// size_t end4 = clock();
// cout << "unordered_set find:" << end4 - begin4 << endl << endl;
//
// cout << s.size() << endl;
// cout << us.size() << endl << endl;;
//
// size_t begin5 = clock();
// for (auto e : v)
// {
// s.erase(e);
// }
// size_t end5 = clock();
// cout << "set erase:" << end5 - begin5 << endl;
//
// size_t begin6 = clock();
// for (auto e : v)
// {
// us.erase(e);
// }
// size_t end6 = clock();
// cout << "unordered_set erase:" << end6 - begin6 << endl << endl;
//
// return 0;
//}
#include "HashTable.h"
int main()
{
HashBucket::TestHashTable2();
return 0;
}