💓博主CSDN主页:麻辣韭菜💓
⏩专栏分类:C++知识分享⏪
🚚代码仓库:C++高阶🚚
🌹关注我🫵带你学习更多C++知识
🔝🔝
前言
1. unordered系列关联式容器
1.1 unordered_map
1.1.1 unordered_map的文档介绍
1.1.2 unordered_map的接口说明
插入与访问元素
删除与查找元素
其他操作
默认构造函数
默认析构函数
默认拷贝和移动构造函数,以及赋值运算符
迭代器类型
迭代器获取
迭代器使用示例
1.2 unordered_set
插入与访问元素
删除元素
其他操作
默认构造函数
列表初始化构造函数
区间构造函数
拷贝构造函数
移动构造函数
迭代器类型
迭代器获取
迭代器使用示例
注意事项
2. 底层结构
2.1 哈希概念
2.2 哈希冲突
2.3 哈希函数
1. 直接定址法--(常用)
闭散列
闭散列 代码实现
开散列
开散列代码实现
开散列增容
开散列的思考
开散列与闭散列比较
3.哈希的应用
3.1 位图
3.1.1 位图概念
位图的实现
3.1.3 位图的应用
3.2 布隆过滤器
3.2.1布隆过滤器提出
3.2.2布隆过滤器概念
布隆过滤器代码实现
3.2.4 布隆过滤器的查找
3.2.5 布隆过滤器删除
3.2.6 布隆过滤器优点
3.2.7 布隆过滤器缺陷
4. 海量数据面试题
前言
如果你会用map和set,那么你就会用哈希表这种数据结构底层实现的unordered_map 和unordered_set。看名字unordered无序,而map和set是有序的。数据结构也是不同,map和set是搜索二叉树,而unordered_map 和unordered_set是哈希表(哈希桶)。
1. unordered系列关联式容器
1.1 unordered_map
1.1.1 unordered_map的文档介绍
1.1.2 unordered_map的接口说明
插入与访问元素
-
operator[]
- 通过键访问或插入元素,并返回对应的值
- 如果键存在,则返回对应值的引用;如果不存在,则插入新元素并返回默认构造的值
std::unordered_map<std::string, int> myMap; myMap["one"] = 1; // 插入键值对 int value = myMap["one"]; // 访问键对应的值
-
insert
- 插入指定键值对
- 返回一个 pair 对象,其
.second
成员指示插入是否成功,.first
指向已存在的元素(如果有)
std::unordered_map<std::string, int> myMap; auto result = myMap.insert(std::make_pair("two", 2)); // 插入并获取结果 if (result.second) {std::cout << "Insertion successful!" << std::endl; }
删除与查找元素
-
erase
- 删除指定键对应的元素
std::unordered_map<std::string, int> myMap = {{"one", 1}, {"two", 2}}; myMap.erase("two"); // 删除键为 "two" 的元素
-
find
- 查找指定键的元素,返回指向该元素的迭代器
- 如果未找到,则返回指向
end()
的迭代器
std::unordered_map<std::string, int> myMap = {{"one", 1}, {"two", 2}}; auto it = myMap.find("one"); if (it != myMap.end()) {std::cout << "Found: " << it->second << std::endl; }
其他操作
-
clear
- 清空哈希表,移除所有元素
std::unordered_map<std::string, int> myMap = {{"one", 1}, {"two", 2}}; myMap.clear(); // 清空哈希表
-
size
- 返回哈希表中元素的数量
std::unordered_map<std::string, int> myMap = {{"one", 1}, {"two", 2}}; std::cout << "Size: " << myMap.size() << std::endl;
默认构造函数
如果我们创建一个没有指定显式构造函数参数的 std::unordered_map
对象,那么编译器会为其生成默认构造函数。这个默认构造函数会创建一个空的哈希表。
std::unordered_map<int, std::string> myMap; // 调用默认构造函数创建空的哈希表
默认析构函数
当 std::unordered_map
对象超出其作用域,或者通过 delete
运算符显式销毁时,编译器会为其生成默认析构函数。这个默认析构函数会释放哈希表占用的内存空间。
{std::unordered_map<int, std::string> myMap; // 对象超出作用域,会调用默认析构函数自动释放内存
} // myMap 被销毁
默认拷贝和移动构造函数,以及赋值运算符
std::unordered_map
也会涉及到默认的拷贝和移动构造函数,以及拷贝和移动赋值运算符。这些默认实现会对键值对进行浅复制或移动操作。
std::unordered_map<int, std::string> myMap1 = {{1, "one"}, {2, "two"}};
std::unordered_map<int, std::string> myMap2 = myMap1; // 调用默认的拷贝构造函数
std::unordered_map<int, std::string> myMap3 = std::move(myMap1); // 调用默认的移动构造函数
myMap3 = myMap2; // 调用默认的拷贝赋值运算符
myMap3 = std::move(myMap2); // 调用默认的移动赋值运算符
迭代器类型
-
iterator
- 用于遍历可修改
std::unordered_map
中的元素
- 用于遍历可修改
-
const_iterator
- 用于遍历
const
修饰的std::unordered_map
,其指向的元素不可被修改
- 用于遍历
迭代器获取
std::unordered_map<int, std::string> myMap = {{1, "one"}, {2, "two"}, {3, "three"}};// 获取起始迭代器
auto it = myMap.begin(); // 返回指向第一个元素的迭代器
auto cit = myMap.cbegin(); // 返回指向第一个元素的 const 迭代器// 获取结束迭代器
auto end = myMap.end(); // 返回指向最后一个元素之后位置的迭代器
auto cend = myMap.cend(); // 返回指向最后一个元素之后位置的 const 迭代器
迭代器使用示例
使用迭代器可以遍历 std::unordered_map
中的元素,并访问每个元素的键和值。
std::unordered_map<int, std::string> myMap = {{1, "one"}, {2, "two"}, {3, "three"}};// 遍历并打印键值对
for (auto it = myMap.begin(); it != myMap.end(); ++it) {std::cout << "Key: " << it->first << ", Value: " << it->second << std::endl;
}
1.2 unordered_set
插入与访问元素
-
insert
- 将新元素插入到无序集合中
- 返回一个 pair 对象,包含一个迭代器指向新元素的位置以及一个 bool 值,指示是否插入成功
std::unordered_set<int> mySet; auto result = mySet.insert(42); if (result.second) {std::cout << "Insertion successful!" << std::endl; }
-
emplace
- 在集合中构造一个新元素
- 返回一个 pair 对象,其中
.first
是迭代器指向新元素的位置,.second
是指示是否插入成功的 bool 值
std::unordered_set<std::string> mySet; auto result = mySet.emplace("hello"); if (result.second) {std::cout << "Insertion successful!" << std::endl; }
-
find
- 查找集合中是否存在指定的元素
- 返回指向匹配元素位置的迭代器,如果没找到则返回指向
end()
的迭代器
std::unordered_set<int> mySet = {1, 2, 3}; auto it = mySet.find(2); if (it != mySet.end()) {std::cout << "Found: " << *it << std::endl; }
删除元素
-
erase
- 从集合中移除指定值或指定位置的元素,或者指定范围的元素
std::unordered_set<int> mySet = {1, 2, 3}; mySet.erase(2); // 移除值为 2 的元素
其他操作
-
clear
- 清空集合,移除所有元素
std::unordered_set<int> mySet = {1, 2, 3}; mySet.clear(); // 清空集合
-
size
- 返回集合中元素的数量
std::unordered_set<int> mySet = {1, 2, 3}; std::cout << "Size: " << mySet.size() << std::endl;
默认构造函数
std::unordered_set<T> mySet;
这是 std::unordered_set
的默认构造函数,创建一个空的无序集合。
列表初始化构造函数
std::unordered_set<T> mySet = {val1, val2, ...};
使用大括号进行列表初始化,可以在创建无序集合的同时插入元素。
区间构造函数
std::unordered_set<T> mySet(otherSet.begin(), otherSet.end());
使用另一个无序集合的迭代器范围进行构造。复制范围内的元素到新的无序集合。
拷贝构造函数
std::unordered_set<T> mySet(otherSet);
通过另一个无序集合进行拷贝构造,复制另一个无序集合的内容到新的无序集合。
移动构造函数
std::unordered_set<T> mySet(std::move(otherSet));
通过移动语义实现的构造函数,将另一个无序集合的内容移动到新的无序集合中,另一个无序集合会变为空。
迭代器类型
-
iterator
- 用于遍历可修改
std::unordered_set
中的元素
- 用于遍历可修改
-
const_iterator
- 用于遍历
const
修饰的std::unordered_set
,其指向的元素不可被修改
- 用于遍历
迭代器获取
std::unordered_set<int> mySet = {1, 2, 3, 4};// 获取起始迭代器
auto it = mySet.begin(); // 返回指向第一个元素的迭代器
auto cit = mySet.cbegin(); // 返回指向第一个元素的 const 迭代器// 获取结束迭代器
auto end = mySet.end(); // 返回指向最后一个元素之后位置的迭代器
auto cend = mySet.cend(); // 返回指向最后一个元素之后位置的 const 迭代器
迭代器使用示例
使用迭代器可以遍历 std::unordered_set
中的元素。
std::unordered_set<int> mySet = {1, 2, 3};// 遍历并打印元素
for (auto it = mySet.begin(); it != mySet.end(); ++it) {std::cout << "Element: " << *it << std::endl;
}
注意事项
在 C++11 及以上版本,也可以使用范围-based for 循环来遍历 std::unordered_set
:
for (const auto& element : mySet) {std::cout << "Element: " << element << std::endl;
}
2. 底层结构
2.1 哈希概念
2.2 哈希冲突
2.3 哈希函数
1. 直接定址法--(常用)
前面说的如果取模的余数和之前已经插入的数的取模余数是相等的,那么会出现哈希冲突,解决哈希冲突两种常用的方法:闭散列和开散列我们先用闭散列
闭散列
闭散列 代码实现
哈希结构
#pragma once
#include <vector>
enum State
{MEPTY, //空EXIST, //存在DELETE //删除
};template <class K, class V>
struct hashData
{pair<K, V> _kv;State _state = MPETY;
};template <class K, class V>
class hashTable
{typedef hashData<K, V> Node;
private:vector<Node> _tables;size_t n;//记录元素个数
};
插入函数
bool Insert(const pair<K, V>& kv){if (Find(kv.first))return false;//当负载因子为大于0.7时就扩容if (_tables.size() == 0 || _n * 10 / _tables.size() >= 7){size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;hashTable <K, V> newht;newht._tables.resize(newsize);//遍历旧表,重新映射到新表for (auto& data : _tables){if (data._state == EXIST){newht.Insert(data._kv);}}_tables.swap(newht._tables);}size_t hashi = kv.first % _tables.size();//线性探测size_t i = 1;size_t index = hashi;while (_tables[index]._state == EXIST){index = hashi + i;index %= _tables.size();++i;}_tables[index]._kv = kv;_tables[index]._state = EXIST;_n++;return true;}
这里解释一下关于扩容后为什么要新创建哈希桶,因为扩容后,映射位置变了,假设以前的size为10 扩容后为20,那之前的插入的值就找不到了,所以我们需要重新创建一个哈希桶,然后根据映射位置重新插入到新的哈希桶中。最后再交换。
查找函数
Node* Find(const K& key){if (_tables.size() == 0){return false;}size_t hashi = key % _tables.size();// 线性探测size_t i = 1;size_t index = hashi;while (_tables[index]._state != EMPTY){if (_tables[index]._state == EXIST&& _tables[index]._kv.first == key){return &_tables[index];}index = hashi + i;index %= _tables.size();++i;// 如果已经查找一圈,那么说明全是存在+删除if (index == hashi){break;}}return nullptr;}
删除函数
bool Erase(const K& key){Node* ret = Find(key);if (ret){ret->_state = DELETE;--_n;return true;}else{return false;}}
这里删除函数并不是真正意义上的删除,如果真的删除了,那么其他没有被删除的数都会受到影响,所以我们标记这个映射位置为DELETE,等下次插入的数映射位置和这个位置一样时,直接覆盖。
开散列
从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素。
开散列代码实现
namespace HashBucket
{template<class K, class V>struct HashNode{HashNode<K, V>* _next;pair<K, V> _kv;HashNode(const pair<K, V>& kv):_next(nullptr), _kv(kv){}};template<class K, class V>class HashTable{typedef HashNode<K, V> Node;public:~HashTable(){for (auto& cur : _tables){while (cur){Node* next = cur->_next;delete cur;cur = next;}cur = nullptr;}}Node* Find(const K& key){if (_tables.size() == 0)return nullptr;size_t hashi = 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){size_t hashi = key % _tables.size();Node* prev = nullptr;Node* cur = _tables[hashi];while (cur){if (cur->_kv.first == key){if (prev == nullptr){_tables[hashi] = cur->_next;}else{prev->_next = cur->_next;}delete cur;return true;}else{prev = cur;cur = cur->_next;}}return false;}bool Insert(const pair<K, V>& kv){if (Find(kv.first)){return false;}// 负载因因子==1时扩容if (_n == _tables.size()){/*size_t newsize = _tables.size() == 0 ? 10 : _tables.size()*2;HashTable<K, V> newht;newht.resize(newsize);for (auto cur : _tables){while (cur){newht.Insert(cur->_kv);cur = cur->_next;}}_tables.swap(newht._tables);*/size_t newsize = _tables.size() == 0 ? 10 : _tables.size() * 2;vector<Node*> newtables(newsize, nullptr);//for (Node*& cur : _tables)for (auto& cur : _tables){while (cur){Node* next = cur->_next;size_t hashi = cur->_kv.first % newtables.size();// 头插到新表cur->_next = newtables[hashi];newtables[hashi] = cur;cur = next;}}_tables.swap(newtables);}size_t hashi = kv.first % _tables.size();// 头插Node* newnode = new Node(kv);newnode->_next = _tables[hashi];_tables[hashi] = newnode;++_n;return true;}private:vector<Node*> _tables; // 指针数组size_t _n = 0; // 存储有效数据个数};
开散列增容
桶的个数是一定的,随着元素的不断插入,每个桶中元素的个数不断增多,极端情况下,可能会导致一个桶中链表节点非常多,会影响的哈希表的性能,因此在一定条件下需要对哈希表进行增容,那该条件怎么确认呢?开散列最好的情况是:每个哈希桶中刚好挂一个节点,再继续插入元素时,每一次都会发生哈希冲突,因此,在元素个数刚好等于桶的个数时,可以给哈希表增容。
void _CheckCapacity()
{size_t bucketCount = BucketCount();if(_size == bucketCount){HashBucket<V, HF> newHt(bucketCount);for(size_t bucketIdx = 0; bucketIdx < bucketCount; ++bucketIdx){PNode pCur = _ht[bucketIdx];while(pCur){// 将该节点从原哈希表中拆出来_ht[bucketIdx] = pCur->_pNext;// 将该节点插入到新哈希表中size_t bucketNo = newHt.HashFunc(pCur->_data);pCur->_pNext = newHt._ht[bucketNo];newHt._ht[bucketNo] = pCur;pCur = _ht[bucketIdx];}}newHt._size = _size;this->Swap(newHt);}
}
开散列的思考
只能存储key为整形的元素,其他类型怎么解决?
// 哈希函数采用处理余数法,被模的key必须要为整形才可以处理,此处提供将key转化为
//整形的方法
// 整形数据不需要转化
template<class T>
class DefHashF
{
public:size_t operator()(const T& val){return val;}
};
// key为字符串类型,需要将其转化为整形
class Str2Int
{
public:size_t operator()(const string& s){const char* str = s.c_str();unsigned int seed = 131; // 31 131 1313 13131 131313unsigned int hash = 0;while (*str){hash = hash * seed + (*str++);}return (hash & 0x7FFFFFFF);}
};
// 为了实现简单,此哈希表中我们将比较直接与元素绑定在一起
template<class V, class HF>
class HashBucket
{// ……
private:size_t HashFunc(const V& data){return HF()(data.first)%_ht.capacity();}
};
除留余数法,最好模一个素数,如何每次快速取一个类似两倍关系的素数?
size_t GetNextPrime(size_t prime){const int PRIMECOUNT = 28;static const size_t primeList[PRIMECOUNT] ={53ul, 97ul, 193ul, 389ul, 769ul,1543ul, 3079ul, 6151ul, 12289ul, 24593ul,49157ul, 98317ul, 196613ul, 393241ul, 786433ul,1572869ul, 3145739ul, 6291469ul, 12582917ul, 25165843ul,50331653ul, 100663319ul, 201326611ul, 402653189ul, 805306457ul,1610612741ul, 3221225473ul, 4294967291ul};size_t i = 0;for (; i < PRIMECOUNT; ++i){if (primeList[i] > prime)return primeList[i];}
开散列与闭散列比较
应用链地址法处理溢出,需要增设链接指针,似乎增加了存储开销 。事实上:由于开地址法必须保持大量的空闲空间以确保搜索效率,如二次探查法要求装载因子 a <=0.7 ,而表项所占空间又比指针大的多,所以使用链地址法反而比开地址法节省存储空间。
3.哈希的应用
3.1 位图
3.1.1 位图概念
位图的实现
#pragma once#include <vector>
#include <string>
#include <time.h>template<size_t N>
class bitset
{
public:bitset(){_bits.resize(N/8 + 1, 0);}void set(size_t x){size_t i = x / 8;size_t j = x % 8;_bits[i] |= (1 << j);}void reset(size_t x){size_t i = x / 8;size_t j = x % 8;_bits[i] &= ~(1 << j);}bool test(size_t x){size_t i = x / 8;size_t j = x % 8;return _bits[i] & (1 << j);}private:vector<char> _bits;
};void test_bitset1()
{bitset<100> bs;bs.set(10);bs.set(11);bs.set(15);cout << bs.test(10) << endl;cout << bs.test(15) << endl;bs.reset(10);cout << bs.test(10) << endl;cout << bs.test(15) << endl;bs.reset(10);bs.reset(15);cout << bs.test(10) << endl;cout << bs.test(15) << endl;
}
3.1.3 位图的应用
3.2 布隆过滤器
3.2.1布隆过滤器提出
3.2.2布隆过滤器概念
布隆过滤器代码实现
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;}
};// N最多会插入key数据的个数
template<size_t N,
class K = string,
class Hash1 = BKDRHash,
class Hash2 = APHash,
class Hash3 = DJBHash>
class BloomFilter
{
public:void set(const K& key){size_t len = N*_X;size_t hash1 = Hash1()(key) % len;_bs.set(hash1);size_t hash2 = Hash2()(key) % len;_bs.set(hash2);size_t hash3 = Hash3()(key) % len;_bs.set(hash3);//cout << hash1 << " " << hash2 << " " << hash3 << " " << endl << endl;}bool test(const K& key){size_t len = N*_X;size_t hash1 = Hash1()(key) % len;if (!_bs.test(hash1)){return false;}size_t hash2 = Hash2()(key) % len;if (!_bs.test(hash2)){return false;}size_t hash3 = Hash3()(key) % len;if (!_bs.test(hash3)){return false;}// 在 不准确的,存在误判// 不在 准确的return true;}
private:static const size_t _X = 6;bitset<N*_X> _bs;
};
3.2.4 布隆过滤器的查找
3.2.5 布隆过滤器删除
3.2.6 布隆过滤器优点
3.2.7 布隆过滤器缺陷
4. 海量数据面试题