发布时间:2024-01-28 19:00
之前已经学过哈希表的概念,哈希函数,哈希冲突等相关知识,如果忘记了可以回顾一下—》了解什么是哈希表
本次我们就简单的实现一下哈希表。哈希函数将采用除留余数法,用开放定址法(一次探测和二次探测)和链地址法处理哈希冲突,话不多说,直接上代码:
template<class K>
struct Hash
{
size_t operator()(const K& key)
{
return key;
}
};
//模板的偏特化
//如果key为字符串,我们就采用这种方式计算哈希地址,从而减少哈希冲突
template<>
struct Hash<string>
{
size_t operator()(const string& s)
{
//BKDR哈希
size_t value = 0;
for (auto ch : s)
{
value *= 31;
value += ch;
}
return value;
}
};
//表长不为素数,二次探测不一定总是能找到一个不发生哈希冲突的地址
//所以将其改为素数扩容,就能解决此问题
size_t GetNextPrime(size_t num)
{
static const unsigned long prime[28] =
{
53, 97, 193, 389, 769,
1543, 3079, 6151, 12289, 24593,
49157, 98317, 196613, 393241, 786433,
1572869, 3145739, 6291469, 12582917, 25165843,
50331653, 100663319, 201326611, 402653189, 805306457,
1610612741, 3221225473, 4294967291
};
for (size_t i = 0; i < 28; ++i)
{
if (prime[i] > num)
{
return prime[i];
}
}
return prime[27];
}
//开放定址法
namespace CloseHash
{
//3总状态,分别表示存在,空和删除
enum status
{
EXIST,
EMPTY,
DELETE
};
template<class K, class V>
struct HashData
{
pair<K, K> _kv;
status _status = EMPTY;
};
template<class K, class V, class HashFunc = Hash<K>>
class HashTable
{
public:
bool Erase(const K& key)
{
HashData<K, V>* ret = Find(key);
if (ret == nullptr)
return false;
else
ret->_status = DELETE;//改变状态即可
_n--;
return true;
}
HashData<K, V>* Find(const K& key)
{
//表中没有数据或者表的大小为0,则直接返回nullptr
if (_tables.size() == 0 || _n == 0)
return nullptr;
HashFunc hf;
size_t start = hf(key) % _tables.size();//不能%capacity,因为不能访问size之外的空间
size_t i = 0;
size_t index = start;
//线性探测
//只要当前位置不为空,就继续找,不可能死循环,因为表中不可能被填满,因为负载因子大于等于0.7就扩容
while (_tables[index]._status != EMPTY)
{
if (_tables[index]._kv.first == key && _tables[index]._status == EXIST)
return &_tables[index];//找到返回数据的引用
++i;
//一次探测
index = start + i;
//二次探测
//index = start + i*i;
index %= _tables.size();
}
//找不到
return nullptr;
}
bool Insert(const pair<K, V>& kv)
{
HashData<K, V>* ret = Find(kv.first);
if (ret != nullptr)
return false;
//当size大小为0(表示插入第一个数据)或者负载因子大于等于0.7就扩容
//负载因子越小冲突的概率越低,空间浪费就越多,负载因子越大,冲突概率就越大,浪费的空间就越小
if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7)
{
//扩容
//size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;//按照二倍进行扩容
//按素数扩容,减少哈希冲突
size_t newsize = GetNextPrime(_tables.size());
//vector> newTable;
//newTable.resize(newsize);
遍历原表,把原表中的数据,重新按newsize映射到新表
//for (size_t i = 0; i < _tables.size(); ++i)
//{
// //但是这么做不够简单
//}
//
HashTable<K, V, HashFunc> newHT;
newHT._tables.resize(newsize);
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i]._status == EXIST)
newHT.Insert(_tables[i]._kv);
}
_tables.swap(newHT._tables);
//这里不需要交换_n,因为这是在把原表的数据放在新表,还没有插入数据,所以_n不变
}
HashFunc hf;
size_t start = hf(kv.first) % _tables.size();//不能%capacity,因为不能访问size之外的空间
size_t i = 0;
size_t index = start;
//线性探测
while (_tables[index]._status == EXIST)
{
++i;
//一次探测
index = start + i;
//二次探测
//index = start + i*i;
index %= _tables.size();
}
_tables[index]._kv = kv;
_tables[index]._status = EXIST;
++_n;
return true;
}
private:
vector<HashData<K, V>> _tables;
//存储有效数据的个数
size_t _n = 0;
};
}
//**********************************************************************************************************
//链地址法
namespace LinkHash
{
template<class K, class V>
struct HashNode
{
pair<K, V> _kv;
HashNode<K, V>* _next;
HashNode(const pair<K, V>& kv)
:_kv(kv)
, _next(nullptr)
{}
};
template<class K, class V, class HashFunc = Hash<K>>
class HashTable
{
typedef HashNode<K, V> Node;
public:
HashTable()
{
_tables.resize(10);
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
return nullptr;
HashFunc hf;
//计算key在哪个链表中
size_t start = hf(key) % _tables.size();//不能%capacity,因为不能访问size之外的空间
Node* cur = _tables[start];
//如果key存在,就返回key在链表的具体位置,不存在就返回nullptr
while (cur != nullptr)
{
if (cur->_kv.first == key)
return cur;
cur = cur->_next;
}
return nullptr;
}
bool erase(const K& key)
{
if (_tables.empty())
{
return false;
}
HashFunc hf;
//计算key在哪个链表中
size_t start = hf(key) % _tables.size();//不能%capacity,因为不能访问size之外的空间
Node* cur = _tables[start];
//保存链表中的前一个结点
Node* prev = nullptr;
while (cur != nullptr)
{
if (cur->_kv.first == key)
{
//头删
if (prev == nullptr)
_tables[start] = cur->_next;
//中间删和尾删
else
prev->_next = cur->_next;
--_n;
delete cur;
return true;
}
//查找链表的后续位置
else
{
prev = cur;
cur = cur->_next;
}
}
return false;
}
bool Insert(const pair<K, V>& kv)
{
HashFunc hf;
Node* ret = Find(kv.first);
//存在kv,不再插入,返回nullptr
if (ret != nullptr)
return false;
//负载因子为1或者插入第一个数据时就扩容
if (_n == _tables.size())
{
//size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;
size_t newsize = GetNextPrime(_tables.size());
vector<Node*> newTables;
newTables.resize(newsize);
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while (cur != nullptr)
{
//保存cur的下一个结点
Node* next = cur->_next;
//重新计算哈希位置
size_t index = hf(cur->_kv.first) % newTables.size();
//链接到newTables上
cur->_next = newTables[index];
newTables[index] = cur;
//继续判断下一个,直到nullptr为止
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);
}
size_t index = hf(kv.first) % _tables.size();
//头插
Node* newnode = new Node(kv);
newnode->_next = _tables[index];
_tables[index] = newnode;
++_n;
return true;
}
private:
vector<Node*> _tables;
size_t _n = 0;
};
}
代码测试:
//测试开放定址法
void TestHashTable1()
{
HashTable<int, int> ht;
int a[] = { 2, 12, 22, 32, 42, 52, 62, 72, 2, 2 };
for (auto e : a)
{
ht.Insert(make_pair(e, e));
}
//打印12的地址
cout << ht.Find(12) << endl;
ht.Erase(12);
//因为12已经被删除,所以打印出的地址应该是0
cout << ht.Find(12) << endl;
//因为表中没有102,所以打印地址为0
cout << ht.Find(102) << endl;
ht.Insert(make_pair(102, 102));
//插入102后,打印的地址不为0
cout << ht.Find(102) << endl;
}
//测试链地址法
void TestHashTable2()
{
HashTable<int, int> ht;
int a[] = { 2, 12, 22, 32, 42, 52, 62, 72, 52, 52 };
for (auto e : a)
{
ht.Insert(make_pair(e, e));
}
//12在表中存在,所以地址不为空
cout << ht.Find(12) << endl;
ht.erase(62);
ht.erase(72);
//62和72都被删除,所打印地址为0
cout << ht.Find(62) << endl;
cout << ht.Find(72) << endl;
ht.Insert(make_pair(62, 62));
//将62插入到表中,打印的地址不为0
cout << ht.Find(62) << endl;
}