大厂高频经典面试题(66)-失踪的整数

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
测试通过!

感谢您的阅读。原创不易,如您觉得有价值,请点赞,关注。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水草

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值