本篇以前一篇模拟实现哈希表为基础进行改造,如果没看过前一篇的先看一下:【C++】模拟实现哈希(闭散列和开散列两种方式)。
由于本篇代码基于上篇中开散列的代码进行改造,我就先把上篇中开散列实现的哈希表代码放到这里,各位可以不用看:
#pragma once
template<class K>
struct HashFunc
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template<>
struct HashFunc<string>
{
size_t operator()(const string& str)
{
size_t res = 0;
for (auto c : str)
{
res += c;
res *= 131;
}
return res;
}
};
namespace FangZhang_OpenHash
{
template<class K, class V>
struct HashNode
{
HashNode(const pair<K, V>& kv = make_pair(K(), V()))
:_kv(kv)
, _next(nullptr)
{}
pair<K, V> _kv;
HashNode<K, V>* _next;
};
template<class K, class V, class Hash = HashFunc<K>>
class HashTable
{
typedef HashNode<K, V> Node;
private:
vector<Node*> _tables;
size_t _size = 0;
public:
~HashTable()
{
for (int i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
delete cur;
cur = next;
}
_tables[i] = nullptr;
}
}
_size = 0;
}
bool Insert(const pair<K, V>& kv)
{
// 去重
if (Find(kv.first))
{
return false;
}
// 扩容
// 此处扩容就不需要让负载因子为0.7了
// 直接_size == _tables.size()就行
// 因为不会出现占用其他数据位置的情况
if (_size == _tables.size())
{
size_t newSize = __stl_next_prime(_size);
// 这里可以利用前面开辟好的节点
// 所以就不需要像闭散列那样再整一个哈希表了
// 复用Insert的话开销比较大,还要重新给节点开空间
// 所以直接给一个vector利用前面开的节点就好
vector<Node*> newTables;
newTables.resize(newSize);
for (auto& node : _tables)
{
Node* cur = node;
Hash hash;
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(cur->_kv.first) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
node = nullptr;
}
_tables.swap(newTables);
}
// 真正插入
Hash hash;
size_t hashi = hash(kv.first) % _tables.size();
// 头插
Node* newNode = new Node(kv);
newNode->_next = _tables[hashi];
_tables[hashi] = newNode;
++_size;
return true;
}
Node* Find(const K& key)
{
if (_tables.size() == 0)
return nullptr;
Hash hash;
size_t hashi = hash(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)
{
if (!Find(key))
{
return false;
}
Hash hash;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur && cur->_kv.first != key)
{
prev = cur;
cur = cur->_next;
}
if (prev)
{
prev->_next = cur->_next;
}
else
{
_tables[hashi] = cur->_next;
}
delete cur;
return true;
}
// 表的长度
size_t TablesSize()
{
return _tables.size();
}
// 桶的个数
size_t BucketNum()
{
size_t num = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
++num;
}
}
return num;
}
// 最长桶的长度
size_t MaxBucketLenth()
{
size_t maxLen = 0;
for (size_t i = 0; i < _tables.size(); ++i)
{
size_t len = 0;
Node* cur = _tables[i];
while (cur)
{
++len;
cur = cur->_next;
}
//if (len > 0)
//printf("[%d]号桶长度:%d\n", i, len);
if (len > maxLen)
{
maxLen = len;
}
}
return maxLen;
}
size_t Size()
{
return _size;
}
private:
inline size_t __stl_next_prime(size_t n)
{
static const size_t __stl_num_primes = 28;
static const size_t __stl_prime_list[__stl_num_primes] =
{
// 这里找的每一位素数都是按照前一位的二倍附近的素数去找的
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 < __stl_num_primes; ++i)
{
if (__stl_prime_list[i] > n)
{
return __stl_prime_list[i];
}
}
return -1;
}
};
}
首先要先搞两个头文件 UnOrderedMap.h 和 UnOrderedSet.h,这样的起名是为了不和库中的冲突。并将map和set写在命名空间FangZhang中。
上面模拟实现中的哈希表是K/V模型的,我们这里要改成既能实现K/V模型的,又能实现K模型的。和我前面模拟实现map和set很相似。要将原来哈希表代码中的模版参数由KV改为T,并将所有的pair类型都换为模版T。
因为上面map和set中增加了第三个模版参数Hash并将其传给了哈希表,所以在哈希表中就不需要让第三个模版参数Hash给默认值HashFunc
Insert要改一下参数,改为data:
data表示当前哈希表中存放的数据,但是有一个问题,就是data的类型T我们不能确定,当模型是K时,有可能是int,但当模型是KV时,也有可能是pair
其中hash函数中传参应该传的是key,而非data,当data为pair类型是key就是其first,当data为key时就直接是data就行,所以我们可以写一个仿函数,将仿函数写在map和set中,传过去data,返回来key就好。所以哈希表中又要加一个模版参数KeyOfT,用来提取出每个data的key。
但上面没写迭代器,不能打印,我们下面实现一下迭代器。
实现前要想清楚逻辑,当我们想遍历哈希表时,要挨着节点遍历,但中间有不想连的地方,如果其中一个非空链表走到头了,想要找下一个非空链表就得要借助其本身的哈希表,所以我们要在迭代器中给一个哈希表的指针,用来方便我们遍历。
我们这里重载一下*、->、!=、++运算符,就能遍历了。
再在哈希表中写一下begin和end:
测试,发现出问题了,在哈希表中_ptb指针所指向的是整个哈希表结构体,其中解引用了_tables,而_tables在哈希表中是私有的,不可直接在迭代器中访问。
而哈希表中也用到了迭代器,所以二者会出现相互调用的问题,此时我们就要用友元来解决这个问题了。迭代器中无法访问哈希表中的成员,所以我们要将迭代器设置为哈希表的友元。
我们用一下统计数量的测试:
但是没有重载[ ]就比较麻烦,所以再来重载一下[ ]。只能在unordered_map中重载。
如果看过我红黑树的模拟实现的话,这里就比较熟悉了。
我们直接复用一下insert就好。
前面的博客中说过了,这里再解释一下:res.first就是对应插入节点的iterator,->返回_data的地址,但是编译器优化了,省略了一个->,所以就能直接得到_data,然后直接拿到second,也就是KV中的V。
unordered_set就不搞了,比unordered_map还简单,感兴趣的老铁自己搞搞。这里模拟实现不是为了把所有的实现,主要是了解一下STL库的底层,更方便于我们学习。以后是绝对用不到这写模拟的。
到此结束。。。