目录
3.2. 给两个文件,分别有100亿个整数,只有1G内存,如何找到两个文件的交集
3.3. 一个文件有100亿个int,1G内存,设计算法找到出现次数不超过两次的整数
1. 位图的概念
位图(Bitset)是一种数据结构,用来表示一个固定大小的位序列。它将每一个位(0 或 1)映射到一个特定的索引位置,如果位为1,那么说明映射到这个位的Key是存在的,反之如果位为0,那么说明这个Key是不存在的。位图用的是直接定址法,没有了哈希冲突 ,并可以进行高效的位操作。
位图通常用于解决一些需要高效存储和查询大量布尔类型数据的场景。它可以以较小的内存消耗存储大量的布尔值信息。
位图的基本操作包括设置位(set),清除位(reset),和查找位(test)。通过这些操作,可以对位图中的特定位进行设置或清除,并查找特定位是否存在。
常见的应用场景包括:
- 1. 压缩存储:位图可以将大量的布尔类型数据以很小的内存占用进行存储,节省存储空间。
- 2. 集合操作:位图可以被用于表示和操作集合。每个位可以代表某个元素是否属于集合,例如在数据库中进行条件过滤和查询等操作。
- 3. 布隆过滤器:布隆过滤器是一种基于位图的数据结构,用于快速判断一个元素是否属于一个集合,具有高效的查找和内存占用优势。
需要注意的是,位图适用于数据集较大、数据分布较稀疏的情况下。对于数据集较小或数据分布较密集的情况,位图可能会导致较大的内存消耗。因此,在选择使用位图时,需要根据具体的需求和数据特点进行权衡和评估。
1.1. 位图的相关应用场景
给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中。
分析如下:
首先,我们可以知道 ,这是40亿个不重复的无符号整数,那么也就是160亿个字节。而我们知道 1024^3 也就是1GB近似于10亿字节,那么也就是说光这些数据就近似需要16GB的空间,而我们的32位系统,其内存就只有4GB,内存都存不下这些数据。
- 思路一:搜索树 + 哈希表
- 经过上面的分析 ,我们的搜索树,以及哈希表都不可以支持,内存存不下这些数据。
- 思路二:外排序 + 二分查找
- 我们的归并排序可以支持,但是这么大的数据,内存是存不下的,那么这些数据只能存储与磁盘中,而磁盘是不好支持二分查找的,效率太低。
- 思路三:位图
- 位图只是用一个位标识 Key 是否存在 (0意味着不在,1意味着在) ,空间消耗小,且位图是直接定址法,其映射位置具有唯一性,效率高。
- 对于无符号整型来讲,其范围是0 至 2^32-1,那么我们也就需要2^32个位即可,也就是 2^29 个字节,而我们知道 2^30 是1GB,那么 2^29 个字节也就是512MB,相较于上面的思路,节省了很大的空间。
- 并且,不论此时有多少个无符号整型的数据,哪怕你有50亿个、100亿个数据,我都只开这么大的空间(512MB)就可以判断某个特殊值是否存在,因为这里的空间不是多少个数据,而是代表着该数据的范围。
而我们知道,vector所开的空间最小单位是一个字节即8个bit位,例如vector<char>,假设我现在已经知道了,某个数组的最大数据是27,那么我应该开多少个空间呢?
27 / 8 等于3余4,显然3个char是不够的,我们应该开4个char空间,也就是说,我们开的空间应该是所需要映射的Key中的最大值 / 8 + 1
例如:有这样一个集合{12,6,18,27}
由于Key的最大值为27,因此我们需要 27 / 8 + 1个char,也就是4个char。
2. 位图的实现
2.1. set
set() ,设置操作,即将Key映射的特定位置的比特位置为1。
- step 1:Key先除8,得到在第几个char;
- step 2:Key在模8,确定在这个char第几个bit位;
- step 3:用这个char |= (将1左移模8的结果)。
注意:
- 0 | 任何bit位 == 任何bit(没有影响);
- 1 | 任何bit位 == 1。
2.2. reset
reset(),清除操作,即将Key映射的特定位置的比特位置为0。
- step 1:Key先除8,得到在第几个char;
- step 2:Key在模8,确定在这个char第几个bit位;
- step 3:用这个char &= ~(将1 左移 (Key模8的结果))。
注意:
- 1 &= 任何bit位 还是任何bit位。
2.3. test
reset(),查找操作,判断Key映射的特定bit位是否存在(0/1)。
- step 1:Key先除8,得到在第几个char;
- step 2:Key在模8,确定在这个char第几个bit位;
- step 3:return 这个char & (将1 左移 (Key模8的结果))。
由于是&,而不是&=,因此不会对位图产生影响。
2.4. 完整实现
#include <iostream>
#include <vector>
namespace Xq
{
// 非类型模板参数,如果已经确定了最大值,那么范围就是0 - 最大值
// 注意:这里开多少空间,是由数据范围决定的,而不是由数据的多少决定的
// 如果没有明确范围,哪怕只有10个数据,你也得给我开2^32个bit
template<size_t N>
class bit_set
{
public:
bit_set()
{
// 根据上面的分析,空间 = Key(max) / 8 + 1
_bit_set.resize(N / 8 + 1, 0);
}
void set(int key)
{
// pos_size 得出是第几个char
size_t pos_size = key / 8;
// pos_count 得出在这个char的第几个bit位
size_t pos_count = key % 8;
// 这个bit位 |= (左移1)
_bit_set[pos_size] |= (1 << pos_count);
}
void reset(int key)
{
// pos_size 得出是第几个char
size_t pos_size = key / 8;
// pos_count 得出在这个char的第几个bit位
size_t pos_count = key % 8;
// ~按位取反
_bit_set[pos_size] &= ~(1 << pos_count);
}
bool test(int key)
{
// pos_size 得出是第几个char
size_t pos_size = key / 8;
// pos_count 得出在这个char的第几个bit位
size_t pos_count = key % 8;
// 注意这里是 &,不是&= 不会影响位图
return _bit_set[pos_size] & (1 << pos_count);
}
private:
std::vector<char> _bit_set;
};
}
3. 位图的应用
- 1. 快速查找某个数据是否在一个集合中;
- 2. 排序 + 去重;
- 3. 求两个集合的交集、并集等;
- 4. 操作系统中磁盘块标记。
3.1. 给定100亿个整数,设计算法找到只出现一次的整数
思路:我们可以用两个位图标识每个数据的的出现次数。
- 如果某个数字出现了0次,那么这个数字映射到的两个位图的位分别是0,0;
- 如果某个数字出现了1次,那么这个数字映射两个位图的位分别是0,1;
- 如果某个数字出现了2次及以上,那么这个数字映射两个位图的位分别是1,0;
实现代码如下:
#include <iostream>
#include <vector>
namespace Xq
{
template<size_t N>
class bit_set
{
public:
bit_set(size_t num = N)
{
_table.resize(num / 8 + 1, 0);
}
void set(size_t key)
{
size_t pos_size = key / 8;
size_t pos_count = key % 8;
_table[pos_size] |= (1 << pos_count);
}
void reset(size_t key)
{
size_t pos_size = key / 8;
size_t pos_count = key % 8;
_table[pos_size] &= ~(1 << pos_count);
}
bool test(size_t key)
{
size_t pos_size = key / 8;
size_t pos_count = key % 8;
return _table[pos_size] & (1 << pos_count); // 000 1 000
}
private:
std::vector<char> _table;
};
template<size_t N>
class two_bit_set
{
public:
void set(size_t key)
{
bool ret1 = bs1.test(key);
bool ret2 = bs2.test(key);
// 如果第一个位图映射的位 == 1,说明这个数已经出现了两次,直接返回即可
if (ret1) return;
// 如果第一个位图映射的位 == 0且第二个位图映射位 == 0,说明这个数已经出现了零次,直接在位图2中set
if (!ret1 && !ret2)
{
// 0 0 -> 0 1
bs2.set(key);
return;
}
// 如果第一个位图映射的位 == 0且第二个位图映射位 == 1,说明这个数已经出现了1次,直接在位图1中set
if (!ret1 && ret2)
{
// 0 1 -> 1 0
bs1.set(key);
bs2.reset(key);
}
}
bool test(size_t key)
{
// 如果第一个位图映射的位 == 0且第二个位图映射位 == 1,说明这个数只出现了1次
if (!bs1.test(key) && bs2.test(key))
return true;
else
return false;
}
private:
bit_set<N> bs1;
bit_set<N> bs2;
};
void Test1()
{
two_bit_set<9> tbs;
std::vector<int> v{ 3, 4, 5, 3, 4, 2, 1, 1, 7, 8, 7, 0, 9 }; // 5 2 8 0 9
for (auto e : v)
{
tbs.set(e);
}
std::cout << "出现一次的数字:> ";
for (auto e : v)
{
if (1 == tbs.test(e))
std::cout << e << " ";
}
std::cout << "\n";
}
}
3.2. 给两个文件,分别有100亿个整数,只有1G内存,如何找到两个文件的交集
与上面的思路一致,同样用两个位图(去重),将文件的数据 set 到两个位图中,遍历两个位图,如果相同位置的位 == 1,则是交集。
实现代码如下:
template<size_t N>
class Intersection_bit_set
{
public:
void set_arr1(size_t key)
{
_bs1.set(key);
}
void set_arr2(size_t key)
{
_bs2.set(key);
}
bool test(size_t key)
{
if (_bs1.test(key) && _bs2.test(key))
return true;
else
return false;
}
private:
bit_set<N> _bs1;
bit_set<N> _bs2;
};
3.3. 一个文件有100亿个int,1G内存,设计算法找到出现次数不超过两次的整数
思路:
与第一个问题稍有差异,只不过第一个问题两个位图记录了三种状态,而这里我们需要用两个位图记录四种状态,具体如下:
- 第一种状态:Key没有出现过,对应的两个位图对应的映射位置分别为0,0;
- 第二种状态:Key出现过一次,对应的两个位图对应的映射位置分别为0,1;
- 第三种状态:Key出现过两次,对应的两个位图对应的映射位置分别为1,0;
- 第四种状态:Key出现过两次以上,对应的两个位图对应的映射位置分别为1,1。
实现代码如下:
template<size_t N>
class two_bit_set_plus
{
public:
void set(size_t key)
{
bool ret1 = bs1.test(key);
bool ret2 = bs2.test(key);
// 如果第一个位图映射的位 == 1且第二个位图映射的位 == 1,说明这个数已经出现了两次以上,直接返回即可
if (ret1 && ret2) return;
// 如果第一个位图映射的位 == 0且第二个位图映射位 == 0,说明这个数已经出现了零次,直接在位图2中set
else if (!ret1 && !ret2)
{
// 0 0 -> 0 1
bs2.set(key);
}
// 如果第一个位图映射的位 == 0且第二个位图映射位 == 1,说明这个数已经出现了1次,直接在位图1中set
else if (!ret1 && ret2)
{
// 0 1 -> 1 0
bs1.set(key);
bs2.reset(key);
}
// 如果第一个位图映射的位 == 1且第二个位图映射位 == 0,说明这个数已经出现了2次,直接在位图2中set
else (ret1 && !ret2)
{
// 1 0 -> 1 1
bs2.set(key);
}
}
bool test(size_t key)
{
// 如果第一个位图映射的位 == 1且第二个位图映射位 == 1,说明这个数已经出现了两次以上
if (bs1.test(key) && bs2.test(key))
return false;
else
return true;
}
private:
bit_set<N> bs1;
bit_set<N> bs2;
};
4. 位图特点
- 效率快、节省空间;
- 相对局限,只能映射处理整形;
- 直接定址法,不存在哈希冲突。