引言:
北京时间:2023/5/30/15:30,刚睡醒,两点的闹钟,硬是睡到了2点40,那种睡不醒的感觉,真的很难受,但是没办法,欠的课越来越多,压的我喘不过气了都,早上把有关unordered_set和unordered_map的内容给写完了,所以哈希表有关代码方面的知识,我们就搞定的差不多了,并且现在外面高温异常,着实比较恐怖,下午四点还要去进行一场有关计算机导论的考试,目前丝毫没有复习,但我也丝毫不慌张,导论这种课现在给我的感觉,就有点像是……具体不好比喻,反正给我的感觉不怎么好,也许以后等我学习的更加深入时,对于这种课程的感觉就会更加清晰吧!并且还是那句话,60分就好,所以咱不慌,哈哈哈!所以今天我们就接着哈希表相关知识,来看看什么是位图结构,虽然之前学习文件操作时学习过,但只是浅浅的了解了一下,这次我们是深入学习位图结构哦!
图是什么
图给我的第一印象
自从我们接触数据结构开始,我们就知道数据结构中有线性结构、非线性结构,并且知道在学习完线性结构相关的容器之后,就有两座大山,也就是有关树状结构和图状结构,当时在还没有学习什么红黑树、AVL树等容器时,树状结构和图在我的感知中都是较为复杂的数据结构,并且因为图在树之后学习,让我认为图比树的学习难度还更大,所以这就是图给我带来的第一印象,今天我们就来看看图,到底有没有想象中的那么复杂
到底什么是图
经过我的一番学习之后,我意识到了,在数据结构中图并不是我们生过中理解的地图或者纸图,而是位图,那么什么是位图呢?
简简单单,想要快速认识位图,我们从一个经典的面试题目进行分析,如下:
给40亿个不重复的无符号整数,没排过序,给定一个无符号整数,如何快速判断这个数是否在这40亿个数中?
首先,看到这种有关查找某个元素的问题,不就是去调用查找接口就可以搞定吗?那么像vector、list这些容器不都有查找接口吗?此时只需要将对应的数据插入到该容器中,然后不就可以调用该容器对应的查找接口去查找该元素是否存在于这些数据之中了吗?当然有的同学会认为,这样的查找方式效率非常的低,可以改进,将数据进行排序,排完序之后,使用二分查找法,在O(logN)的时间复杂就可以轻轻松松搞定,又快又好,或者是使用查找效率更高的数据结构,如红黑树或哈希表,这样就可以将查找效率大大提高啦!这个问题不是很简单吗?但是此时由于数据量是40亿的原因,所以上述方法并不适用,主要涉及到了存储相关的知识,数据量大越大,存储就越困难,并且明白,我们电脑的内存一般是4G~16G,并且这其中还包括了操作系统等软硬件使用的那部分,所以真正能够提供给我们使用的内存大约只有12G到15G,然而,此时如果是40亿个整形数据的话,单单是这方面的数据就需要16G的内存,还没有包括像红黑树中每个结点需要有一个指针,否则就又需要16G,哈希表同理,有而外指针需要存储,所以使用哈希表和红黑树来处理这个问题非常的不明智
充分认识位图
所以想要解决这个问题,那么此时最好的方法就是使用位图结构,原因就是,位图是一种用比特位来表示数据集合的数据结构,它将一个整数集合视为一个由0或1组成的二进制序列,并使用其中每一个比特位来代表对应被存储的整形数据 ,表示的意思就是我们可以使用比特位来标示每一个整形数据,例:此时某一个整数集合{1,2,3,4,5},某一个位图结构00000000,如果对应的整数集合中存在1,我们就把位图结构中的某个比特位由0置1,00000001,如果存在2,那么同理将位图结构中的某个比特位由0置成1,00000011,以此类推,最终把整数集合中的所有数据通过比特位的形式标示出来,这样我们就极大的节省了空间,假如还是40亿个整形数据,那么就不需要16G内存=160亿字节=1280亿个比特位,而只需要40亿个比特位=5亿字节=0.5G内存就可以将40亿个整形数据存储到内存啦!
位图结构代码实现
所以上述就是有关位图结构的基本认识,也就是今天我们学习的重点,文字描述起来,比较抽象,如果对于新手来收,那么想要理解,还是有一定成本的,所以为了更好的认识位图结构,此时我们就来看看如何使用代码来实现位图结构吧!如下:
1.位图基本模型
2.位图结构代码实现
根据上述的基本模型,此时我们对位图结构就有了更深一步的认识,此时我们再通过代码实现,将该结构给搞定吧!当然我们只是简单实现位图中最关键的接口,如下:
将对应位置由0置1
将对应位置由1置0
判断该数据是否存在
当然也就是判断该数据对应的比特位是1还是0,判断方法如下图所示:
位图结构有关变形问题
1.在100亿个数据中找只出现一次的数据
本质还是在利用上述位图结构有关的知识,只不过此时使用的是两个位图结构,其中一个位图结构用于记录第一次出现的数据,另一个位图结构用于记录第二次出现的数据,具体代码如下图所示:
2.两个有100亿整数的文件,只有1G内存,如何找到两个文件的交集
最优思路读取文件1的数据映射到位图1,读取文件2的数据映射到位图2,如果两个位置都是1就说明是交集,反之不是交集(注意:每个位图的位置代表的都是不同的值)
总结:位图有关知识就是用来解决类似上述这种海量数据处理问题,使用位图结构就是在使用最少的空间,去存储最多的数据
上述完整代码如下:
#pragma once
//明白计算机的移位,无论如何都是1左移几位,然后到达第几位 00000001左移几位得到0000x000
//左移和右移的本质一定是低位到高位(左移)或者高位到低位(右移)
#include<iostream>
#include<vector>
using namespace std;
//1.40亿个数据中,找某一个数据
template<size_t N>//这样子的模板参数需要注意
class bitmap
{
public:
bitmap()
{
_bits.resize(N / 8 + 1, 0);//这样子去开比特位会导致取整时比特位丢失(并且注意:此时N代表的是N个比特位,所以此时resize代表的是开辟多少个字节)
}
void set(size_t x)//将对应的比特位置成0
{
size_t i = x / 8;
size_t j = x % 8;
//找到对应的位之后,此时就需要将对应的位置成1
_bits[i] |= (1 << j);//这个不敢看不同,就是数组下标1处的char(8个比特位)去按位或上1左移j为后的二进制,如果j为4,那么也就是按位或上00010000(注意:是从右边开始)
//也就是 00000000按位或上00010000 最终得到 00010000(并且注意此时是按位或等)
}
void reset(size_t x)//将对应的比特位置成1
{ // 0 1 2 3
//1.先算是在第几个8比特位(也就是第几个第几个char) 00000000 00000000 00000000 00000000 (注意:需要从右开始计算比特位,因为此时是char类型)
size_t i = x / 8;//也就是计算x映射的位,在数组位置中的第几个char,如果x为12,那么此时该x的位就在第一个数组下标中
size_t j = x % 8;//也就是计算x映射的位,在该数组下标char中的第几个位,同理x为12,那么此时x就位于char中的第4个位
//所以总的来说,此时该x的位置就是第二个char(也就是数组下标为1)中的第四个位
//所以代码来到这里,就表示我们已经找到对应的位置了
//明白一个点,0跟任何数按位或还是任何数,0跟任意数按位与还是0
//所以此时我们想要将对应的比特位置0,此时使用的就是按位与(让对应为1的位置和0进行按位与)
_bits[i] &= ~(1 << j);//同理,00010000按位与上~(00010000)=>11101111,最终得到00000000
}
bool test(size_t x)//判断在不在
{
size_t i = x / 8;
size_t j = x % 8;
return _bits[i] & (1 << j);//本质就是在判断对应的那个位置是1还是0,只要不是0,那么就是真(true)例:00111100 & 00010000 => 00010000 ,虽然不是1,但是肯定不是0,所以返回true,只有当 00101111 & 00010000这种情况,当对应位置为0,那么就会返回false
//同理写法_bits[i] >> j & 1; //但是要注意运算符的优先级
}
private:
vector<char> _bits;
};
void test_bitmap()//切记类名哪里都可以使用,需要指明类域的是该类里面的公共成员函数,但是可以直接使用类型构造对象使用
{
bitmap<100> bm;//此时传的模板参数是100,结合上述代码表示的就是开辟100个比特位的空间(并且注意:此时一个比特位表示的就是存储一个整形数据)
bm.set(12);//这个表示的就是将12按照比特位的形式表示出来,也就是上述的00010000 --> 16 --> 10
bm.set(11);//00011000 --> 24 --> 18
bm.set(15);//10011000 --> 152 --> 98(16进制)
bm.reset(15);
cout << bm.test(12) << endl;
cout << bm.test(15) << endl;
//如何直接将空间开到最大
bitmap<-1> bm1;//此时类似之前学习的npos(反正表示的就是一个43亿左右的数),也就是表示此时在对应的位图中可以映射出了43亿个数据
}
//2.位图有关的变形问题(在100亿个数据中找只出现一次的数据)
template<size_t N>
class twobitmap
{
public:
void set(size_t x)
{
if (_bm1.test(x) == false && _bm2.test(x) == false)
{
_bm2.set(x);//00->01 //满足该条件,就说明该数据是第一次出现,此时我们只要将该数据映射的位图置成1就行
}
else if (_bm1.test(x) == false && _bm2.test(x) == true)
{
_bm1.set(x);
_bm2.reset(x); //01->10 满足该条件,就说明,该数据出现了一次以上,此时就只要将对应的位图由01(只出现了一次),置成10(出现了多次)就行
}
else
{
//10 多次出现
}
}
void print()//打印只出现一次的值
{
for (size_t i = 0; i < N; i++)//遍历43亿个数
{
if (_bm2.test(i) == 1)//此时表示的就是01结果(表示该数据只出现了一次)
{
cout << i << endl;
}
}
}
private:
bitmap<N> _bm1;//直接去封装上面的,实现两个不一样的就行,00表示该数据没有出现,01表示该数据只出现了一次,10表示该数据出现多次
bitmap<N> _bm2;
};
void test_twobitmap()
{
int arr[] = { 3,6,8,10,45,67,90,5,144,6,1,0,22,222,67,14 };//注意:这个位置给值的时候要小心越界访问
twobitmap<1000> bm;
for (auto e : arr)
{
bm.set(e);
}
bm.print();
}