尼恩说在前面
全国14亿人的数据中,统计出重名人数最多的前100位姓名
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取
本文的2个重量级作者:
第一重量级作者 yupeng(资深架构师,负责写初稿 ) 第二重量级作者 尼恩 (40岁老架构师, 负责提升此文的 技术高度,让大家有一种 俯视 技术的感觉)
本文目录
- 尼恩说在前面
- 本文的2个重量级作者:
- 1. 问题描述:
- 2. 问题分析:
- 3. 如何选择一种最低成本、最高性能的数据结构?
- 4. 如何快速筛选出Top 100?
- 5. 前缀树Trie树介绍
- 6. Trie树的基本操作:
- 7. Trie树的应用场景:
- 8. Trie树的代码实现:
- 9. TOP N问题发散:
- 10. topK问题,典型的解题思路
说在最后:有问题找老架构取经
1. 问题描述:
2. 问题分析:
需要有一个高效的数据结构来统计每个名字出现的次数, 并快速找到出现次数最多的前100个名字.
3. 如何选择一种最低成本、最高性能的数据结构?
数组:
链表: 链表的插入和查找的操作时间复杂度为O(N), 并且,在大规模数据下性能低下,也不适合快速查找的场景 跳表:
哈希表:
平衡二叉搜索树(如AVL树或红黑树):
前缀树:
4. 如何快速筛选出Top 100?
如果堆的大小小于100,将当前姓名及其出现次数插入堆中。 如果当前姓名的出现次数大于堆顶元素的出现次数,则移除堆顶元素,并将当前姓名及其出现次数插入堆中。
5. 前缀树Trie树介绍
根节点没有数据 从根节点到某一个节点,将他们的路径进行连接就组成了对应的字符串
Trie树,又称为前缀树或字典树, 是一种用于高效存储和检索字符串集合的数据结构, 每个节点代表一个字符, 边表示从一个字符到另一个字符的路径, Trie树通过共享相同前缀的节点来节省存储空间
6. Trie树的基本操作:
插入:将一个字符串逐字符插入到Trie树中 查找:检查Trie树中是否存在某个字符串 前缀匹配:查找所有以某个前缀开头的字符串 删除:从Trie树中删除一个字符串
7. Trie树的应用场景:
应用场景:快速检索字典中的单词 使用原因:Trie树通过逐字符匹配,可以在O(L)时间内完成字符串的检索,其中L是字符串的长度,比传统的线性搜索更加高效
应用场景:搜索引擎和输入法中的自动补全功能 适用原因:Trie树可以通过前缀查找快速提供所有以给定前缀开头的单词,有效提升用户输入体验
应用场景:寻找以特定前缀开头的所有字符串,如电话号码前缀匹配 适用原因:Trie树天生适合处理前缀匹配问题,可以在O(L)时间内找到所有以特定前缀开头的字符串
应用场景:文本分析中统计单词出现频率 适用原因:Trie树可以在插入过程中记录每个单词的出现次数,通过遍历Trie树可以快速统计所有单词的频率
应用场景:从文本中同时搜索多个模式(模式匹配算法) 适用原因:Trie树可以构建多个模式的结构,通过一次遍历文本同时匹配多个模式,提高匹配效率
共享前缀:Trie树通过共享前缀节点,减少了重复存储相同前缀的空间开销。 节省内存:对于大量前缀相同的字符串集合,Trie树显著节省内存使用。
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问题发散:
这里使用hashmap,而不适用 trie树的原因是? trie树是按照字符为粒度组织树的节点的,进行磁盘操作性能不高,而且进行磁盘操作时算法更加复杂。 hashmap 是以key为单位操作的, 磁盘操作的效率高。而且 hashmap 统计的时候,代码简洁清晰。
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问题,典型的解题思路
先统计数量, 使用前缀树,hashmap等 再用小顶堆或者 大顶堆
说在最后:有问题找老架构取经
大龄男的最佳出路是 架构+ 管理 大龄女的最佳出路是 DPM,
部分历史案例
实现职业转型,极速上岸
关注职业救助站公众号,获取每天职业干货
助您实现职业转型、职业升级、极速上岸
---------------------------------
实现架构转型,再无中年危机
关注技术自由圈公众号,获取每天技术千货
一起成为牛逼的未来超级架构师
几十篇架构笔记、5000页面试宝典、20个技术圣经
请加尼恩个人微信 免费拿走
暗号,请在 公众号后台 发送消息:领电子书
如有收获,请点击底部的"在看"和"赞",谢谢