腾讯"奇葩"面试:全国14亿个姓名,统计出重名最多的前10个

208 阅读16分钟

大家都懂,面试的时候,面试官总喜欢抛出一些“奇奇怪怪”的问题,问你点“看似不可能”的事情。

比如下面这道题:

全国14亿个姓名,统计出重名最多的前10个。

这题乍一看,是不是像极了老板甩下一句:“你用两块钱搞个全公司团建,最后还得拍成《向往的生活》那种质感。”

不过别急,虽然题目看起来“不讲道理”,其实只要抓住核心方法,是有办法搞定的!

一、问题背景分析

1.1 数据规模的挑战

我们先来看看这个问题到底有多“大”:

  • 数据量巨大 :假设全国有14亿条姓名记录,每条平均占用10字节,那么整体数据约为 14GB
  • 超出内存限制 :这已经远超普通单机程序可完全装载进内存的范围(普通服务器内存为64GB~256GB)。
  • 单机方法受限
  • 常规的哈希表统计方法,时间复杂度虽是 O(N),但数据超过内存后,频繁的磁盘I/O会严重拖慢处理速度。

换句话说,用单机方法处理,全量统计的时间可能达到 数小时乃至一天以上


最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的 7701页的BAT大佬写的刷题笔记,让我offer拿到手软

1.2 为何需要分布式计算?

面对如此大体量的数据,我们不得不考虑使用 分布式系统 来解决:

  • 数据切分并行处理 :将14亿条数据平均分配到多个计算节点上,每个节点各自处理一部分,充分利用分布式集群的横向扩展能力。
  • 解决数据倾斜问题 :分布时要注意数据分区均衡,避免某些节点数据过多,形成瓶颈。
  • 高效合并结果
  • 各节点本地先统计出Top-K名单。

最后集中合并这些结果,使用 高效的 Top-K 算法 保证整体结果准确又高效。

1.3 算法与性能优化

为了保证整个过程高效,我们对统计与合并两个阶段提出如下优化:

(1)统计阶段

  • 使用哈希表 :每个节点使用哈希表统计本地姓名出现的频次,操作时间复杂度为 **O(1)**。
  • 空间占用小,操作迅速 ,适合并发处理。

(2)合并阶段

  • 使用 小顶堆(优先队列) 来维护 Top-K 结果(假设 K=10),插入操作时间复杂度为 **O(logK)**。
  • 将所有节点的 Top-K 名单合并后,得到最终的全局 Top-10。

(3)总体复杂度

  • 统计阶段:**O(N)**(每条数据哈希计数)
  • 合并阶段:**O(N × logK)**(合并所有节点 Top-K)
  • 总体时间复杂度:**O(N + NlogK)**,接近线性处理能力,性能优良。

二、系统架构设计

为应对14亿级别的姓名数据处理需求,系统设计可以采用经典的 MapReduce 分布式计算框架,结合 分治策略优先队列算法,实现高效的三阶段处理流程。

2.1 整体技术方案

阶段一:数据分片与分布式统计(Map 阶段)

  • 将原始姓名数据通过 哈希分区 分发到多个 Map 任务节点。
  • 每个 Map 任务在本地内存中使用 HashMap 对姓名进行频次统计,时间复杂度 O(1)。
  • 引入 Combiner(本地聚合器) ,在 Map 输出阶段对重复数据做本地合并,从而显著减少数据传输量。

阶段二:局部结果合并(Reduce 阶段)

  • Reduce 任务收集同一分区内所有 Map 输出结果,并进一步统计姓名出现总频次。
  • 每个 Reduce 任务输出一个完整的「姓名-频次」统计列表,为最终排序做准备。

阶段三:全局 Top-10 计算

  • 遍历所有 Reduce 输出,使用 最小优先队列(小顶堆) 实时维护当前频次最高的前10个姓名。
  • 插入操作时间复杂度为 O(logK),最终输出完整的 Top-10 排序结果。

2.2 数据分区策略

为了确保分布式计算过程中各节点负载均衡、避免数据倾斜,采用改进版 一致性哈希算法 实现高质量的数据分区。

示例代码:

public class NamePartitioner extends Partitioner<Text, IntWritable> {
    privatefinalint numPartitions;

    public NamePartitioner(int numPartitions) {
        this.numPartitions = numPartitions;
    }

    @Override
    public int getPartition(Text key, IntWritable value, int numReduceTasks) {
        // 使用 MurmurHash3 算法提升哈希分布均匀性
        int hash = MurmurHash3.hash32(key.toString().getBytes());
        return Math.abs(hash) % numPartitions;
    }
}

策略说明:

  • MurmurHash3 :相比传统哈希取模,具有更强的“雪崩效应”与更均匀的散列分布,减少热点分区问题。
  • 分区数量建议
  • 设置为 Reduce 节点数的 2~3 倍 ,例如 500 个 Reduce 节点对应 1000 个分区,提升并发处理能力与弹性伸缩能力。

2.3 内存管理策略

为提升Map 阶段处理效率与内存利用率,建议配置如下:

Map 任务内存配置:

  • 每个 Map 任务建议配置 8~16GB 内存 ,根据节点硬件资源灵活调整。

HashMap 初始化参数:

  • 预计每个 Map 任务处理约 140 万条数据(14亿 / 1000分区)。
  • 设置初始容量为 2^18 = 262144 ,负载因子为 0.75 ,避免频繁扩容造成性能损耗。

内存泄漏防控:

  • 使用 弱引用(WeakReference) 管理大对象,避免内存泄漏。
  • 定期触发 GC,释放无效对象,保证任务稳定运行。

三、核心模块实现

整个统计流程由三个核心模块组成:数据统计模块(Map)、局部合并模块(Combiner)全局合并模块(Reduce)。以下分别对每个模块的实现细节进行说明:

3.1 数据统计模块(Map 任务)

3.1.1 自定义输入格式

为了提高数据读取效率,可以通过自定义 InputFormat 实现对原始数据的定制化读取。

public class NameInputFormat extends TextInputFormat {
    @Override
    public RecordReader<LongWritable, Text> createRecordReader(
        InputSplit split, TaskAttemptContext context) throws IOException, InterruptedException {
        returnnew NameRecordReader();
    }

    privatestaticclass NameRecordReader extends RecordReader<LongWritable, Text> {
        privatefinal Text line = new Text();
        private LineReader lineReader;

        @Override
        public boolean nextKeyValue() throws IOException, InterruptedException {
            long position = 0;
            while (lineReader.readLine(line) > 0) {
                currentKey.set(position);
                position += line.getLength() + 1;
                returntrue;
            }
            returnfalse;
        }
        // 省略初始化、关闭方法等...
    }
}

说明:

  • 实现 LineReader 逐行读取逻辑。
  • 保留偏移量作为 key,文本作为 value。
  • 在海量数据场景中可根据文件格式进一步优化读取逻辑。

3.1.2 Mapper 实现

public class NameMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
    privatefinal IntWritable one = new IntWritable(1);
    private Map<Text, Integer> localCount = new HashMap<>();

    @Override
    protected void map(LongWritable key, Text value, Context context) 
        throws IOException, InterruptedException {
        String name = value.toString().trim();
        if (!StringUtils.isBlank(name)) {
            localCount.put(new Text(name), localCount.getOrDefault(new Text(name), 0) + 1);
        }
    }

    @Override
    protected void cleanup(Context context) throws IOException, InterruptedException {
        for (Map.Entry<Text, Integer> entry : localCount.entrySet()) {
            context.write(entry.getKey(), new IntWritable(entry.getValue()));
        }
    }
}

优化点:

  • 使用 cleanup 方法在 Map 任务结束时统一输出,减少 I/O 调用次数,提升性能。
  • 增加空值判断和去重逻辑,确保数据清洗质量。
  • 可按需设置 HashMap 的初始容量,避免扩容。

3.2 局部合并模块(Combiner)

public class NameCombiner extends Reducer<Text, IntWritable, Text, IntWritable> {
    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) 
        throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable value : values) {
            sum += value.get();
        }
        context.write(key, new IntWritable(sum));
    }
}

说明:

  • Combiner 相当于“本地版”的 Reducer,用于在 Map 端提前合并重复数据,减少网络传输量。
  • 实际运行中可降低 30%~70% 的数据传输压力。
  • 需确保 Combiner 的输出类型与 Reducer 输入类型一致,防止类型不匹配导致错误。

3.3 全局合并模块(Reduce 任务)

public class NameReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
    private Map<Text, Integer> globalCount = new HashMap<>();

    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) 
        throws IOException, InterruptedException {
        int sum = 0;
        for (IntWritable value : values) {
            sum += value.get();
        }
        globalCount.put(key, sum);
    }

    @Override
    protected void cleanup(Context context) throws IOException, InterruptedException {
        for (Map.Entry<Text, Integer> entry : globalCount.entrySet()) {
            context.write(entry.getKey(), new IntWritable(entry.getValue()));
        }
    }
}

说明:

  • 将每个姓名的频次汇总到 globalCount 中,待所有 reduce 完成后一次性输出。
  • 统一输出可以方便后续的 Top-10 汇总计算
  • 为防止内存溢出,建议对单个 Reducer 限定最大处理量(如不超过 100 万条),可结合分区策略调整 Reduce 数量。

四、Top-10 计算模块

在分布式统计完成后,最终需要从所有 Reduce 任务的输出中找出 重名最多的前 10 个姓名。为此,我们设计了以下两个核心步骤:优先队列管理全局合并流程

4.1 优先队列实现

为了高效维护当前统计的 Top-10 姓名,我们使用 最小堆(小顶堆) 作为优先队列来存储数据:

实现代码

public class TopNameCollector {
    privatefinalint topK;
    private PriorityQueue<NameCount> minHeap;

    public TopNameCollector(int topK) {
        this.topK = topK;
        // 小顶堆:根据 count 值进行升序排列
        this.minHeap = new PriorityQueue<>(topK, Comparator.comparingInt(NameCount::getCount));
    }

    public void add(String name, int count) {
        NameCount entry = new NameCount(name, count);

        if (minHeap.size() < topK) {
            // 堆未满,直接加入
            minHeap.offer(entry);
        } elseif (count > minHeap.peek().getCount()) {
            // 如果当前频次大于堆顶,则替换
            minHeap.poll();
            minHeap.offer(entry);
        } elseif (count == minHeap.peek().getCount()) {
            // 如果频次相同,按字典序比较,保留字典序小的
            if (name.compareTo(minHeap.peek().getName()) < 0) {
                minHeap.poll();
                minHeap.offer(entry);
            }
        }
    }

    public List<NameCount> getTopResults() {
        // 将最小堆内容按降序输出
        List<NameCount> result = new ArrayList<>(minHeap);
        result.sort((o1, o2) -> o2.getCount() - o1.getCount());
        return result;
    }
}

// 数据封装类
class NameCount {
    private String name;
    privateint count;

    public NameCount(String name, int count) {
        this.name = name;
        this.count = count;
    }

    public String getName() {
        return name;
    }

    public int getCount() {
        return count;
    }
}

设计思路

  • 使用 小顶堆 保存当前 Top-10:
  • 堆大小超过 10 时,自动淘汰堆顶最小元素。

维护一个稳定的 Top-10 集合,保证高效插入与删除。

  • 处理同频次的姓名时,额外比较字典序:
  • 保留字典序更小的姓名,以确保排序稳定性。

4.2 全局合并流程

分布式统计的最后一步是将所有 Reduce 节点 的输出文件(part-* 文件)统一合并,计算出最终的 Top-10 结果。

实现代码

public class GlobalMergeTask {
    public List<NameCount> process(Path reduceOutputPath, int topK) throws IOException {
        TopNameCollector collector = new TopNameCollector(topK);
        FileSystem fs = FileSystem.get(reduceOutputPath.toUri(), new Configuration());

        // 遍历所有 Reduce 任务的输出文件
        for (FileStatus status : fs.listStatus(reduceOutputPath)) {
            if (status.isFile() && status.getPath().getName().startsWith("part-")) {
                try (FSDataInputStream inputStream = fs.open(status.getPath());
                     BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {

                    String line;
                    while ((line = reader.readLine()) != null) {
                        String[] parts = line.split("\t");
                        if (parts.length == 2) {
                            String name = parts[0];
                            int count = Integer.parseInt(parts[1]);
                            // 将数据加入到优先队列
                            collector.add(name, count);
                        }
                    }
                }
            }
        }
        // 最终获取 Top-10 的结果
        return collector.getTopResults();
    }
}

设计思路

1、 遍历HDFS中所有part-文件,逐行读取;
2、 使用BufferedReader进行流式处理,解析每行的姓名频次
3、 调用TopNameCollectoradd方法,将姓名和频次插入最小堆;
4、 最终从堆中导出Top-10,并按频次降序排列输出;

优化细节

  • 使用 BufferedReader 提升文件读取速度,减少 IO 次数。
  • 避免一次性加载所有文件到内存,通过流式读取降低内存占用。
  • 分布式文件系统(HDFS)的 listStatus 高效扫描目录,确保并发安全。

至此,整个流程的所有核心模块已经完整实现:

1、 Map阶段:本地统计频次;
2、 Combiner阶段:局部聚合;
3、 Reduce阶段:全局统计;
4、 Top-10计算:优先队列筛选;

五、分布式计算框架集成(Hadoop MapReduce)

为实现对大规模姓名数据的高效统计与分析,本系统基于 Hadoop MapReduce 框架进行分布式计算部署。以下为核心作业配置与集群部署建议:

5.1 作业配置驱动类

public class NameCountJob {
    public static void main(String[] args) throws Exception {
        Configuration conf = new Configuration();
        Job job = Job.getInstance(conf, "Name Count Job");
        job.setJarByClass(NameCountJob.class);

        // 设置输入输出格式
        job.setInputFormatClass(NameInputFormat.class);
        FileInputFormat.addInputPath(job, new Path(args[0]));
        job.setOutputFormatClass(TextOutputFormat.class);
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        // 设置 Mapper、Combiner 和 Reducer 类
        job.setMapperClass(NameMapper.class);
        job.setCombinerClass(NameCombiner.class);
        job.setReducerClass(NameReducer.class);

        // 设置 Map 输出与最终输出的数据类型
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(IntWritable.class);
        job.setOutputKeyClass(Text.class);
        job.setOutputValueClass(IntWritable.class);

        // 设置自定义分区策略
        job.setPartitionerClass(NamePartitioner.class);
        job.setNumReduceTasks(1000); // 启动 1000 个 Reduce 任务

        // 启动作业
        System.exit(job.waitForCompletion(true) ? 0 : 1);
    }
}

5.2 集群部署建议

为确保系统的高并发处理能力与稳定性,建议参考以下部署规范:

  • 节点配置 :每个节点建议配置为 8 核 CPU、16GB 内存、1TB 磁盘;
  • 集群规模 :建议部署 100~500 台节点,以支撑高并发计算;
  • 资源分配
  • 每个 Map 任务分配 8GB 内存;

每个 Reduce 任务分配 12GB 内存;

  • 数据存储 :使用 HDFS,副本数建议设置为 3;
  • 任务调度器 :采用 Capacity Scheduler 实现资源的公平调度。

六、性能优化策略

为了提升系统整体计算效率与稳定性,特别引入以下三方面的性能优化策略:

6.1 数据倾斜处理

在海量数据中,部分高频姓名(如“张伟”、“王伟”等)可能导致数据倾斜,影响 Reduce 任务的负载均衡。

高频词预处理策略

// Mapper 中识别高频词并添加特殊前缀
Set<String> highFreqNames = loadHighFreqNamesFromHDFS("/data/high_freq_names.txt");

if (highFreqNames.contains(name)) {
    context.write(new Text("__HIGH_FREQ__" + name), one); // 加前缀打散到多个 Reducerelse {
    context.write(new Text(name), one);
}

动态分区调整策略

conf.set("mapreduce.job.reduce.slowstart.completedmaps""0.8"); // Reduce 启动时机调整
conf.set("mapreduce.job.reduces""-1"); // 自动计算最优 Reduce 数量

通过动态调整 Reduce 启动比例与任务数量,可在运行时自动应对数据倾斜情况。


6.2 内存优化技巧

在处理超大规模数据时,内存使用效率至关重要:

紧凑型数据结构替换

  • 使用 IntWritable 替代 Integer ,减少对象封装带来的额外开销;
  • 引入 Fastutil 库的 Int2ObjectMap 替代标准 HashMap ,可节省约 50% 内存;

JVM 参数优化

-Dmapreduce.map.java.opts=-Xmx8g -XX:+UseG1GC -XX:MaxGCPauseMillis=200
-Dmapreduce.reduce.java.opts=-Xmx12g -XX:+HeapDumpOnOutOfMemoryError

使用G1 收集器,并设置最大 GC 暂停时间,提升垃圾回收效率。


6.3 容错机制实现

为了提高系统的鲁棒性,在 Mapper/Reducer 中加入容错重试机制:

@Override
protected void reduce(Text key, Iterable<IntWritable> values, Context context) {
    try {
        // 正常处理逻辑
    } catch (Exception e) {
        // 捕获异常并记录错误计数器
        context.getCounter("REDUCE_ERRORS""RETRY").increment(1);
        // 可加入重新处理逻辑或记录日志
    }
}

通过自定义计数器 REDUCE_ERRORS,可便于后期监控与调试 Reduce 失败情况。

七、压力测试方案

为验证系统在大规模数据处理下的稳定性与性能,设计如下测试方案:

7.1 数据生成工具

通过以下程序可自动生成模拟姓名数据:

public class NameGenerator {
    privatestaticfinal String[] SUR_NAMES = {"张""王""李""刘""陈"};
    privatestaticfinal String[] FIRST_NAMES = {"伟""芳""秀英""建军""建国"};

    public static void generateData(String outputPath, long count) throws IOException {
        try (PrintWriter writer = new PrintWriter(new File(outputPath))) {
            Random random = new Random();
            for (long i = 0; i < count; i++) {
                String surName = SUR_NAMES[random.nextInt(SUR_NAMES.length)];
                String firstName = FIRST_NAMES[random.nextInt(FIRST_NAMES.length)];
                writer.println(surName + firstName);
            }
        }
    }
}

该工具可快速生成包含 5 个姓氏与 5 个名字组合的测试数据,具备可控的高频词分布,适合模拟真实场景下的数据分布情况。

7.2 性能指标监控

  • 吞吐量 :应达到 100MB/s 以上;
  • 内存利用率 :建议保持在 70% - 80% 之间;
  • 处理时间 :对于 14 亿条数据 ,在 500 节点集群 中应控制在 30 分钟以内完成

八、扩展方案

本系统设计具备良好的扩展性,可灵活适配 Spark、Flink 等新型大数据处理框架,以满足更复杂的应用场景。

8.1 基于 Spark 的优化实现

Spark 提供更高层次的内存计算抽象,适合迭代式分析任务。以下为 Spark RDD 模型的改写实现:

JavaRDD<String> names = sc.textFile("hdfs://names.txt");
JavaPairRDD<String, Integer> counts = names
    .mapToPair(name -> new Tuple2<>(name, 1))
    .reduceByKey(Integer::sum)
    .cache(); // 缓存中间结果以提升后续性能

JavaPairRDD<String, Integer> top10 = counts
    .transformToPair(rdd -> {
        List<Tuple2<String, Integer>> topList = rdd.collect();
        topList.sort((o1, o2) -> o2._2 - o1._2); // 按频次降序排序
        return rdd.context().parallelizePairs(topList.subList(010));
    });

优势总结

  • 编程模型简洁,逻辑清晰;
  • 利用内存加速计算,适合多次迭代和交互式分析任务;
  • 支持丰富的生态组件(如 Spark SQL、MLlib)进行进一步扩展。

8.2 实时流处理扩展(Kafka + Flink)

针对实时新增的姓名数据统计需求,可引入流式计算架构:

  • Kafka :作为消息中间件,实时接收用户输入的姓名流;
  • Flink :进行流式计算,维护各个姓名的滚动计数;
  • KeyedProcessFunction :按姓名键分区,维护各分区 Top-10;
  • 窗口合并 :周期性地将所有分区结果合并,形成全局 Top-10。

该方案适用于实时去重、排行榜更新等场景,具备高吞吐与低延迟优势。


九、总结与面试要点

9.1 核心技术点总结

技术点 描述说明
分布式分治策略 利用哈希分区将数据拆分至多个节点并行处理
高效数据结构 哈希表(O(1) 计数)+ 优先队列(O(logK) 维护 Top-K)
MapReduce 模型 熟练掌握 Mapper / Reducer / Combiner 的职责划分与协作机制
性能优化策略 包括数据倾斜处理、内存优化、容错机制等

9.2 面试应答要点

  • 问题拆解能力 :由单机实现切入,逐步过渡到分布式处理,体现架构演进思维;
  • 技术选型逻辑 :能清晰对比 MapReduce 与 Spark 的适用场景,并结合业务背景做出合理选择;
  • 边界问题思考
  • 脏数据过滤与异常处理;

高频词导致的数据倾斜;

Reduce 任务内存溢出应急方案;

  • 复杂度分析能力
  • 时间复杂度:整体为 O(N logK),其中 N 为姓名总数,K 为 Top-K 的个数;

空间复杂度:O(M + K),M 为唯一姓名数。

📌 工程能力强调:结合具体应用场景,灵活调整分区逻辑与参数配置,才能真正展现对大数据计算的工程实践能力与优化思维。

最后说一句(求关注,求赞,别白嫖我)

最近无意间获得一份阿里大佬写的刷题笔记,一下子打通了我的任督二脉,进大厂原来没那么难。 这是大佬写的 7701页的BAT大佬写的刷题笔记,让我offer拿到手软

本文,已收录于,我的技术网站 cxykk.com:程序员编程资料站,有大厂完整面经,工作技术,架构师成长之路,等经验分享

求一键三连:点赞、分享、收藏

点赞对我真的非常重要!在线求赞,加个关注我会非常感激!

**近期技术热文

**面试必看!腾讯面试问:MySQL缓存有几级?你能答上来吗?

行业案例:Shopee 70W人在线的 弹幕 系统,是怎么架构的?

行业案例:12306亿级流量架构分析(史上最全)

行业经典案例:从30万到100万单,某电商平台如何用16库16表扛住亿级订单?

第3版:互联网大厂面试题包括 Java 集合、JVM、多线程、并发编程、设计模式、算法调优、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat、Python、HTML、CSS、Vue、React、JavaScript、Android 大数据、阿里巴巴等大厂面试题等、等技术栈!