C++ 哈希

💓博主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系列关联式容器

C++98 中, STL 提供了底层为红黑树结构的一系列关联式容器,在查询时效率可达到 $log_2
N$ ,即最差情况下需要比较红黑树的高度次,当树中的节点非常多时,查询效率也不理想。最好
的查询是,进行很少的比较次数就能够将元素找到,因此在 C++11 中, STL 又提供了 4
unordered 系列的关联式容器,这四个容器与红黑树结构的关联式容器使用方式基本类似,只是
其底层结构不同,本文中只对 unordered_map unordered_set 进行介绍。

1.1 unordered_map

1.1.1 unordered_map的文档介绍

unordered_map 在线文档说明
1. unordered_map 是存储 <key, value> 键值对的关联式容器,其允许通过 keys 快速的索引到与
其对应的 value
2. unordered_map 中,键值通常用于惟一地标识元素,而映射值是一个对象,其内容与此
键关联。键和映射值的类型可能不同。
3. 在内部 ,unordered_map 没有对 <kye, value> 按照任何特定的顺序排序 , 为了能在常数范围内
找到 key 所对应的 value unordered_map 将相同哈希值的键值对放在相同的桶中。
4. unordered_map 容器通过 key 访问单个元素要比 map 快,但它通常在遍历元素子集的范围迭
代方面效率较低。
5. unordered_maps 实现了直接访问操作符 (operator[]) ,它允许使用 key 作为参数直接访问
value
6. 它的迭代器至少是前向迭代器。

1.1.2 unordered_map的接口说明

插入与访问元素

  1. operator[]

    • 通过键访问或插入元素,并返回对应的值
    • 如果键存在,则返回对应值的引用;如果不存在,则插入新元素并返回默认构造的值
    std::unordered_map<std::string, int> myMap;
    myMap["one"] = 1;  // 插入键值对
    int value = myMap["one"];  // 访问键对应的值
    
  2. 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;
    }
    

删除与查找元素

  1. erase

    • 删除指定键对应的元素
    std::unordered_map<std::string, int> myMap = {{"one", 1}, {"two", 2}};
    myMap.erase("two");  // 删除键为 "two" 的元素
    
  2. 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;
    }
    

其他操作

  1. clear

    • 清空哈希表,移除所有元素
    std::unordered_map<std::string, int> myMap = {{"one", 1}, {"two", 2}};
    myMap.clear();  // 清空哈希表
    
  2. 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);  // 调用默认的移动赋值运算符

迭代器类型

  1. iterator

    • 用于遍历可修改 std::unordered_map 中的元素
  2. 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  

参见 unordered_set 在线文档说明

插入与访问元素

  1. insert

    • 将新元素插入到无序集合中
    • 返回一个 pair 对象,包含一个迭代器指向新元素的位置以及一个 bool 值,指示是否插入成功
    std::unordered_set<int> mySet;
    auto result = mySet.insert(42);
    if (result.second) {std::cout << "Insertion successful!" << std::endl;
    }
    
  2. 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;
    }
    
  3. 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;
    }
    

删除元素

  1. erase

    • 从集合中移除指定值或指定位置的元素,或者指定范围的元素
    std::unordered_set<int> mySet = {1, 2, 3};
    mySet.erase(2);  // 移除值为 2 的元素
    

其他操作

  1. clear

    • 清空集合,移除所有元素
    std::unordered_set<int> mySet = {1, 2, 3};
    mySet.clear();  // 清空集合
    
  2. 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));

通过移动语义实现的构造函数,将另一个无序集合的内容移动到新的无序集合中,另一个无序集合会变为空。

迭代器类型

  1. iterator

    • 用于遍历可修改 std::unordered_set 中的元素
  2. 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. 底层结构 

unordered 系列的关联式容器之所以效率比较高,是因为其底层使用了哈希结构。

2.1 哈希概念  

顺序结构以及平衡树 中,元素关键码与其存储位置之间没有对应的关系,因此在 查找一个元素
时,必须要经过关键码的多次比较 顺序查找时间复杂度为 O(N) ,平衡树中为树的高度,即
O($log_2 N$) ,搜索的效率取决于搜索过程中元素的比较次数。
理想的搜索方法:可以 不经过任何比较,一次直接从表中得到要搜索的元素
如果构造一种存储结构,通过某种函数 (hashFunc) 使元素的存储位置与它的关键码之间能够建立
一一映射的关系,那么在查找时通过该函数可以很快找到该元素
当向该结构中:
插入元素
根据待插入元素的关键码,以此函数计算出该元素的存储位置并按此位置进行存放
搜索元素
对元素的关键码进行同样的计算,把求得的函数值当做元素的存储位置,在结构中按此位置
取元素比较,若关键码相等,则搜索成功
该方式即为哈希 ( 散列 ) 方法, 哈希方法中使用的转换函数称为哈希 ( 散列 ) 函数,构造出来的结构称
为哈希表 (Hash Table)( 或者称散列表 )
例如:数据集合 {1 7 6 4 5 9}
哈希函数设置为: hash(key) = key % capacity ; capacity为存储元素底层空间总的大小。

 

用该方法进行搜索不必进行多次关键码的比较,因此搜索的速度比较快
问题:按照上述哈希方式,向集合中插入元素 99 ,会出现什么问题?

2.2 哈希冲突

对于两个数据元素的关键字 $k_i$ $k_j$(i != j) ,有 $k_i$ != $k_j$ ,但有: Hash($k_i$) ==
Hash($k_j$) ,即: 不同关键字通过相同哈希哈数计算出相同的哈希地址,该种现象称为哈希冲突
或哈希碰撞
把具有不同关键码而具有相同哈希地址的数据元素称为 同义词
发生哈希冲突该如何处理呢?

2.3 哈希函数

引起哈希冲突的一个原因可能是: 哈希函数设计不够合理
哈希函数设计原则
哈希函数的定义域必须包括需要存储的全部关键码,而如果散列表允许有 m 个地址时,其值
域必须在 0 m-1 之间
哈希函数计算出来的地址能均匀分布在整个空间中
哈希函数应该比较简单
常见哈希函数

1. 直接定址法--(常用)

取关键字的某个线性函数为散列地址: Hash Key = A*Key + B
优点:简单、均匀
缺点:需要事先知道关键字的分布情况
使用场景:适合查找比较小且连续的情况
前面说的如果取模的余数和之前已经插入的数的取模余数是相等的,那么会出现哈希冲突,
解决哈希冲突两种常用的方法:闭散列和开散列
我们先用闭散列
闭散列 
线性探测:从发生冲突的位置开始,依次向后探测,直到寻找到下一个空位置为止
插入
通过哈希函数获取待插入元素在哈希表中的位置
如果该位置中没有元素则直接插入新元素,如果该位置中有元素发生哈希冲突,
使用线性探测找到下一个空位置,插入新元素

 

删除
采用闭散列处理哈希冲突时,不能随便物理删除哈希表中已有的元素,若直接删除元素
会影响其他元素的搜索 。比如删除元素 4 ,如果直接删除掉, 44 查找起来可能会受影
响。因此 线性探测采用标记的伪删除法来删除一个元素

 闭散列 代码实现

哈希结构

#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,等下次插入的数映射位置和这个位置一样时,直接覆盖。

研究表明: 当表的长度为质数且表装载因子 a 不超过 0.5 时,新的表项一定能够插入,而且任
何一个位置都不会被探查两次。因此只要表中有一半的空位置,就不会存在表满的问题。在
搜索时可以不考虑表装满的情况,但在插入时必须确保表的装载因子 a 不超过 0.5 如果超出
必须考虑增容。
因此:闭散列最大的缺陷就是空间利用率比较低,这也是哈希的缺陷。

开散列
1. 开散列概念
开散列法又叫链地址法 ( 开链法 ) ,首先对关键码集合用散列函数计算散列地址,具有相同地
址的关键码归于同一子集合,每一个子集合称为一个桶,各个桶中的元素通过一个单链表链
接起来,各链表的头结点存储在哈希表中

 从上图可以看出,开散列中每个桶中放的都是发生哈希冲突的元素

开散列代码实现  
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 位图概念 

1. 面试题
40 亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在
40 亿个数中。【腾讯】
1. 遍历,时间复杂度 O(N)
2. 排序 (O(NlogN)) ,利用二分查找 : logN
3. 位图解决
数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一
个二进制比特位来代表数据是否存在的信息,如果二进制比特位为 1 ,代表存在,为 0
代表不存在。比如:

2. 位图概念
所谓位图,就是用每一位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用
来判断某个数据存不存在的。

位图的实现  

#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 位图的应用

1. 快速查找某个数据是否在一个集合中
2. 排序 + 去重
3. 求两个集合的交集、并集等
4. 操作系统中磁盘块标记

 3.2 布隆过滤器

3.2.1布隆过滤器提出

我们在使用新闻客户端看新闻时,它会给我们不停地推荐新的内容,它每次推荐时要去重,去掉
那些已经看过的内容。问题来了,新闻客户端推荐系统如何实现推送去重的? 用服务器记录了用
户看过的所有历史记录,当推荐系统推荐新闻时会从每个用户的历史记录里进行筛选,过滤掉那
些已经存在的记录。 如何快速查找呢?
1. 用哈希表存储用户记录,缺点:浪费空间
2. 用位图存储用户记录,缺点:位图一般只能处理整形,如果内容编号是字符串,就无法处理
了。
3. 将哈希与位图结合,即布隆过滤器

3.2.2布隆过滤器概念

布隆过滤器是 由布隆( Burton Howard Bloom )在 1970 年提出的 一种紧凑型的、比较巧妙的
率型数据结构 ,特点是 高效地插入和查询,可以用来告诉你 某样东西一定不存在或者可能存
,它是用多个哈希函数,将一个数据映射到位图结构中。此种方式 不仅可以提升查询效率,也
可以节省大量的内存空间

 

 

 布隆过滤器代码实现

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 布隆过滤器的查找

布隆过滤器的思想是将一个元素用多个哈希函数映射到一个位图中,因此被映射到的位置的比特
位一定为 1 。所以可以按照以下方式进行查找: 分别计算每个哈希值对应的比特位置存储的是否为
零,只要有一个为零,代表该元素一定不在哈希表中,否则可能在哈希表中
注意:布隆过滤器如果说某个元素不存在时,该元素一定不存在,如果该元素存在时,该元素可
能存在,因为有些哈希函数存在一定的误判。
比如:在布隆过滤器中查找 "alibaba" 时,假设 3 个哈希函数计算的哈希值为: 1 3 7 ,刚好和其
他元素的比特位重叠,此时布隆过滤器告诉该元素存在,但实该元素是不存在的。

3.2.5 布隆过滤器删除

布隆过滤器不能直接支持删除工作,因为在删除一个元素时,可能会影响其他元素。
比如:删除上图中 "tencent" 元素,如果直接将该元素所对应的二进制比特位置 0 “baidu” 元素也
被删除了,因为这两个元素在多个哈希函数计算出的比特位上刚好有重叠。
一种支持删除的方法:将布隆过滤器中的每个比特位扩展成一个小的计数器,插入元素时给 k 个计
数器 (k 个哈希函数计算出的哈希地址 ) 加一,删除元素时,给 k 个计数器减一,通过多占用几倍存储
空间的代价来增加删除操作。
缺陷:
1. 无法确认元素是否真正在布隆过滤器中
2. 存在计数回绕

3.2.6 布隆过滤器优点

1. 增加和查询元素的时间复杂度为 :O(K), (K 为哈希函数的个数,一般比较小 ) ,与数据量大小无
2. 哈希函数相互之间没有关系,方便硬件并行运算
3. 布隆过滤器不需要存储元素本身,在某些对保密要求比较严格的场合有很大优势
4. 在能够承受一定的误判时,布隆过滤器比其他数据结构有这很大的空间优势
5. 数据量很大时,布隆过滤器可以表示全集,其他数据结构不能
6. 使用同一组散列函数的布隆过滤器可以进行交、并、差运算

3.2.7 布隆过滤器缺陷

1. 有误判率,即存在假阳性 (False Position) ,即不能准确判断元素是否在集合中 ( 补救方法:再
建立一个白名单,存储可能会误判的数据 )
2. 不能获取元素本身
3. 一般情况下不能从布隆过滤器中删除元素
4. 如果采用计数方式删除,可能会存在计数回绕问题

 4. 海量数据面试题

哈希切割
给一个超过 100G 大小的 log file, log 中存着 IP 地址 , 设计算法找到出现次数最多的 IP 地址?  
与上题条件相同,如何找到 top K IP ?如何直接用 Linux 系统命令实现?

 

位图应用
1. 给定 100 亿个整数,设计算法找到只出现一次的整数?

2. 给两个文件,分别有 100 亿个整数,我们只有 1G 内存,如何找到两个文件交集?

3. 位图应用变形: 1 个文件有 100 亿个 int 1G 内存,设计算法找到出现次数不超过 2 次的所有整

 

布隆过滤器
1. 给两个文件,分别有 100 亿个 query ,我们只有 1G 内存,如何找到两个文件交集?分别给出
精确算法和近似算法
2. 如何扩展 BloomFilter 使得它支持删除元素的操作

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/4082.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

必应bing国内广告开户注册教程!

今天搜索引擎广告成为企业推广产品与服务、提升品牌知名度的重要渠道之一。作为全球第二大搜索引擎&#xff0c;必应Bing凭借其高质量的用户群体和广泛的国际覆盖&#xff0c;为广告主提供了独特的市场机遇。在中国&#xff0c;虽然必应的市场份额相对较小&#xff0c;但对于寻…

磁密固定下的三次谐波与电压谐波的关系

同相位或者相位差为180的情况下&#xff0c;磁通密度三次谐波含量占比 α \alpha α&#xff0c;则电压三次谐波含量占比为 3 α 3\alpha 3α 同相位&#xff0c;磁通密度三次谐波含量占比 α \alpha α情况下&#xff0c; B B 0 sin ⁡ ( ω t ) α B 0 sin ⁡ ( 3 ω t )…

航空企业数字化解决方案(207页PPT)

一、资料描述 航空企业数字化解决方案是一项针对航空公司在数字化转型过程中所面临挑战的全面应对策略&#xff0c;旨在通过先进的信息技术提升航空企业的运营效率、客户服务水平以及市场竞争力。这份207页的PPT详细介绍了航空企业数字化的各个方面&#xff0c;包括关键技术的…

Web3技术解析:区块链在去中心化应用中的角色

引言 在过去几年中&#xff0c;Web3技术已经成为了互联网领域的一个热门话题。作为区块链技术的延伸&#xff0c;Web3不仅仅是数字货币的代名词&#xff0c;更是一个能够为各种应用提供去中心化解决方案的强大工具。本文将深入探讨区块链在Web3去中心化应用中的关键角色&#…

ubuntu查看opencveigen

ubuntu查看opencv&eigen&cmake版本的方法 eigen eigen版本号在/usr/include/eigen3/Eigen/src/Core/util/Macros.h文件中&#xff0c;下图代表版本3.3.7 opencv版本 pkg-config --modversion opencv4也可能最后的字符串是opencv2&#xff0c;opencv

W801学习笔记十二:掌机进阶V3版本之驱动(PSRAM/SD卡)

本次升级添加了两个模块&#xff0c;现在要把他们驱动起来。 一&#xff1a;PSRAM 使用SDK自带的驱动&#xff0c;我们只需要写一个初始化函数&#xff0c;并在其中添加一些自检代码。 void psram_heap_init(){wm_psram_config(0);//实际使用的psram管脚选择0或者1&#xff…

Java学习路线及自我规划

荒废了一段时间&#xff0c;这段时间的总结开始了JavaWeb的学习但是困难重重&#xff0c;例如Maven&#xff0c;Vue的路由等&#xff0c;所以我反省了一段时间&#xff0c;因为基础薄弱&#xff0c;加之学习的资源是速成视频&#xff0c;导致大厦将倾的局面&#xff08;也算不上…

RabbitMQ工作模式(5) - 主题模式

概念 主题模式&#xff08;Topic Exchange&#xff09;是 RabbitMQ 中一种灵活且强大的消息传递模式&#xff0c;它允许生产者根据消息的特定属性将消息发送到一个交换机&#xff0c;并且消费者可以根据自己的需求来接收感兴趣的消息。主题交换机根据消息的路由键和绑定队列的路…

盲人地图使用的革新体验:助力视障人士独立、安全出行

在我们日常生活中&#xff0c;地图导航已经成为不可或缺的出行工具。而对于盲人群体来说&#xff0c;盲人地图使用这一课题的重要性不言而喻&#xff0c;它不仅关乎他们的出行便利性&#xff0c;更是他们追求生活独立与品质的重要一环。 近年来&#xff0c;一款名为蝙蝠…

echarts地图叠加百度地图底板实现数据可视化

这里写自定义目录标题 echarts地图叠加百度地图实现数据可视化echarts地图叠加百度地图实现数据可视化 实现数据可视化时,个别情况下需要在地图上实现数据的可视化,echarts加载geojson数据可以实现以地图形式展示数据,例如分层设色或者鼠标hover展示指标值,但如果要将echa…

运筹系列91:vrp算法包PyVRP

1. 介绍 PyVRP使用HGS&#xff08;hybrid genetic search&#xff09;算法求解VRP类问题。在benchmark上的评测结果如下&#xff0c;看起来还不错&#xff1a; 2. 使用例子 2.1 CVRP COORDS [(456, 320), # location 0 - the depot(228, 0), # location 1(912, 0), …

通往AGI路上,DPU将如何构建生成式AI时代的坚实算力基石?

4月19日&#xff0c;在以“重构世界 奔赴未来”为主题的2024中国生成式AI大会上&#xff0c;中科驭数作为DPU新型算力基础设施代表&#xff0c;受邀出席了中国智算中心创新论坛&#xff0c;发表了题为《以网络为中心的AI算力底座构建之路》主题演讲&#xff0c;勾勒出在通往AGI…

Xcode 15构建问题

构建时出现的异常&#xff1a; 解决方式&#xff1a; 将ENABLE_USER_SCRIPT_SANDBOXING设为“no”即可&#xff01;

GateWay具体的使用!!!

一、全局Token过滤器 在Spring Cloud Gateway中&#xff0c;实现全局过滤器的目的是对所有进入系统的请求或响应进行统一处理&#xff0c;比如添加日志、鉴权等。下面是如何创建一个全局过滤器的基本步骤&#xff1a; 步骤1: 创建过滤器类 首先&#xff0c;你需要创建一个实现…

表---商场 nine

CREATE TABLE gao25 (id int(11) NOT NULL AUTO_INCREMENT COMMENT 自增ID,shopId int(11) NOT NULL COMMENT 店铺ID,goodsId int(11) NOT NULL COMMENT 商品ID,attrId int(11) NOT NULL COMMENT 属性名称,attrVal text NOT NULL COMMENT 属性值,createTime datetime NOT NULL …

实验 1--SQL Server2008数据库开发环境

文章目录 实验 1--SQL Server2008数据库开发环境2.4.1 实验目的2.4.2 实验准备2.4.3 实验内容1.利用 SSMS 访问系统自带的Report Server 数据库。2.熟悉了解 SMSS对象资源管理器树形菜单相关选择项的功能。(1)右键单击数据库Report Server&#xff0c;查看并使用相关功能;(2)选…

[C++基础学习]----02-C++运算符详解

前言 C中的运算符用于执行各种数学或逻辑运算。下面是一些常见的C运算符及其详细说明&#xff1a;下面详细解释一些常见的C运算符类型&#xff0c;包括其原理和使用方法。 正文 01-运算符简介 算术运算符&#xff1a; a、加法运算符&#xff08;&#xff09;&#xff1a;对两个…

基于openwrt交叉编译opencv4.9.0版本

源码包的获取 源码获取有两种方式&#xff0c;一种是通过编译时在makefile指定它的git地址&#xff0c;在编译时下载&#xff0c;这种很依赖网速&#xff0c;网速不好时&#xff0c;编译会失败。另一种是我们将源码的压缩包下载到本地&#xff0c;放到我们的SDK中&#xff0c;…

以场景驱动CMDB数据治理经验分享

数据治理是 CMDB 项目实施中难度最大、成本最高的环节&#xff0c;是一个长期治理的过程&#xff0c;而行业很少提出 CMDB 数据治理的技术实现方案。CMDB 数据治理不仅需要解决配置管理工程性的技术问题&#xff0c;还要基于运维组织的特点&#xff0c;建立适应性的配置运营能力…

DS进阶:并查集

一、并查集的原理 在一些应用问题中&#xff0c;需要将n个不同的元素划分成一些不相交的集合。开始时&#xff0c;每个元素自成一个单元素集合&#xff0c;然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于那个集合的运算。适合于描述这…