讨论一道面试题啊(take home task)

32 天前
 wangpugod2003

题目很简单,就是一个文件,里面的数据就是 ID->value:

<unique ID> <space> <value>

例如: 123131321   100

135235423   101

132523121    80

...

给定一个值 n ,返回最大的 n 个值的 ID: 比如上面 n=2 ,就应该返回:

135235423   

123131321   

就是个 top k 的问题,也不要求返回的 id 的顺序。唯一的要求是这个文件极端的大。

我理解单个文件,哪怕几百 G 吧,我直接 java 中按照 BufferedReader 一行行的读,再用 size 为 n 的 PriorityQueue 梳理一遍整个<id->value>即可,时间复杂度就是 O(lines * logn)。

也写了单元测试+集成测试,各种不合法的 corn case 都处理了,生成了个 10G 的文件(几十亿条)测试了就一分多钟就出来结果。不知道怎么提交上去还是挂了。。

大家觉得应该用啥高级些的算法么?

3136 次点击
所在节点    程序员
35 条回复
zhy0216
32 天前
size 为 k 就够吧 每一个和最小的比较 大才进这个堆
wangpugod2003
32 天前
@zhy0216 哦,我题目没写清楚,就是这里的 n 就是 k ,我改下。
Sawyerhou
32 天前
可能用大顶堆更好,queue 底层是二叉堆吧,另外,算法题代码最好手搓,别用库,体现不出水平
coyove
32 天前
工作碰到过类似场景,从 mq 读一组数据,取 topk 打入下游 mq ,问题是 k 很大。
第一版是把 k 拆成比如 k=10m ,然后串行跑 10 遍,取 top 0-m, top m-2m, ... top 8m-9m
第二版直接存 redis zset 了,更符合工程师思维
第三版直接交给 data 那边做,上 flink
wxf666
32 天前
极端情况下,每个 ID 只出现一次,

你是要在内存里,保留整个 几百 GB 的 ID 吗?

wxf666
32 天前
@wxf666 #5 等会儿,我以为是,求出现最多次数的 ID 了。。

那你这个算法,应该没问题的呀?

geelaw
32 天前
@Sawyerhou #3 堆是否是大顶和是否是二项是两个正交的概念。

回到楼主的问题,描述实在是 underspecified ,数据范围是什么?是否大到需要使用非托管代码? ID 和 value 是 int 可返程表示的吗?挂了是指代码机器评测不通过,还是指面试之后拒绝录用?后者的情况下可能和代码是否机器评测通过无关。
GeekGao
32 天前
外部排序:

1.将文件分割成多个小块,每个小块可以在内存中排序。
2. 对每个小块进行排序,并将排序后的小块写入临时文件。
3. 合并所有排序后的小块,得到一个大的排序数组。
4.从排序后的数组中选择最大的 n 个值的 ID 。
aboutyang
32 天前
PriorityQueue 中的排序没必要,可以用个数组不断替换其中的最小值。
GeekGao
32 天前
意思是说这思路不行???

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

public class ExternalSorter {
public static void main(String[] args) throws IOException {
String inputFile = "bigfile.txt"; // 输入文件路径
String tempDirectory = "temp"; // 临时文件目录
int maxMemory = 1024*1024*1024; // 假设最大内存使用量限制为 1GB

try (BufferedReader reader = new BufferedReader(new FileReader(inputFile))) {
String line;
int fileCounter = 0;
while ((line = reader.readLine()) != null) {
// 使用 PriorityQueue 在内存中对行进行排序
PriorityQueue<String> sortedLines = new PriorityQueue<>();
sortedLines.add(line);

// 继续读取行,直到内存使用达到限制
int memoryUsage = line.length();
while (memoryUsage <= maxMemory && (line = reader.readLine()) != null) {
sortedLines.add(line);
memoryUsage += line.length();
}

// 将排序后的行写入临时文件
File tempFile = new File(tempDirectory, "tempfile" + fileCounter + ".txt");
try (BufferedWriter writer = new BufferedWriter(new FileWriter(tempFile))) {
while (!sortedLines.isEmpty()) {
writer.write(sortedLines.poll());
writer.newLine();
}
}

fileCounter++;
}
}

// 合并临时文件
mergeTemporaryFiles(tempDirectory);

// 清理临时文件
cleanUpTemporaryFiles(tempDirectory);
}

private static void mergeTemporaryFiles(String tempDirectory) throws IOException {
File[] tempFiles = new File(tempDirectory).listFiles();
PriorityQueue<BufferedReader> readers = new PriorityQueue<>((br1, br2) -> {
try {
String line1 = br1.readLine();
String line2 = br2.readLine();
return line1.compareTo(line2);
} catch (IOException e) {
throw new RuntimeException(e);
}
});

for (File tempFile : tempFiles) {
BufferedReader reader = new BufferedReader(new FileReader(tempFile));
readers.add(reader);
}

String outputFile = "output.txt"; // 输出文件路径
try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {
while (!readers.isEmpty()) {
BufferedReader reader = readers.poll();
String line = reader.readLine();
if (line != null) {
writer.write(line);
writer.newLine();
readers.add(reader);
} else {
reader.close();
}
}
}
}

private static void cleanUpTemporaryFiles(String tempDirectory) {
File[] tempFiles = new File(tempDirectory).listFiles();
for (File tempFile : tempFiles) {
tempFile.delete();
}
}
}
lyminghao
32 天前
value 的范围有条件吗
fkdtz
32 天前
这题如果是纯算法题,那么除了外部排序加最小堆真没别的思路了。
如果是个实际工程题那就可以并发处理,每组并发线程都做 topK ,最后汇总 topK ,类似 MapReduce 。

蹲个后续看看纯算法的话有其他什么方案。
wxf666
32 天前
@wangpugod2003 #2 楼主,我突然对你的 10GB 文件感兴趣。。


假设你 10GB 二十亿条,那平均每条 (10 << 30) / 2e9 = 5 字节,

去除空格、换行 2 字节,还剩 3 字节,你是怎么存得下 10 位数的 ID ,和几位数的 value 呢?


Sawyerhou
32 天前
@geelaw 我默认 priority queue 就是二叉堆,二叉堆就大顶堆和小顶堆,确实有三叉四叉,或者二叉无序堆,咬文嚼字没啥意思

op 的算法没问题,我理解有误,不过我还是觉得手搓算法题比较好,也可能如楼上说的,这是生产问题,不是在考算法,或者单纯就是人招到了
geelaw
32 天前
@Sawyerhou #14 我很难理解 #3 想表达的意思,因为前两个分句的意思听起来风马牛不相及,我尝试的理解是“应该用大顶堆,pq 自带的二叉堆(不是大顶堆所以)不够好”,这当然是 nonsense 。堆不考虑无序,也通常不考虑二叉和多叉的区别,在“二叉”这个属性上的另外选项是指“二项”和 Fibonacci 等。

另外我需要更正一下#7 ,应该在例子里面用“二叉”而不是“二项”。

最后,比较自然的想法是使用小顶堆,因为需要反复查询、替换堆中的最小值,这表示堆顶应该是最小值。
lvlongxiang199
32 天前
感觉可以并行来跑, 充分利用 CPU 多核. 启动多个线程, 每个线程同时做 topK, 最后 merge 成一个
Sawyerhou
32 天前
@geelaw 我一开始记错了,把 pq 底层当成有序二叉树了,所以才说大顶堆更好,op 写的没问题,这里我理解的不对

至于用小顶堆,我不太理解为什么,为了方便把所有数据都放进堆里?
kenberkeley
32 天前
@GeekGao 我觉得你的这个思路很好。

其中第 3 步「合并所有排序的小块」其实还能优化一下,只要凑够前 n 个元素就够了,不用真的把所有小块都处理完毕。由此第 4 步也省了。
Sawyerhou
32 天前
@geelaw 哦,是的是的,你说的对,op 的帖子我看的太不仔细了,下次要检查检查再回复了:P
fkdtz
32 天前
@Sawyerhou topK 用小顶堆恰恰就是为了不把所有数据放进堆里吧,这样复杂度 logk ,要是用大顶堆 logn 了。
还是说我没理解你的意思,用大顶堆是有什么特殊考虑吗?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1032381

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX