我最近参加了一次面试,被问到“编写一个程序,在10亿个数字的数组中找出最大的100个数字”。
我只能提供一种暴力解决方案,即在O(nlogn)的时间复杂度内对数组进行排序并取最后100个数字。
Arrays.sort(array);
面试官在寻求更好的时间复杂度,我尝试了几种其他的解决方案,但都无法回答他。是否有更好的时间复杂度解决方案?
我最近参加了一次面试,被问到“编写一个程序,在10亿个数字的数组中找出最大的100个数字”。
我只能提供一种暴力解决方案,即在O(nlogn)的时间复杂度内对数组进行排序并取最后100个数字。
Arrays.sort(array);
面试官在寻求更好的时间复杂度,我尝试了几种其他的解决方案,但都无法回答他。是否有更好的时间复杂度解决方案?
仅用一行C++代码,就可以在N log(100)复杂度(而不是N log N)下回答这个问题。
std::vector<int> myvector = ...; // Define your 1 billion numbers.
// Assumed integer just for concreteness
std::partial_sort (myvector.begin(), myvector.begin()+100, myvector.end());
#include <stdlib.h>
#include <stdio.h>
#define TOTAL_NUMBERS 1000000000
#define N_TOP_NUMBERS 100
int
compare_function(const void *first, const void *second)
{
int a = *((int *) first);
int b = *((int *) second);
if (a > b){
return 1;
}
if (a < b){
return -1;
}
return 0;
}
int
main(int argc, char ** argv)
{
if(argc != 2){
printf("please supply a path to a binary file containing 1000000000"
"integers of this machine's wordlength and endianness\n");
exit(1);
}
FILE * f = fopen(argv[1], "r");
if(!f){
exit(1);
}
int top100[N_TOP_NUMBERS] = {0};
int sorts = 0;
for (int i = 0; i < TOTAL_NUMBERS; i++){
int number;
int ok;
ok = fread(&number, sizeof(int), 1, f);
if(!ok){
printf("not enough numbers!\n");
break;
}
if(number > top100[0]){
sorts++;
top100[0] = number;
qsort(top100, N_TOP_NUMBERS, sizeof(int), compare_function);
}
}
printf("%d sorts made\n"
"the top 100 integers in %s are:\n",
sorts, argv[1] );
for (int i = 0; i < N_TOP_NUMBERS; i++){
printf("%d\n", top100[i]);
}
fclose(f);
exit(0);
}
dd if=/dev/urandom/ count=1000000000 bs=1
生成了一个二进制文件来运行此程序。 Although in this question we should search for top 100 numbers, I will
generalize things and write x. Still, I will treat x as constant value.
算法:从n中选择最大的x个元素
我将返回值称为LIST。它是x个元素的集合(在我看来应该是链表)
那么,最坏的情况是什么?
x log(x) + (n-x)(log(x)+1) = nlog(x) + n - x
所以最坏情况下的时间复杂度为O(n)。+1是检查数字是否大于LIST中最小的数字。平均情况下的预期时间取决于这些n个元素的数学分布。
可能的改进
这个算法可以针对最坏情况进行轻微改进,但在我看来(我无法证明这一点),这会降低平均行为。渐近行为将保持不变。O(n)
的时间内完成。只需遍历列表并跟踪你在任何给定点看到的100个最大数字和该组中的最小值。当你找到一个比你的十个最小数更大的新数字时,就替换它并更新你的100个最小值的新最小值(每次执行此操作可能需要100的固定时间,但这不会影响整体分析)。在理论层面上:插入堆中需要多少比较。我知道它是O(log(n)),但常数因子有多大?
在机器层面上:缓存和分支预测对堆插入和数组线性搜索的执行时间有什么影响。
在实现层面上:由库或编译器提供的堆数据结构中隐藏了哪些额外的成本?
我认为在尝试估计100个元素堆和100个元素数组性能差异之前,必须回答这些问题。因此进行实验并测量实际性能是有意义的。
总之,这是一些值得思考的内容。也许这能帮助你给未来的面试官一个深思熟虑的答案。如果有人在回应这样的问题时问我这样的问题,我会印象深刻——这会告诉我他们正在考虑优化。只要认识到并不总是有优化的可能性。
我写了自己的代码,不确定它是否符合“面试官”的要求。
private static final int MAX=100;
PriorityQueue<Integer> queue = new PriorityQueue<>(MAX);
queue.add(array[0]);
for (int i=1;i<array.length;i++)
{
if(queue.peek()<array[i])
{
if(queue.size() >=MAX)
{
queue.poll();
}
queue.add(array[i]);
}
}
array[0]
上方的值少于MAX-1
个时,队列queue
中的元素将少于MAX
个。 - greybeard我已经用Python写了一个简单的解决方案,如果有人感兴趣的话可以看一下。它使用了bisect
模块和一个临时返回列表,该列表保持排序。这类似于优先队列实现。
import bisect
def kLargest(A, k):
'''returns list of k largest integers in A'''
ret = []
for i, a in enumerate(A):
# For first k elements, simply construct sorted temp list
# It is treated similarly to a priority queue
if i < k:
bisect.insort(ret, a) # properly inserts a into sorted list ret
# Iterate over rest of array
# Replace and update return array when more optimal element is found
else:
if a > ret[0]:
del ret[0] # pop min element off queue
bisect.insort(ret, a) # properly inserts a into sorted list ret
return ret
使用100,000,000个元素和最坏情况输入,即已排序列表:
>>> from so import kLargest
>>> kLargest(range(100000000), 100)
[99999900, 99999901, 99999902, 99999903, 99999904, 99999905, 99999906, 99999907,
99999908, 99999909, 99999910, 99999911, 99999912, 99999913, 99999914, 99999915,
99999916, 99999917, 99999918, 99999919, 99999920, 99999921, 99999922, 99999923,
99999924, 99999925, 99999926, 99999927, 99999928, 99999929, 99999930, 99999931,
99999932, 99999933, 99999934, 99999935, 99999936, 99999937, 99999938, 99999939,
99999940, 99999941, 99999942, 99999943, 99999944, 99999945, 99999946, 99999947,
99999948, 99999949, 99999950, 99999951, 99999952, 99999953, 99999954, 99999955,
99999956, 99999957, 99999958, 99999959, 99999960, 99999961, 99999962, 99999963,
99999964, 99999965, 99999966, 99999967, 99999968, 99999969, 99999970, 99999971,
99999972, 99999973, 99999974, 99999975, 99999976, 99999977, 99999978, 99999979,
99999980, 99999981, 99999982, 99999983, 99999984, 99999985, 99999986, 99999987,
99999988, 99999989, 99999990, 99999991, 99999992, 99999993, 99999994, 99999995,
99999996, 99999997, 99999998, 99999999]
计算 1 亿个元素大约需要 40 秒,所以我害怕去计算 10 亿个元素。不过公平地说,我使用的是最坏情况的输入(具有讽刺意味的是一个已经排序好的数组)。
O(log K)
。将数组插入到已排序的数组中(如一次插入排序)的成本为O(K)
,因此仍然优于完全排序的O(K log K)
,而不利用它已经排序的优势。del ret [0]
可能会复制数组,或者就地向下复制一个;从末尾删除可能更便宜。所以,是的,在Python中,最坏情况下每个元素都运行insort
,我并不感到惊讶它很慢。 - Peter Cordes
O(1)
,因为没有维度增加。面试官应该问:“如何从一个长度为n的数组中找到m个最大的元素,其中n>>m?” - Bakuriu