阿里面试:全国14亿人,统计出重名最多的前100个姓名

文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :

免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取


阿里面试:全国14亿人,统计出重名最多的前100个姓名

尼恩特别说明: 尼恩的文章,都会在 《技术自由圈》 公号 发布, 并且维护最新版本。 如果发现图片 不可见, 请去 《技术自由圈》 公号 查找

尼恩说在前面

在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的面试题:

全国14亿人的数据中,统计出重名人数最多的前100位姓名

最近有小伙伴在面试阿里,遇到这个面试题。小伙伴没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。

TOP N面试题是常见的算法题。

TOP N 统计的面试题,是一道非常常见的题目,大家一定要掌握好。

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

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

《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取

本文的2个重量级作者:

  • 第一重量级作者 yupeng(资深架构师,负责写初稿 )
  • 第二重量级作者 尼恩 (40岁老架构师, 负责提升此文的 技术高度,让大家有一种 俯视 技术的感觉)

《尼恩Java面试宝典》 是大家 面试的杀手锏, 此文当最新PDF版本,可以找43岁老架构师尼恩获取。

1.问题描述:

我们需要从全国14亿人的数据中,统计出重名人数最多的前100位姓名

2.问题分析:

我们的目标:是找到重名人数最多的前100个姓名,

这意味着需要两步:

  • 需要有一个高效的数据结构来统计每个名字出现的次数,

  • 并快速找到出现次数最多的前100个名字.

所以这个问题就转化成了下一个问题: 使用一种低成本、高性能的数据结构,来统计每个名字出现的次数。

3.如何选择一种最低成本、最高性能的数据结构?

常规的数据结构,选型如下:

  • 数组:

如果姓名的字符集范围很大(支持所有的Unicode字符),那么,需要极大且稀疏的数组,导致内存浪费严重,也不适合处理动态长度和多样性的字符串集合

  • 链表:

    链表的插入和查找的操作时间复杂度为O(N), 并且,在大规模数据下性能低下,也不适合快速查找的场景

  • 跳表:

跳表的插入、删除和查找操作的平均事件复杂度都是O(logN),

跳表式空间换时间的思想,主要是它需要额外的空间来维护多级索引,每个元素在最坏的情况下需要额外的存储空间,导致总的空间复杂度为O(N log N),

在频繁的插入和查询的场景中,效率不高。

来到我们现在这个场景,统计每个名字出现的次数时,不如哈希表在时间和空间的效率高效,哈希表的O(1)时间复杂度更适合大规模的数据频繁的插入和查询。

  • 哈希表:

哈希表的插入和查找的时间复杂度都是O(1),

但是在极端的情况下,哈希冲突会导致时间复杂度退化到O(N),

在空间效率中,哈希表需要额外的空间来维护键值对,来到这个场景,空间效率和哈希冲突都有潜在风险,

最重要的是哈希表不能共享前缀,在处理大量的具有共同前缀的数据时候,也不适合。

  • 平衡二叉搜索树(如AVL树或红黑树):

能够维护有序数据,支持快速的插入、删除和查找操作,但在字符串的比较上,性能不如哈希表和Trie高效

  • 前缀树:

前缀树通过共享前缀节点,节省了大量存储空间, 实现了成本的最低化

前缀树对于字符串操作非常高效, 在这个问题中, 有很多名字共享相同前缀, Trie的结构能有效利用这一特点。

经过上面的分析,能够看到Trie更适合统计每个名字出现的次数

4 如何快速筛选出Top 100?

当知道了所有姓名出现的次数之后,、怎么样快速筛选出其中出现次数最多的前100个?

首先想到的是直接排序。

这个问题中,对14亿数据直接排序会有效率的问题,操作非常耗时。

所以直接排序, 这种方法不可取。

我们的目标是找到次数最多的前100个,可以利用堆的性质来完成。

小顶堆总是保持堆顶为当前堆中最小的元素,这样可以确保当新的元素插入时,如果新元素大于堆顶元素,堆顶元素会被替换掉

使用小顶堆的步骤:

1.初始化一个小顶堆:设为100

2.遍历每个姓名及其出现的次数:

  • 如果堆的大小小于100,将当前姓名及其出现次数插入堆中。

  • 如果当前姓名的出现次数大于堆顶元素的出现次数,则移除堆顶元素,并将当前姓名及其出现次数插入堆中。

3.遍历完所有的姓名后,堆中即为重名人数最多的前100个姓名

所以解决这个问题使用了前缀树 + 小顶堆

5. 前缀树Trie树介绍

在计算机科学中,trie,又称前缀树或字典树,使用一些单词来构建Trie树,如下图所示:

从图片中可以看到一些有意思的特性:

  • 根节点没有数据
  • 从根节点到某一个节点,将他们的路径进行连接就组成了对应的字符串

定义:

Trie树,又称为前缀树或字典树, 是一种用于高效存储和检索字符串集合的数据结构, 每个节点代表一个字符, 边表示从一个字符到另一个字符的路径, Trie树通过共享相同前缀的节点来节省存储空间

Trie树是一种有序树,用于保存关联数组,其中的键通常是字符串。

与二叉查找树不同,Trie树 的 键不是直接保存在节点中,而是由节点在树中的位置决定。

一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。

一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

trie中的键通常是字符串,但也可以是其它的结构。

trie的算法可以很容易地修改为处理其它结构的有序序列,比如一串数字或者形状的排列。

比如,bitwise trie中的键是一串位元,可以用于表示整数或者内存地址

Trie树基本性质

1,根节点不包含字符,除根节点意外每个节点只包含一个字符。

2,从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串。

3,每个节点的所有子节点包含的字符串不相同。

Trie树优点

可以最大限度地减少无谓的字符串比较,故可以用于词频统计和大量字符串排序。

  跟哈希表比较:

    1,最坏情况时间复杂度比hash表好

    2,没有冲突,除非一个key对应多个值(除key外的其他信息)

    3,自带排序功能(类似Radix Sort),中序遍历trie可以得到排序。

Trie树缺点

当所有关键字都不具有相同或类似的前缀,空间消耗过大.

6. Trie树的基本操作:

  • 插入:将一个字符串逐字符插入到Trie树中

  • 查找:检查Trie树中是否存在某个字符串

  • 前缀匹配:查找所有以某个前缀开头的字符串

  • 删除:从Trie树中删除一个字符串

7. Trie树的应用场景:

1.字符串检索:

  • 应用场景:快速检索字典中的单词

  • 使用原因:Trie树通过逐字符匹配,可以在O(L)时间内完成字符串的检索,其中L是字符串的长度,比传统的线性搜索更加高效

2.自动补全:

  • 应用场景:搜索引擎和输入法中的自动补全功能

  • 适用原因:Trie树可以通过前缀查找快速提供所有以给定前缀开头的单词,有效提升用户输入体验

3.前缀匹配:

  • 应用场景:寻找以特定前缀开头的所有字符串,如电话号码前缀匹配

  • 适用原因:Trie树天生适合处理前缀匹配问题,可以在O(L)时间内找到所有以特定前缀开头的字符串

4.词频统计:

  • 应用场景:文本分析中统计单词出现频率

  • 适用原因:Trie树可以在插入过程中记录每个单词的出现次数,通过遍历Trie树可以快速统计所有单词的频率

为什么适合这些场景:

5.多模式匹配:

  • 应用场景:从文本中同时搜索多个模式(模式匹配算法)

  • 适用原因:Trie树可以构建多个模式的结构,通过一次遍历文本同时匹配多个模式,提高匹配效率

为什么适用于这些场景:

1.空间效率:

  • 共享前缀:Trie树通过共享前缀节点,减少了重复存储相同前缀的空间开销。

  • 节省内存:对于大量前缀相同的字符串集合,Trie树显著节省内存使用。

2.时间效率:

  • O(L)复杂度:插入、查找和前缀匹配操作的时间复杂度为O(L),其中L是字符串的长度,显著提高了操作效率

  • 快速检索:相比于其他线性结构(如数组或链表),Trie树在处理大量字符串时更快

8. Trie树的代码实现:

以下是尼恩架构团队写的一个 参考代码:

import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

class TrieNode {
    Map<Character, TrieNode> children;
    int count;

    public TrieNode() {
        children = new HashMap<>();
        count = 0;
    }
}

class Trie {
    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    public void insert(String name) {
        TrieNode node = root;
        for (char ch : name.toCharArray()) {
            node = node.children.computeIfAbsent(ch, k -> new TrieNode());
        }
        node.count++;
    }

    public void getAllNames(TrieNode node, StringBuilder prefix, PriorityQueue<NameCount> minHeap, int k) {
        if (node == null) return;
        if (node.count > 0) {
            if (minHeap.size() < k) {
                minHeap.offer(new NameCount(prefix.toString(), node.count));
            } else if (node.count > minHeap.peek().count) {
                minHeap.poll();
                minHeap.offer(new NameCount(prefix.toString(), node.count));
            }
        }
        for (Map.Entry<Character, TrieNode> entry : node.children.entrySet()) {
            prefix.append(entry.getKey());
            getAllNames(entry.getValue(), prefix, minHeap, k);
            prefix.deleteCharAt(prefix.length() - 1);
        }
    }

    public PriorityQueue<NameCount> getTopKNames(int k) {
        PriorityQueue<NameCount> minHeap = new PriorityQueue<>(k);
        getAllNames(root, new StringBuilder(), minHeap, k);
        return minHeap;
    }
}

class NameCount implements Comparable<NameCount> {
    String name;
    int count;

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

    @Override
    public int compareTo(NameCount other) {
        return Integer.compare(this.count, other.count);
    }

    @Override
    public String toString() {
        return name + ": " + count;
    }
}

public class Main {
    public static void main(String[] args) {
        String[] names = {"张伟", "王伟伟", "王芳", "李伟", "李娜"}; // 示例数据
        int k = 100; // 找到前100个重名人数最多的姓名

        Trie trie = new Trie();
        for (String name : names) {
            trie.insert(name);
        }

        PriorityQueue<NameCount> topKNames = trie.getTopKNames(k);
        while (!topKNames.isEmpty()) {
            System.out.println(topKNames.poll());
        }
    }
}

9 TOP N问题发散:

上面的问题进行改进一下, 如果我们对内存有一个限制,比如:要求内存的使用不能超过2G,

注意,这里的内存受限,尽量使用磁盘处理。

这里使用hashmap,而不适用 trie树的原因是?

trie树是按照字符为粒度组织树的节点的,进行磁盘操作性能不高,而且进行磁盘操作时算法更加复杂。

hashmap 是以key为单位操作的, 磁盘操作的效率高。而且 hashmap 统计的时候,代码简洁清晰。

尽管我们hashmap,也不能直接将所有数据加载到内存中处理,

所以可以采取分治的策略,使用外部排序和哈希映射的方法,

以下是详细的步骤:

1.分块读取数据:将14亿条记录分成多个较小的块,每次读取一部分数据到内存中进行处理

2.哈希映射统计词频:对每个块的数据进行哈希映射,统计每个名字出现的次数,将结果写入到磁盘文件

3.合并词频统计结果:读取所有中间文件,合并词频统计结果,得到全局的词频统计

4.使用小顶堆找出前100个重复最多的名字

import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.PriorityQueue;

class NameCount implements Comparable<NameCount> {
    String name;
    int count;

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

    @Override
    public int compareTo(NameCount other) {
        return Integer.compare(this.count, other.count);
    }

    @Override
    public String toString() {
        return name + ": " + count;
    }
}

public class ExternalMemoryTopK {
    private static final int CHUNK_SIZE = 1000000; // 每个块处理100万条记录

    public static void main(String[] args) throws IOException {
        String inputFile = "names.txt";
        String outputFile = "top100names.txt";
        int k = 100;

        // 第一步:分块读取数据并统计词频
        int chunkIndex = 0;
        BufferedReader reader = new BufferedReader(new FileReader(inputFile));
        String line;
        while ((line = reader.readLine()) != null) {
            Map<String, Integer> frequencyMap = new HashMap<>();
            int lineCount = 0;
            while (line != null && lineCount < CHUNK_SIZE) {
                frequencyMap.put(line, frequencyMap.getOrDefault(line, 0) + 1);
                line = reader.readLine();
                lineCount++;
            }
            writeFrequencyMapToFile(frequencyMap, "chunk_" + chunkIndex + ".txt");
            chunkIndex++;
        }
        reader.close();

        // 第二步:合并所有块的词频统计结果
        Map<String, Integer> globalFrequencyMap = new HashMap<>();
        for (int i = 0; i < chunkIndex; i++) {
            mergeFrequencyMapFromFile(globalFrequencyMap, "chunk_" + i + ".txt");
        }

        // 第三步:使用小顶堆找出前100个重复最多的名字
        PriorityQueue<NameCount> minHeap = new PriorityQueue<>(k);
        for (Map.Entry<String, Integer> entry : globalFrequencyMap.entrySet()) {
            if (minHeap.size() < k) {
                minHeap.offer(new NameCount(entry.getKey(), entry.getValue()));
            } else if (entry.getValue() > minHeap.peek().count) {
                minHeap.poll();
                minHeap.offer(new NameCount(entry.getKey(), entry.getValue()));
            }
        }

        // 输出结果
        BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile));
        while (!minHeap.isEmpty()) {
            writer.write(minHeap.poll().toString());
            writer.newLine();
        }
        writer.close();
    }

    private static void writeFrequencyMapToFile(Map<String, Integer> frequencyMap, String filename) throws IOException {
        BufferedWriter writer = new BufferedWriter(new FileWriter(filename));
        for (Map.Entry<String, Integer> entry : frequencyMap.entrySet()) {
            writer.write(entry.getKey() + " " + entry.getValue());
            writer.newLine();
        }
        writer.close();
    }

    private static void mergeFrequencyMapFromFile(Map<String, Integer> globalFrequencyMap, String filename) throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader(filename));
        String line;
        while ((line = reader.readLine()) != null) {
            String[] parts = line.split(" ");
            String name = parts[0];
            int count = Integer.parseInt(parts[1]);
            globalFrequencyMap.put(name, globalFrequencyMap.getOrDefault(name, 0) + count);
        }
        reader.close();
    }
}

10.topK问题,典型的解题思路

这是一种典型的topK问题,一般的问法如下:

从一堆数据中选出多少个最大或最小数?

解题思想:

  1. 先统计数量, 使用前缀树,hashmap等

  2. 再用小顶堆或者 大顶堆

取大用小,取小用大。简单来说就是取最大的K个数就用小顶堆,取最小的K个数,就用大顶堆

取海量数据里面最小的K个数?

要找出数组中最小的K个数,就要构造一个有K个元素的大顶堆,因为大顶堆的堆顶值是最大的,其它元素和堆顶的元素比较,大于堆顶的元素,换一个元素继续,小于堆顶的元素,将堆顶元素出堆,将更小的元素插入堆顶,如此反复,堆里面就是最小的数

取海量数据里面最大的K个数?

要找出数组中最大的K个数,就要构造一个有K个元素的小顶堆,因为小顶堆的堆顶值是最小的,其它元素和堆顶的元素比较,大于堆顶的元素,堆顶的元素出堆,将元素插入到小顶堆,将更大的元素换到堆中,如此反复,堆里面就是最大的数

说在最后:有问题找老架构取经

TOP N面试题是常见的算法题。

面试的时候,如果大家能按照上面的思路去回答,并且如数家珍,基本上 面试官会被你 震惊到、吸引到。

最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。

在面试之前,建议大家系统化的刷一波 5000页《尼恩Java面试宝典》V174,在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。

另外,如果没有面试机会,可以找尼恩来帮扶、领路。

  • 大龄男的最佳出路是 架构+ 管理
  • 大龄女的最佳出路是 DPM,

![图片](../../../weichat/WeChat Files/wxid_86y8alzoyvrl22/FileStorage/File/2024-05/topK面试题大纲v2.assets/640.webp)

女程序员如何成为DPM,请参见:

DPM (双栖)陪跑,助力小白一步登天,升格 产品经理+研发经理

领跑模式,尼恩已经指导了大量的就业困难的小伙伴上岸。

前段时间,领跑一个40岁+就业困难小伙伴拿到了一个年薪100W的offer,小伙伴实现了 逆天改命

技术自由的实现路径:

实现你的 架构自由:

吃透8图1模板,人人可以做架构

10Wqps评论中台,如何架构?B站是这么做的!!!

阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了

峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?

100亿级订单怎么调度,来一个大厂的极品方案

2个大厂 100亿级 超大流量 红包 架构方案

… 更多架构文章,正在添加中

实现你的 响应式 自由:

响应式圣经:10W字,实现Spring响应式编程自由

这是老版本 《Flux、Mono、Reactor 实战(史上最全)

实现你的 spring cloud 自由:

Spring cloud Alibaba 学习圣经》 PDF

分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)

实现你的 linux 自由:

Linux命令大全:2W多字,一次实现Linux自由

实现你的 网络 自由:

TCP协议详解 (史上最全)

网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!

实现你的 分布式锁 自由:

Redis分布式锁(图解 - 秒懂 - 史上最全)

Zookeeper 分布式锁 - 图解 - 秒懂

实现你的 王者组件 自由:

队列之王: Disruptor 原理、架构、源码 一文穿透

缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)

缓存之王:Caffeine 的使用(史上最全)

Java Agent 探针、字节码增强 ByteBuddy(史上最全)

实现你的 面试题 自由:

4800页《尼恩Java面试宝典 》 40个专题

免费获取11个技术圣经PDF:

posted @ 2024-05-29 21:44  疯狂创客圈  阅读(174)  评论(0编辑  收藏  举报