1. 题目:
给定一个输入文件,包含40亿个非负整数,请设计一种算法,生成一个不包含在该文件中的整数,假定你有1GB内存来完成这项任务。
进阶:如果只有10MB内存可用,该怎么办?假设所有值均不同,且有不超过10亿个非负整数。
2. 解题思路:
算法说明
1)1GB内存方案:
- 使用位图法,每个bit代表一个数字是否存在
- 40亿个数字需要2^32 bits = 512MB内存
- 遍历文件设置位图,然后扫描位图找到第一个未设置的位
2)10MB内存方案:
- 使用两次遍历方法
- 第一次遍历:统计每个范围内的数字数量,找到数量不足的范围
- 第二次遍历:只在目标范围内使用位图查找具体缺失的数字
- 内存消耗主要由范围计数数组和一个小位图决定
这两种方法都能高效地找到缺失的整数,分别适用于不同的内存限制场景。
3. 代码完整实现(C++):
#include <algorithm>
#include <climits>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <random>
#include <vector>
// --------------------1GB内存解决方案--------------------
// const uint32_t MAX_NUM_1GB = 4294967295; // 2^32 - 1
const uint32_t MAX_NUM_1GB = 65535; // 2^16 - 1
const size_t BITMAP_SIZE_1GB = MAX_NUM_1GB / 8 + 1;
uint32_t find_missing_number_1gb(const std::string& filename) {
std::vector<unsigned char> bitmap(BITMAP_SIZE_1GB, 0);
std::ifstream input(filename, std::ios::binary);
uint32_t num;
while (input.read(reinterpret_cast<char*>(&num), sizeof(num))) {
size_t index = num / 8;
unsigned char mask = 1 << (num % 8);
bitmap[index] |= mask;
}
for (uint32_t i = 0; i <= MAX_NUM_1GB; ++i) {
size_t index = i / 8;
unsigned char mask = 1 << (i % 8);
if (!(bitmap[index] & mask)) {
return i;
}
}
return MAX_NUM_1GB + 1;
}
// --------------------10MB内存解决方案--------------------
// const uint32_t RANGE_SIZE = 100000; // 每个范围的大小 10w
// const uint32_t MAX_NUM_10MB = 1000000000; // 10亿
const uint32_t RANGE_SIZE = 1000; // 每个范围的大小 1k
const uint32_t MAX_NUM_10MB = 999999; // 100万
uint32_t find_missing_number_10mb(const std::string& filename) {
uint32_t range_count = MAX_NUM_10MB / RANGE_SIZE + 1;
std::vector<uint32_t> range_counts(range_count, 0);
std::ifstream input(filename, std::ios::binary);
uint32_t num;
while (input.read(reinterpret_cast<char*>(&num), sizeof(num))) {
uint32_t range_index = num / RANGE_SIZE;
++range_counts[range_index];
}
uint32_t target_range = 0;
for (; target_range < range_count; ++target_range) {
if (range_counts[target_range] < RANGE_SIZE) {
break;
}
}
std::vector<bool> bitmap(RANGE_SIZE, false);
input.clear();
input.seekg(0, std::ios::beg);
uint32_t range_start = target_range * RANGE_SIZE;
uint32_t range_end = range_start + RANGE_SIZE;
while (input.read(reinterpret_cast<char*>(&num), sizeof(num))) {
if (num >= range_start && num < range_end) {
bitmap[num - range_start] = true;
}
}
for (uint32_t i = 0; i < RANGE_SIZE; ++i) {
/*
扫描位图,找到第一个未被标记的位置
计算实际缺失数字:区间起始值 + 偏移量
*/
if (!bitmap[i]) {
return range_start + i;
}
}
return MAX_NUM_10MB + 1;
}
// 生成测试文件,故意去掉一个数字
void generate_test_file(const std::string& filename,
uint32_t missing_num,
uint32_t max_num) {
std::ofstream out(filename, std::ios::binary);
std::vector<uint32_t> numbers;
// 生成0到max_num的所有数字
for (uint32_t i = 0; i <= max_num; ++i) {
if (i != missing_num) {
numbers.push_back(i);
}
}
// 打乱顺序
std::random_device rd;
std::mt19937 g(rd());
std::shuffle(numbers.begin(), numbers.end(), g);
// 写入文件
for (uint32_t num : numbers) {
out.write(reinterpret_cast<const char*>(&num), sizeof(num));
}
}
int main() {
// 测试1GB内存方案
{
const std::string filename = "1gb_test.bin";
// uint32_t missing_num = 123456789; // 故意去掉的数字
uint32_t missing_num = 12345; // 故意去掉的数字
std::cout << "生成1GB内存方案的测试文件..." << std::endl;
generate_test_file(filename, missing_num, MAX_NUM_1GB);
std::cout << "使用1GB内存方案查找缺失数字..." << std::endl;
uint32_t found = find_missing_number_1gb(filename);
std::cout << "预期缺失数字: " << missing_num << std::endl;
std::cout << "实际找到的缺失数字: " << found << std::endl;
if (found == missing_num) {
std::cout << "测试通过!" << std::endl;
} else {
std::cout << "测试失败!" << std::endl;
}
}
// 测试10MB内存方案
{
const std::string filename = "10mb_test.bin";
// uint32_t missing_num = 987654; // 故意去掉的数字
uint32_t missing_num = 98765; // 故意去掉的数字
std::cout << "\n生成10MB内存方案的测试文件..." << std::endl;
generate_test_file(filename, missing_num, MAX_NUM_10MB);
std::cout << "使用10MB内存方案查找缺失数字..." << std::endl;
uint32_t found = find_missing_number_10mb(filename);
std::cout << "预期缺失数字: " << missing_num << std::endl;
std::cout << "实际找到的缺失数字: " << found << std::endl;
if (found == missing_num) {
std::cout << "测试通过!" << std::endl;
} else {
std::cout << "测试失败!" << std::endl;
}
}
return 0;
}
4. 代码分析:
10MB内存解决方案详细解释
这个算法通过两次遍历文件的方式,在有限内存(10MB)下找出缺失的整数。核心思想是分而治之,将大问题分解为小问题处理。
算法主要步骤:
1)初始化阶段
uint32_t range_count = MAX_NUM_10MB / RANGE_SIZE + 1;
std::vector<uint32_t> range_counts(range_count, 0);
- 将整个数字范围(0-10亿)划分为多个小区间(每个区间10万个数字)
- 创建
range_counts
数组记录每个区间内的数字数量 - 内存消耗:10亿/10万=1万个计数器,约40KB内存
2)第一次遍历 - 统计区间计数
while (input.read(reinterpret_cast<char*>(&num), sizeof(num))) {
uint32_t range_index = num / RANGE_SIZE;
++range_counts[range_index];
}
- 遍历整个文件,对每个数字:
- 计算它所属的区间索引(
num / RANGE_SIZE
) - 对应区间的计数器加1
- 计算它所属的区间索引(
- 结果:得到每个区间实际包含的数字数量
3)查找不完整的区间
for (; target_range < range_count; ++target_range) {
if (range_counts[target_range] < RANGE_SIZE) {
break;
}
}
- 遍历所有区间,找到第一个数字数量不足10万的区间
- 完整区间应有10万个数字,缺失数字的区间会少一个
4)第二次遍历 - 精确定位缺失数字
std::vector<bool> bitmap(RANGE_SIZE, false);
// ...读取文件...
if (num >= range_start && num < range_end) {
bitmap[num - range_start] = true;
}
- 创建位图(10万位,约12.5KB内存)记录目标区间内数字出现情况
- 再次遍历文件,只关注目标区间内的数字:
- 计算数字在区间内的偏移量(
num - range_start
) - 在位图中标记该位置为已出现
- 计算数字在区间内的偏移量(
5)找出缺失数字
for (uint32_t i = 0; i < RANGE_SIZE; ++i) {
if (!bitmap[i]) {
return range_start + i;
}
}
- 扫描位图,找到第一个未被标记的位置
- 计算实际缺失数字:
区间起始值 + 偏移量
内存使用分析
- 范围计数器数组:10000个uint32_t ≈ 40KB
- 位图:100000位 ≈ 12.5KB
- 其他变量:少量临时变量
- 总计:远小于10MB限制
为什么这样设计?
1)两次遍历的权衡:
- 第一次快速定位可能有缺失的区间(O(n)时间)
- 第二次只在目标区间详细检查
2)空间效率:
- 避免存储全部数字的位图(10亿数字位图需要125MB)
- 只存储当前区间的位图
3)通用性:
- 适用于任何满足条件的输入规模
- 通过调整RANGE_SIZE可以平衡内存使用和IO次数
这种设计完美体现了"空间换时间"和"时间换空间"的经典思想,在严格内存限制下依然能高效解决问题。
5. 运行结果:
生成1GB内存方案的测试文件...
使用1GB内存方案查找缺失数字...
预期缺失数字: 12345
实际找到的缺失数字: 12345
测试通过!
生成10MB内存方案的测试文件...
使用10MB内存方案查找缺失数字...
预期缺失数字: 98765
实际找到的缺失数字: 98765
测试通过!
感谢您的阅读。原创不易,如您觉得有价值,请点赞,关注。