文章目录
- unordered_map的封装
- 所有接口的声明与实现
- operator[]重载
- unordered_set的封装
上篇博客模拟实现了哈希的开散列结构,并且将迭代器与泛型进行了封装,至此我们可以将开散列作为底层结构对STL标准容器——unordered_map和unordered_set进行封装。但是在封装之前还需要对我们实现的开散列结构进行改造
可以看到标准库中的插入接口会返回一个pair对象,因为这两个容器是不允许键值冗余的,所以如果插入成功(容器中没有同样的key值),insert返回的pair对象的first迭代器指向了插入后,该节点在容器中的位置,并且second对象为true。如果插入失败,pair的first迭代器指向了容器中已经存在的节点的位置,second对象为false。下面是修改后的插入接口
template <class K, class T, class KeyOfT, class HashTrans>
pair<typename HashBucket<K, T, KeyOfT, HashTrans>::iterator, bool> HashBucket<K, T, KeyOfT, HashTrans>::Insert(const T& data)
{
// 仿函数对象的定义
KeyOfT get_key;
HashTrans trans;
iterator it = Find(get_key(data));
if (it._pnode) // 出现了键值冗余
{
return make_pair(it, false);
}
if (_bucket.size() == 0 || _n * 10 / _bucket.size() >= 7) // 负载因子的维护,扩容
{
// 计算新桶的大小
size_t new_size = _bucket.size() == 0 ? 10 : 2 * _bucket.size();
// 新桶的创建与空间开辟
HashBucket new_bucket;
new_bucket._bucket.resize(new_size);
// 遍历旧表完成节点转移
for (size_t i = 0; i < _bucket.size(); ++i)
{
Data* cur = _bucket[i];
// 当头指针不为空,进入遍历单链表
while (cur)
{
Data* next = cur->_next;
// 获取新的哈希值
size_t new_hashi = trans(get_key(cur->_data)) % new_size;
// 头插操作
cur->_next = new_bucket._bucket[new_hashi];
new_bucket._bucket[new_hashi] = cur;
// 更新遍历旧表的cur
cur = next;
}
_bucket[i] = nullptr; // 好习惯,这里不置空是真的会出问题
}
// 交换完成后将新旧表交换
_bucket.swap(new_bucket._bucket);
}
// 获取哈希值
size_t hashi = trans(get_key(data)) % _bucket.size();
// 构造节点
Data* new_node = new Data(data);
// 将节点头插
new_node->_next = _bucket[hashi];
_bucket[hashi] = new_node;
// 记得负载因子的维护
++_n;
return make_pair(iterator(new_node, this), true);
}
unordered_map的封装
我们知道,哈希桶作为作为底层的结构HashBucket,需要接收上层结构传入的模板参数,包括泛型以及支持key值提取(从泛型数据中提取出key值)和哈希转换函数(将key值转换成可以被除模的整形数据),这是HashBucket的模板参数
template <class K, class T, class KeyOfT, class HashTrans>
其中K是key值的类型,因为有些函数需要key值作为参数或者返回值,所以需要把K值的类型单独接收。T是泛型的类型,T可能是int,可能是string,可能是pair对象,总之就是容器需要存储的数据的类型。然后KeyOfT和HashTrans就是key值提取和哈希转换的仿函数接收。对于上层结构
unordered_map和unordered_set,它们的模板参数化不需要接收仿函数以提取key值。因为set容器存储的就是一个key值,所以set需要设计一个仿函数,其接收一个泛型对象,然后再将泛型对象返回(因为对于set,其泛型对象就是key值,不需要什么提取),封装底层结构HashBucket时将仿函数传入。而map容器存储的是一个pair值,first为key对象,second为value对象,所以map容器需要设计一个仿函数,返回pair对象的first值,并将其传入底层容器HashBucket。为什么底层容器HashBucket需要接收key值提取仿函数,因为对于使用它的上层容器来说,它们知道要怎么从存储的数据中提取key值,而作为底层容器,它怎么知道使用者存储数据的key值要怎么提取?所以这里需要接收仿函数。
而哈希转换的仿函数,不论是底层结构,还是上层结构都需要接收。这是为什么?因为哈希映射到数组时需要取模,取模的对象必须是一个整数,但key值可以是任意类型的数据,不论底层还是上层,哪知道更上层的用户需要存储什么类型的数据?只是对一个特殊类型,如string,上层容器可以通过函数特化构建哈希转换,但是数据类型是无限的,容器只能根据一些典型的数据进行模板特化,对于自定义类型的哈希转换就需要我们手动构建并传入。
// 这是unordered_map的模板参数,接收key值与value值
// 将它们封装成pair对象进行存储,用户还可以传入哈希转换函数
// 如果不传入则使用默认的转换函数,这个函数只是将数据强转成size_t
// 如果此时key值并不是整数,程序将出错
template <class K, class V, class HashTrans = DefaultTrans<K>>
所有接口的声明与实现
template <class K>
struct DefaultTrans
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
struct StringTrans
{
size_t operator()(const string& str)
{
size_t ret = 0;
for (auto c : str)
{
ret = ret + 131 * c;
}
return ret;
}
};
template <class K, class V, class HashTrans = DefaultTrans<K>>
class unordered_map
{
public:
typedef pair<K, V> data_type;
struct key_of_data
{
const K& operator()(const data_type& data)
{
return data.first;
}
};
typedef HashBucket<K, data_type, key_of_data, HashTrans> Bucket;
typedef __HashIterator<K, data_type, key_of_data, HashTrans> map_iterator;
// 迭代器接口
map_iterator begin() { return _hash_bucket.begin(); }
map_iterator end() { return _hash_bucket.end(); }
// 修改接口
pair<map_iterator, bool> Insert(const data_type& data) { return _hash_bucket.Insert(data); }
bool Erase(const K& key) { return _hash_bucket.Erase(key); }
// 查找接口
map_iterator Find(const K& key) { return _hash_bucket.Find(key); }
// []的重载
V& operator[](const K& key)
{
// 创建一个pair对象,其中second为默认值
data_type data = make_pair(key, V());
// 将data插入到哈希桶中,接收其返回值
pair<map_iterator, bool> ret_it = _hash_bucket.Insert(data);
// 返回插入后的value引用
return (ret_it.first)->second;
}
private:
Bucket _hash_bucket;
};
其中比较重要的是对底层结构HashBucket
typedef pair<K, V> data_type;
typedef HashBucket<K, data_type, key_of_data, HashTrans> Bucket;
typedef __HashIterator<K, data_type, key_of_data, HashTrans> map_iterator;
封装底层结构时,由于HashBucket的第一个模板参数是key值的类型,所以我们把K传入即可,第二个参数是存储的数据类型,由于map存储键值对,所以我们将pair对象封装成data_type,将其传入HashBucket的模板参数。至于最后两个就是key值提取和哈希转换仿函数的传参。将unordered_map和unordered_set对底层结构的封装对比
typedef HashBucket<K, K, key_of_data, HashTrans> Bucket;
typedef __HashIterator<K, K, key_of_data, HashTrans> set_iterator;
因为底层结构HashBucket的第二个模板参数是一个泛型,map传入pair对象,set传入的就是key值对象,所以这就是封装Bucket时,为什么前两个参数都是K的原因
operator[]重载
V& operator[](const K& key)
{
// 创建一个pair对象,其中second为默认值
data_type data = make_pair(key, V());
// 将data插入到哈希桶中,接收其返回值
pair<map_iterator, bool> ret_it = _hash_bucket.Insert(data);
// 返回插入后的value引用
return (ret_it.first)->second;
}
对于map的operator[]重载是一个老生常谈的问题,这里再说明一下。重载函数接收一个key值,因为map存储的是一个键值对,所以可以用传入的key和value的默认值创建一个pair对象,调用insert接口将其插入,并接收其返回的pair对象。pair的first为一个迭代器,指向插入数据(无论是否存在,不存在会先插入再返回,存在会直接返回)所在的位置,因为迭代器指向的数据是一个键值对,函数返回键值对的second对象引用,就是返回键值对的value值引用。综上,operator[]的作用类似于插入,不同的是该重载将key值作为函数参数,用一个默认value值与之配对后插入,但是如果key值之前存在,插入是无法成功的,但其最不同的地方是value引用的返回,也就是用将key值传入后,不论key值之前是否存在,我们都可以修改其value值,因为函数返回的是引用。
unordered_set的封装
template <class K>
struct DefaultTrans
{
size_t operator()(const K& key)
{
return (size_t)key;
}
};
template <class K, class HashTrans = DefaultTrans<K>>
class unordered_set
{
struct key_of_data
{
size_t operator()(const K& data)
{
return (size_t)data;
}
};
public:
typedef HashBucket<K, K, key_of_data, HashTrans> Bucket;
typedef __HashIterator<K, K, key_of_data, HashTrans> set_iterator;
// 迭代器接口
set_iterator begin() { return _hash_bucket.begin(); }
set_iterator end() { return _hash_bucket.end(); }
// 修改接口
pair<set_iterator, bool> Insert(const K& key) { return _hash_bucket.Insert(key); }
bool Erase(const K& key) { return _hash_bucket.Erase(key); }
// 查找接口
set_iterator Find(const K& key) { return _hash_bucket.Find(key); }
private:
Bucket _hash_bucket;
};
关于这两个上层容器的接口实现,主要都是复用底层容器HashBucket,没有什么技术含量,所以这里不再赘述。真正需要理解的地方是,底层容器对泛型的封装,与其在上层容器中的使用,还有哈希转换与key值提取仿函数,在上层容器与底层容器中是否要使用函数模板接收?