前言
本节是对于STL库中的unordered_map和unordered_set的底层的哈希表的一个基本的模仿实现,有助于读者理解unordered_map和unordered_set的底层逻辑
底层的哈希表
哈希表的原理
首先,先来理解哈希表的实现原理,其实就是通过哈希函数建立一一对应的映射关系,这样就可以通过o(1)的次数实现数据的查找,如图所示
建立对应的直接映射关系,这种是直接映射方式,但是如果数据集合当中有一个99999,那么就会造成空间的浪费,于是采用除留余数法,设散列表中允许的地址数位m,取一个不大于m,但是最接近或者是等于m的质数p作为除数,按照哈希函数:Hash(key)=key&p,将关键码转换成哈希地址
但是,有一个问题,就是如果两个关键码对应的同一个哈希地址怎么处理,比如说4和14,在除数都是10的情况下,余数都是4,这样就发生了哈希冲突。
哈希冲突的解决办法很多,这里采用开散列的方式来处理
开散列
形象得说,就是将地址重复的所有值全部用链表存储起来,然后将链表头放入哈希表当中存储起来,这样就能很好的解决问题,下面开始实现基本的代码,中间其实还有很多的细节问题,我们边写边分析边解决
代码实现
首先分析哈希表中应该存储什么,首先是基本的数据,其次就是下一个的位置,使用链表串联起来的。
template<T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T&data)
:_data(data)
,_next(nullptr)
{}
};
实现的代码比较长,中间的注释放到各个代码当中实现,但是由于代码的整体性,请读者先大致了解整个代码框架,然后在逐步分析,上下的紧密相关的,如果上面看不懂就先跳过,看到下面就自然而言就理解了
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
, _next(nullptr)
{}
};
//说明一下泛型参数,为了实现map和set的底层逻辑,在这里使用K和T,注意如果T传递的是pair<K,T>类型的话,其实就是map的KV类型
// 如果T传递的是K,就是set类型。
// 这里的仿函数的使用说明
// KeyOfT 这个是为了pair<K,V>选取K而单独设置的仿函数
// Hash防函数的作用,T是泛型,不一定支持%预算,如果是int,size_t类型自然支持,但是如果是string或者是某些结构体,那么就要特殊处理
// 如何特殊处理(当然大部分的情况是int或者是size_t,少部分是string,其他的结构体就要遇到在处理了)
// 列举一个,比如说一个学生对象,我们可以以他们的学号作为比较,就要只要是具有唯一性的都可以。
//
// 有关负载因子的介绍
// 在HashTable这个类里面由vector<Node*>和_size这两个成员变量,而负载因子的一般设置成1,也就是_size == _tables.size()
// 负载因子的意义是什么,其实最理想的情况就是每一个哈希地址都只挂了一个节点,但是这中情况就很少,所以一般是有一些哈希地址上没有任何节点
// 但是在某一些哈希地址上却挂了很多个节点,这个时候访问的效率就会大大降低,这个时候,如果当_size == _tables.size()的时候,我们就扩大
// _tables.size(),并且重新定义地址映射,这样就是减少一个哈希地址上所挂的节点数量
// 举一个例子 ,比如一开是_tables.size()=20;这个时候,1,11,21,41,61都是挂到一个哈希地址下面的,我们一般将_tables.size()扩大两倍
// _tables.size()=40,这个时候1,11,41分开,21和61挂在一起,这样就分散了,提高效率
// 前置声明
template<class K, class T, class Hash, class KeyOfT>
class HashTable; //注意HashTable的前置声明的方式方法
template<class K, class T, class Hash, class KeyOfT>
struct __HashIterator
{
typedef HashNode<T> Node;
typedef HashTable<K, T, Hash, KeyOfT> HT; //这个地方使用了HashTable<K,T,Hash,KeyOfT>,所以要前置什么一下,避免编译不通过
typedef __HashIterator<K, T, Hash, KeyOfT> Self;//注意这种写法,在内核当中经常使用,主要是为了简化写法。
Node* _node; //这个是封装的核心对象
HT* _pht;
__HashIterator(Node* node, HT* pht)
:_node(node)
, _pht(pht)
{}
T& operator*()
{
return _node->_data; //注意这个地方的_data是泛型T
}
T* operator->()
{
return &_node->_data;
}
Self& operator++()//注意哈希表的特性,每一个地址中是挂了一个链表,当这个链表遍历完了以后是要走到下一个地址上去的,所以就是要传递HashTable的原因
{
if (_node->_next)
{
// 当前桶中迭代
_node = _node->_next;
}
else
{
// 找下一个桶
Hash hash;
KeyOfT kot;
size_t i = hash(kot(_node->_data)) % _pht->_tables.size();//注意这个地方,上面有说明
++i;
for (; i < _pht->_tables.size(); ++i)
{
if (_pht->_tables[i])//注意在哈希表的下一个位置其实不一定是有桶的
{
_node = _pht->_tables[i];
break;
}
}
// 说明后面没有有数据的桶了
if (i == _pht->_tables.size())
{
_node = nullptr;
}
}
return *this;
}
bool operator!=(const Self& s) const
{
return _node != s._node;
}
bool operator==(const Self& s) const
{
return _node == s._node;
}
};
template<class K, class T, class Hash, class KeyOfT>
class HashTable
{
typedef HashNode<T> Node;
template<class K, class T, class Hash, class KeyOfT>
friend struct __HashIterator;//注意这个地方,为什么要使用友元
public:
typedef __HashIterator<K, T, Hash, KeyOfT> iterator;
iterator begin()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
if (_tables[i])
{
return iterator(_tables[i], this);//注意这个地方的this,为了迭代器++的时候的操作,这个地方是要传递整个哈希表的,也就是this指针
}
return end();
}
iterator end()
{
return iterator(nullptr, this);//其实在底层并不是这样的,这里做了简化处理,就是直接认为最后是nullptr
}
~HashTable()//注意析构函数只需要将创建的节点释放,_tables是vector类型,默认会调用vector的析构函数
{
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;
}
}
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;
}
//这里说明一下插入的返回值为什么是pair<iterator,bool>类型
//首先,插入节点如果已经存在,那么bool值就是false
//如果是新插入的,就返回true。
pair<iterator, bool> Insert(const T& data)
{
Hash hash;
KeyOfT kot;
// 去重
iterator ret = Find(kot(data));
if (ret != end())//说明节点存在
{
return make_pair(ret, false);//注意make_pair是
}
// 负载因子到1就扩容,有关负载因子的概述,上面由叙述
if (_size == _tables.size())
{
size_t newSize = _tables.size() == 0 ? 10 : _tables.size() * 2;//注意一开的 _tables.size()=0.所以这个地方要注意
vector<Node*> newTables;
newTables.resize(newSize, nullptr);
// 旧表中节点移动映射新表
for (size_t i = 0; i < _tables.size(); ++i)//重新构建映射关系
{
Node* cur = _tables[i];
while (cur)
{
Node* next = cur->_next;
size_t hashi = hash(kot(cur->_data)) % newTables.size();
cur->_next = newTables[hashi];
newTables[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTables);//注意这个地方的写法,交换_tables和newTables,因为newTables是在这个函数当中创建的,
//出了这个函数作用域就会调用析构函数,我们交换之后,将一开始的_tables指向的空间给了newTables,让它帮我们清空空间
}
size_t hashi = hash(kot(data)) % _tables.size();
// 头插 这个地方尾插也是一样的。
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_size;
return make_pair(iterator(newnode, this), true);
}
iterator Find(const K& key)
{
if (_tables.size() == 0)
{
return end();
}
Hash hash;
KeyOfT kot;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
return iterator(cur, this);
}
cur = cur->_next;
}
return end();
}
bool Erase(const K& key)
{
if (_tables.size() == 0)
{
return false;
}
Hash hash;
KeyOfT kot;
size_t hashi = hash(key) % _tables.size();
Node* prev = nullptr;
Node* cur = _tables[hashi];
while (cur)
{
if (kot(cur->_data) == key)
{
// 1、头删
// 2、中间删
if (prev == nullptr)
{
_tables[hashi] = cur->_next;
}
else
{
prev->_next = cur->_next;
}
delete cur;
--_size;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
size_t Size()
{
return _size;
}
// 表的长度
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;
}
private:
vector<Node*> _tables;
size_t _size = 0; // 存储有效数据个数
};