编写程序以从10亿个数字数组中查找100个最大数字


300

我最近参加了一次采访,被问到“编写一个程序以从10亿个数字中找出100个最大的数字”。

我只能给出一种蛮力的解决方案,即以O(nlogn)时间复杂度对数组进行排序,并获取最后100个数字。

Arrays.sort(array);

面试官正在寻找更好的时间复杂度,我尝试了其他一些解决方案,但未能回答他。有更好的时间复杂度解决方案吗?


70
也许问题在于这不是一个排序问题,而是一个寻求解决的问题。
geomagas 2013年

11
作为技术说明,排序可能不是解决问题的最佳方法,但我认为这不是蛮力-我可以想到很多更差的解决方法。
Bernhard Barker 2013年

88
我只是想到了一个更愚蠢的蛮力方法...从10亿个元素数组中找到100个元素的所有可能组合,然后看看这些组合中哪一个组合的总和最大。
沙申克

10
请注意,在这种情况下,所有确定性(和正确的)算法都是有效的O(1),因为不会增加尺寸。面试官应该问“如何从n >> m的n数组中找到m个最大元素?”。
2013年

Answers:


328

您可以保留一个包含100个最大数字的优先级队列,遍历十亿个数字,只要遇到一个大于该队列中最小数字(队列的开头)的数字,请删除队列的开头并添加新的数字排队。

编辑: 正如开发人员指出的那样,通过使用堆实现优先级队列,插入队列的复杂度为O(logN)

在最坏的情况下,您会得到billionlog2(100)billionlog2(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次迭代(与十亿次迭代相比,这仍然很小),将数字插入队列的机会也很小。


6
每个插入实际上只有O(100)
MrSmith42 2013年

8
@RonTeller您不能有效地对链接列表进行二进制搜索,这就是为什么通常使用堆来实现优先级队列的原因。所描述的插入时间是O(n)而不是O(logn)。您第一次(顺序队列或优先级队列)拥有正确的权限,直到Skizz让您第二次猜测自己为止。
2013年

17
@ThomasJungblut十亿也是一个常数,因此如果是O(1):P
罗恩·泰勒

9
@RonTeller:通常,此类问题引起人们的关注,例如从数十亿个Google搜索结果中找到10个首页,或者一个单词云找到50个最常见的单词,或者在MTV上找到10个最受欢迎的歌曲,等等。所以,我相信,在正常情况下与相比,可以认为k 常量较小n。但是,应该始终牢记这种“正常情况”。
ffriend

5
由于您有1G项,因此请随机抽样1000个元素,并选择最大的100个元素。这应该避免退化的情况(排序,反向排序,大部分排序),从而大大减少了插入次数。
ChuckCottrill

136

如果是在面试中提出的,我认为面试官可能希望看到您的问题解决过程,而不仅仅是您的算法知识。

描述很笼统,所以也许您可以问他这些数字的范围或含义,以使问题清楚。这样做可能会使面试官印象深刻。例如,如果这些数字代表一个国家(例如中国)内某人的年龄,那么这将是一个容易得多的问题。在合理的假设下,没有一个人的年龄大于200,可以使用大小为200(也许为201)的int数组在一次迭代中计算具有相同年龄的人数。这里的索引表示年龄。在此之后,找到100个最大数字就是小菜一碟。顺便说一下,这种算法称为计数排序

无论如何,在面试中使问题更具体,更明确对您有好处。


26
非常好点。没有其他人询问或表明这些数字的分布情况了,这可能会在解决问题的方式上大为不同。
NealB

13
我想要这个答案足以扩展它。一次读取数字以获得最小/最大值,以便您可以假设分布。然后,选择两个选项之一。如果范围足够小,则构建一个数组,您可以在其中简单地检查数字。如果范围太大,请使用上面讨论的排序堆算法...。
Richard_G

2
我同意,向面试官提问确实有很大的不同。实际上,诸如您是否受计算能力限制之类的问题也可以通过使用多个计算节点来帮助您并行化解决方案。
Sumit Nigam 2013年

1
@R_G无需遍历整个列表。足够抽样列表中一小部分(例如一百万)的随机成员,以获得有用的统计信息。
Itamar

对于那些不想考虑该解决方案的人,我建议您阅读有关计数排序的信息en.wikipedia.org/wiki/Counting_sort。这实际上是一个非常常见的面试问题:您能以比O(nlogn)更好的方式对数组进行排序吗?这个问题只是一个扩展。
MaximeChéramy13年

69

您可以遍历需要O(n)的数字

每当发现大于当前最小值的值时,请将新值添加到大小为100的循环队列中。

该循环队列的最小值是您的新比较值。继续添加到该队列。如果已满,请从队列中提取最小值。


3
这行不通。的例如查找顶部2 {1,100 2,99}将得到{100,1}作为顶部2
Skizz

7
您无法四处走动以保持排序的队列。(如果您不想每次都在孔队列中搜索下一个最小的元素)
MrSmith42

3
@ MrSmith42在堆中进行部分排序就足够了。请参阅罗恩·泰勒(Ron Teller)的答案。
Christopher Creutzig 2013年

1
是的,我无声地认为一个extract-min-queue被实现为一个堆。
Regenschein

而不是循环队列使用大小为100的最小堆,它的顶部至少要有一百个数字。与在队列情况下的o(n)相比,仅需O(log n)即可插入
techExplorer 2013年

33

我意识到这被标记为“算法”,但是会抛出其他选择,因为它可能也应该被标记为“面试”。

10亿个数字的来源是什么?如果是数据库,那么“从表顺序中按值desc限制100选择值”会做得很好-可能存在方言差异。

这是一次性的还是会重复的?如果重复,多久一次?如果是一次性数据,并且数据在文件中,则'cat srcfile | 排序(根据需要的选项)| 头-100'将让您快速地完成生​​产工作,而计算机却在处理这些琐碎的琐事,您将获得报酬。

如果重复执行此操作,则建议您选择任何体面的方法来获得初始答案并存储/缓存结果,以便您能够连续报告前100名。

最后,有这个考虑。您是否正在寻找入门级工作并采访一个令人讨厌的经理或未来的同事?如果是这样,那么您可以抛弃所有描述相对技术利弊的方法。如果您正在寻找管理工作,请像经理一样处理,关注解决方案的开发和维护成本,然后说“非常感谢”,然后离开,如果面试官希望专注于CS琐事。他和你在那里不太可能有很大的发展潜力。

下一次面试好运。


2
出色的答案。其他所有人都集中在问题的技术方面,而此响应解决了它的商业社会问题。
vbocan 2013年

2
我从未想过您会说谢谢并离开面试而不等它结束。感谢您的开放。
UrsulRosu

1
为什么我们不能创建数十亿个元素并提取100个最大元素。这样成本= O(十亿)+ 100 * O(log(十亿))??
Mohit Shah

17

我对此的直接反应是使用堆,但是有一种使用QuickSelect的方法,而不必随时保留所有输入值。

创建一个大小为200的数组,并用前200个输入值填充它。运行QuickSelect并丢弃低的100,剩下100个免费位置。读入下一个100个输入值,然后再次运行QuickSelect。继续,直到以100为批次运行整个输入为止。

最后,您拥有前100个值。对于N个值,您已运行QuickSelect大约N / 100次。每个Quickselect的成本约为某个常数的200倍,因此总成本为某个常数的2N倍。不管我在此说明中将参数大小强制设置为100,这对我来说看起来都是线性的。


10
您可以添加一个小的但可能重要的优化:运行QuickSelect对大小为200的数组进行分区之后,已知前100个元素中的最小值。然后,当遍历整个数据集时,如果当前值大于当前最小值,则仅填充较低的100个值。这种算法在C ++中的简单实现与libstdc ++的partial_sort运行直接int相同,后者直接在2亿个32位数据集(通过MT19937创建,均匀分布)上运行。
dyp

1
好主意-不会影响最坏情况的分析,但看起来值得做。
mcdowella

@mcdowella值得一试,我会做的,谢谢!
userx 2013年

8
这正是番石榴的 Ordering.greatestOf(Iterable, int)作用。它绝对是线性时间和单遍,并且是一种超可爱的算法。FWIW,我们还有一些实际的基准:在平均情况下,其恒定因素要比传统优先级队列慢一点,但是此实现更能抵抗“最坏情况”的输入(例如严格的递增输入)。
Louis Wasserman

15

您可以使用快速选择算法在(按顺序)索引[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)


8
您正在遍历整个列表三遍。1个生物。整数大约为4gb,如果无法将它们装入内存,该怎么办?在这种情况下,快速选择是最糟糕的选择。恕我直言,迭代一次并保留前100个项的堆是O(n)中性能最好的解决方案(请注意,因为堆中的n为100 =常量=非常小,所以可以切断堆插入的O(log n) )。
Thomas Jungblut13年

3
即使仍然如此O(N),执行两个QuickSelect和另一个线性扫描的开销也远远超过了需要。
凯文(Kevin)

这是PSEUDO代码,此处的所有解决方案将花费更多时间(O(NLOG(N)或100 * O(N)))
一名机组人员

1
100*O(N)(如果这是有效的语法)= O(100*N)= O(N)(当然,可能有100个变量,但实际上并非如此)。哦,Quickselect在O(N ^ 2)(ouch)的情况下表现最差。而且,如果它不适合内存,您将需要两次从磁盘重新加载数据,这比一次要糟得多(这是瓶颈)。
Bernhard Barker,

问题在于,这是预期的运行时间,而不是最坏的情况,但是通过使用适当的数据透视选择策略(例如,随机选择21个元素,然后选择21个元素的中位数作为数据透视),可以将比较次数设为对于任意小的常数c,可以保证最大概率为(2 + c)n。
一名船员,

10

尽管其他快速选择解决方案已被否决,但事实仍然是,快速选择将比使用大小为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的数组,其中包含随机索引。

如果问题不允许您在原始数组中移动,并且分配内存的成本很高,那么复制数组就不可行了,那就是另一回事了。但是严格来说,这是最好的解决方案。


4
最后一段是关键点:拥有十亿个数字,将所有数据保存在内存中或在周围交换元素是不可行的。(考虑到这是一个面试问题,至少我会这样解释这个问题。)
Ted Hopp 2013年

14
在任何算法问题中,如果读取数据是一个问题,则必须在问题中提及它。该问题指出“给定数组”而不是“给磁盘上不适合内存且无法根据算法分析标准的冯·诺伊曼模型进行操纵的数组”。这些天,您可以获得一台配备8gig内存的笔记本电脑。我不确定在内存中保存十亿个数字的想法从何而来。我的工作站上现在有数十亿个内存。
mrip

FYI quickselect的最坏情况运行时为O(n ^ 2)(请参阅en.wikipedia.org/wiki/Quickselect),并且它还会修改输入数组中元素的顺序。可能有一个最坏情况的O(n)解决方案,它的常数很大(en.wikipedia.org/wiki/Median_of_medians)。
pts

快速选择的最坏情况不可能发生指数级增长,这意味着从实际出发,这是无关紧要的。修改快速选择很容易,因此对于任意小的c而言,比较数的概率很高(2 + c)n + o(n)。
mrip

“事实是,与使用大小为100的队列相比,快速选择将找到解决方案的速度更快” –不。堆解决方案大约需要N + Klog(N)比较,而快速选择的平均值为2N,中位数的中位数为2.95。这显然是针对给定的K.更快
尼尔摹

5

将十亿的前100个数字进行排序。现在只需迭代十亿,如果源编号大于最小的100,则按排序顺序插入。在集合的大小上,最终得到的结果更接近O(n)。


3
哎呀,我没有比我更详细的答案了。
塞缪尔·瑟斯顿

以前500个左右的数字为准,直到列表填满后才停止排序(扔掉低400)。(毋庸置疑,只有当新数字>所选100中的最低数字时,您才添加到列表中。)
Hot Licks 2013年

4

两种选择:

(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编码会很麻烦,因为您必须知道每个确切的参数。实践没有危害。祝好运。


对于MapReduce +1,我简直不敢相信您是十亿个提及Hadoop的唯一人。如果面试官要求提供10亿个数字怎么办?我认为您应该获得更多投票。
Silviu Burcea,2013年

@Silviu Burcea非常感谢。我也很重视MapReduce。:)
克里斯·苏

尽管在此示例中100的大小是恒定的,但您实际上应该将其概括为一个单独的变量,即。k。因为100等于10亿的常数,那么为什么要给大型数字集的大小赋予n的大小变量,而不给较小的数字集呢?确实,您的复杂度应该是O(nlogk)而不是O(n)。
汤姆·希德

1
但是我的意思是,如果您只是在回答这个问题,那么问题中的10亿也是固定的,那么为什么将10亿推广到n而不是100推广到k。按照您的逻辑,复杂度实际上应为O(1),因为在此问题中10亿和100都是固定的。
Tom Heard 2014年

1
@TomHeard好吧。O(nlogk)只有一个因素会影响结果。这意味着,如果n越来越大,则“结果水平”将线性增加。或者我们可以说,即使给出了数万亿的数字,我仍然可以获得100个最大的数字。但是,您不能说:随着n的增加,k会增加,因此k会影响结果。这就是为什么我使用O(nlogk)而不使用O(nlogn)的原因
Chris Su

4

一个非常简单的解决方案是遍历数组100次。哪个是O(n)

每次您提取最大数(并将其值更改为最小值,以免在下一次迭代中看不到它,或者跟踪先前答案的索引(通过跟踪原始数组可以具有的索引)同一数字的倍数))。经过100次迭代后,您获得了100个最大的数字。


1
有两个缺点-(1)您正在破坏过程中的输入-最好避免这种情况。(2)您需要多次遍历该数组-如果该数组存储在磁盘上并且无法容纳到内存中,则这很容易比接受的答案慢近100倍。(是的,它们都是O(n),但仍然是)
Bernhard Barker 2013年

@Dukeling,打个招呼,我增加了一些措辞,说明如何通过跟踪先前的答案索引来避免更改原始输入。仍然很容易编写代码。
James Oravec

一个比O(n log n)慢得多的O(n)解决方案的出色示例。log2(10亿)只有30 ...
gnasher729 '16

@ gnasher729 O(n log n)中隐藏的常量有多大?
miracle173

1

受@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个字节存在性能问题,但这是出于示例的缘故。从好的方面来说,只需要很少的内存。


1

最简单的解决方案是扫描十亿个大型数组,并将到目前为止找到的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个元素数组的性能之间的真正差异。因此,进行实验并评估实际效果会很有意义。


1
那就是堆的作用。
Neil G

@Neil G:什么“那个”?
miracle173

1
堆的顶部是堆中的最小元素,并且一次比较就拒绝了新元​​素。
Neil G

1
我明白您在说什么,但是即使您按绝对比较数而不是渐近比较数进行,数组仍然要慢得多,因为“插入新元素,丢弃旧的最小值并找到新的最小值”的时间是100,而不是约7
尼尔ģ

1
好的,但是您的估算值非常接近。您可以直接计算期望的插入数为k(digamma(n)-digamma(k)),小于klo​​g(n)。无论如何,堆和数组解决方案都只花费一个比较就丢弃一个元素。唯一的不同是,对于您的解决方案,插入元素的比较数量是100,而堆的数量最多是14(尽管平均情况可能要少得多。)
Neil G

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个元素从“它们来时”池中提取,并在LIST中排序(由于x被视为常量-O(x log(x)),因此它在常量时间内完成)
  • 对于接下来出现的每个元素,我们检查它是否大于LIST中的最小元素,如果是,则弹出最小元素并将当前元素插入LIST。由于该列表是有序的,因此每个元素都应在对数时间(二进制搜索)中找到其位置,并且由于该列表是有序的,所以插入不是问题。每个步骤也都以固定时间(O(log(x))time)完成。

那么,最坏的情况是什么?

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元素。


1

仅使用一行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(标准库)对于此类问题非常方便。

注意:我并不是说这是最佳解决方案,但可以节省您的采访时间。


1

一种简单的解决方案是使用优先级队列,将前100个数字添加到队列中,并跟踪队列中最小的数字,然后遍历其他十亿个数字,每次我们发现一个大于最大数字的数字在优先级队列中,我们删除最小的数字,添加新的数字,然后再次跟踪队列中的最小数字。

如果数字按随机顺序排列,那么效果很好,因为当我们遍历十亿个随机数字时,下一个数字是迄今为止排在前100位的数字之中非常罕见。但是数字可能不是随机的。如果数组已经按升序排序,那么我们总是将元素插入优先级队列。

因此,我们首先从数组中选择说100,000个随机数。为了避免随机访问(可能会很慢),我们添加了400个随机组,包含250个连续数字。通过这种随机选择,我们可以完全确定剩下的数字中只有很少的几百位,因此执行时间将非常接近将十亿个数字与某个最大值进行比较的简单循环的执行时间。


1

最好使用100个元素的最小堆来查找十亿个数字中的前100 个。

首先用遇到的前100个数字填充最小堆。min-heap将在根(顶部)中存储前100个数字中的最小数字。

现在,当您处理其余数字时,仅将它们与根(100个中的最小个)进行比较。

如果遇到的新数字大于min-heap的根,则用该数字替换根,否则将其忽略。

作为在min-heap中插入新数字的一部分,堆中的最小数字将位于顶部(根)。

一旦我们遍历了所有数字,我们将在最小堆中拥有最大的100个数字。


0

如果有人感兴趣,我已经用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亿个时间来计算。为了公平起见,我正在向它提供最坏情况的输入(具有讽刺意味的是,该数组已经排序)。


0

我看到了很多O(N)的讨论,因此我为思想练习提出了一些不同的建议。

是否有关于这些数字性质的已知信息?如果本质上是随机的,那就别无所求了。您不会得到比他们更好的结果。

然而!查看是否有任何列表填充机制以特定顺序填充了该列表。它们是否处于定义明确的模式中,您可以肯定地知道最大数量的数字将出现在列表的特定区域或特定间隔中?可能有一个模式。如果是这样,例如,如果保证它们处于某种形式的正态分布且中间具有特征性的驼峰,则始终在已定义的子集中重复出现上升趋势,并在数据中间的某个时间T处出现尖峰延长设置为可能发生内幕交易或设备故障,或者只是在灾难发生后的力量分析中每N个数字都有一个“尖峰”,您可以减少必须检查的记录数。

无论如何,还有一些值得深思的地方。也许这可以帮助您给未来的面试官一个深思熟虑的答案。我知道如果有人问我这样的问题来回答这样的问题,我会印象深刻-它会告诉我他们正在考虑优化。只要认识到可能并不总是有可能进行优化。


0
Time ~ O(100 * N)
Space ~ O(100 + N)
  1. 创建一个包含100个空插槽的空列表

  2. 对于输入列表中的每个数字:

    • 如果数字小于第一个,则跳过

    • 否则用这个数字代替

    • 然后,将数字推过相邻的交换;直到小于下一个

  3. 返回清单


注意:如果使用log(input-list.size) + c < 100,则最佳方法是对输入列表进行排序,然后拆分前100个项目。


0

复杂度为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为模:-)对于学生:确保代码的最后一行在代码退出之前不会比有效数据更重要


0

另一种O(n)算法-

该算法通过消除找到最大的100

考虑其二进制表示形式中的所有百万个数字。从最高有效位开始。可以通过布尔运算乘以适当的数字来确定MSB是否为1。如果这100万个中的100个以上,则消除其他零个数。现在,其余数字中的下一个最高有效位继续。对消除后的剩余数字进行计数,只要该数字大于100,就继续进行。

主要的布尔运算可以在GPU上并行执行


0

我会找出谁有时间将十亿个数字放入数组并解雇他。必须为政府工作。至少如果您有一个链表,则可以在中间插入一个数字,而无需移动十亿个空间。更好的是Btree允许二进制搜索。每次比较都会消除总数的一半。哈希算法可以让您像棋盘一样填充数据结构,但对于稀疏数据却不太好。最好的选择是拥有一个100个整数的解决方案数组,并跟踪解决方案数组中的最小数字,以便在原始数组中遇到较大数字时可以替换它。您必须查看原始数组中的每个元素,前提是该元素未排序开始。


0

您可以O(n)及时完成。只需遍历列表并跟踪您在任何给定时间点看到的100个最大数字和该组中的最小值。当您发现一个新的数字大于最小的十个数字时,请替换它并更新新的最小值(100)(每次执行此操作可能要花费100的恒定时间来确定,但这不会影响整体分析)。


1
这种方法几乎与该问题的最高和第二高答案完全相同。
2013年

0

管理一个单独的列表是一项额外的工作,每次找到另一个替代列表时,您都必须在整个列表中移动内容。只需对它进行qsort并进入前100名。


-1 quicksort是O(n log n),这正是OP所做的并要求改进。您无需管理单独的列表,只需管理100个号码。您的建议还具有更改原始列表或复制它的不受欢迎的副作用。那就是4GiB左右的内存了。

0
  1. 使用第n个元素获得第100个元素O(n)
  2. 第二次但仅迭代一次,并输出大于此特定元素的每个元素。

请注意特别是。第二步可能很容易并行计算!当您需要一百万个最大元素时,它也将非常有效。


0

这是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;
                        }
                    }
                }
            }
        }
    }
}

0

我做了我自己的代码,不确定它在看什么“采访者”

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]);

            }

        }

0

可能的改进。

如果文件包含10亿个数字,那么读取它可能会长...

为了改善这项工作,您可以:

  • 将文件拆分为n个部分,创建n个线程,使n个线程在文件部分中分别查找最大的100个数字(使用优先级队列),最后获得所有线程输出的最大100个数字。
  • 使用集群来完成此类任务,并提供类似hadoop的解决方案。在这里,您可以进一步分割文件,并更快地输出10亿个(或10 ^ 12)数字文件。

0

首先获取1000个元素,并将其添加到最大堆中。现在取出最大的前100个元素并将其存储在某个位置。现在从文件中选择下一个900个元素,并将它们与最后100个最高元素一起添加到堆中。

继续重复此过程,从堆中拾取100个元素,然后从文件中添加900个元素。

最终选择100个元素将使我们从十亿个数字中最多选择100个元素。


-1

问题:找到n个项目的m个最大元素,其中n >>> m

每个人都应该知道的最简单的解决方案是简单地对冒泡排序算法进行m次传递。

然后打印出数组的最后n个元素。

这不需要外部数据结构,并且使用了众所周知的算法。

运行时间估计为O(m * n)。到目前为止,最好的答案是O(n log(m)),因此对于小m而言,该解决方案的成本并不昂贵。

我并不是说这无法改善,但这是迄今为止最简单的解决方案。


1
没有外部数据结构?十亿个数字数组如何排序?这种大小的阵列在填充时间和存储空间上都是巨大的开销。如果所有“大”数字都位于数组的错误末端怎么办?您将需要大约1000亿个掉期,以将它们“泡沫化”到位-另一个巨大的开销...最后,M N = 1000亿vs M Log2(N)= 66.4亿,这几乎是两个数量级的差异。也许重新考虑一下这一点。在保持最大数量的数据结构的同时进行一次扫描将显着提高执行此方法的效率。
NealB
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.