哈希的应用——位图

文章目录

  • 前言
  • 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

  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

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

首先第一种思路:

我们可以先读取一个文件的值放到内存中,然后再读取第二个文件,依次判断第二个文件里面的值在不在第一个位图里面,在的就是交集。
当然如果这样做的话有一个问题就是我们求出来的交集可能会有重复值出现,而求交集一般是要去重的。
当然我们可以解决一下:
我们读取第二个文件判断在不在位图里面的时候,如果在的话第一次就把它映射的这个位置置成0,这样后续再遇到就不再把它添到交集里面,就达到去重的效果了。

然后另外一种思路是这样的:

把两个文件的数据分别映射到两个位图里面。
然后遍历其中一个文件依次取值,判断如果某个值在两个位图里面映射的位置
都是1,那说明它在两个文件里都存在,就是交集
在这里插入图片描述
或者我们可以直接对两个位图进行按位与,结果中为1的位置对应的下标就是交集

那这两种方法的话:

如果数据量比较小用第一种其实比较好,因为第一种是遍历文件里面的数据去判断,而第二种我们遍历的数据的范围(跟数据量无关),所以如果数据量比较大可以用第二种。
所以要看场景选择合适的方法。

习题3

  1. 位图应用变形: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;
}

在这里插入图片描述

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

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

相关文章

赞奇科技参与华为云828 B2B企业节,云工作站入选精选产品解决方案

8月27日&#xff0c;由华为云携手上万家伙伴共同发起的第二届 828 B2B 企业节拉开帷幕&#xff0c;围绕五大系列活动&#xff0c;为万千中小企业带来精细化商机对接。 聚焦行业数字化所需最优产品&#xff0c;举办超1000场供需对接会&#xff0c;遍及20多个省100多个城市&…

Python小知识 - 使用Python进行数据分析

使用Python进行数据分析 数据分析简介 数据分析&#xff0c;又称为信息分析&#xff0c;是指对数据进行综合处理、归纳提炼、概括总结的过程&#xff0c;是数据处理的第一步。 数据分析的目的是了解数据的内在规律&#xff0c;为数据挖掘&#xff0c;并应用于商业决策、科学研究…

运行速度高达1.8GHz可编程采集网关

今天介绍下BL304这个设备。嵌入式ARM控制器BL304系列是一款专为工业控制领域设计的坚固型工控设备。它采用NXP公司的处理器I.MX8M Mini四核64位处理器&#xff0c;搭配四核 Cortex-A53单核Cortex-M4构架&#xff0c;运行速度高达1.8GHz&#xff0c;具有高度的稳定性和可靠性。 …

从0到1学会Git(第二部分):Git的本地操作和管理

写在前面:本文介绍了在本地仓库进行文件的处理以及本地的合并等操作。 前置知识:文件可以处在三个区域&#xff0c;分别为工作区&#xff0c;暂存区和本地仓库&#xff0c;我们此文的目标即是将文件存储在本地仓库中。我们可以将文件的区域理解为&#xff0c;cpu中&#xff0c…

计算机视觉主要任务

计算机视觉&#xff1a;使用计算机及相关设备对生物视觉的一种模拟。 主要包含6大任务&#xff0c;图像分类&#xff0c;目标检测&#xff0c;目标跟踪&#xff0c;语义分割&#xff0c;实例分割&#xff0c;影像重构。 图像分类&#xff1a;根据图像信息中所反映的不同特征&am…

数据结构与算法复杂度介绍

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

Redis List类型命令 - Set类型命令 - SortedSet类型命令

目录 List类型 什么是双向链表呢&#xff1f; List类型的特征&#xff1a; List的常用命令 LPUSH和RPUSH的区别&#xff1a; LPOP和RPOP的区别&#xff1a; LPUSH和RPUSH的使用 LPOP和RPOP的使用 LRANGE key star end&#xff1a;返回一段距离范围内所有的元素 BLPOP…

最小生成树Kruskal、Prim算法C++

什么是最小生成树 连通图&#xff1a; 在无向图中&#xff0c;若从顶点v1到顶点v2有路径&#xff0c;则称顶点v1和顶点v2是连通的。如果图中任意一对顶点都是连通的&#xff0c;则称此图为连通图。 生成树&#xff1a; 一个连通图的最小连通子图称作为图的生成树。有n个顶点的…

Pytorch 的基本概念和使用场景介绍

文章目录 一、基本概念1. 张量&#xff08;Tensor&#xff09;2. 自动微分&#xff08;Autograd&#xff09;3. 计算图&#xff08;Computation Graph&#xff09;4. 动态计算图&#xff08;Dynamic Computation Graph&#xff09;5. 变量&#xff08;Variable&#xff09; 二、…

【用unity实现100个游戏之8】用Unity制作一个炸弹人游戏

文章目录 前言素材开始一、绘制地图二、玩家设置三、玩家移动四、玩家四方向动画运动切换 五、放置炸弹六、生成爆炸效果七、墙壁和可破坏障碍物的判断八、道具生成和效果九、玩家死亡十、简单的敌人AI十一、简单敌人AI十二、随机绘制地图十三、虚拟摇杆 最终效果待续源码完结 …

2023年行研行业研究报告

第一章 行业概述 1.1 行研行业 行业定义为同一类别的经济活动&#xff0c;这涉及生产相似产品、应用相同生产工艺或提供同类服务的集合&#xff0c;如食品饮料行业、服饰行业、机械制造行业、金融服务行业和移动互联网行业等。 为满足全球金融业的需求&#xff0c;1999年8月…

Linux之autofs自动挂载服务

目录 Linux之autofs自动挂载服务 产生原因 安装 配置文件分析 文件路径 作用 etc/auto.master文件内容格式 挂载参数 案例 案例1 --- 服务器创建共享目录&#xff0c;客户端实现自动挂载 案例2 --- 自动挂载光盘 Linux之autofs自动挂载服务 产生原因 在一般NFS文件系…

$attrs,$listeners

vue实现组件通信的方式有&#xff1a; 父子通信 父组件向子组件传递通过props定义各个属性来传递&#xff0c;子组件向父组件传递通过$emit触发事件 ref也可以访问组件实例跨级通信 vuex bus provide / inject $attrs / $listeners解释 $attrs / $listeners $attrs 将父组件中…

服务端请求伪造(SSRF)及漏洞复现

文章目录 渗透测试漏洞原理服务端请求伪造1. SSRF 概述1.1 SSRF 场景1.1.1 PHP 实现 1.2 SSRF 原理1.3 SSRF 危害 2. SSRF 攻防2.1 SSRF 利用2.1.1 文件访问2.1.2 端口扫描2.1.3 读取本地文件2.1.4 内网应用指纹识别2.1.5 攻击内网Web应用 2.2 SSRF 经典案例2.2.1 访问页面2.2.…

自然语言处理实战项目17-基于多种NLP模型的诈骗电话识别方法研究与应用实战

大家好&#xff0c;我是微学AI&#xff0c;今天给大家介绍一下自然语言处理实战项目17-基于NLP模型的诈骗电话识别方法研究与应用&#xff0c;相信最近小伙伴都都看过《孤注一掷》这部写实的诈骗电影吧&#xff0c;电影主要围绕跨境网络诈骗展开&#xff0c;电影取材自上万起真…

基于Java+SpringBoot+Vue前后端分离善筹网(众筹)设计和实现

博主介绍&#xff1a;✌全网粉丝30W,csdn特邀作者、博客专家、CSDN新星计划导师、Java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专…

vue3在路由route.js中获取不到仓库pinia中store里面的值

原因&#xff1a;小仓库(useUserStore )必须有大仓库(pinia)才能运行&#xff0c;在组件中能使用pinia仓库的数据&#xff0c;是因为在main.ts中已经在vue上面挂载了大仓库(pinia)&#xff0c;但是route.js不是vue组件&#xff0c;没有被挂载大仓库&#xff0c;所以不能运行 解…

使用(七牛云)为例子实现将文件上传到云服务器

目的 目前&#xff0c;用户的头像、分享生成的长图等文件都是存放在本地的&#xff0c;我们可以将他们存放在云服务器中&#xff0c;此处我们使用七牛云作为例子示范。 七牛云 创建账户并申请如下的两个bucket&#xff0c;分别是用户头像的存储空间和分享长图的存储空间。 …

数据库设计DDL

DDL&#xff1a;数据定义语言&#xff0c;用来定义数据库对象&#xff08;数据库、表&#xff09; DDL&#xff08;数据库操作&#xff09; 查询&#xff1a; 查询所有数据库&#xff1a;show databases; 查询当前数据库&#xff1a;select database(); 使用&#xff1a; 使用…

Python 之 match 表达式

Python 从 3.10 版本开始增加了 match 语句&#xff0c;和其他语言常见的 switch 语句极其相似&#xff0c;但功能更加强大。 本文通过实例&#xff0c;了解下其用法。 基本的 match 语句 def http_code(status): match status: case 400 | 404 | 418: …