我最近参加了一次采访,被问到“编写一个程序以从10亿个数字中找出100个最大的数字”。
我只能给出一种蛮力的解决方案,即以O(nlogn)时间复杂度对数组进行排序,并获取最后100个数字。
Arrays.sort(array);
面试官正在寻找更好的时间复杂度,我尝试了其他一些解决方案,但未能回答他。有更好的时间复杂度解决方案吗?
O(1)
,因为不会增加尺寸。面试官应该问“如何从n >> m的n数组中找到m个最大元素?”。
我最近参加了一次采访,被问到“编写一个程序以从10亿个数字中找出100个最大的数字”。
我只能给出一种蛮力的解决方案,即以O(nlogn)时间复杂度对数组进行排序,并获取最后100个数字。
Arrays.sort(array);
面试官正在寻找更好的时间复杂度,我尝试了其他一些解决方案,但未能回答他。有更好的时间复杂度解决方案吗?
O(1)
,因为不会增加尺寸。面试官应该问“如何从n >> m的n数组中找到m个最大元素?”。
Answers:
您可以保留一个包含100个最大数字的优先级队列,遍历十亿个数字,只要遇到一个大于该队列中最小数字(队列的开头)的数字,请删除队列的开头并添加新的数字排队。
编辑:
正如开发人员指出的那样,通过使用堆实现优先级队列,插入队列的复杂度为O(logN)
在最坏的情况下,您会得到比billionlog2(100)
billion
log2(billion)
通常,如果您需要一组N个数中最大的K个数,则复杂度O(NlogK)
而不是O(NlogN)
,当K与N相比非常小时,这可能非常重要。
编辑2:
该算法的预期时间非常有趣,因为在每次迭代中都可能会或可能不会发生插入。第i个数字插入队列的概率是随机变量至少比i-K
来自同一分布的随机变量大的概率(前k个数字自动添加到队列)。我们可以使用订单统计信息(请参阅链接)来计算该概率。例如,假设从中均匀地随机选择数字{0, 1}
,第(iK)个数字的期望值(第i个数字中)为(i-k)/i
,并且随机变量大于此值的机会为1-[(i-k)/i] = k/i
。
因此,预期的插入次数为:
预期的运行时间可以表示为:
(k
生成具有第一个k
元素的队列,然后n-k
进行比较以及如上所述的预期插入次数所需的log(k)/2
时间,每个均花费平均时间)
请注意,当N
与相比非常大时K
,此表达式更接近n
而不是NlogK
。这有点直观,例如在问题的情况下,即使经过10000次迭代(与十亿次迭代相比,这仍然很小),将数字插入队列的机会也很小。
k
常量较小n
。但是,应该始终牢记这种“正常情况”。
如果是在面试中提出的,我认为面试官可能希望看到您的问题解决过程,而不仅仅是您的算法知识。
描述很笼统,所以也许您可以问他这些数字的范围或含义,以使问题清楚。这样做可能会使面试官印象深刻。例如,如果这些数字代表一个国家(例如中国)内某人的年龄,那么这将是一个容易得多的问题。在合理的假设下,没有一个人的年龄大于200,可以使用大小为200(也许为201)的int数组在一次迭代中计算具有相同年龄的人数。这里的索引表示年龄。在此之后,找到100个最大数字就是小菜一碟。顺便说一下,这种算法称为计数排序。
无论如何,在面试中使问题更具体,更明确对您有好处。
您可以遍历需要O(n)的数字
每当发现大于当前最小值的值时,请将新值添加到大小为100的循环队列中。
该循环队列的最小值是您的新比较值。继续添加到该队列。如果已满,请从队列中提取最小值。
我意识到这被标记为“算法”,但是会抛出其他选择,因为它可能也应该被标记为“面试”。
10亿个数字的来源是什么?如果是数据库,那么“从表顺序中按值desc限制100选择值”会做得很好-可能存在方言差异。
这是一次性的还是会重复的?如果重复,多久一次?如果是一次性数据,并且数据在文件中,则'cat srcfile | 排序(根据需要的选项)| 头-100'将让您快速地完成生产工作,而计算机却在处理这些琐碎的琐事,您将获得报酬。
如果重复执行此操作,则建议您选择任何体面的方法来获得初始答案并存储/缓存结果,以便您能够连续报告前100名。
最后,有这个考虑。您是否正在寻找入门级工作并采访一个令人讨厌的经理或未来的同事?如果是这样,那么您可以抛弃所有描述相对技术利弊的方法。如果您正在寻找管理工作,请像经理一样处理,关注解决方案的开发和维护成本,然后说“非常感谢”,然后离开,如果面试官希望专注于CS琐事。他和你在那里不太可能有很大的发展潜力。
下一次面试好运。
我对此的直接反应是使用堆,但是有一种使用QuickSelect的方法,而不必随时保留所有输入值。
创建一个大小为200的数组,并用前200个输入值填充它。运行QuickSelect并丢弃低的100,剩下100个免费位置。读入下一个100个输入值,然后再次运行QuickSelect。继续,直到以100为批次运行整个输入为止。
最后,您拥有前100个值。对于N个值,您已运行QuickSelect大约N / 100次。每个Quickselect的成本约为某个常数的200倍,因此总成本为某个常数的2N倍。不管我在此说明中将参数大小强制设置为100,这对我来说看起来都是线性的。
partial_sort
运行直接int
相同,后者直接在2亿个32位数据集(通过MT19937创建,均匀分布)上运行。
Ordering.greatestOf(Iterable, int)
作用。它绝对是线性时间和单遍,并且是一种超可爱的算法。FWIW,我们还有一些实际的基准:在平均情况下,其恒定因素要比传统优先级队列慢一点,但是此实现更能抵抗“最坏情况”的输入(例如严格的递增输入)。
您可以使用快速选择算法在(按顺序)索引[billion-101]处找到数字,然后遍历这些数字并从该数字中查找更大的数字。
array={...the billion numbers...}
result[100];
pivot=QuickSelect(array,billion-101);//O(N)
for(i=0;i<billion;i++)//O(N)
if(array[i]>=pivot)
result.add(array[i]);
该算法时间为:2 XO(N)= O(N)(平均用例性能)
像Thomas Jungblut所建议的第二种选择是:
使用Heap构建MAX堆将占用O(N),然后前100个最大数字将在Heap的顶部,您所需要做的就是从堆中取出它们(100 XO(Log(N)))。
该算法的时间为:O(N)+ 100 XO(Log(N))= O(N)
O(N)
,执行两个QuickSelect和另一个线性扫描的开销也远远超过了需要。
100*O(N)
(如果这是有效的语法)= O(100*N)
= O(N)
(当然,可能有100个变量,但实际上并非如此)。哦,Quickselect在O(N ^ 2)(ouch)的情况下表现最差。而且,如果它不适合内存,您将需要两次从磁盘重新加载数据,这比一次要糟得多(这是瓶颈)。
尽管其他快速选择解决方案已被否决,但事实仍然是,快速选择将比使用大小为100的队列更快地找到解决方案。根据比较,快速选择的预期运行时间为2n + o(n)。一个非常简单的实现是
array = input array of length n
r = Quickselect(array,n-100)
result = array of length 100
for(i = 1 to n)
if(array[i]>r)
add array[i] to result
平均需要3n + o(n)比较。此外,可以使用quickselect将数组中最大的100个项目保留在最右边的100个位置这一事实来提高效率。因此,实际上,运行时间可以提高到2n + o(n)。
问题在于,这是预期的运行时间,而不是最坏的情况,但是通过使用适当的数据透视选择策略(例如,随机选择21个元素,然后选择21个元素的中位数作为数据透视),可以将比较次数设为对于任意小的常数c,可以保证最大概率为(2 + c)n。
实际上,通过使用优化的采样策略(例如,随机采样sqrt(n)个元素,并选择第99个百分位数),对于任意小的c,运行时间可以降低到(1 + c)n + o(n) (假设K,要选择的元素数为o(n))。
另一方面,使用大小为100的队列将需要O(log(100)n)比较,并且100的对数底数2大约等于6.6。
如果从更抽象的意义上考虑这个问题,即从大小为N的数组中选择最大的K个元素,其中K = o(N)但K和N都变为无穷大,则quickselect版本的运行时间为O(N)和队列版本将为O(N log K),因此从这个意义上说,快速选择在渐近性上也更好。
在评论中,提到了队列解决方案将在随机输入的预期时间N + K log N上运行。当然,除非问题明确指出,否则随机输入假设永远是无效的。可以使队列解决方案以随机顺序遍历该数组,但是这将招致对随机数生成器进行N次调用的额外费用,以及置换整个输入数组或分配一个新的长度为N的数组,其中包含随机索引。
如果问题不允许您在原始数组中移动,并且分配内存的成本很高,那么复制数组就不可行了,那就是另一回事了。但是严格来说,这是最好的解决方案。
将十亿的前100个数字进行排序。现在只需迭代十亿,如果源编号大于最小的100,则按排序顺序插入。在集合的大小上,最终得到的结果更接近O(n)。
两种选择:
(1)堆(priorityQueue)
保持最小堆大小为100。遍历数组。一旦元素小于堆中的第一个元素,请替换它。
InSERT ELEMENT INTO HEAP: O(log100)
compare the first element: O(1)
There are n elements in the array, so the total would be O(nlog100), which is O(n)
(2)Map-reduce模型。
这与hadoop中的字数示例非常相似。地图作业:计算每个元素的出现频率或次数。减少:获取前K个元素。
通常,我会给招聘者两个答案。给他们他们喜欢的任何东西。当然,map reduce编码会很麻烦,因为您必须知道每个确切的参数。实践没有危害。祝好运。
一个非常简单的解决方案是遍历数组100次。哪个是O(n)
。
每次您提取最大数(并将其值更改为最小值,以免在下一次迭代中看不到它,或者跟踪先前答案的索引(通过跟踪原始数组可以具有的索引)同一数字的倍数))。经过100次迭代后,您获得了100个最大的数字。
受@ron Teller答案的启发,这是一个准系统C程序,可以执行您想要的操作。
#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);
}
在我的机器(具有快速SSD的i3核心)上,它需要25秒,并且需要1724种排序。我dd if=/dev/urandom/ count=1000000000 bs=1
为此运行生成了一个二进制文件。
显然,一次仅从磁盘读取4个字节存在性能问题,但这是出于示例的缘故。从好的方面来说,只需要很少的内存。
最简单的解决方案是扫描十亿个大型数组,并将到目前为止找到的100个最大值保存在小型数组缓冲区中,而不进行任何排序,并记住该缓冲区的最小值。首先,我认为此方法是由fordprefect提出的,但在评论中他说他假定100号数据结构被实现为堆。每当发现一个较大的新数字时,缓冲区中的最小值就会被找到的新值覆盖,并再次在缓冲区中搜索当前的最小值。如果十亿个数字数组中的数字大部分时间是随机分布的,则将大数组中的值与小数组中的最小值进行比较,然后将其丢弃。仅对于很小一部分数字,该值必须插入小数组中。因此,可以忽略处理包含少量数字的数据结构的差异。对于少数元素,很难确定优先级队列的使用是否实际上比我的幼稚方法要快。
我想估计扫描10 ^ 9元素数组时小的100元素数组缓冲区中的插入数。该程序将扫描此大型数组的前1000个元素,并且必须在缓冲区中最多插入1000个元素。缓冲区包含扫描的1000个元素中的100个元素,即扫描的元素的0.1个。因此,我们假设大数组中的值大于缓冲区的当前最小值的可能性约为0.1。必须在缓冲区中插入一个元素。现在,程序将扫描大型数组中的下一个10 ^ 4个元素。因为每次插入新元素时缓冲区的最小值都会增加。我们估计,大于我们当前最小值的元素之比约为0.1,因此要插入0.1 * 10 ^ 4 = 1000个元素。实际上,插入缓冲区的元素的预期数量会更少。扫描完这10 ^ 4个元素后,缓冲区中数字的分数将是到目前为止扫描的元素的约0.01。因此,当扫描下一个10 ^ 5数字时,我们假定将不超过0.01 * 10 ^ 5 = 1000插入到缓冲区中。继续这一论点,我们在扫描大型数组的1000 + 10 ^ 4 + 10 ^ 5 + ... + 10 ^ 9〜10 ^ 9个元素后插入了大约7000个值。因此,当扫描随机大小为10 ^ 9的元素的数组时,我们期望缓冲区中的插入不超过10 ^ 4(=向上舍入为7000)。每次插入缓冲区后,必须找到新的最小值。如果缓冲区是一个简单的数组,我们需要进行100次比较才能找到新的最小值。如果缓冲区是另一个数据结构(如堆),则我们至少需要进行1次比较才能找到最小值。为了比较大型数组的元素,我们需要进行10 ^ 9比较。所以总的来说,使用数组作为缓冲区时,我们需要进行大约10 ^ 9 + 100 * 10 ^ 4 = 1.001 * 10 ^ 9的比较,而在使用另一种类型的数据结构(例如堆)时,至少需要进行1.000 * 10 ^ 9的比较。 。因此,如果性能由比较次数决定,则使用堆只会带来0.1%的收益。但是,在将一个元素插入100个元素堆中和替换一个100个元素数组中的元素并找到其新的最小值之间的执行时间有何不同?使用另一种类型的数据结构(如堆)时进行000 * 10 ^ 9比较。因此,如果性能由比较次数决定,则使用堆只会带来0.1%的收益。但是,在将一个元素插入100个元素堆中和替换一个100个元素数组中的元素并找到其新的最小值之间的执行时间有何不同?使用另一种类型的数据结构(如堆)时进行000 * 10 ^ 9比较。因此,如果性能由比较次数决定,则使用堆只会带来0.1%的收益。但是,在将一个元素插入100个元素堆中和替换一个100个元素数组中的元素并找到其新的最小值之间的执行时间有何不同?
从理论上讲:插入堆需要多少次比较。我知道它是O(log(n)),但是常数因子有多大?一世
在机器级别:缓存和分支预测对堆插入和数组中线性搜索的执行时间有何影响?
在实现级别:库或编译器提供的堆数据结构中隐藏了哪些额外成本?
我认为这些是必须尝试回答的一些问题,然后才能尝试估计100个元素堆或100个元素数组的性能之间的真正差异。因此,进行实验并评估实际效果会很有意义。
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)+(nx)(log(x)+1)= nlog(x)+ n-x
因此最坏的情况是O(n)时间。+1是检查数字是否大于LIST中最小的数字。平均情况的预期时间将取决于这n个元素的数学分布。
可能的改进
在最坏的情况下,可以对该算法进行一些改进,但是恕我直言(我无法证明这一说法)会降低平均行为。渐近行为将是相同的。
此算法的改进之处在于,我们将不检查element是否大于最小元素。对于每个元素,我们将尝试插入它,如果它小于最小元素,我们将忽略它。尽管如果仅考虑最坏的情况,这听起来很荒谬,
x log(x)+(nx)log(x)= nlog(x)
操作。
对于此用例,我看不到任何进一步的改进。但是您必须问自己-如果我必须做的次数超过log(n)次并且对于不同的x-es,该怎么办?显然,我们将在O(n log(n))中对该数组进行排序,并在需要时使用x元素。
仅使用一行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());
最终答案将是一个向量,其中前100个元素保证是数组中的前100个最大数字,而其余元素是无序的
C ++ STL(标准库)对于此类问题非常方便。
注意:我并不是说这是最佳解决方案,但可以节省您的采访时间。
一种简单的解决方案是使用优先级队列,将前100个数字添加到队列中,并跟踪队列中最小的数字,然后遍历其他十亿个数字,每次我们发现一个大于最大数字的数字在优先级队列中,我们删除最小的数字,添加新的数字,然后再次跟踪队列中的最小数字。
如果数字按随机顺序排列,那么效果很好,因为当我们遍历十亿个随机数字时,下一个数字是迄今为止排在前100位的数字之中非常罕见。但是数字可能不是随机的。如果数组已经按升序排序,那么我们总是将元素插入优先级队列。
因此,我们首先从数组中选择说100,000个随机数。为了避免随机访问(可能会很慢),我们添加了400个随机组,包含250个连续数字。通过这种随机选择,我们可以完全确定剩下的数字中只有很少的几百位,因此执行时间将非常接近将十亿个数字与某个最大值进行比较的简单循环的执行时间。
如果有人感兴趣,我已经用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(N)的讨论,因此我为思想练习提出了一些不同的建议。
是否有关于这些数字性质的已知信息?如果本质上是随机的,那就别无所求了。您不会得到比他们更好的结果。
然而!查看是否有任何列表填充机制以特定顺序填充了该列表。它们是否处于定义明确的模式中,您可以肯定地知道最大数量的数字将出现在列表的特定区域或特定间隔中?可能有一个模式。如果是这样,例如,如果保证它们处于某种形式的正态分布且中间具有特征性的驼峰,则始终在已定义的子集中重复出现上升趋势,并在数据中间的某个时间T处出现尖峰延长设置为可能发生内幕交易或设备故障,或者只是在灾难发生后的力量分析中每N个数字都有一个“尖峰”,您可以减少必须检查的记录数。
无论如何,还有一些值得深思的地方。也许这可以帮助您给未来的面试官一个深思熟虑的答案。我知道如果有人问我这样的问题来回答这样的问题,我会印象深刻-它会告诉我他们正在考虑优化。只要认识到可能并不总是有可能进行优化。
复杂度为O(N)
首先创建一个100个整数的数组,将该数组的第一个元素初始化为N个值的第一个元素,并使用另一个变量来跟踪当前元素的索引,将其称为CurrentBig
遍历N个值
if N[i] > M[CurrentBig] {
M[CurrentBig]=N[i]; ( overwrite the current value with the newly found larger number)
CurrentBig++; ( go to the next position in the M array)
CurrentBig %= 100; ( modulo arithmetic saves you from using lists/hashes etc.)
M[CurrentBig]=N[i]; ( pick up the current value again to use it for the next Iteration of the N array)
}
完成后,从CurrentBig中将M数组打印100次,以100为模:-)对于学生:确保代码的最后一行在代码退出之前不会比有效数据更重要
另一种O(n)算法-
该算法通过消除找到最大的100
考虑其二进制表示形式中的所有百万个数字。从最高有效位开始。可以通过布尔运算乘以适当的数字来确定MSB是否为1。如果这100万个中的100个以上,则消除其他零个数。现在,其余数字中的下一个最高有效位继续。对消除后的剩余数字进行计数,只要该数字大于100,就继续进行。
主要的布尔运算可以在GPU上并行执行
这是Google或其他行业巨头提出的问题。也许以下代码是您的面试官所期望的正确答案。时间成本和空间成本取决于输入数组中的最大数量。对于32位整数数组输入,最大空间成本为4 * 125M字节,时间成本为5 *十亿。
public class TopNumber {
public static void main(String[] args) {
final int input[] = {2389,8922,3382,6982,5231,8934
,4322,7922,6892,5224,4829,3829
,6892,6872,4682,6723,8923,3492};
//One int(4 bytes) hold 32 = 2^5 value,
//About 4 * 125M Bytes
//int sort[] = new int[1 << (32 - 5)];
//Allocate small array for local test
int sort[] = new int[1000];
//Set all bit to 0
for(int index = 0; index < sort.length; index++){
sort[index] = 0;
}
for(int number : input){
sort[number >>> 5] |= (1 << (number % 32));
}
int topNum = 0;
outer:
for(int index = sort.length - 1; index >= 0; index--){
if(0 != sort[index]){
for(int bit = 31; bit >= 0; bit--){
if(0 != (sort[index] & (1 << bit))){
System.out.println((index << 5) + bit);
topNum++;
if(topNum >= 3){
break outer;
}
}
}
}
}
}
}
问题:找到n个项目的m个最大元素,其中n >>> m
每个人都应该知道的最简单的解决方案是简单地对冒泡排序算法进行m次传递。
然后打印出数组的最后n个元素。
这不需要外部数据结构,并且使用了众所周知的算法。
运行时间估计为O(m * n)。到目前为止,最好的答案是O(n log(m)),因此对于小m而言,该解决方案的成本并不昂贵。
我并不是说这无法改善,但这是迄今为止最简单的解决方案。