unordered_map与unordered_set底层实现
前面我们就说明了unordered_map与unordered_set的底层容器是hash表;现在我们就来看看它们究竟是如何实现的:
和map与set一样,因为RBTree为了适配两种容器,需要一些改造;所以这里的hash表也需要改造;
改造hash表
为了同时适配unordered_map与unordered_set两个容器;首先将hash表的节点改变为:
template<class T>
struct hashnode {
hashnode<T>(const T& data)
{
_next = nullptr;
_data = data;
}
hashnode<T>* _next;
T _data;
};
可以接收不同T类型的数据(这里与RBTree的接收非常相似);
再来看改造后的hash表的模板参数列表与成员变量(开散列与除留余数法):
template<class K, class T, class Hash, class KofT>
class hash_table {
public:
typedef hashnode<T> node;
private:
vector<node*> _table;
size_t _size = 0;
};
这上面hash表的模板参数其实和前面红黑树的模板参数十分类似,我们可以用红黑树哪里的思路来理解hash表;
其实改造的地方也就只有获得T中的key数据和hash函数来获得数据所在表中的位置两个点;我们直接看代码会更好:
模拟实现stl容器/模拟实现unordered_map与unordered_set/hash_table.hpp · future/my_road - 码云 - 开源中国 (gitee.com)https://gitee.com/little-lang/my_road/blob/master/%E6%A8%A1%E6%8B%9F%E5%AE%9E%E7%8E%B0stl%E5%AE%B9%E5%99%A8/%E6%A8%A1%E6%8B%9F%E5%AE%9E%E7%8E%B0unordered_map%E4%B8%8Eunordered_set/hash_table.hpp上面就是改造好的hash表代码;我们拥有了unordered_ map与unordered_set的底层容器hash表之后;我们就可以通过hash表来封装了这两个容器了;
封装unordered_map
template<class K, class V, class Hash = hashfunc<K>>
class my_unordered_map {
public:
typedef pair<const K, V> T;
struct mapKofT {
const K& operator()(const T& data)
{
return data.first;
}
};
bool insert(const T& data)
{
return _hash.insert(data);
}
bool erase(const K& key)
{
return _hash.erase(key);
}
V& operator [](const K& key)
{
return insert(make_pair(key,V())).first->second;
}
private:
hash_table<K, T, Hash, mapKofT> _hash;
};
我们直接调用hash表中的接口即可;
封装unordered_set
template<class K, class Hash = hashfunc<K>>
class my_unordered_set {
public:
struct setKofT {
const K& operator()(const K& key)
{
return key;
}
};
pair<iterator, bool> insert(const K& data)
{
return _hash.insert(data);
}
bool erase(const K& key)
{
return _hash.erase(key);
}
private:
hash_table<K, K, Hash, setKofT> _hash;
};
同理unordered_map封装都是调用底层的hash表接口;
由于这里和上篇博客的模板参数传递相似,我们这就不做过多的讲解了;我们直接看重要的迭代器部分:
hash表的迭代器
template<class K, class T, class Ptr, class Ref, class Hash, class KofT>
struct hash_iterator {
typedef hashnode<T> node;
typedef hash_table<K, T, Hash, KofT> hash_table;
typedef hash_iterator<K, T, Ptr, Ref, Hash, KofT> self;
typedef hash_iterator<K, T, T*, T&, Hash, KofT> iterator;
hash_iterator(node* newnode, const hash_table* ht)
{
_node = newnode;
_hash = ht;
}
hash_iterator(const iterator& it)
{
_node = it._node;
_hash = it._hash;
}
self operator ++()
{
if (_node == nullptr)
return *this;
KofT kot;
node* cur = _node;
cur = cur->_next;
Hash hash;
if (cur)
{
_node = cur;
}
else
{
size_t pos = hash(kot(_node->_data)) % _hash->_table.size();
pos++;
while (pos < _hash->_table.size() && _hash->_table[pos] == nullptr)
{
pos++;
}
if (pos >= _hash->_table.size())
{
_node = nullptr;
}
else
{
_node = _hash->_table[pos];
}
}
return *this;
}
bool operator !=(const self& it)
{
return _node != it._node;
}
Ref operator *()
{
return _node->_data;
}
Ptr operator ->()
{
return &_node->_data;
}
node* _node;
const hash_table* _hash;
};
迭代器的模板参数
这里我们可以发现,hash表的迭代器传递的模板参数要比红黑树多很多,这是因为:
1.需要通过KofT和Hash来帮助计算下一节点位置,所以需要这两个模板参数
2.hash表的数据不再是一个个分散节点而是由vector封装的连续空间,我们的节点无法全部直接获得下一个节点的位置(不在一个hash桶时),所以我们需要hash表封装的vector来帮助我们获得下一节点位置,而要hash表就需要hash表的类型接收,而hash表类型的模板参数又有K所以需要增加一个K模板参数;
总而言之,不同的场景我们需要的参数是不同的要随机应变,灵活运用;
注意
1.相互包含时的声明:由于我们迭代器包含了hash表,而hash表也包含了迭代器,所以这里一定有先后定义,导致某个类无法找到另一个类,所以我们这里需要在前方声明一下另一个类;
2.hash表成员变量是const类型:这是为了适配接收的const参数类型,我们如果参数也不添加const类型,那hash表成员变量也不需要是const类型的;
上面的内容都完成后unordered_map/set的实现基本上也就完成了,下面可以查看完整代码:
实现时出现的错误
我在实现时更改map的T类型为pair <const K,V>,使得K变量在任何时候都无法被修改,就是这个时候我们前面的node节点数据并没有在初始化列表中初始化,导致编译错误,而编译器显示的错误是引用了被删除的函数,导致我找了很久的错误,非常苦恼;
这里实际上的错误是一位我们将K修改为了const K变量使得前面的模板类实例化类型出来的pair也具有了const性质,无法在构造函数内初始化,需要在初始化列表中初始化,导致编译错误了;
记录错误,警醒自己成员变量最好在初始化列表中初始化;
bitset位图(表)
使用场景
位图的运用场景一般是在大量数据中查询某一数据是否存在;
示例:在100亿个整数中查询某个数是否在这100亿个整数中;
这个问题我们一开始可能会想到set,用set来处理这100亿个数据,但是100亿个整数一个整数4个字节100亿等于4*100 0000 0000字节等于40G的内存,而我们的内存一般计算机标配是16G,这样的内存是不够的;所以为了节省空间而且只需要判断数据是否存在就可以使用位图来操作;
位图是怎么做到的呢?我们先来实现一下位图然后再使用它:
位图实现与讲解
template<size_t N>
class bitset {
public:
bitset()
{
_bits.resize(N / 8 + 1, 0);
}
void set(size_t num)
{
size_t i = num / 8;
size_t j = num % 8;
_bits[i] |= (1 << j);
}
void reset(size_t num)
{
size_t i = num / 8;
size_t j = num % 8;
_bits[i] &= ~(1 << j);
}
bool test(size_t num)
{
size_t i = num / 8;
size_t j = num % 8;
return _bits[i] & (1 << j);
}
private:
vector<char> _bits;
};
位图有着三个重要的接口,set,reset,test;当我们需要将数据存储进入位图的时候,使用test接口接收数据;数据就会在对应的bit位上置为1;
例如:我们存储4这个数据到位图中;首先我们寻找4位置;
我们当我们查询数据的时候调用test接口即可查询到相应的bit位是否为1,为1则代表数据存在;
通过代码和上面的图片辅助理解可以很好的理解位图的工作原理;
判断大量数据出现次数
这是对位图功能的证明也是对位图的功能的提升;
我们可以通过位图封装来测试位图出现的次数:
template <size_t N>
class timesBitset {
public:
void set(size_t num)
{
if (_bits1.test(num) == false
&& _bits2.test(num) == false)//开始为00
{
_bits1.set(num);//变为01
}
else if (_bits1.test(num) == true
&& _bits2.test(num) == false)
{
_bits2.set(num);//变为10
_bits1.reset(num);
}
else if (_bits1.test(num) == false
&& _bits2.test(num) == true)
{
_bits1.set(num);//变为11
}
}
void test1()
{
for (size_t i = 0; i < N; i++)
{
if (_bits1.test(i) == true
&& _bits2.test(i) == false)
{
cout << i << " ";
}
}
cout << endl;
}
void test2()
{
for (size_t i = 0; i < N; i++)
{
if (_bits1.test(i) == false
&& _bits2.test(i) == true)
{
cout << i << " ";
}
}
cout << endl;
}
private:
bitset<N> _bits1;//1是低位2是高位
bitset<N> _bits2;
};
void testBitset()//输出只出现一次的数
{
int arr[] = { 1,5,3,6,9,1,5,3,6,9,15,12,11 };
timesBitset<100> b;
for (auto e : arr)
{
b.set(e);
}
cout<<"出现1次的数"<<endl;
b.test1();
}
void testBitset1()//出现两次的数
{
int arr[] = { 1,5,3,6,9,1,5,3,6,9,15,12,11 };
timesBitset<100> b;
for (auto e : arr)
{
b.set(e);
}
cout<<"出现2次的数"<<endl;
b.test2();
}
上面我们将两个位图封装进了次数位图中,两个位图就可以使得一个数具有两个bit位,而两个bit位可以代表四种状态:00 01 10 11这四种状态分别就可以代表出现的次数 1 2 3 多余3次;通过上面的代码也可以辅助理解;
这证明了位图的正确,也证明了位图的可操作性;
这样全是整数的位图还具有排序的效果,且搜索数据的时间复杂度和hash表一样是O(1);但这样的位图具有一定局限性,它只能用来存储整数,其他类型无法存储进位图中;为了让所有数据都可以存储进入位图中,出现了一种容器——布隆过滤器
布隆过滤器
布隆过滤器是一种可以准确判断数据数据不存在,不是非常准确的判断出数据存在的容器;这样的容器虽然不是非常准确但是在某些场景下非常适用;
适用场景
当在对大量数据进行处理的时候,我们可以先进行一次相对不准确的判断,当判断数据不存在时即可直接退出判断,而这样的判断速度非常快,效率极高;当判断数据存在时可以进入数据库中进行比对判断数据是否真的存在;这样可以大大的数据的比对效率,有人可能会说在数据存在时不是有两次判断吗,效率不是会降低吗;但事实不是这样的,我们布隆过滤器的查找效率可是O(1)所以可以忽略掉这层的查找时间,而在数据不存在时直接就是O(1)的复杂度了效率会更高;
示例:在我们博客刚刚开始注册的时候,是不是有一个取名的过程,如果我们的名字重复立马就会提示出现一个名字重复的标识;但是每次我们得点击提交才会提示名字重复
如果优化这样的提示在边上加上一个小标识,当名字不重复就会出现一个小勾勾的提示,是不是就会优化用户的体验,就和我们的密码复杂度检查一样,我们在注册账号输密码时边上会有小提示
优化成这样:
这就是适用场景如果感觉不太理解可以先看下面的实现应该就可以理解了;
实现与讲解
struct BKDRHash
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (auto ch : s)
{
hash += ch;
hash *= 31;
}
return hash;
}
};
struct APHash
{
size_t operator()(const string& s)
{
size_t hash = 0;
for (long i = 0; i < s.size(); i++)
{
size_t ch = s[i];
if ((i & 1) == 0)
{
hash ^= ((hash << 7) ^ ch ^ (hash >> 3));
}
else
{
hash ^= (~((hash << 11) ^ ch ^ (hash >> 5)));
}
}
return hash;
}
};
struct DJBHash
{
size_t operator()(const string& s)
{
size_t hash = 5381;
for (auto ch : s)
{
hash += (hash << 5) + ch;
}
return hash;
}
};
template<size_t N
, class K=string
, class Hash1 = BKDRHash
, class Hash2 = APHash
, class Hash3 = DJBHash>
class bloomfilter
{
public:
void set(const K & key)
{
Hash1 hash1;
Hash2 hash2;
Hash3 hash3;
size_t size = _X * N;
size_t pos1 = hash1(key) % size;
size_t pos2 = hash2(key) % size;
size_t pos3 = hash3(key) % size;
_bitset.set(pos1);
_bitset.set(pos2);
_bitset.set(pos3);
}
bool test(const K& key)
{
Hash1 hash1;
Hash2 hash2;
Hash3 hash3;
size_t size = _X * N;
size_t pos1 = hash1(key) % size;
size_t pos2 = hash2(key) % size;
size_t pos3 = hash3(key) % size;
if (_bitset.test(pos1)==false)
{
return false;
}
if (_bitset.test(pos2) == false)
{
return false;
}
if (_bitset.test(pos3) == false)
{
return false;
}
}
private:
static const int _X = 6;
bitset<N*_X> _bitset;
};
布隆过滤器通过封装位图,并将数据通过hash函数转换为无符号整形,之后才会存储进入位图中,但是这样通过转换的数据一定会发送hash冲突,整数时可以开辟整数的42亿9千万的bit位,但例如字符串这样的数有无限的可能,并且例如abc,cab这样的字符串他们通过hash函数转换的整数也可能是冲突的,这就可能会导致,一个字符串明明不存在但过滤器说它存在这样的情况出现;
这就是布隆过滤器无法准确判断数据是否存在,但可以准确判断数据是否不存在的原因;
为了减少这样的误判率,可以通过增加hash函数的方式来提高准确率;
这用可以提高准确率;
但是这样增加hash函数是有一些要求的;
通过从网上学习到的一篇博客:
详解布隆过滤器的原理,使用场景和注意事项 - 知乎 (zhihu.com)
我们可以知道:(下面两张图片是通过上面博客中图片进行编辑的)
并且hash函数的个数与准确率也是有很大关联的:
所以如果想要准确一些的判断,需要这样来控制容器;
注意
布隆过滤器不支持直接删除,由于无法准确判断数据是否存在所以无法支持直接删除;引用计数似乎也不可以,因为也无法判断这个数是否存在,那就不好--计数;
接下来我们来看看位图与布隆过滤器的题目:
题目
哈希切割
首先文件太大了,我们就将这个文件切分成一个个小文件,切分成一个个小文件也是有要切的;如果随机平均切分的话,那我们比对数据时,就得将一个文件中的数据存储进内存中,然后再比对剩下的一个个文件,这样的效率非常低;
所以为了提高效率,我们可以进行hash切割;名字叫hash切割,但是其实就是我们前面hash表中的hash函数,我们通过hash函数计算出相同的数据放在一个小文件中,就是前面的开散列一样,这样只要是相似相同的数据就会在一个文件中,我们就不需要一一比较了,只需要自己文件中的数据进行处理即可;
如果hash切割的文件太大,我们可以先进行载入unordered_set(hash表)如果hash表段错误抛出异常就代表,相似数据太多了,如果是大量相同数据,就会直接插入成功比较成功;
这就是hash切割;
位图应用
方法1:用两个位图分别存储两个文件中的数据,再来对比两个位图中数据;
方法2:将大的文件存入位图中,再一个个比较小的文件中的数据,如果小的文件中的数据存在于位图中则是交集并reset此数据防止再次被当作交集;
两种方法适用场景不同,需灵活使用;
以上就是本篇的全部内容;