C++ | 哈希的应用

引言

哈希的核心应用:位图(bitset)与布隆过滤器(Bloom Filter)。位图(bitset)是哈希的基础轻量实现 ,布隆过滤器是哈希 + 位图的经典组合优化 ,二者均基于哈希映射 思想解决海量数据的存在性判断 / 去重问题,是哈希在 "空间高效利用" 场景下的核心应用,且布隆过滤器完美弥补了位图的天然缺陷。


std::bitset位图

介绍

std::bitset位图 是 C++ 标准库(<bitset> 头文件)提供的固定长度位集容器 ,专门用于高效管理二进制位(bit) ------ 每个元素仅占 1 个二进制位(0 或 1),长度编译期固定即 声明时指定大小,不可动态修改,位操作原生支持访问 / 操作效率极高 ,适合已知二进制位长度的场景,如 32 位 / 64 位标志位、二进制掩码、位运算计算等。

cplusplus中关于位图的文档:bitset - C++ Reference

如果需要更全面的介绍的话,在这更推荐官网的bitset介绍:std::bitset - cppreference.cn - C++参考手册

很多人会混淆bitsetbool数组,二者核心差异在空间占用操作支持,看对比直观易懂:

特性 std::bitset<N> bool 数组(bool arr [N])
单个元素空间占用 1 个二进制位(bit) 1 个字节(byte)=8 个二进制位
空间利用率 高(是 bool 数组的 8 倍) 低(冗余 7/8 的空间)
长度特性 编译期固定,不可动态修改 编译期固定(数组),可改用 vector<bool>动态修改
位操作支持 原生支持(&、、^、~、<<、>> 等) 需手动通过位运算实现,繁琐
访问方式 随机访问([] 运算符),支持位编号 下标访问,无原生位编号映射
便捷接口 内置置位、复位、统计 1 的个数等接口 需手动编写函数实现

处理二进制位场景,bitset空间 + 效率的双重最优选择,无需手动管理位运算细节。

使用

构造函数

1. 默认构造
  • 创建一个包含 N 位的位集,所有位初始化为 0
  • C++11 起为 constexpr(编译期初始化)且无异常抛出。
cpp 复制代码
bitset();
2. 无符号整数初始化

用无符号整数 val 的二进制位填充位集,低位对齐, 整数的第 0 位对应 bitset 的第 0 位。若整数的二进制位数不足 N,剩余高位补 0;若超过 N,则截断高位

  • unsigned long 版本在 C++11 前就已存在。
  • unsigned long long 版本是 C++11 新增,支持更大整数且为 constexpr
cpp 复制代码
bitset(unsigned long val);
constexpr bitset(unsigned long long val) noexcept;

// eg:
bitset<8> bs(0b1010); // 初始化后:00001010

// 0b 是 C++ 中的二进制数字字面量前缀(C++14 及以上支持),专门用来表示「二进制数」,替代传统的十进制 / 十六进制
3. 字符串初始化(基础字符串)
  • 从字符串 strpos 位置开始,取 n 个字符初始化位集,**高位对齐,**字符串第一个字符对应 bitset 的最高位。字符 zero 对应位 0one 对应位 1,其他字符会引发异常。
  • C++23 起为 constexpr
cpp 复制代码
template<class CharT, class Traits, class Alloc>
explicit bitset(
    const std::basic_string<CharT, Traits, Alloc>& str,
    typename std::basic_string<CharT, Traits, Alloc>::size_type pos = 0,
    typename std::basic_string<CharT, Traits, Alloc>::size_type n = std::basic_string<CharT, Traits, Alloc>::npos,
    CharT zero = CharT('0'), CharT one = CharT('1')
);


bitset<8> bs("1010"); // 初始化后:00001010
  1. 字符串初始化(字符串视图)
  • 与基础字符串版本完全一致,但使用 std::basic_string_view 作为输入,避免了字符串拷贝,效率更高。
  • C++26 新增。
cpp 复制代码
template<class CharT, class Traits>
constexpr explicit bitset(
    std::basic_string_view<CharT, Traits> str,
    std::size_t pos = 0, std::size_t n = std::size_t(-1),
    CharT zero = CharT('0'), CharT one = CharT('1')
);
5. C 风格字符串初始化
  • 从 C 风格字符串 str 初始化位集,逻辑与基础字符串版本一致。若未指定 n,则默认取整个字符串。
  • C++23 起为 constexpr
cpp 复制代码
template<class CharT>
explicit bitset(const CharT* str, std::size_t n = std::size_t(-1),
                CharT zero = CharT('0'), CharT one = CharT('1'));


bitset<8> bs("1010"); // 等价于基础字符串版本
关键注意
  • 整数初始化:低位对齐(整数的二进制最低位对应 bitset 的第 0 位);
  • 字符串初始化:高位对齐(字符串的第一个字符对应 bitset 的最高位);
  • 若初始化值的二进制位数超过 bitset 长度,自动截断高位,仅保留低位有效部分。
常见初始化场景示例
cpp 复制代码
#include <bitset>
#include <string>
using namespace std;

int main() {
    // 1. 默认初始化(全0)
    bitset<4> bs1; // 结果:0000

    // 2. 无符号整数初始化(低位对齐)
    bitset<4> bs2(5); // 5的二进制101 → 结果:0101

    // 3. 基础字符串初始化(高位对齐)
    bitset<4> bs3("101"); // 结果:0101

    // 4. C风格字符串初始化
    bitset<4> bs4("1101"); // 结果:1101

    return 0;
}

成员函数的使用

bitset提供了直观的成员函数,用于操作单个位或全部位,比手动位运算更简洁:

接口 功能说明 示例(bs 初始为 0000)
set() 所有位置 1(置位) bs.set (); → bs 变为 1111
set(pos) 第 pos 位置 1(pos 是位编号) bs.set (2); → bs 变为 0100
reset() 所有位置 0(复位) bs.reset (); → bs 变为 0000
reset(pos) 第 pos 位置 0 bs.set(2); bs.reset(2); → 0000
flip() 所有位取反(0→1,1→0) bs.set(2); bs.flip(); → 1011
flip(pos) 第 pos 位取反 bs.set(2); bs.flip(2); → 0000
test(pos) 判断第 pos 位是否为 1,返回 bool(越界抛异常) bs.set(2); bs.test(2); → true
any() 判断是否有任意一位为 1,返回 bool bs.set(2); bs.any(); → true
none() 判断是否所有位都是 0,返回 bool bs.none (); → true(初始状态)
count() 统计位集中 1 的个数,返回 size_t bs.set(0); bs.set(2); → count=2

示例代码

cpp 复制代码
bitset<4> bs; // 初始0000
bs.set(0);    // 第0位置1 → 0001
bs.set(2);    // 第2位置1 → 0101

cout << "1的个数:" << bs.count() << endl; // 输出2
cout << "第2位是否为1:" << bs.test(2) << endl; // 输出1(true)
cout << "是否有位为1:" << bs.any() << endl; // 输出1(true)

bs.flip();    // 所有位取反 → 1010
cout << "取反后:" << bs << endl; // 输出1010

bs.reset();   // 所有位置0 → 0000
cout << "是否全为0:" << bs.none() << endl; // 输出1(true)

运算符的使用

bitset直接支持 C++ 所有位运算符,操作逻辑和整数位运算完全一致,无需额外处理,核心运算符:

  • \]位访问:**下标运算符 \[\]** 访问 / 修改指定位,下标就是**位的编号**

  • 按位或:| → 对应位有一个为 1 时结果为 1,否则为 0
  • 按位异或:^ → 对应位不同时结果为 1,相同时为 0
  • 按位取反:~ → 所有位 0→1、1→0(单目运算符)
  • 左移:<< → 所有位左移 n 位,低位补 0,高位截断
  • 右移:>> → 所有位右移 n 位,高位补 0,低位截断
  • 复合赋值:&=|=^=<<=>>=
cpp 复制代码
bitset<4> bs("1101"); // 初始:1101(位3=1,位2=1,位1=0,位0=1)

bitset<4> bs1("1010"); // 1010
bitset<4> bs2("0110"); // 0110

// 读指定位
cout << bs[0] << endl; // 输出1(第0位)
cout << bs[3] << endl; // 输出1(第3位)

// 改指定位(直接赋值0/1)
bs[1] = 1; // 第1位置1,bs变为1111
bs[3] = 0; // 第3位置0,bs变为0111

cout << bs << endl; // 直接输出位集,结果:0111


// 按位与
bitset<4> bs3 = bs1 & bs2; // 1010 & 0110 = 0010
cout << "bs1 & bs2:" << bs3 << endl; // 输出0010

// 按位或
bitset<4> bs4 = bs1 | bs2; // 1010 | 0110 = 1110
cout << "bs1 | bs2:" << bs4 << endl; // 输出1110

// 左移2位
bs1 <<= 2; // 1010 << 2 = 1000(左移2位,低位补0,高位10截断)
cout << "bs1<<2:" << bs1 << endl; // 输出1000

// 按位取反
bitset<4> bs5 = ~bs2; // ~0110 = 1001
cout << "~bs2:" << bs5 << endl; // 输出1001

类型转换(转整数 / 字符串)

bitset支持和整数字符串的双向转换,满足不同场景的需求,核心转换接口:

1. 转整数:to_ulong () /to_ullong ()
  • to_ulong():转换为unsigned long类型(适用于位数≤unsigned long 长度,通常 32 位);
  • to_ullong():转换为unsigned long long类型(适用于位数≤64 位,更通用);
  • 若位集值超过目标整数范围,会抛出overflow_error异常。
cpp 复制代码
bitset<4> bs("1010"); // 二进制1010 = 十进制10
unsigned long long num = bs.to_ullong();
cout << "转整数:" << num << endl; // 输出10

bitset<8> bs8("10000000"); // 二进制10000000 = 十进制128
cout << bs8.to_ulong() << endl; // 输出128
2. 转字符串:to_string ()

转换为std::string类型,默认返回高位在前的二进制字符串(和直接输出的格式一致),也可指定自定义的 0/1 字符。

cpp 复制代码
bitset<4> bs("0101");
string s = bs.to_string();
cout << "转字符串:" << s << endl; // 输出"0101"
cout << "字符串长度:" << s.size() << endl; // 输出4(和位集长度一致)

位图的模拟实现

cpp 复制代码
#pragma once
#include <iostream>


template<size_t N>
class bitset
{
private:
	// 编译期计算所需int数量:(N >>5)+1,向上取整(N/32)
	static const size_t _bitSize = (N >> 5) + 1;
	// 静态数组存储:编译期确定大小,栈上分配(无动态内存开销)
	int _bit[_bitSize];


public:
	bitset()
	{
		// 初始化
		for (size_t i = 0; i < _bitSize; ++i) {
			_bit[i] = 0;
		}
	}

	// 将pos比特位置1
	void set(size_t pos)
	{
		if (pos >= N) return; // 越界检查

		size_t indexArry = pos >> 5; // 所在int的下标(/32)
		size_t indexBit = pos % 32; // 所在int中的比特位(%32)
		_bit[indexArry] |= (1 << indexBit);
	}

	// 将pos比特位置0
	void reset(size_t pos)
	{
		if (pos >= N) return; // 越界检查

		size_t indexArry = pos >> 5; // 所在int的下标(/32)
		size_t indexBit = pos % 32; // 所在int中的比特位(%32)
		_bit[indexArry] &= ~(1 << indexBit);
	}

	// 测试pos比特位是否为1
	bool test(size_t pos) const
	{
		if (pos >= N) return false; // 越界检查

		size_t indexArry = pos >> 5; // 所在int的下标(/32)
		size_t indexBit = pos % 32; // 所在int中的比特位(%32)

		return _bit[indexArry] & (1 << indexBit);
	}

	// 获取位图总比特数
	size_t size() const
	{
		return N;
	}

	// 统计1的个数
    // 空间换时间
	size_t count() const
	{
		const int bitCntTable[256] = {
	0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4,1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,
	1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
	1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
	2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
	1,2,2,3,2,3,3,4,2,3,3,4,3,4,4,5,2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,
	2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
	2,3,3,4,3,4,4,5,3,4,4,5,4,5,5,6,3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,
	3,4,4,5,4,5,5,6,4,5,5,6,5,6,6,7,4,5,5,6,5,6,6,7,5,6,6,7,6,7,7,8
		};

		size_t count = 0;
		for (size_t i = 0; i < _bitSize; ++i) {
			int value = _bit[i];
			for (int j = 0; j < sizeof(int); ++j) {
				unsigned char c = static_cast<unsigned char>(value);
				count += bitCntTable[c];
				value >>= 8;
			}
		}

		return count;
	}

};

位图的应用

1. 快速查找某个数据是否在一个集合中

利用位图比特位与整数的一一映射关系, 将集合中的所有非负整数作为位图下标,对应位置 1。查找时仅需检测对应下标的比特位是否为 1,O (1) 时间复杂度,远超数组(O (n))、链表(O (n)),略优于哈希表(O (1) 但有哈希冲突开销)。

cpp 复制代码
#include <iostream>
#include <vector>
#include <bitset>
using namespace std;


int main() {
    vector<int> numSet = {5, 18, 32, 99, 6, 5}; // 待处理集合(含重复)
    int maxNum = 99; // 集合中最大整数
    bitset bs(maxNum + 1); // 初始化位图,下标0~99

    // 1. 插入集合所有元素
    for (int x : numSet) {
        bs.set(x);
    }

    // 2. 快速查找
    cout << boolalpha;
    cout << "18是否存在:" << bs.test(18) << endl; // true
    cout << "100是否存在:" << bs.test(100) << endl; // false(超出位图范围)
    cout << "6是否存在:" << bs.test(6) << endl; // true
    cout << "9是否存在:" << bs.test(9) << endl; // false
    return 0;
}

2. 海量整数的排序 + 去重

利用位图下标天然有序比特位唯一性的特性:

  1. 去重 :同一个整数多次调用set(x),对应位仅会被置 1 一次,自动过滤重复值;
  2. 排序 :位图下标从 0 到maxNum天然升序,遍历位图时,将值为 1 的下标依次取出,即为升序无重复序列
  3. 仅适用于非负整数 ,若含负数可通过偏移量映射(如负数 + 1000 转为非负);
  4. 整数范围 M 不宜过大(如 M=10^18 则内存溢出),此时需改用布隆过滤器 + 哈希表。
cpp 复制代码
int main() {
    // 海量测试数据:含重复、无序,范围0~100
    vector<int> nums = {55, 2, 88, 2, 99, 55, 0, 36, 88, 10, 77, 36};
    int maxNum = 99;
    bitset bs(maxNum + 1);

    // 1. 入位图:自动去重
    for (int x : nums) {
        bs.set(x);
    }

    // 2. 遍历位图:升序排序
    vector<int> sortedUniqueNums;
    for (size_t i = 0; i <= maxNum; ++i) {
        if (bs.test(i)) {
            sortedUniqueNums.push_back(i);
        }
    }

    // 输出结果:0 2 10 36 55 77 88 99
    cout << "排序去重结果:";
    for (int x : sortedUniqueNums) {
        cout << x << " ";
    }
    return 0;
}

3. 求两个集合的交集、并集等

位图的底层是整数数组 ,集合的交 / 并 / 差集可直接通过位运算实现,核心映射关系:

  • 位图 A:表示集合 A,A.test(x)=1 → x∈A;
  • 位图 B:表示集合 B,B.test(x)=1 → x∈B;
  • 位运算规则:
    1. 并集 :A∪B → 对应比特位有 1 则 1 (位运算|,或操作);
    2. 交集 :A∩B → 对应比特位都 1 则 1 (位运算&,与操作);
    3. 差集 :A-B → 对应比特位A=1 且 B=0 (位运算& ~,与非操作)。
cpp 复制代码
// 辅助函数:将bitset的交/并/差集结果解析为vector
vector<int> bitsetToSet(const bitset& bs) {
    vector<int> res;
    for (size_t i = 0; i < bs.size(); ++i) {
        if (bs.test(i)) {
            res.push_back(i);
        }
    }
    return res;
}

int main() {
    // 两个待处理集合
    vector<int> setA = {1, 3, 5, 7, 9};
    vector<int> setB = {5, 7, 9, 11, 13};
    int maxNum = 13; // 两个集合的最大整数
    bitset A(maxNum + 1), B(maxNum + 1), res(maxNum + 1);

    // 1. 集合入位图
    for (int x : setA) A.set(x);
    for (int x : setB) B.set(x);

    // 2. 位运算求集合(直接操作底层_vector<int>_bit)
    size_t bitSize = A._bit.size(); // 需将_bit改为protected(或提供访问接口)
    for (size_t i = 0; i < bitSize; ++i) {
        res._bit[i] = A._bit[i] | B._bit[i]; // 并集:A∪B,替换为&则是交集,A&~B则是差集
    }

    // 3. 解析结果
    vector<int> unionSet = bitsetToSet(res); // 并集:1,3,5,7,9,11,13
    cout << "并集:";
    for (int x : unionSet) cout << x << " ";

    // 同理求交集(A._bit[i] & B._bit[i]):5,7,9
    // 差集A-B(A._bit[i] & ~B._bit[i]):1,3
    return 0;
}

4. 操作系统中磁盘块标记

操作系统的磁盘由大量磁盘块 组成,系统需要快速管理磁盘块的空闲 / 占用状态, 分配时找空闲块,释放时标记为空闲,位图是该场景的最优实现方案

  • 磁盘块与位图下标映射 :将每个磁盘块的编号(0,1,2,...,n-1)作为位图的下标,一一对应;
  • 状态标记规则
    • 比特位 = 0 → 对应磁盘块空闲(可分配);
    • 比特位 = 1 → 对应磁盘块被占用(不可分配);
  • 核心操作映射
    • 分配磁盘块:遍历位图找到第一个值为 0 的下标,调用set(x)标记为占用,返回磁盘块编号;
    • 释放磁盘块:根据磁盘块编号,调用reset(x)标记为空闲;
    • 状态查询:调用test(x)判断磁盘块是否被占用。

为何操作系统首选位图?

  1. 空间极致省 :若磁盘有 1TB 空间,按 4KB / 块计算,共 268435456 个块,仅需32MB位图(268435456/8/1024/1024),远优于链表法(需存储块编号 + 状态 + 指针,至少占用 2GB);
  2. 操作速度快:分配 / 释放 / 查询均为 O (1) 位运算,CPU 缓存命中率高(位图体积小,可常驻内存);
  3. 易于连续分配 :遍历位图可快速找到连续的空闲磁盘块(满足文件连续存储需求),这是其他算法(如空闲链表法)无法高效实现的。
cpp 复制代码
// 磁盘块管理器:基于bitset实现
class DiskBlockManager {
public:
    // 构造函数:diskBlockNum为磁盘总块数
    DiskBlockManager(size_t diskBlockNum) : _bs(diskBlockNum) {}

    // 分配磁盘块:返回第一个空闲块编号,无空闲则返回-1
    int allocateBlock() {
        for (size_t i = 0; i < _bs.size(); ++i) {
            if (!_bs.test(i)) { // 找到第一个0的位
                _bs.set(i); // 标记为占用
                return i;
            }
        }
        return -1; // 磁盘满
    }

    // 释放磁盘块:blockNum为要释放的块编号
    bool freeBlock(size_t blockNum) {
        if (blockNum >= _bs.size() || !_bs.test(blockNum)) {
            return false; // 块编号无效或已空闲
        }
        _bs.reset(blockNum); // 标记为空闲
        return true;
    }

    // 查询磁盘块状态:true=占用,false=空闲
    bool isOccupied(size_t blockNum) {
        if (blockNum >= _bs.size()) return false;
        return _bs.test(blockNum);
    }

private:
    bitset _bs; // 核心:位图标记磁盘块状态
};

// 测试
int main() {
    DiskBlockManager disk(1000); // 模拟1000个磁盘块的磁盘
    // 分配块
    int block1 = disk.allocateBlock(); // 0
    int block2 = disk.allocateBlock(); // 1
    cout << "分配的块1:" << block1 << endl;
    cout << "分配的块2:" << block2 << endl;
    // 查询状态
    cout << boolalpha << "块0是否占用:" << disk.isOccupied(0) << endl; // true
    // 释放块
    disk.freeBlock(0);
    cout << "释放后块0是否占用:" << disk.isOccupied(0) << endl; // false
    // 重新分配,优先分配空闲块0
    int block3 = disk.allocateBlock(); // 0
    cout << "重新分配的块3:" << block3 << endl;
    return 0;
}

四大场景核心总结

应用场景 核心实现逻辑 核心优势 适用条件
快速存在性判断 整数映射下标,test(x)检测 O (1) 时间,极致空间,无额外开销 非负整数集合,范围可控
海量整数排序 + 去重 下标天然有序,set(x)自动去重 线性时间 O (n+M),比快速排序快,无重复开销 非负整数,范围 M 远小于数据量 n
集合交 / 并 / 差集 底层数组直接执行位运算(|/&/&~) 位运算硬件支持,效率是遍历对比的 32 倍 两个非负整数集合,最大整数一致
操作系统磁盘块标记 磁盘块编号映射下标,0 = 空闲 1 = 占用 空间极省,操作快,易实现连续块分配 磁盘块编号为非负整数,需快速状态管理

腾讯面试题

给40亿个不重复的无符号整数,没排过序。给一个无符号整数,如何快速判断一个数是否在这40亿个数中

你可能最开始想遍历?那肯定是不行的,因为40亿个无符号数占用将近15G内存。

那使用散列表呢?那这里使用的空间能在内存上吗?显然不可以,就算可以,散列表需要大量内存,因为需要为40亿个整数维护哈希表。哈希冲突可能会导致性能下降,需要选择合适的哈希函数以避免碰撞,又会遇到各种各样的问题

那么这里就可以用到我们上面提到的位图概念,位图(Bitset)方法:

1. 位图解法的核心原理

利用无符号整数与位图比特位的一一映射关系 (哈希直接定址法 hash(x)=x),用1 个比特位标记 1 个无符号整数的 "存在状态":

  • 位图的下标 = 无符号整数的数值(0 ~ 2³²-1);
  • 比特位值为 1 → 该数存在于 40 亿个数中;
  • 比特位值为 0 → 该数不存在

2. 内存占用精准计算

无符号整数最大范围是 2³² 个值,位图需要的总比特位数 = 2³² 位,换算为实际内存:

仅需512MB 内存即可存储整个无符号整数范围的存在状态,40 亿个数的标记完全能放入常规内存,且远低于遍历 / 哈希表的 15GB + 内存开销。

3. 核心执行步骤

步骤 1:初始化位图

创建一个长度为 2³² 位的位图,初始状态所有比特位均为 0(表示所有数初始为 "不存在")。

步骤 2:加载 40 亿个数,标记位图

遍历 40 亿个无符号整数(可分批从磁盘 / 文件加载,无需一次性加载所有数),对每个数x,将位图中第x个比特位置 1set(x)),标记该数存在。

  • 分批加载原因:40 亿个数的原始数据占 15GB,无法一次性加载,但标记操作仅需单个数的值,可按 1GB / 批加载,每批处理完释放内存,不影响位图状态。
步骤 3:快速判断目标数是否存在

对给定的无符号整数target,直接检测位图中第target个比特位的状态(test(target)):

  • 比特位为 1 → 存在;
  • 比特位为 0 → 不存在;
  • 操作时间复杂度O(1),纯位运算,无任何循环 / 额外开销。
cpp 复制代码
#include <iostream>
#include <vector>
#include <cstdint> // 用于uint32_t(无符号32位整数,对应无符号整数)
using namespace std;

// 适配无符号32位整数的位图类(0 ~ 2^32-1)
class UInt32Bitset {
public:
    // 构造函数:初始化2^32位的位图,共2^32/32 = 134217728个uint32_t元素
    UInt32Bitset() : _bit(1ULL << 28, 0) {}
    // 将无符号整数x对应的比特位置1(x: 0 ~ 2^32-1)
    void set(uint32_t x) {
        // 计算x所在的uint32_t元素下标:x / 32 (右移5位,效率更高)
        size_t index = static_cast<size_t>(x) >> 5;
        // 计算x在该元素中的比特位位置:x % 32
        size_t pos = static_cast<size_t>(x) & 0x1F; // 0x1F=31,取模优化,更快
        // 按位或置1,不影响其他位
        _bit[index] |= (1ULL << pos); // 用1ULL避免移位溢出
    }

    // 检测x是否存在:存在返回true,不存在返回false
    bool test(uint32_t x) const {
        size_t index = static_cast<size_t>(x) >> 5;
        size_t pos = static_cast<size_t>(x) & 0x1F;
        // 按位与,非0则存在
        return (_bit[index] & (1ULL << pos)) != 0;
    }

private:
    // 底层存储:vector<uint32_t>,每个元素占32位,总长度2^28个元素,刚好存储2^32位
    vector<uint64_t> _bit; // 用uint64_t可减少元素个数,提升缓存命中率(可选)
};

// 面试简化测试示例
int main() {
    UInt32Bitset bs; // 初始化512MB位图,适配所有无符号32位整数

    // 模拟步骤2:加载并标记数据(此处仅模拟少量数据,实际为40亿个)
    vector<uint32_t> sampleNums = {100, 123456, 4294967295, 0, 999999999};
    for (uint32_t x : sampleNums) {
        bs.set(x);
    }

    // 模拟步骤3:快速判断目标数
    uint32_t target1 = 123456;
    uint32_t target2 = 666666;
    cout << boolalpha;
    cout << "数" << target1 << "是否存在:" << bs.test(target1) << endl; // true
    cout << "数" << target2 << "是否存在:" << bs.test(target2) << endl; // false

    return 0;
}
相关推荐
weixin_649555672 小时前
C语言程序设计第四版(何钦铭、颜晖)第八章指针之循环后移
c语言·c++·算法
_饭团2 小时前
C语言数组全解析:从入门到精通
c语言·开发语言·数据结构·经验分享·笔记·学习·算法
陕西小伙伴网络科技有限公司2 小时前
kettle单转换实现分页查询
开发语言·前端·javascript
张李浩2 小时前
Leetcode 454 四数相加II 采用哈希表解决
leetcode·哈希算法·散列表
快乐柠檬不快乐2 小时前
C++中的代理模式实现
开发语言·c++·算法
良木生香2 小时前
【C++初阶】:C++类和对象(上):类的定义 & 类的实例化 & this指针
c语言·开发语言·c++
70asunflower2 小时前
CUDA基础知识巩固检验练习题【附有参考答案】(8)
c++·人工智能·cuda
月明长歌2 小时前
【码道初阶-Hot100】LeetCode 560. 和为 K 的子数组:从前缀和到哈希计数,彻底讲透为什么“统计前缀和”就等价于统计子数组个数
算法·leetcode·哈希算法
会编程的土豆2 小时前
【影院票务管理系统】
开发语言