腾讯面试:有40亿整数,如何 判断一个 int 是在其中,越快越好 ?

本文 的 原文 地址

本文 的 原文 地址

尼恩说在前面:

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如腾讯、得物、阿里、滴滴、极兔、有赞、shein 希音、shopee、百度、网易的面试资格,遇到很多很重要的海量数据处理 相关面试题:

前几天 小伙伴面试 腾讯,遇到了这个问题。但是由于 没有回答好,导致面试挂了。

小伙伴面试完了之后,来求助尼恩。那么,遇到 这个问题,该如何才能回答得很漂亮,才能 让面试官刮目相看、口水直流。

所以,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增,可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。

当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典》V145版本PDF集群,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。

最新《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请关注本公众号【技术自由圈】获取,后台回复:领电子书

原始的 面试问题:

下面是小伙伴面试腾讯,遇到的 完整问题:

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

常规的思路

下面内容从这道面试题开始引入,这道面试题,怎么回答 好呢?

容易想到的做法:

  • 直接遍历:比如set,map存储,然后直接遍历时间复杂度o(n)
  • 二分查找,比如array存储,先排序o(nlogn),二分查找o(logn),二分查找时间复杂度o(nlogn) + o(logn)

如果面试被问到类似的这种问题,上面两种回答绝对是挂了的。 那么我们该用什么方法来解决这个问题?

尼恩团队告诉大家,一定要讲到 下面的三种方案, 越全面越好。

第1种方案: 位图

为什么要用位图?

首先我们对40亿个无符号整数算一下,它到底是多少G呢?

40亿个整数大概是 40亿*4个字节=160亿个字节 ,4G=2^32byte,大概为42亿九千万字节,所以1G大概就是10亿字节 ,所以40亿个整数大概就是16G。

那这么大数据放到内存中,一般 是放不下的,怎么办呢? 可以首先 考虑二分查找,比如 map,set等等。

但是,map、set 还有额外的消耗,这更不可能完成了。

于是我们可以 用另一个 更加高效率 的结构: 位图!

什么是位图?

位图本质:是一种直接定址法的哈希,因此效率很高,用O(1)就可以探测到对应位是0还是1,效率非常高,因此可以快速判断。

判断数据是否在给定的 整形 集合中,结果是在或者不在,刚好是两种状态。

那么可以使用一个二进制比特位来代表数据是否存在的信息,如果二进制比特位为1,代表存在,为0 代表不存在。

利用每一个比特位的0或1的情况,来判断数在不在,所以40亿不重复的数,开辟2^32-1个比特位,转化为G,也就512m,从 16G 到 512M, 效能提升了 32倍。

image-20250523123129366

位图的操作方法:

  • 设置位(set):将某一位置上的位设为 1,表示该位置对应的元素存在。
  • 清除位(reset):将某一位置上的位设为 0,表示该位置对应的元素不存在。
  • 检查位(test):检查某一位置上的位是 1 还是 0,以判断该元素是否存在。

位图存储的案例:

通过位图判断数组元素是否存在,如下图

image-20250523123159696

位图解决方案分析

1、确定位图大小

举例说明:

image-20250523123159696

位图所给的大小取决于给的数的范围值,最大数是23,所以我们只需要开23个比特位就可以。

但是,内存是 无法按比特位来分配,所以我们可以使用byte(或者int)来分配 位图,byte相对于int粒度更细。

接下来的任务就是找到数在位图中的位置,然后置1

2、怎么算数在位图中那个位?

这里内存中的位图以byte为例,参考后面代码实现

  • 第一步,计算在哪一个 byte或者 int(这个案例是byte)。每个数我们可以先 num/8,算出他在第几个byte里,比如:3 / 8 = 0,在第1个byte;
  • 第二步,计算在byte里边的 哪一位。 然后再num%8算出在哪一位,比如3 % 8=3,在第4位上面。

image-20250523123423617

3、置位操作(set)

如何把任意一位置1,且不改变其他位?

  • 第一步,先把 目标位置 设置为1 (移位运算) 。 就是 把 “1” (这个是一个干净的1 )左移 n位,就是 向高位移动 ,移动上面计算的位数(即其他位是0,只有要改变那一位是1),比如num=3 ,先算 1 << 3 = 1000
  • 第二步,再把 目标位和 原来的字节 合并 (或运算)。 然后把上面的数和原来的数进行或运算,就可以得到结果。保证了其他位不变,只有该位被改变为了1.

4、清除位操作(reset)

那么在把某一位置为1以后,要重新置为0的话,应该怎么搞呢?

直接将1移位以后,再取反,将结果和原数进行 合并 ,不过,这个是 与运算。

  • 第一步,先把 目标位置 设置为1 (移位运算) 。

  • 第二步,再 把 第一步的结果 取反 , 将结果和原数进行 与运算 ,剩下其他的位,目标位为 0。

5、检查位(test)

那要测试这个数在不在位图中,怎么测试呢?

  • 第一步,先把 目标位置 设置为1 (移位运算) 。

  • 第二步,再 把 目标位和 原来的字节 相与,结果 不为0则存在,结果 为0则不存在

6、参考实现

基于字节 byte 编排的位图 参考实现

如果基于字节 byte 进行 bit 编排,也就是 整数 维度分配位图,参考代码如下:

怎么算数在位图中那个位置 ?

  • 第一步,计算在哪一个 byte或者 int(这个案例是byte)。每个数我们可以先 num/8,算出他在第几个byte里,比如:3 / 8 = 0,在第1个byte;
  • 第二步,计算在byte里边的 哪一位。 然后再num%8算出在哪一位,比如3 % 8=3,在第4位上面。

以位图数据元素为byte为例,我们拿到一个数x,进行如下计算:

  • i = x / 8;

  • j = x % 8;

这样

  • i 就表示x在第 i 个byte值当中;

  • j 表示它在该值的第 j 位。


public class CustomBitSet {
    private byte[] bits;

    // 构造函数:初始化 bitset,最大支持 maxBits 个无符号整数
    public CustomBitSet(long maxBits) {
        //通过加上 7 再进行 / 8 运算,可以实现向上取整的效果,确保即使有余数也能分配足够的字节数
//        long bytesNeeded = (maxBits + 7) / 8;  // 等价于 (maxBits >> 3) + 1
        long bytesNeeded = (maxBits >> 3) + 1; //左移3位就相当于/8,效率更快一些,但要注意运算符的优先级
        bits = new byte[(int) bytesNeeded];
    }

    // 设置第 x 位为 1
    public void set(long x) {
        int i = (int) (x >> 3);  // 字节索引(等价于 x / 8)
        int j = (int) (x & 7);   // 位偏移(等价于 x % 8)
        bits[i] |= (1 << j); //  将 bits[i] 的第 j 位设为 1,其余位保持不变。
    }

    // 将第 x 位重置为 0
    public void reset(long x) {
        int i = (int) (x >> 3);
        int j = (int) (x & 7);
        bits[i] &= ~(1 << j); //将 bits[i] 的第 j 位设为 0,其余位保持不变。
    }

    // 判断第 x 位是否为 1
    public boolean test(long x) {
        int i = (int) (x >> 3);
        int j = (int) (x & 7);
        return (bits[i] & (1 << j)) != 0; // 判断 bits[i] 的第 j 位是否为 1
    }

    public static void main(String[] args) {
        CustomBitSet bitSet = new CustomBitSet(4294967295L); // 40亿个数

        bitSet.set(123456L);
        bitSet.set(789012L);

        System.out.println(bitSet.test(123456L)); // true
        System.out.println(bitSet.test(999999L)); // false
    }

}

基于整数 编排的位图 参考实现

如果基于int整数 进行 bit 编排,也就是 整数 维度分配位图,参考代码如下:

以位图数据元素为int为例,我们拿到一个数x,进行如下计算:

  • i = x / 32;
  • j = x % 32;

这样, i 就表示x在第 i 个int值当中;j 表示它在该值的第 j 位。

public class XnyBitSet {
    private int[] bits;

    // 构造函数:初始化 bitset,最大支持 maxBits 个无符号整数
    public XnyBitSet(long maxBits) {
        // 计算数组大小:(maxBits + 31) / 32,确保向上取整
        int size = (int) ((maxBits + 31) / 32); // 向上取整
        bits = new int[size];
    }

    // 设置第 x 位为 1
    public void set(long x) {
        int index = (int) (x >> 5);     // 等价于 x / 32
        int offset = (int) (x & 0x1F);  // 等价于 x % 32
        bits[index] |= (1 << offset);   //  在 bits[index] 中设置对应位
    }

    // 将第 x 位重置为 0
    public void reset(long x) {
        int index = (int) (x >> 5);
        int offset = (int) (x & 0x1F);
        bits[index] &= ~(1 << offset);       // 清除对应位
    }

    // 判断第 x 位是否为 1
    public boolean test(long x) {
        int index = (int) (x >> 5);
        int offset = (int) (x & 0x1F);
        return (bits[index] & (1 << offset)) != 0; // 判断 bits[index] 的第 offset 位是否为 1
    }

    public static void main(String[] args) {
        XnyBitSet bitSet = new XnyBitSet(4294967295L); // 42.9 亿个数

        bitSet.set(123456L);
        bitSet.set(789012L);

        System.out.println(bitSet.test(123456L)); // true
        System.out.println(bitSet.test(999999L)); // false
    }

}

讲到这里, 再回顾一下咱们的问题:

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

到此为止,用位图就可以解决了。

7 位图的问题

还是占内存。

位图的问题是: 内存占用比较大, 这里还需要 512M内存。

那么 , 能不能 更小的内存,比如 51M内存, 甚至 5M内存呢?

可以。

下一种就是 哈希切分 + 位图。

第2种方案: 哈希切分+位图

哈希切分(Hash Partitioning)是针对海量数据存在性检测的高效方法,尤其适用于无法一次性将所有数据加载到内存的场景。

针对40亿数据中检测某个数据是否存储的问题,其核心实现步骤如下:

‌1、分治思想‌

将40亿数据通过哈希函数划分为多个独立的子集(桶),每个子集的数据规模适配内存处理能力。

  • 第一次哈希‌:将所有数据根据哈希值分发到多个磁盘文件中;
  • 第二次位图‌:对每个文件单独加载到内存,使用精确数据结构( 这里是 位图)构建快速查询机制。

‌2、哈希分桶‌

选择一个合适的哈希函数(如MurmurHashCRC32),将40亿数据分配到N个桶文件中。

假设内存限制为1GB,单个桶数据量应控制在内存可处理范围内。

例如:

  • N设置为1000(1000个桶),那么 每个桶平均存储400万数据;
  • 每个桶文件大小约为400万×4字节≈16MB,仅仅消耗 16M内存。

‌3、构建哈希索引‌

对每个桶文件执行以下操作:

  • 加载到内存‌:

读取单个桶文件;

  • ‌构建 位图‌:

将数据存入 位图(整数类型适用,其他类型的数据,这里可以使用第二级的hash表),例如:


// 位图示例(假设数据类型为无符号整数)
Bitmap bitmap(MAX_VALUE); // MAX_VALUE为桶内最大值
for (auto num : bucket_data) {
    bitmap.set(num); // 标记存在性
}

4、查询流程

给定待检测数据x

  • 计算哈希值h = hash(x) % N,确定其所属桶文件bucket_h

  • 加载bucket_h对应的 位图到内存;

  • 通过bitmap.test(x)hash_table.contains(x)判断是否存在 。

5、性能优化

(‌1) 哈希函数选择
需保证哈希分布均匀,避免数据倾斜导致单个桶过大5。

(‌2)‌ 并行处理
多线程分桶和查询加速,减少磁盘I/O等待时间。

(‌3)‌ 压缩存储
若数据为整数,使用位图压缩存储(每数仅占1bit),内存占用可降至约500MB 。

6、代码示例(哈希切分+位图)



import java.io.*;
import java.util.BitSet;
import java.util.HashMap;

public class HashPartitionDetector {
    private static final int BUCKET_COUNT = 1000; // 分桶数量
    private static final String BUCKET_PREFIX = "bucket_"; // 桶文件前缀

    /**
     * 分桶阶段:将数据根据哈希值分散到不同文件
     * @param dataFile 原始数据文件路径
     */
    public static void partitionData(String dataFile) throws IOException {
        BufferedWriter[] buckets = new BufferedWriter[BUCKET_COUNT];
        
        // 初始化桶文件写入器
        for (int i = 0; i < BUCKET_COUNT; i++) {
            buckets[i] = new BufferedWriter(new FileWriter(BUCKET_PREFIX + i + ".txt"));
        }

        try (BufferedReader br = new BufferedReader(new FileReader(dataFile))) {
            String line;
            while ((line = br.readLine()) != null) {
                long num = Long.parseLong(line.trim());
                int bucketId = (int) (hash(num) % BUCKET_COUNT);
                buckets[bucketId].write(line + "\n"); // 写入对应桶文件
            }
        }

        // 关闭所有文件写入器
        for (BufferedWriter writer : buckets) {
            writer.close();
        }
    }

    /**
     * 检测阶段:检查目标值是否存在
     * @param target 待检测数值
     * @return 是否存在
     */
    public static boolean checkExistence(long target) throws IOException {
        int bucketId = (int) (hash(target) % BUCKET_COUNT);
        String bucketFile = BUCKET_PREFIX + bucketId + ".txt";
        
        // 使用BitSet存储桶内数据(适用于整数)
        BitSet bitmap = new BitSet();
        
        try (BufferedReader br = new BufferedReader(new FileReader(bucketFile))) {
            String line;
            while ((line = br.readLine()) != null) {
                long num = Long.parseLong(line.trim());
                if (num >= 0) {
                    bitmap.set((int) num); // 标记存在位
                }
            }
        }
        
        return bitmap.get((int) target);
    }

    /**
     * 简易哈希函数(实际生产环境建议用MurmurHash等)
     */
    private static long hash(long key) {
        key = (~key) + (key << 21);
        key = key ^ (key >>> 24);
        key = (key + (key << 3)) + (key << 8);
        return key ^ (key >>> 14);
    }

    public static void main(String[] args) throws IOException {
        // 示例用法
        partitionData("bigdata.txt");  // 先分桶处理原始数据
        System.out.println(checkExistence(123456789L)); // 检查数值是否存在
    }
}


通过哈希切分结合位图的方式,可高效解决40亿级数据的存在性检测问题,兼顾内存限制与查询性能 。

还有没有其他方法呢?

讲到这里, 再回顾一下咱们的问题:

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

到此为止,用位图、 哈希表+位图 就可以解决了。

哈希表+位图 需要 占用磁盘空间。 那么, 还有没有其他的 不用占用磁盘空间的方法呢。

可以。下一种就是 布隆过滤器。 这是一种牺牲 准确度,换取 内存 的方法。

第3种方案:布隆过滤器

在讲布隆过滤器之前,我们要说一说位图结构的缺点是什么?

最大的缺点就是:

  • 开空间得看数据范围,一般要求范围集中,分散的话空间消耗就会上升
  • 只能针对整型 如果给了一堆字符串,可不可以使用位图判断是否存在呢? 当然可以,可以使用哈希函数,将字符串转化为整型,再去映射到位图中。

当针对字符串来判断是否存在时,位图+多次哈希其实就是布隆过滤器了。

布隆过滤器是由布隆(Burton Howard Bloom)在1970年提出的 一种紧凑型的、比较巧妙的 概率型数据结构。

布隆(Burton Howard Bloom) 特点是 高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,它是用多个哈希函数,将一个数据映射到位图结构中。

布隆(Burton Howard Bloom) 不仅可以提升查询效率,也可以节省大量的内存空间。

话不多说,上例子来理解这段话:

image-20250523123830355

当不同的字符串通过哈希函数转化为整型映射到位图中时,就会发生哈希碰撞!

比如find通过哈希函数可能和insert映射到同一位置,那么当find不存在时,但是他的位置的确已经被置为1,所以这就导致了:

  • 判断存在是不准确的
  • 判断不存在一定是准确的,因为位置是0,那一定不存在

image-20250523124005598

于是,我们就要想一些办法,让他的误判率低一些:

可以增加不同的哈希函数,转化为不同的哈希值,去映射到多个位置,降低误判率

image-20250523124043741

这样的话,我们可以看到,只有当一个字符串映射的全部位置都置为1时,这个数才可能存在,说的是可能存在,因为也可能存在哈希碰撞。但降低了哈希碰撞的概率,降低了误判率。

那还有问题就是:一个字符串映射多个位图的位置,那位图应该开多大呢? 或者说如何选择哈希函数个数和布隆过滤器长度?

大佬研究出来的一个公式:

k为哈希函数个数,m 为布隆过滤器长度,n为插入的元素个数,p 为误报率

布隆过滤器长度计算公式:


m = k * n / 0.7

哈希函数个数计算公式:


k = m / n * ln2

现在来实现布隆过滤器:

  • 假设k==3,m=4.2n
  • 假设k==4,m=5.7n

布隆过滤器参考实现:


import java.util.BitSet;

/**
 * 布隆过滤器Java实现
 * @param <K> 键类型,默认为String
 */
public class BloomFilter<K> {
    private final BitSet bitSet;      // 位数组
    private final int size;           // 位数组大小(N*M)
    private final HashFunction<K> hash1;
    private final HashFunction<K> hash2;
    private final HashFunction<K> hash3;

    /**
     * 构造函数
     * @param n 预期元素数量因子
     * @param m 每个元素占用的位数因子
     * @param hash1 第一个哈希函数实现
     * @param hash2 第二个哈希函数实现
     * @param hash3 第三个哈希函数实现
     */
    public BloomFilter(int n, int m, 
                      HashFunction<K> hash1, 
                      HashFunction<K> hash2,
                      HashFunction<K> hash3) {
        this.size = n * m;
        this.bitSet = new BitSet(size);
        this.hash1 = hash1;
        this.hash2 = hash2;
        this.hash3 = hash3;
    }

    /**
     * 添加元素到布隆过滤器
     * @param key 要添加的键
     */
    public void set(K key) {
        // 计算三个哈希位置
        int i = Math.abs(hash1.hash(key) % size);
        int j = Math.abs(hash2.hash(key) % size);
        int k = Math.abs(hash3.hash(key) % size);

        // 设置对应位为1
        bitSet.set(i);
        bitSet.set(j);
        bitSet.set(k);
    }

    /**
     * 检查元素是否存在(可能有误判)
     * @param key 要检查的键
     * @return false表示一定不存在,true表示可能存在
     */
    public boolean test(K key) {
        // 检查第一个哈希位
        int i = Math.abs(hash1.hash(key) % size);
        if (!bitSet.get(i)) return false;

        // 检查第二个哈希位
        int j = Math.abs(hash2.hash(key) % size);
        if (!bitSet.get(j)) return false;

        // 检查第三个哈希位
        int k = Math.abs(hash3.hash(key) % size);
        if (!bitSet.get(k)) return false;

        return true; // 所有位都为1,元素可能存在
    }

    /**
     * 哈希函数接口
     * @param <T> 键类型
     */
    public interface HashFunction<T> {
        int hash(T key);
    }

    // 示例哈希函数实现(BKDRHash)
    public static class BKDRHash implements HashFunction<String> {
        @Override
        public int hash(String key) {
            int seed = 131; // 31/131/1313等质数
            int hash = 0;
            for (char c : key.toCharArray()) {
                hash = hash * seed + c;
            }
            return hash & 0x7FFFFFFF; // 确保为正数
        }
    }
}

public class BloomFilterTest {
    public static void main(String[] args) {
        // 创建布隆过滤器(预期100万元素,每个元素5位)
        BloomFilter<String> filter = new BloomFilter<>(1_000_000, 5);

        // 添加元素
        filter.set("google.com");
        filter.set("baidu.com");

        // 测试存在性
        System.out.println(filter.test("google.com")); // true(可能存在)
        System.out.println(filter.test("baidu.com"));  // true
        System.out.println(filter.test("twitter.com")); // false(一定不存在)
    }
}


当然也可以采用java 中的 现成的布隆过滤器实现方案, 常见的有:

  • Apache Commons Collections,提供基础版布隆过滤器实现

  • Guava库实现,基于内存,适合单机环境:

  • Redisson分布式实现,基于Redis的分布式布隆过滤器,适合集群环境

那布隆过滤器支持删除吗?

no,当然不支持!

没有reset(不可以删除)的原因是:因为存在哈希冲突,修改一个数的哈希值映射位置的值,会影响到其他的数,导致结果不准确。

硬要有reset,就需要计数,通过计数(--)来控制,那就需要成倍的位图来表示个数,严重浪费内存空间。

image-20250523124200680

如上图所示这样实现

海量数据处理 相关面试题

如果上面的题目不过瘾,再来几个 海量数据处理 面试题

面试题2: 给定100亿个整数,设计算法找到只出现一次的整数?

这是一个 位图 相关面试题。

给定100亿个整数,设计算法找到只出现一次的整数?

这个面试题,不仅仅是判存在,而是 统计次数的话。

那么,就需要 通过多状态位图(两个位图)。

  • 00:未出现。 第一个 位图 上 对应位置 的值 0,第一个 位图 上 对应位置 的值 0, 表示 一次都没有出现。
  • 01:出现一次。第一个 位图上 对应位置 的值 0,第一个 位图上 对应位置 的值 1, 表示出现一次。
  • 10:出现两次。第一个 位图 上 对应位置 的值 1,第一个 位图 上 对应位置 的值 0, 表示出现两次。
  • 11:出现三次及以上。第一个 位图 上 对应位置 的值 0,第一个 位图 上 对应位置 的值 0, 表示出现三次及以上。

我们可以在1GB内存中高效处理100亿个整数,找出只出现一次的值。 该方法利用位图的紧凑存储和位运算的快速操作,解决了海量数据下的内存瓶颈问题。

如果 内存大点, 位图可以扩展。

以此内推,其实可以用三个位图, 统计 8 次以内的次数。 分别是 000/001/....等。

以此内推,其实可以用四个位图, 统计 16 次以内的次数。 分别是 0000/0001/....等。

一个使用多状态位图案例:

image-20250523124302398

直接上代码:



public class TwoBitSet {
    private final XnyBitSet _bs1;
    private final XnyBitSet _bs2;
    private final long n;

    // 构造函数:初始化两个位图,最大支持 n 个无符号整数
    public TwoBitSet(long n) {
        this.n = n;
        this._bs1 = new XnyBitSet(n);
        this._bs2 = new XnyBitSet(n);
    }

    // 设置整数 x 的出现次数
    public void set(long x) {
        if (!_bs1.test(x) && !_bs2.test(x)) { // 如果 x 不在两个位图中,则将其设置为 01
            // 00 -> 01(第一次出现)
            _bs2.set(x);
        } else if (!_bs1.test(x) && _bs2.test(x)) { // 如果 x 在 _bs1 中,则将其设置为 10
            // 01 -> 10(第二次出现)
            _bs1.set(x);
            _bs2.reset(x);
        }
        // 其他状态(如 10 或 11)不处理
    }

    // 打印只出现一次的整数
    public void printOnce() {
        for (long i = 0; i < n; ++i) {
            if (!_bs1.test(i) && _bs2.test(i)) { // 如果 01,则输出 x
                System.out.println(i);
            }
        }
        System.out.println();
    }

    public static void main(String[] args) {
        TwoBitSet twoBitSet = new TwoBitSet(100); // 最大支持 100 个整数

        twoBitSet.set(10);
        twoBitSet.set(20);
        twoBitSet.set(30);
        twoBitSet.set(10); // 重复出现
        twoBitSet.set(20); // 重复出现
        twoBitSet.set(20); // 再次出现

        twoBitSet.printOnce(); // 输出:10, 30
    }


}


set方法逻辑:

  • 一个数如果在两个位图中的同一位置都是0,那么说明就是0次 ,
  • 再进来的数就要将00第二位set为01,表示出现一次,
  • 后面同理可得。其他状态不用处理

printOnce方法逻辑:

  • 遍历元素,判断是01,则输出

面试题3: 1个文件有100亿个int,1G内存,设计算法找到出现次数不超过2次的所有整数

这是一个 位图 相关面试题。

首先我们知道,整数的范围最大是42亿多,所以100亿个整数中,一定存在许多重复的整数。

所以将文件中的数据都放入位图中,只看 存在或者不存在两种状态,占用内存很少。

通过两个位图来表示次数,00(0次),01(1次),10(2次),11(3次及以上),然后控制条件(只找01,10)输出结果,和上一个问题其实是一样的。

代码如下:


public class TwoBitSet2 {
    private final XnyBitSet bits1;
    private final XnyBitSet bits2;
    private final long maxBits;

    // 构造函数:初始化两个位图,最大支持 maxBits 个无符号整数
    public TwoBitSet2(long maxBits) {
        this.maxBits = maxBits;
        this.bits1 = new XnyBitSet(maxBits);
        this.bits2 = new XnyBitSet(maxBits);
    }

    // 设置整数 num 的出现次数状态
    public void set(long num) {
        // 00 -> 01(第一次出现)
        if (!bits1.test(num) && !bits2.test(num)) {
            bits2.set(num);
        }
        // 01 -> 10(第二次出现)
        else if (!bits1.test(num) && bits2.test(num)) {
            bits1.set(num);
            bits2.reset(num);
        }
        // 10 -> 11(第三次及以上出现)
        else if (bits1.test(num) && !bits2.test(num)) {
            bits2.set(num);
        }
        // 11 表示出现超过两次,无需处理
    }

    // 判断整数 num 是否出现超过两次
    public boolean overTwice(long num) {
        return bits1.test(num) && bits2.test(num); // bits1 和 bits2 都为 1 表示出现超过两次
    }

    // 判断整数 num 是否未出现过
    public boolean empty(long num) {
        return !bits1.test(num) && !bits2.test(num); // bits1 和 bits2 都为 0 表示未出现过
    }


    public static void main(String[] args) {
        // 定义数组
        int[] nums = {1, 2, 3, 4, 4, 5, 3, 2, 5, 6, 7, 5, 4};

        // 初始化双位图,最大支持 10 个无符号整数(0 ~ 9)
        TwoBitSet2 twoBitSet = new TwoBitSet2(10);

        // 将 nums 中的数字设置到位图中
        for (int num : nums) {
            twoBitSet.set(num);
        }

        // 查询每个数字的出现状态
        for (int i = 0; i < 10; ++i) {
            if (!twoBitSet.empty(i) && !twoBitSet.overTwice(i)) {
                System.out.println(i + " 出现不超过两次");
            }
        }
    }

}

面试题4:给两个文件,分别有100亿个整数,我们只有1G内存,如何找到两个文件交集?

这是一个 位图 相关面试题。

思路一:

将其中一个文件的所有值映射到一个位图,然后去遍历另一个文件中的数据去判断在不在。

这种思路存在一个缺陷,得到的交集中可能出现重复值的情况,而正常情况下交集中是不应该出现重复值,因此在前面求得交集后,还需要用 set 进行去重。

这里可能会出现重复的数据过多的情况,去使用 set 可能会超过 1G 内存。

思路二:

将两个文件中的整数分别映射到两个位图中,然后再将两个位图按位与一下,值为 1 的比特位对应的整数就是交集。

代码实现:


public class IntersectionDemo {

    public static void main(String[] args) {
        // 定义两个数组
        int[] nums1 = {1, 2, 3, 4, 4, 5, 3, 2, 5, 6, 7, 5, 4};
        int[] nums2 = {2, 2, 3, 3, 4, 3, 3, 2, 4, 4};

        // 初始化两个位图,最大支持 10 个无符号整数(0 ~ 9)
        XnyBitSet bitSet1 = new XnyBitSet(10);
        XnyBitSet bitSet2 = new XnyBitSet(10);

        // 将 nums1 中的数字设置到位图1中
        for (int num : nums1) {
            bitSet1.set(num);
        }

        // 将 nums2 中的数字设置到位图2中
        for (int num : nums2) {
            bitSet2.set(num);
        }

        // 找出交集并输出
        System.out.print("交集:");
        for (int i = 0; i < 10; ++i) {
            if (bitSet1.test(i) && bitSet2.test(i)) {
                System.out.print(i + " ");
            }
        }
        System.out.println("\n是 nums1 和 nums2 的交集");
    }

}

面试题5:给一个超过100G大小的log file, log中存着IP地址, 设计算法找到出现次数最多的IP地址?

这是一个 哈希切分相关面试题。

为什么不用位图?

由前面所学,我们可能会想到位图,但是行不通,要统计次数就要开辟多个位图,

image-20250523124344338

成倍的开辟位图来表示次数的话,会占用大量的内存空间,内存也存不下。

哈希切分怎么做?

我们用的是map记录,但是在用map之前,要把大文件处理:

那我们就可以利用哈希的思想来把100G的文件分成100个小文件,每个1G,那么不就可以进内存了吗?

那怎么分???

  • 平均分?那当然不行,平均分对于分散的ip地址,都不在同一个小文件中,进入内存用map统计时,结果是不正确的。
  • 需要哈希切分,让相同的IP进入同一个分区小文件。

哈希切分示意图

image-20250523124418500

我们可以对100G大文件中的ip进行哈希切分,利用哈希表的思想,将哈希值相同的放入同一个小文件中,然后通过一个一个的小文件进入内存读取并统计个数,搞完一个clear掉,记录再进下一个。

实现步骤:

1、哈希切分:

  • 使用 IP 的哈希值对分区数取模,将 IP 分配到不同的小文件中。
  • 保证相同 IP 始终被分配到同一个文件,从而保证统计正确。

2、递归处理:

  • 如果某个小文件超过内存限制(如 1GB),递归切分该文件为更小的子文件。
  • 使用不同的分区数(如每次乘以 10),确保最终每个文件都能被内存处理。

3、内存统计:

  • 每个小文件在内存中使用 HashMap<String, Integer> 统计 IP 出现次数。
  • 记录每个小文件中出现次数最多的 IP,最后合并所有结果。
特性 说明
哈希切分 使用 IP 哈希值 % 分区数,保证相同 IP 总在同一个文件中。
递归处理 若某小文件过大,继续切分,直到文件大小在内存限制内。
统计效率 每个小文件使用 HashMap 统计,时间复杂度 O(N),空间复杂度 O(K)。
适用性 适用于大文件日志分析、IP 统计、TopK 等任务。

参考实现


public class HashPartitioning {

    // 每个文件的最大大小(1GB)
    private static final long MAX_FILE_SIZE = 1024L * 1024 * 1024;

    public static void main(String[] args) throws IOException {
        String inputFilePath = "F:\\work\\lxs\\69_interview\\code\\demo\\src\\main\\java\\com\\example\\demo\\algorithm\\ip_log.txt"; // 100G 的日志文件
        List<File> smallFiles = new ArrayList<>();

        // 初始切分 100 个小文件
        splitFile(inputFilePath, 100, smallFiles);

        // 统计所有小文件中的最大 IP 出现次数
        Map<String, Integer> globalMax = new HashMap<>();

        for (File file : smallFiles) {
            if (file.length() > MAX_FILE_SIZE) {
                // 如果某个小文件超过 1GB,递归切分
                List<File> subFiles = new ArrayList<>();
                splitFile(file.getAbsolutePath(), 10, subFiles);
                for (File subFile : subFiles) {
                    processFile(subFile, globalMax);
                }
            } else {
                processFile(file, globalMax);
            }
        }

        // 找出全局出现次数最多的 IP
        String maxIP = null;
        int maxCount = 0;
        for (Map.Entry<String, Integer> entry : globalMax.entrySet()) {
            if (entry.getValue() > maxCount) {
                maxIP = entry.getKey();
                maxCount = entry.getValue();
            }
        }

        System.out.println("出现次数最多的 IP 是: " + maxIP);
        System.out.println("出现次数为: " + maxCount);
    }

    /**
     * 将大文件切分成多个小文件
     */
    private static void splitFile(String inputFilePath, int partitionCount, List<File> outputFiles) throws IOException {
        File inputFile = new File(inputFilePath);
        outputFiles.clear();

        // 创建输出文件
        for (int i = 0; i < partitionCount; i++) {
            String outputFileName = inputFilePath + "_part_" + i;
            File outFile = new File(outputFileName);
            outFile.delete(); // 清空旧文件
            outFile.createNewFile();
            outputFiles.add(outFile);
        }

        // 读取并切分文件
        try (BufferedReader reader = new BufferedReader(new FileReader(inputFile))) {
            String line;
            while ((line = reader.readLine()) != null) {
                String ip = line.trim();
                int hash = ip.hashCode();
                int index = Math.abs(hash % partitionCount);
                try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFiles.get(index), true))) {
                    writer.write(ip);
                    writer.newLine();
                }
            }
        }
    }

    /**
     * 统计单个小文件中 IP 出现的次数
     */
    private static void processFile(File file, Map<String, Integer> result) throws IOException {
        Map<String, Integer> counter = new HashMap<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
            String line;
            while ((line = reader.readLine()) != null) {
                String ip = line.trim();
                counter.put(ip, counter.getOrDefault(ip, 0) + 1);
            }
        }

        // 找出该文件中出现次数最多的 IP
        String maxIP = null;
        int maxCount = 0;
        for (Map.Entry<String, Integer> entry : counter.entrySet()) {
            if (entry.getValue() > maxCount) {
                maxIP = entry.getKey();
                maxCount = entry.getValue();
            }
        }

        // 更新全局最大值
        result.put(maxIP, Math.max(result.getOrDefault(maxIP, 0), maxCount));
    }
}

日志文件示例(ip_log.txt


192.168.1.1
10.0.0.1
192.168.1.1
10.0.0.1
10.0.0.1
192.168.1.1
192.168.1.1
10.0.0.1
192.168.1.1
10.0.0.1
10.0.0.1

输出结果


出现次数最多的 IP 是: 10.0.0.1
出现次数为: 6


面试题4:给两个文件,分别有100亿个query,我们只有1G内存,如何找到两个文件交集?分别给出 精确算法和近似算法。

这是一个 布隆过滤 相关面试题。

query-般是查询指令,比如可能是一个网络请求,是一个数据库sq|语句 假设平均每个query是50byte, 100亿个query合计多少内存? -- 500G

当看到这个题目时,可能就会想到位图来解决,但是100亿个字符串都是不相同的,100亿个字符串已经超过了1G,不可行。

可以有两种方案:

  • 精确算法:交集中一定是准确的(哈希切分)
  • 近似算法:那么一定是允许有误判的情况(有误差),那么就可以使用布隆过滤器。

精确算法:

利用哈希函数,将100亿个query是500G,因为要到内存中比较两个文件,所以需要分为1000个小文件,每个小文件占用0.5G,那么两个小文件就可以都进内存中比较了。

如图所示:

image-20250523124504224

当然也会出现哈希冲突超过0.5G的情况,若是重复数较多,但是我们是找交集,所以用位图来存或不在时,0.5G的小文件中数据个数占的内存一定小于0.5G,然后两个位图相与即可。

但如果是都不重复,就需要递归继续分割。用位图找交集

近似算法:

使用位数组和多个哈希函数,以概率方式判断元素是否存在

  • ‌预处理阶段‌:将文件A所有query存入布隆过滤器(需约1GB内存)
  • ‌检测阶段‌:遍历文件B的query,若布隆过滤器返回存在则视为交集

遇到问题,找老架构师取经

借助此文的问题 套路 ,大家可以 放手一试,保证 offer直接到手,还有可能会 涨薪 100%-200%。

后面,尼恩java面试宝典回录成视频, 给大家打造一套进大厂的塔尖视频。

在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典PDF》,里边有大量的大厂真题、面试难题、架构难题。

很多小伙伴刷完后, 吊打面试官, 大厂横着走。

在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

另外,如果没有面试机会,可以找尼恩来改简历、做帮扶。

遇到职业难题,找老架构取经, 可以省去太多的折腾,省去太多的弯路。

尼恩指导了大量的小伙伴上岸,前段时间,刚指导 32岁 高中生,冲大厂成功。特批 成为 架构师,年薪 50W,逆天改命 !!!。

狠狠卷,实现 “offer自由” 很容易的, 前段时间一个武汉的跟着尼恩卷了2年的小伙伴, 在极度严寒/痛苦被裁的环境下, offer拿到手软, 实现真正的 “offer自由” 。

posted @ 2025-05-23 15:42  技术自由圈  阅读(27)  评论(0)    收藏  举报