当前位置: 首页 > news >正文

C++篇(17)哈希拓展学习

一、位图

1.1 位图的引入

腾讯/百度等公司曾出过这样一道面试题:给你40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。

解题思路1:暴力遍历,时间复杂度O(N),太慢

解题思路2:排序+二分查找。时间复杂度O(N*logN) + O(logN)

深入分析一下,解题思路2是否可行,我们先来算一算40亿个数据大概需要多少内存?

1G = 1024MB = 1024 * 1024KB = 1024*1024*1024Byte,约等于10亿多Byte,那么40亿个整数约等于16G,说明40亿个数是无法直接放到内存中的,只能放到硬盘文件中,但是二分查找只能对内存数组中的有序数据进行查找。

解题思路3:判断数据是否在给定的整型数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,二进制位1代表存在,为0代表不存在。那么我们设计一个用位表示数据是否存在的数据结构,这个数据结构就叫位图。

1.2 位图的模拟实现

#pragma once
#include <vector>namespace bit
{template<size_t N>class bit_set{public:bit_set(){_bs.resize(N / 32 + 1);}//x映射的位标记成1void set(size_t x){size_t i = x / 32;size_t j = x % 32;_bs[i] |= (1 << j);}//x映射的位标记成0void reset(size_t x){size_t i = x / 32;size_t j = x % 32;_bs[i] &= (~(1 << j));}//x映射的位是1返回真//x映射的位是0返回假bool test(size_t x){size_t i = x / 32;size_t j = x % 32;return _bs[i] & (1 << j);}private:std::vector<size_t> _bs;};
}
#include <iostream>
#include "BitSet.h"
using namespace std;int main()
{bit::bit_set<100> bs;bs.set(32);bs.set(33);bs.reset(33);bs.set(34);cout << bs.test(31) << endl;cout << bs.test(32) << endl;cout << bs.test(33) << endl;cout << bs.test(34) << endl;cout << bs.test(35) << endl;return 0;
}

回到一开始的问题上来,我们如何开42亿九千万的空间呢?有以下三种方式:

bit::bit_set<-1> bs1;
bit::bit_set<0xffffffff> bs2;
bit::bit_set<UINT_MAX> bs3;

1.3 C++库的位图

https://legacy.cplusplus.com/reference/bitset/

这里要注意一下,标准库里的位图是不能像我们模拟实现的位图那样开42亿九千万的,因为库里面的位图是用静态数组来实现的,而静态数组的空间是在栈上开辟的,空间大小是不够的。想要解决这个问题的话,我们可以把它转移到堆上去(像下面这样)。

std::bitset<-1>* ptr = new std::bitset<-1>();

1.4 位图的优缺点

优点:增删查改快,节省空间

缺点:只适用于整型

1.5 位图的相关考察题目

Q:给定100亿个整数,设计算法找到只出现一次的整数?

     A:虽然是100亿个数,但还是按范围开空间,所以还是开2^32个位,和前面题目是一样的。

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

     A:把数据读出来,分别放到两个位图中,依次遍历,同时在两个位图的值就是交集。

Q:一个文件有100亿个整数,1G内存,设计算法找到出现次数不超过两次的所有整数?

     A:之前我们是标记在不在,只需要一个位即可。这里要统计出现次数不超过两次的,可以每个值用两个位标记,00表示出现0次,01表示出现1次,10表示出现2次,11表示出现2次以上。最后统计出所有01和10标记的值即可。

#pragma once
#include <vector>namespace bit
{template<size_t N>class bit_set{public:bit_set(){_bs.resize(N / 32 + 1);}//x映射的位标记成1void set(size_t x){size_t i = x / 32;size_t j = x % 32;_bs[i] |= (1 << j);}//x映射的位标记成0void reset(size_t x){size_t i = x / 32;size_t j = x % 32;_bs[i] &= (~(1 << j));}//x映射的位是1返回真//x映射的位是0返回假bool test(size_t x){size_t i = x / 32;size_t j = x % 32;return _bs[i] & (1 << j);}private:std::vector<size_t> _bs;};template<size_t N>class twobitset{public:void set(size_t x){bool bit1 = _bs1.test(x);bool bit2 = _bs2.test(x);if (!bit1 && !bit2)  // 00->01{_bs2.set(x);}else if (!bit1 && bit2)  // 01->10{_bs1.set(x);_bs2.reset(x);}else if (bit1 && !bit2)  // 10->11{_bs1.set(x);_bs2.set(x);}}//返回0  出现0次//返回1  出现1次//返回2  出现2次//返回3  出现2次以上int get_count(size_t x){bool bit1 = _bs1.test(x);bool bit2 = _bs2.test(x);if (!bit1 && !bit2){return 0;}else if (!bit1 && bit2){return 1;}else if (bit1 && !bit2){return 2;}else{return 3;}}private:bit_set<N> _bs1;bit_set<N> _bs2;};
}

二、布隆过滤器

2.1 布隆过滤器的引入

在一些场景下面,有大量数据需要判断是否存在,而这些数据不是整型,那么位图就不能使用了,使用红黑树/哈希表等内存空间可能不够。这些场景就需要用布隆过滤器来解决。

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

布隆过滤器的思路就是把key先映射转成哈希整型值,再映射一个位。但是如果只映射一个位的话,冲突率会比较高,所以可以通过多个哈希函数映射多个位,降低冲突率。

布隆过滤器和哈希表不一样,它无法解决哈希冲突,因为它压根不存储这个值,只标记映射的位。它的思路是尽可能降低哈希冲突。判断一个值key在是不准确的,判断一个值key不在是准确的。

2.2 布隆过滤器的模拟实现

#pragma once
#include <string>
#include "BitSet.h"struct HashFuncBKDR
{size_t operator()(const std::string& s){size_t hash = 0;for (auto ch : s){hash *= 31;hash += ch;}return hash;}
};struct HashFuncAP
{size_t operator()(const std::string& s){size_t hash = 0;for (long i = 0; i < s.size(); i++){if ((i & 1) == 0){hash ^= ((hash << 7) ^ (s[i]) ^ (hash >> 3));}else{hash ^= (~((hash << 11) ^ (s[i]) ^ (hash >> 5)));}}return hash;}
};struct HashFuncDJB
{size_t operator()(const std::string& s){size_t hash = 5381;for (auto ch : s){hash = hash * 33 ^ ch;}return hash;}
};template<size_t N, size_t X = 5, class K = std::string, class Hash1 = HashFuncBKDR,class Hash2 = HashFuncAP,class Hash3 = HashFuncDJB>
class BloomFilter
{
public:void Set(const K& key){size_t hash1 = Hash1()(key) % M;size_t hash2 = Hash2()(key) % M;size_t hash3 = Hash3()(key) % M;_bs.set(hash1);_bs.set(hash2);_bs.set(hash3);}bool Test(const K& key){size_t hash1 = Hash1()(key) % M;if (!_bs.test(hash1)){return false;}size_t hash2 = Hash2()(key) % M;if (!_bs.test(hash2)){return false;}size_t hash3 = Hash3()(key) % M;if (!_bs.test(hash3)){return false;}return true;  //可能存在误判}private:const size_t M = N * X;bit::bit_set<N * X> _bs;
};

2.3 布隆过滤器的应用

优点:效率高,节省空间。相比位图,可以适用于各种类型的标记过滤。

缺点:存在误判(在是不准确的,不在是准确的),不好支持删除。

布隆过滤器在实际中还有爬虫系统中URL去重、垃圾邮件过滤、预防缓存穿透、数据库查询提效等应用。

三、海量数据处理

3.1 十亿个整数里面求最大的前100个

经典的TopK问题,用堆解决。这个在数据结构初阶堆章节具体讲解过。

3.2 位图章节讲解的位图相关题目

3.3 给两个文件,分别有100亿个query,我们只有1G内存,如何找出两个文件的交集?

分析:假设平均每个query字符串50Byte,100亿个query就是5000亿Byte,约等于500G,用哈希表/红黑树等数据结构肯定是无能为力的。

解决方案1:首先可以用布隆过滤器解决,一个文件的query放进布隆过滤器,另一个文件一次查找,在就是交集。问题就是找到的交集不够准确,因为在的值可能是误判的,但是交集一定被找到了。

解决方案2:哈希切分。首先内存的访问速度远大于硬盘,大文件放到内存中搞不定,那么我们可以考虑切分为小文件,再放进内存处理。但是这里不能平均切分,因为平均切分之后,每个小文件都需要依次暴力处理,效率还是太低了。这里就要利用到哈希切分,依次读取文件中的query,i = HashFunc(query) % N,N为准备切分多少份小文件。N取决于切成多少份内存能放得下,query放进第i号小文件,这样A和B中相同的query算出的hash值i是一样的,相同的query就进入的编号相同的小文件就可以和编号相同的文件直接找交集,不用交叉找,效率就提升了。

但是哈希切分也会有一个小问题,就是每个小文件不是均匀切分的,可能会导致某个小文件很大,内存放不下。而某个小文件很大有两种情况:1.这个小文件中大部分都是同一个query。 2.这个小文件是由很多不同的query构成的,本质是这些query冲突了。针对情况1,其实放到内存的set是可以放的下的,因为set能去重。针对情况2,需要换个哈希函数继续二次哈希切分。所以我们遇到大于1G的小文件,可以继续读到set中找交集。若set insert时抛出了异常(set插入数据抛异常只可能是申请内存失败了,不会有其他情况),那么说明内存放不下,是情况2,换个哈希函数进行二次哈希切分后再对应找交集。

3.4 给一个超过100G大小的log file,log中存着IP地址,设计算法找到出现次数最多的IP地址?查找出现次数前十的IP地址?

本题思路和上一题完全类似,依次读取文件A中的ip,i = HashFunc(ip) % 500,ip放进Ai号小文件,然后依次用map<string,int>对每个Ai小文件统计ip次数,同时求出现次数最多的ip或者topK的ip。本质是相同的ip在哈希切分的过程中,一定进入同一个小文件Ai,不可能出现同一个ip进入Ai和Aj的情况,所以对Ai进行统计次数就是准确的ip次数。

http://www.dtcms.com/a/617905.html

相关文章:

  • 做建筑材料的网站wordpress后台左侧菜单显示
  • 基于SpringBoot的热门旅游推荐系统设计与实现
  • leetcode 1513 仅含1的子串数
  • 2014网站怎么备案网站怎么做口碑
  • 【微服务】SpringBoot 整合高性能时序数据库 Apache IoTDB 实战操作详解
  • 【电路笔记】-单稳态多谐振荡器
  • Java数据结构-Map和Set-通配符?-反射-枚举-Lambda
  • 在那里能找到网站网络营销与网站推广的区别
  • 架构之路(六):把框架拉出来
  • 【Linux驱动开发】Linux SPI 通信详解:从硬件到驱动再到应用
  • 【ASP.NET进阶】Controller层核心:Action方法全解析,从基础到避坑
  • Imec实现了GaN击穿电压的记录
  • Streaming ELT with Flink CDC · Iceberg Sink
  • AI(新手)
  • 海南城乡建设厅网站百度竞价关键词查询
  • QT开发——常用控件(2)
  • 【Java架构师体系课 | MySQL篇】⑥ 索引优化实战二
  • Spring Boot、Redis、RabbitMQ 在项目中的核心作用详解
  • 做完整的网站设计需要的技术长治建立公司网站的步骤
  • 南宁京象建站公司网站建设留言板实验心得
  • AI、LLM全景图
  • pip升级已安装包方法详解
  • 【Linux日新月异(六)】CentOS 7网络命令深度解析:从传统到现代网络管理
  • LangChain 构建 AI 代理(Agent)
  • 人工智能训练师备考——3.1.1题解
  • 【RL】ORPO: Monolithic Preference Optimization without Reference Model
  • 公益平台网站怎么做网站跳出
  • QT的5种标准对话框
  • 用Rust构建一个OCR命令行工具
  • 网站代码大全国内网站设计作品欣赏