本章内容分为源码看框架讲解和结构模拟实现两部分,源码框架是让我们了解容器结构在设计时的思路,模拟实现才是重点。因此如果在看源码结构式感到疑惑,不妨继续往下看,相信一切都会慢慢了解~
源码及框架分析
在C++98 / SGI-STL30版本的源代码中没有 unordered_map 和 unordered_set(SGI-STL30版本是C++11之前的STL版本),这两个容器是C++11之后才更新的,但是SGI-STL实现了哈希表,容器的名字是hash_map 和 hash_set,作为非标准容器出现(非C++标准规定必须实现),源代码在hash_map / hash_set / stl_map / stl_set / stl_hashtable.h 中。
源代码中 hash_map / hash_set 的实现结构框架核心部分如下:
// stl_hash_set
template <class Value, class HashFcn = hash<Value>,
class EqualKey = equal_to<Value>,
class Alloc = alloc>
class hash_set
{
private:
typedef hashtable<Value, Value, HashFcn, identity<Value>,EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;
typedef typename ht::const_iterator iterator;
typedef typename ht::const_iterator const_iterator;
hasher hash_funct() const { return rep.hash_funct(); }
key_equal key_eq() const { return rep.key_eq(); }
};
// stl_hash_map
template <class Key, class T, class HashFcn = hash<Key>,
class EqualKey = equal_to<Key>,class Alloc = alloc>
class hash_map
{
private:
typedef hashtable<pair<const Key, T>, Key, HashFcn,
select1st<pair<const Key, T> >, EqualKey, Alloc> ht;
ht rep;
public:
typedef typename ht::key_type key_type;
typedef T data_type;
typedef T mapped_type;
typedef typename ht::value_type value_type;
typedef typename ht::hasher hasher;
typedef typename ht::key_equal key_equal;
typedef typename ht::iterator iterator;
typedef typename ht::const_iterator const_iterator;
};
// stl_hashtable.h
template <class Value, class Key, class HashFcn,class ExtractKey,
class EqualKey,class Alloc>
class hashtable {
public:
typedef Key key_type;
typedef Value value_type;
typedef HashFcn hasher;
typedef EqualKey key_equal;
private:
hasher hash;
key_equal equals;
ExtractKey get_key;
typedef __hashtable_node<Value> node;
vector<node*,Alloc> buckets;
size_type num_elements;
public:
typedef __hashtable_iterator<Value, Key, HashFcn, ExtractKey,
EqualKey,Alloc> iterator;
pair<iterator, bool> insert_unique(const value_type& obj);
const_iterator find(const key_type& key) const;
};
template <class Value>
struct __hashtable_node
{
__hashtable_node* next;
Value val;
};
通过源码,我们可以看到结构上 hash_map / hash_set 与 map / set 完全类似,复用了同一个hashtable实现 key / key_value 结构,hash_set 传给 hash_table 的是两个 key,hash_map 传给hash_table的是pair<const key, value>
需要注意的是源码里面跟 map / set 源码类似,命名风格比较乱,这里比 map 和 set 还乱,hash_set 模板参数居然用的 value 命名,hash_map 用的是key 和 T 命名,课件大佬优势写代码也不规范,乱弹琴。下面我们模拟一份结构出来,在命名上我会保持之前模拟的map和set的风格。
参考源码框架,unordered_map和unordered_set复用之前我们实现的哈希表。
我们这里相比源码调整一下,key参数就用K,value参数就用V,哈希表中的数据类型,我们使用T。
其次跟map和set相比而言unordered_map和unordered set的模拟实现类结构更复杂一点,但是大框架和思路是完全类似的。因为HashTable实现了泛型不知道T参数导致是K,还是pair<K,V>,那么insert内部进行插入时要用K对象转换成整形取模和K比较相等,因为pair的value不参与计算取模,且默认支持的是key和value一起比较相等,我们需要时的任何时候只需要比较K对象,所以我们在unordered map和unordered set层分别实现一个MapKeyOfT和SetKeyOfT的仿函数传给HashTable的KeyOfT,然后HashTable中通过KeyOfT仿函数取出T类型对象中的K对象,再转换成整形取模和K比较相等。
iterator的源码实现
template <class Value, class Key, class HashFcn,
class ExtractKey, class EqualKey, class Alloc>
struct __hashtable_iterator {
typedef hashtable<Value, Key, HashFcn, ExtractKey, EqualKey, Alloc>
hashtable;
typedef __hashtable_iterator<Value, Key, HashFcn,
ExtractKey, EqualKey, Alloc>
iterator;
typedef __hashtable_const_iterator<Value, Key, HashFcn,
ExtractKey, EqualKey, Alloc>
const_iterator;
typedef __hashtable_node<Value> node;
typedef forward_iterator_tag iterator_category;
typedef Value value_type;
node* cur;
hashtable* ht;
__hashtable_iterator(node* n, hashtable* tab) : cur(n), ht(tab) {}
__hashtable_iterator() {}
reference operator*() const { return cur->val; }
#ifndef __SGI_STL_NO_ARROW_OPERATOR
pointer operator->() const { return &(operator*()); }
#endif /* __SGI_STL_NO_ARROW_OPERATOR */
iterator& operator++();
iterator operator++(int);
bool operator==(const iterator& it) const { return cur == it.cur; }
bool operator!=(const iterator& it) const { return cur != it.cur; }
};
template <class V, class K, class HF, class ExK, class EqK, class A>
__hashtable_iterator<V, K, HF, ExK, EqK, A>&
__hashtable_iterator<V, K, HF, ExK, EqK, A>::operator++()
{
const node* old = cur;
cur = cur->next;
if (!cur) {
size_type bucket = ht->bkt_num(old->val);
while (!cur && ++bucket < ht->buckets.size())
cur = ht->buckets[bucket];
}
return* this;
}
iterator实现的大体框架跟list的iterator思路是一致的,用一个类型封装节点的指针,再通过重载运算符实现,迭代器像指针一样的访问行为,需要注意的是哈希表的迭代器是单向迭代器。
这里的难点是operator++的实现。iterator中有一个指向节点的指针,如果当前哈希桶下面还有节点,则节点的指针指向下一个节点即可。如果当前哈希桶的下一个节点为nullptr,则需要想办法计算找到下一个桶。这里的难点是反而是结构设计的问题,参考上面的源码,我们看到iterator中除了有节点的指针,还有哈希表对象的指针,这样当前桶走完时,要计算下一个桶就相对容易多了,用key值计算出当前桶位置,依次往后找一个不为空的桶即可。
begin()返回第一个桶中第一个节点指针构造的迭代器,这里end()返回的迭代器用空表示。
unordered_set的iterator也不支持修改,我们把unordered_set的第二个模板参数改成const K即可,HashTable<K,const K,SetkeyofT,Hash>_ht;
unordered_map的iterator不支持修改key但是可以修改value,我们把unordered map的第二
模板参数pair的第一个参数改成constK即可,
HashTable<K,pair<const K,V>,MapKeyofT, Hash> _ht;
模拟实现unordered_map 和 unordered_set
HashTable.h
#pragma once
#include<iostream>
#include<vector>
#include<string>
using namespace std;
// 实现步骤:
// 1、实现哈希表
// 2、封装unordered_map和unordered_set的框架 解决KeyOfT
// 3、iterator
// 4、const_iterator
// 5、key不⽀持修改的问题
// 6、operator[]
inline unsigned long __stl_next_prime(unsigned long n)
{
// Note: assumes long is at least 32 bits.
static const int __stl_num_primes = 28;
static const unsigned long __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
};
const unsigned long* first = __stl_prime_list;
const unsigned long* last = __stl_prime_list +
__stl_num_primes;
const unsigned long* pos = lower_bound(first, last, n);
return pos == last ? *(last - 1) : *pos;
}
namespace hash_bucket
{
enum State
{
ESXIT,
EMPTY,
DELETE
};
template<class T>
struct HashNode
{
T _data;
HashNode<T>* _next;
HashNode(const T& data)
:_data(data)
,_next(nullptr)
{}
};
template<class k>
struct HashFunc
{
size_t operator()(const k& key)
{
return (size_t)key;
}
};
template<>
struct HashFunc<string>
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto& s1 : s)
{
hash += s1;
}
return hash;
}
};
//前置声明
template<class k, class T, class KeyOfT, class Hash>
class HashTable;
template<class k, class T, class Ref, class Ptr, class KeyOfT, class Hash>
struct HTIterator
{
typedef HashNode<T> Node;
typedef HashTable<k, T, KeyOfT, Hash> HT;
typedef HTIterator<k, T, Ref, Ptr, KeyOfT, Hash> Self;
Node* _node;
const HT* _ht;
HTIterator(Node* node, const HT* ht)
:_node(node)
,_ht(ht)
{}
Ref operator*()
{
return _node->_data;
}
Ptr operator->()
{
return &_node->_data;
}
bool operator!=(const Self& s)
{
return _node != s._node;
}
Self operator++()
{
if (_node->_next)
{
//下一个节点存在
_node = _node->_next;
}
else
{
Hash hash;
KeyOfT kot;
size_t hashi = hash(kot(_node->_data)) % _ht->_tables.size();
++hashi;
while (hashi < _ht->_tables.size())
{
if (_ht->_tables[hashi])
break;
++hashi;
}
if (hashi == _ht->_tables.size())
_node = nullptr;
else
_node = _ht->_tables[hashi];
}
return *this;
}
};
template<class k, class T, class KeyOfT, class Hash = HashFunc<k>>
class HashTable
{
typedef HashNode<T> Node;
template<class k, class T, class Ref, class Ptr, class KeyOfT, class Hash>
friend struct HTIterator;
public:
typedef HTIterator<k, T, T&, T*, KeyOfT, Hash> Iterator;
typedef HTIterator<k, T, const T&, const T*, KeyOfT, Hash> const_Iterator;
Iterator Begin()
{
if (_n == 0)
return End();
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
return Iterator(cur, this);
}
return End();
}
Iterator End()
{
return Iterator(nullptr, this);
}
const_Iterator Begin() const
{
if (_n == 0)
return End();
for (size_t i = 0; i < _tables.size(); i++)
{
Node* cur = _tables[i];
if (cur)
return Iterator(cur, this);
}
return End();
}
const_Iterator End() const
{
return Iterator(nullptr, this);
}
HashTable()
{
_tables.resize(__stl_next_prime(_tables.size()+1), nullptr);
}
~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;
}
}
pair<Iterator, bool> Insert(const T& data)
{
Hash hash;
KeyOfT kot;
Iterator it = Find(kot(data));
if (it != End())
return make_pair(it, false);
size_t hashi = hash(kot(data)) % _tables.size();
//扩容
if (_n == _tables.size())
{
vector<Node*> newTable(__stl_next_prime(_tables.size()+1));
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)) % newTable.size();
cur = newTable[hashi];
newTable[hashi] = cur;
cur = next;
}
_tables[i] = nullptr;
}
_tables.swap(newTable);
}
//头插
Node* newnode = new Node(data);
newnode->_next = _tables[hashi];
_tables[hashi] = newnode;
++_n;
return make_pair(Iterator(newnode, this), true);
}
Iterator Find(const k& key)
{
Hash hash;
KeyOfT kot;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
while (cur)
{
if (hash(kot(cur->_data)) == hash(key))
{
return Iterator(cur,this);
}
cur = cur->_next;
}
return Iterator(nullptr,this);
}
bool Erase(const k& key)
{
Hash hash;
KeyOfT kot;
size_t hashi = hash(key) % _tables.size();
Node* cur = _tables[hashi];
Node* prev = nullptr;
while (cur)
{
if (hash(kot(cur->_data)) == hash(key))
{
if (prev)
{
prev->_next = cur->_next;
}
else
{
_tables[hashi] = cur->_next;
}
delete cur;
return true;
}
prev = cur;
cur = cur->_next;
}
return false;
}
private:
vector<Node*> _tables;
size_t _n = 0;
};
}
这段代码实现了一个自定义的哈希表(HashTable
)数据结构以及基于该哈希表构建的unordered_set
和unordered_map
容器,模仿了 C++ 标准库中对应容器的部分功能。代码中包含了一些模板类和函数,利用了哈希算法来实现高效的数据存储和查找等操作。 这里需要注意的是:由于迭代器类和核心哈希表类都需要使用双方内部的成员,所以这里将迭代器放在哈希表上方,然后将哈希表模板声明在迭代器上方,将迭代器类声明为哈希表类的友元类模板。有各种各样的细节需要注意,各位如果对某一方面有疑问,可以将问题发在评论区,我会一 一解答。
__stl_next_prime
函数用于查找大于给定数字n
的下一个质数。它通过维护一个静态的质数列表__stl_prime_list
,利用lower_bound
算法在列表中查找第一个不小于n
的质数。如果查找位置到达列表末尾,则返回列表中的最后一个质数,否则返回找到的那个质数。
该函数在哈希表的实现中,常用来确定哈希表合适的桶数量(一般取质数可以让数据分布更均匀,减少冲突),例如在哈希表初始化或者扩容时,根据当前元素数量来确定合适的桶大小(取质数)。HashNode类
是哈希表中链表节点的类模板定义。每个节点存储了实际的数据_data
以及指向下一个节点的指针_next
,用于解决哈希冲突(当不同的键通过哈希函数映射到同一个桶位置时,采用链表法将冲突的元素链接起来)。- HashFunc函数: 普通版本的
HashFunc
是一个函数对象结构体模板,对于传入的键类型k
,简单地将其转换为size_t
类型作为哈希值(这种方式可能对于自定义类型不太合适,只是一个简单示例)。而特化版本针对string
类型,通过将字符串中每个字符的 ASCII 值相加来计算哈希值,是一种简单的字符串哈希计算方式。 HTIterator类
是哈希表的迭代器类模板定义。它内部包含指向当前节点的指针_node
以及指向所属哈希表的指针_ht
,通过重载*
、->
、!=
和++
等运算符,实现了像遍历普通容器一样去遍历哈希表中的元素。例如,*
运算符返回当前节点存储的数据引用,++
运算符实现了迭代器向后移动到下一个有效元素(如果当前节点所在链表还有下一个节点就移动过去,否则去查找下一个非空桶位置的节点)。- 哈希表的核心类定义:
迭代器相关:定义了Iterator
和const_Iterator
类型,分别用于可读写和只读遍历哈希表,并实现了Begin
和End
函数来返回对应迭代器,方便外部对哈希表元素进行遍历操作。
构造函数和析构函数:构造函数中会调用__stl_next_prime
函数来初始化哈希表的桶数组_tables
大小为一个合适的质数(初始化为比 0 大一点的质数对应的大小)。析构函数则负责释放每个桶链表中的所有节点内存,避免内存泄漏。
插入操作(Insert
):首先通过Find
函数检查要插入的数据是否已经存在,如果不存在,计算出数据应该插入的桶位置(通过哈希函数取模得到)。如果当前元素数量等于桶数量(意味着可能出现冲突过多等情况,需要扩容),则进行扩容操作,创建新的桶数组,将旧桶中的元素重新哈希到新桶中(采用头插法插入新桶),然后将新数据插入到对应桶位置的链表头部,最后返回插入后的迭代器和表示插入成功的true
(如果元素已存在则返回已存在元素的迭代器和false
表示插入失败)。
查找操作(Find
):根据给定的键,通过哈希函数计算桶位置,然后在对应桶的链表中依次比较节点元素的键(通过KeyOfT
提取键)与给定键是否相等,找到则返回对应节点的迭代器,没找到返回指向空的迭代器。
删除操作(Erase
):同样根据键找到对应的桶位置,在桶链表中查找并删除对应节点,如果找到并删除成功则返回true
,没找到则返回false。
unordered_set.h
#include"HashTable.h"
namespace zy
{
template<class k, class Hash = hash_bucket::HashFunc<k>>
class unordered_set
{
public:
struct SetKeyOfT
{
const k& operator()(const k& key)
{
return key;
}
};
typedef typename hash_bucket::HashTable<k, const k, SetKeyOfT, Hash>::Iterator iterator;
typedef typename hash_bucket::HashTable<k, const k, SetKeyOfT, Hash>::const_Iterator const_iterator;
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin() const
{
return _ht.Begin();
}
const_iterator end() const
{
return _ht.End();
}
pair<iterator, bool> insert(const k& key)
{
return _ht.Insert(key);
}
iterator find(const k& key)
{
return _ht.Find(key);
}
bool erase(const k& key)
{
return _ht.Erase();
}
private:
hash_bucket::HashTable<k, const k, SetKeyOfT, Hash> _ht;
};
}
unordered_set
类:基于前面定义的HashTable
实现了一个无序集合类。它内部定义了一个用于提取键的函数对象SetKeyOfT
(这里简单地返回传入的键本身,因为unordered_set
中元素本身就是键),然后通过typedef
定义了对应的迭代器类型。其成员函数基本都是调用内部HashTable
对象_ht
的相应函数来实现,比如insert
、find
、erase
等操作,分别对应向集合中插入元素、查找元素和删除元素,begin
和end
函数则用于返回遍历集合的迭代器。
unordered_map.h
#include"HashTable.h"
namespace zy
{
template<class k, class v, class Hash = hash_bucket::HashFunc<k>>
class unordered_map
{
public:
struct MapKeyOfT
{
const k& operator()(const pair<k,v>& kv)
{
return kv.first;
}
};
typedef typename hash_bucket::HashTable<k, pair<const k, v>, MapKeyOfT, Hash>::Iterator iterator;
typedef typename hash_bucket::HashTable<k, pair<const k, v>, MapKeyOfT, Hash>::const_Iterator const_iterator;
iterator begin()
{
return _ht.Begin();
}
iterator end()
{
return _ht.End();
}
const_iterator begin() const
{
return _ht.Begin();
}
const_iterator end() const
{
return _ht.End();
}
pair<iterator, bool> insert(const pair<k, v>& kv)
{
return _ht.Insert(kv);
}
v& operator[](const k& key)
{
pair<iterator, bool> ret = _ht.Insert(make_pair(key, v()));
return ret.first->second;
}
iterator find(const k& key)
{
return _ht.Find(key);
}
bool erase(const k& key)
{
return _ht.Erase();
}
private:
hash_bucket::HashTable<k, pair<const k, v>, MapKeyOfT, Hash> _ht;
};
}
- 这里定义了三个模板参数,
k
表示键的类型,v
表示值的类型,Hash
是一个可选的哈希函数类型,默认使用hash_bucket::HashFunc<k>
来计算键的哈希值(这里hash_bucket
应该是某个自定义的命名空间,里面定义了HashFunc
这个哈希函数相关的类或者函数等)。 - 私有成员变量:声明了一个
HashTable
类型的私有对象_ht
,整个unordered_map
的各种操作实际上都是基于这个内部的HashTable
来实现的,对外部隐藏了具体的哈希表实现细节。 MapKeyOfT:
这个类定义了一个函数调用运算符,它的作用是从pair<k, v>
类型的元素中提取键(也就是返回pair
中的第一个元素)。这个结构体在后续和HashTable
结合使用时,用于告诉HashTable
如何获取键来进行哈希相关操作。- 类型别名(
typedef
)定义:这里通过typedef
为HashTable
中的迭代器类型定义了别名,方便在unordered_map
类中使用,分别定义了普通迭代器iterator
和常量迭代器const_iterator
。这样使得unordered_map
的使用者可以像使用标准库容器的迭代器那样来遍历unordered_map
中的元素。typename的作用是声明定义为类型而非变量,因为域作用限定符既可以声明类型,又可以声明变量。 - 迭代器相关函数:这些函数用于获取
unordered_map
的起始和结束迭代器,它们直接调用了内部HashTable
对象_ht
的相应Begin
和End
函数来返回迭代器,从而可以遍历unordered_map
中的所有键值对元素。 - 插入元素函数
insert:
该函数接受一个pair<k, v>
类型的参数(代表要插入的键值对),并将插入操作委托给内部的HashTable
对象_ht
的Insert
函数来执行,最后返回插入操作的结果(以包含迭代器和表示是否插入成功的布尔值的pair
形式返回,和标准库中unordered_map
的insert
函数行为类似)。 - 下标运算符重载
operator[]:
这个运算符重载实现了类似标准库unordered_map
中通过键获取对应值的功能。如果键不存在,它会先插入一个默认构造的pair
(值部分通过v()
来默认构造),然后返回对应迭代器指向的pair
中的值的引用。这样就可以方便地通过map[key]
的形式来访问或修改值了。 -
查找元素函数
find:
调用内部HashTable
对象的Find
函数来查找给定键对应的元素,若找到则返回指向该元素的迭代器,否则返回表示末尾的迭代器(类似标准库容器查找函数的行为)。 -
删除元素函数
erase:
-
委托给内部
HashTable
对象的Erase
函数来执行删除操作,根据键删除对应的元素,并返回删除是否成功的布尔值。
总之,代码通过模板和对 HashTable
的封装,实现了类似于 C++ 标准库中的 unordered_map
和 unordered_set
的基本功能框架,包括元素的插入、查找、删除以及迭代器遍历等操作。不过,要使其完整可用,还需要确保 HashTable
类正确地实现了如 Begin
、End
、Insert
、Find
、Erase
等相关函数,并且相关的命名空间、类型等定义(如 hash_bucket
相关内容)都是正确和完整的。 同时,代码中可能还需要考虑一些异常处理、内存管理等更完善的细节方面的内容,目前只是实现了核心的功能逻辑部分。
我们下期再会~