哈希的应用——位图
文章目录
- 前言
- 1. 面试题思考
- 2. 位图
- 2.1 位图的概念
- 2.2 思路讲解及代码实现
- 结构定义
- 构造函数
- set和reset接口实现
- set和reset测试观察
- test接口实现
- test接口测试
- 思考
- 3. 位图的应用
- 习题1
- 习题2
- 习题3
- 4. 总结
- 5. 源码
- 5.1 bitset.h
- 5.2 Test.c
前言
前面的文章里我们学习了哈希表,并用哈希表模拟实现了STL里面的unordered_map和unordered_set。
那接下来呢我们要再来学习一下哈希的应用——位图和布隆过滤器。
这篇文章先来看第一个——位图
1. 面试题思考
首先我们来看一道腾讯曾经考过的面试题,引出我们今天要讨论的问题
问题是这样的:
给40亿个不重复的无符号整数,没排过序。然后给一个无符号整数,如何快速判断这个数是否在这40亿个数中?
那我们看到这个问题可能会想到这样的思路:
1. 遍历,时间复杂度O(N)
2. 排序+二分查找
3. 利用哈希表或红黑树,就是放到set或unordered_set里面进行查找嘛
那大家思考一下,上面这些方法有没有什么问题?
那这里我们要注意到的是它这里给的是40亿个整数。
那大家先算一下,40亿个整数大概占多少空间?
首先:
1G=1024MB=1024*
1024KB=1024*
1024*
1024byte
就约等于10亿byte(字节)
那40亿个整数(一个整型4字节)就约等于16G。
那大约占16G空间的话我们上面的方法还可行吗?
首先最关键的问题是16G的数据可能都不能一次全部放到到内存中,内存可能都不够用。
要遍历的话就得把它们全部放到内存里面啊
然后如果要排序那就是用归并排序了,分开放到一个个的小文件里面,进行归并。
但是文件里面也没法二分查找啊,都不能下标访问。
那你像放到set或unordered_set里面查找也是一样,内存可能不够,哈希表或红黑树还有额外的消耗,因为还要存一些指针啥的,记录颜色啥的。
当然你可以分开每次处理一小部分,但这样效率就不太行了。
所以上面这些思路都不太合适,而且我们这里只是要判断在不在,其实没必要把它们全部存起来。
那像这样的问题用我们接下来要学的位图来解决就比较好。
2. 位图
2.1 位图的概念
所谓位图,就是用一个个比特位来存放某种状态,适用于海量数据,数据无重复的场景。通常是用来判断某个数据存不存在的。
那对于上面的那个问题我们其实就是判断在不在嘛,所以我们只需要存储每个值的存放状态(是否存在)即可,那我们就可以用位图来解决。
怎么做呢?
判断一个数据是否在给定的整形数据中,结果是在或者不在,刚好是两种状态,那么可以使用一个二进制比特位来代表数据是否存在的信息,比如可以用二进制比特位为1代表存在,为0代表不存在
题目说的是40亿个不重复的无符号整数,所以我们可以这样做:
无符号整数的最大值是2^32-1,即4294967295
所以我们开这样一个数组
下标从0到无符号整型的最大值2^32-1
每个元素的大小呢是1个比特位,因为我们用1个比特位就可以来标识当前位置下标所对应的值存不存在,所以它其实就是一个直接定址法的思想。
当然没有类型的大小是1个bit,所以我们可以开成char数组(当然不一定非要用char),那每个位置就是8个比特位。
0到7映射到第一个char,8到15映射到第二个char,以此类推。
那大家看现在我们标识40亿个整数存不存在用了多大空间?
前面我们算了40亿个整数大约16G,一个整数4字节=32bit,现在相当于缩到了1bit,原来的1/32,那就是大约0.5G即512MB,这个大小肯定是没问题的。
2.2 思路讲解及代码实现
那接下来我们就来详细讲解一下如何使用位图解决上面那道题,并实现相应的代码。
结构定义
首先我们可以先简单定义一下位图的结构:
那它实际就是一个数组嘛。
构造函数
我们来看一下构造函数应该怎么写?
那我们初始的时候就把vector里面全放成0嘛。
那现在要开N的比特位的空间的话,我们的元素个数应该给多少呢?
🆗,这里给N/8+1是比较合适的
因为我们vector里面放的是char嘛,一个char8个bit,所以N/8,但是可能有余数,所以再+1,多开8个bit,这个确保肯定是够用的。
set和reset接口实现
然后这里我们首先要实现的两个核心操作叫做set和reset:
set就是把x映射的那个位置的比特位设置成1,reset就是把它设置成0。
那首先第一个问题我们如何通过x找到它映射的那个比特位?
🆗,那首先第一步我们要找到x映射到第几个char里面,因为我们现在开的是char数组,里面是一个个的char(8bit),所以这里就是去找它映射到第几个8比特位里面。
那要算这个的话我们是不是用x除8就得到了
比如就这个图我们算8,那就是8/10等于1,所以它就在下标为1(实际是第二个)的那个char里面,这没有问题。
那然后我们就要找它映射的是在这个char里面的哪个bit位上?
那用x%8是不是就行了
那紧接着,我们找到了这个比特位,如何把它设置成1(set)或者设置成0(reset)呢?
先来看set,把x映射的比特位设置成1,怎么做呢?
🆗,我们给这个位置按位或
|
上一个1是不是就可以了。
因为按位或的话是有1则1,全0才0嘛。
不论它原来是1还是0,按位或1之后都是1。
当然改变这个位置的同时我们不能影响其它位置。
那我们如何做到呢?
我们让1左移j位(实际是向高位移动,只不过一般情况下都是左边为高位,所以如果我们画的图不同,移动方向可能就变了)然后和x映射的这个char进行按位或就行了
这里char和int进行按位或的话会发生整型提升
因为1的补码只有最低位一个1,其余位置都是0,所以向高位移动j个位置这个唯一的1就和x映射的那个位置对上了。
所以:
那reset呢?把x映射的比特位设置成0
很简单,让这个比特位按位与0就行了
按位与是有0则0,全1才1
那我们只需让x映射的这个char跟1左移j位之后再取反(按位取反~)的结果按位与就行了,这样x映射的比特位变成0,其它位置也不受影响(因为其它位置是跟1进行&)。
set和reset测试观察
我们测试测试,并调式观察一下
首先看一下set:
我们开一个大小100bit的,然后把10这个位置置成1,就表示插入10这个值,set之后10就存在了
我们来调试观察一下:
先拿到bs的起始地址
我们看到现在都是0(现在一列是1个字节,不支持按比特位监视)
然后我们执行set
那就是把10这个位置置成1
我们看一下对不对
没问题(我们看到监视窗口其实就对应我们后面画的那个图,他也是为了方便我们观察,符合人的习惯),大家可以自己再换一些值测试测试。
那我们再来测试一下reset:
刚才对10进行了set,现在再对10进行reset,看一下
🆗,没问题。
test接口实现
除了这两个呢,还有一个比较核心的接口——test,他其实就是去判断某个值存不存在(它映射的位置是否被设置成了1):
那其实也很简单,让x映射的这个比特位和1进行按位与,如果结果是0,就表明这个比特位是0,如果结果是1,就表明这个比特位是1。
所以让x映射的这个char跟1左移j位之后的结果进行按位与就行了
结果为0就是假,非0就是真。
test接口测试
来测试一下test:
没问题。
那现在的话我们就可以很好的解决最开始的那个面试题了。
思考
那大家来思考一下:对于上面那个问题40亿个无符号整数,我们的位图应该给多大?
就开40亿个可以吗?
不可以的。
要注意我们不能按个数去开,而是要按照范围去开。
就算现在变成10亿个无符号整数,我们也应该开4294967295(即2^32-1,无符号整型最大值)个,因为我们不知道这10亿个整数的取值范围,它可能就包含了最大值,所以我们要确保不论它多大,就可以映射到位图中一个确定的位置上。
那我们如何让位图开这么大空间呢?4294967295这个值也不好记啊?
🆗,我们给一个-1是不是就好了啊。
bitset<-1> bs;
因为我们的N是无符号整数啊,那-1变成无符号整数不就是整型最大值嘛,就跟那个npos是一样的玩法。
那除了用-1,其实我们还可以这样写:
字面值是可以这样写的。
那我们可以看一下,给这个值他是不是按我们上面分析的一样开了512MB空间:
大家看现在是0.4MB
然后我们继续执行一步
🆗,就变成512.4MB了
最后想告诉大家的是:
我们上面讲的位图其实C++库里面也是提供的有现成的
我们上面实现的命名风格其实就是跟着库里面走的
比较核心的接口我们都带大家实现了
其它的接口大家用的的时候可以自己查阅文档
3. 位图的应用
下面我们再来一起看几个位图相关的练习题
习题1
- 给定100亿个整数,设计算法找到只出现一次的整数?
大家思考一下,可以怎么解决?
首先这里是100亿个整数欸,我们还开
0xFFFFFFFF
这么多空间够吗?
当然是够的,虽然有100亿个,但它的范围还是不变的,肯定不会超过整型最大值,只能说明有很多重复值。
那我们肯定还是用位图来解决嘛,这道题我们可以这样做:
这道题让我们找出只出现一次的整数,其实通过两个比特位就可以判断。
我们只看两位,00就是0次,01就是一次,10就是1次以上,不一定就是两次,因为我们set的时候如果是10就可以不进行操作了,反正set到10的时候就已经超过1次了
所以我们可以给上面实现的位图改造一下,改造成每个位置占两个比特位的位图。
当然也可以不改造,我们还是用上面的位图,我们开两个位图,如果一个整数第一次出现就在第一个位图中把它映射的位置置成1,第二次出现就把它在第二个位图中映射的位置置成1。
所以我们可以封装一个twobitset:
那对于它的set,就应该是这样的:
看当前值在两个位图中映射位置的值,如果是00,就变成01,如果是01,就变成10,如果是10,已经超过1次了,后续就可以不进行任何操作了
那然后我们可以写一个Print打印所有出现一次的值:
那我们如何找出twoset里面所有只出现一次的值呢?
很简单,我们去遍历找哪个值在第二个位图里面的映射位置是1(那一定是01,因为我们只有00、01、10三种状态),那它就是只出现一次的
那我们来搞一组数据测试一下:
测试的话我们数据量就不高那么大了
我们看一下打印的是不是只出现一次的
🆗,没有问题,大家可以自己再测试。
那我们再来看下一个问题:
习题2
- 给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?
首先第一种思路:
我们可以先读取一个文件的值放到内存中,然后再读取第二个文件,依次判断第二个文件里面的值在不在第一个位图里面,在的就是交集。
当然如果这样做的话有一个问题就是我们求出来的交集可能会有重复值出现,而求交集一般是要去重的。
当然我们可以解决一下:
我们读取第二个文件判断在不在位图里面的时候,如果在的话第一次就把它映射的这个位置置成0,这样后续再遇到就不再把它添到交集里面,就达到去重的效果了。
然后另外一种思路是这样的:
把两个文件的数据分别映射到两个位图里面。
然后遍历其中一个文件依次取值,判断如果某个值在两个位图里面映射的位置
都是1,那说明它在两个文件里都存在,就是交集
或者我们可以直接对两个位图进行按位与,结果中为1的位置对应的下标就是交集
那这两种方法的话:
如果数据量比较小用第一种其实比较好,因为第一种是遍历文件里面的数据去判断,而第二种我们遍历的数据的范围(跟数据量无关),所以如果数据量比较大可以用第二种。
所以要看场景选择合适的方法。
习题3
- 位图应用变形:1个文件有100亿个int,我们只有1G内存,设计算法找到出现次数不超过2次的所有整数
这个其实跟习题1有点类似嘛,就是第一题的一个变形:
还是用两个位图,这里我们可以分为4种状态
那还是两个比特位就够用了
我们最终要找到就是出现1次和2次的
那这个把第一题的代码稍微修改一下就行了
测试一下:
没有问题
4. 总结
最后总结一下:
那它的缺陷有没有办法解决呢?
有的就是我们下一篇文章要学的——布隆过滤器。
那我们下一篇文章在详细介绍
5. 源码
5.1 bitset.h
#pragma once
#include <vector>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 test1()
{bitset<100> bs;bs.set(10);cout << bs.test(10) << endl;cout << bs.test(15) << endl;bs.reset(10);cout << bs.test(10) << endl;cout << bs.test(15) << endl;
}void test2()
{//bitset<-1> bs;bitset<0xFFFFFFFF> bs;
}template <size_t N>
class twobitset
{
public://找到只出现一次的整数//void set(size_t x)//{// //00->01// if (_bs1.test(x) == false// && _bs2.test(x) == false)// {// _bs2.set(x);// }// //01->10// else if (_bs1.test(x) == false// && _bs2.test(x) == true)// {// _bs2.reset(x);// _bs1.set(x);// }// //10不用管//}/*void Print(){for (size_t i = 0; i < N; i++){if (_bs2.test(i) == true){cout << i << " ";}}cout << endl;}*///找到出现次数不超过2次的所有整数void set(size_t x){//00->01if (_bs1.test(x) == false&& _bs2.test(x) == false){_bs2.set(x);}//01->10else if (_bs1.test(x) == false&& _bs2.test(x) == true){_bs2.reset(x);_bs1.set(x);}//10->11else if (_bs1.test(x) == true&& _bs2.test(x) == false){_bs2.set(x);}//11不用管}void Print(){for (size_t i = 0; i < N; i++){if ((_bs1.test(i) == true && _bs2.test(i) == false)|| (_bs1.test(i) == false && _bs2.test(i) == true)){cout << i << " ";}}cout << endl;}
private:bitset<N> _bs1;bitset<N> _bs2;
};void test_twobitset()
{int a[] = { 1,1,1,2,3,3,3,3,4,4,5,6,9,12,33,45,45,45,6 };twobitset<100> bs;for (auto e : a){bs.set(e);}bs.Print();
}
5.2 Test.c
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
#include "bitset.h"int main()
{test_twobitset();return 0;
}
相关文章:

哈希的应用——位图
文章目录 前言1. 面试题思考2. 位图2.1 位图的概念2.2 思路讲解及代码实现结构定义构造函数set和reset接口实现set和reset测试观察test接口实现test接口测试思考 3. 位图的应用习题1习题2习题3 4. 总结5. 源码5.1 bitset.h5.2 Test.c 前言 前面的文章里我们学习了哈希表&#x…...

2023开学礼《乡村振兴战略下传统村落文化旅游设计》许少辉八一新书对外经济贸易大学图书馆
2023开学礼《乡村振兴战略下传统村落文化旅游设计》许少辉八一新书对外经济贸易大学图书馆...

合并两个有序链表(每日一题)
“路虽远,行则将至” ❤️主页:小赛毛 ☕今日份刷题:合并两个有序链表 题目描述: 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例1: 输入:l1 …...
React Hooks总览
总览 hooks 功能分类具体 hooks具体功能React v18新特性跨端支持数据更新驱动useState定义要在页面中渲染的数据❌✔useReducer定义要在页面中渲染的数据,且这个数据有多种处理逻辑❌✔useSyncExternalStoreconcurrent 模式下,订阅外部 store 的行为&am…...

风向变了!智能汽车何以「降本」
随着软件定义汽车的概念逐步落地,以及底盘、动力、座舱、智驾、车身等不同域(分布式或者混合式)的功能更新迭代和融合,汽车行业正在意识到:底层硬件架构重构的迫切性。 事实上,早在2016年,作为传…...
后端面试话术集锦第 十五 篇:java线程面试话术
这是后端面试集锦第十五篇博文——java线程面试话术❗❗❗ 1. 创建线程的方式 首先呢,Thread类本质上是实现了Runnable接口,代表一个线程的实例。 所以,我们可以编写一个类,继承Thread类,或者直接实现Runnable接口。然后,再重写下~run方法就行了。启动线程的方式就是调…...

cocos creator配置终端调试
在launch.json里添加"preLaunchTask":“CocosCreator compile” 在cocos creator里选择开发者,visual studio code工作流,选择添加编译任务。 添加 settings.json {"files.exclude":{"**/.git": true,"**/.DS_Sto…...

达梦类型转换问题-float转换为varchar
表结构 CREATE TABLE "SYSDBA"."TABLE_2" ( "COLUMN_1" FLOAT, "COLUMN_2" NUMERIC(22,6)) STORAGE(ON "MAIN", CLUSTERBTR) ; 表数据: 查询,将numeric转换为float,再转换为varchar&…...

怎么用postman连接websocket
点击右侧栏的Collections,然后点击旁边的New,然后点击其中的WebSocket Request,然后输入Url,点击Connection,这里需要注意的是Url不能加上http://,因为这个不是http协议。...

需求分析入门
认识管理软件 什么是管理软件 管理软件就是用来辅助企业进行管理的软件,既包括对企业“人、财、物”相关的资产信息的管理,也包括对企业“供、产、销”相关的业务活动信息的管理。管理软件的重点在于管理信息的收集、流转,资源的共享、集成…...

攻防世界-php_rce
原题 解题思路 thinkPHP.0有漏洞,ThinkPHP5.x rec 漏洞分析与复现。本题就是利用漏洞查找。格式是: ?sindex/\think\app/invokefunction&functioncall_user_func_array&vars[0]system&vars[1][]命令。 ls查看文件没什么东西,r…...

最小生成树Kruskal、Prim算法C++
什么是最小生成树 连通图: 在无向图中,若从顶点v1到顶点v2有路径,则称顶点v1和顶点v2是连通的。如果图中任意一对顶点都是连通的,则称此图为连通图。 生成树: 一个连通图的最小连通子图称作为图的生成树。有n个顶点的…...

系统架构设计师-计算机系统基础知识(2)
目录 一、存储管理 1、页式存储 2、段式存储 3、段页式存储 二、磁盘管理 1、先来先服务FCFS 2、最短寻道时间优先SSTF 三、文件系统 1、文件基本概念 2、文件的类型: 3、索引文件结构 4、位示图 四、性能指标 五、性能设计 1、阿姆达尔定律 六、性能评估 1、…...

二叉树的介绍
写在前面: 二叉树是数据结构课程中非常重要的内容,我们针对二叉树的概念、性质以及类型展开详细介绍。 一、概念 二叉树(Binary Tree)是n(n>0)个结点的有限集合,该集合或者空集࿰…...

数据结构与算法复杂度介绍
目录 一、基本概念 二、时间复杂度 【2.1】时间复杂度概念 【2.2】大O的渐进表示法 【2.3】举例时间复杂度计算 三、空间复杂度 一、基本概念 数据结构:相互之间存在一种或者多种特定关系的数据元素的集合。在逻辑上可以分为线性结构,散列结构、树…...

CentOS 安装蒲公英
官方教程链接: https://service.oray.com/question/5063.html 教程使用的是2.3版本,官网下载的最新版是2.4,所以命令会有所不同 安装成功后, 任意路径下执行pgyvisitor,调出交互界面pgyvisitor login,登录…...

英语语法基础--思维导图
思维导图通常用于可视化和整理信息,而英文语法非常广泛且复杂,无法在一个简单的思维导图中完整表示。然而,我可以提供一个简化版本的英文语法思维导图,列出一些主要的语法概念和部分示例。 请注意,这只是一个基本的概…...
Android泛型详解
参考文献:https://pingfangx.github.io/java-tutorials/java/generics/types.html 1,什么是泛型? Java泛型(generics)是JDK5中引入的一个新特性,泛型提供了 编译时类型安全检测机制, 该机制允许程序员在编译时检测到…...

C++信息学奥赛1178:成绩排序
#include<bits/stdc.h> using namespace std; int main(){int n;cin>>n; // 输入整数 n,表示数组的大小int arr[n]; // 创建大小为 n 的整型数组 arrstring brr[n]; // 创建大小为 n 的字符串数组 brrfor(int i0;i<n;i) cin>>brr[i]>>ar…...
【计算机视觉 | 目标检测】目标检测常用数据集及其介绍(七)
文章目录 一、Cops-Ref二、FAT (Falling Things)三、GEN1 Detection (Prophesee GEN1 Automotive Detection Dataset)四、RIT-18五、AGAR (Annotated Germs for Automated Recognition)六、EuroCity Persons七、Freiburg Groceries八、Lytro Illum九、PFN-PIC (PFN Picking Ins…...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...

7.4.分块查找
一.分块查找的算法思想: 1.实例: 以上述图片的顺序表为例, 该顺序表的数据元素从整体来看是乱序的,但如果把这些数据元素分成一块一块的小区间, 第一个区间[0,1]索引上的数据元素都是小于等于10的, 第二…...

工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...

前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...

华为OD机试-食堂供餐-二分法
import java.util.Arrays; import java.util.Scanner;public class DemoTest3 {public static void main(String[] args) {Scanner in new Scanner(System.in);// 注意 hasNext 和 hasNextLine 的区别while (in.hasNextLine()) { // 注意 while 处理多个 caseint a in.nextIn…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...

C# 类和继承(抽象类)
抽象类 抽象类是指设计为被继承的类。抽象类只能被用作其他类的基类。 不能创建抽象类的实例。抽象类使用abstract修饰符声明。 抽象类可以包含抽象成员或普通的非抽象成员。抽象类的成员可以是抽象成员和普通带 实现的成员的任意组合。抽象类自己可以派生自另一个抽象类。例…...

(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...

AI书签管理工具开发全记录(十九):嵌入资源处理
1.前言 📝 在上一篇文章中,我们完成了书签的导入导出功能。本篇文章我们研究如何处理嵌入资源,方便后续将资源打包到一个可执行文件中。 2.embed介绍 🎯 Go 1.16 引入了革命性的 embed 包,彻底改变了静态资源管理的…...