从无限列表中获得100个最高数字


53

我的一位朋友被问到这个面试问题-

“从一些无限的数字列表中不断涌入数字,您需要维护其中的数据结构以在任何给定的时间点返回前100个最高数字。假定所有数字都是整数。”

这很简单,您需要按降序排列已排序的列表,并跟踪该列表中的最低编号。如果获得的新号码大于该最低号码,则必须删除该最低号码,然后根据需要在排序列表中插入新号码。

然后问题扩大了-

“您可以确保插入顺序为O(1)吗?可以吗?”

据我所知,即使您添加一个新数字到列表中并使用任何排序算法对其进行再次排序,对于quicksort(我认为),最好也应该是O(logn)。所以我的朋友告诉那是不可能的。但是他没有被说服,他要求维护任何其他数据结构而不是列表。

我想到了平衡二叉树,但即使在那儿,您也不会得到顺序为1的插入。因此,我现在也遇到了同样的问题。想知道是否有任何这样的数据结构可以针对上述问题以1的顺序进行插入,或者根本不可能。


19
也许这只是我误解了这个问题,但是为什么您需要保留一个排序列表?为什么不只是跟踪最低的数字,如果遇到一个比该数字高的数字,请删除最低的数字并放入新数字,而无需对列表进行排序。那会给你O(1)。
EdoDodo 2011年

36
@EdoDodo-在执行该操作之后,您如何知道新的最低数字是多少?
Damien_The_Unbeliever

19
对列表[O(100 * log(100))= O(1)]进行排序,或通过线性搜索最小值[O(100)= O(1)]以获取新的最低数字。您的列表大小是固定的,因此所有这些操作也是固定的时间。
2011年

6
您不必对整个列表进行排序。您不在乎最高或第二高的数字是多少。您只需要知道最低的是什么。因此,在插入新数字后,您只需遍历100个数字,然后看哪个现在最低。那是固定时间。
汤姆·齐奇

27
当问题的大小可以无限扩大时,操作的渐近顺序才有意义从您的问题中并不清楚哪个数量在无限增长。听起来您在问问题的渐近阶是什么,问题的大小以100为界;这甚至不是一个明智的问题。必须无限发展。如果问题是“您是否可以在O(1)时间内保持前n个而不是前100个?” 那么这个问题是明智的。
埃里克·利珀特

Answers:


35

假设k是您想知道的最高数字(在您的示例中为100)。然后,您可以添加一个新号码,O(k)其中也为O(1)。因为O(k*g) = O(g) if k is not zero and constant


6
O(50)是O(n),而不是O(1)。插入的长度N为O的列表(1)的时间是指时间不依赖于N的值这意味着,如果100成为10000,50绝不能成为5000

18
@hamstergene-但是在这个问题上,N排序列表的大小还是到目前为止已处理的项目数?如果您处理10000个项目,并在列表中保留前100个项目,或者处理1000000000个项目,并在排序后的列表中保留前100个项目,则该列表中的插入成本保持不变。
Damien_The_Unbeliever

6
@hamstergene:在这种情况下,您的基础知识是错误的。在您的Wikipedia链接中有一个属性(“乘以常数”)O(k*g) = O(g) if k not zero and constant。=> O(50*1) = O(1)
duedl0r 2011年

9
我认为Duedl0r是正确的。让我们减少问题,说您只需要最小值和最大值。这个O(n)是因为最小值和最大值是2吗?(n = 2)。问题2是问题定义的一部分。是一个常数,所以它在O(k * something)中等于O(something)
xanatos 2011年

9
@hamstergene:您在说什么功能?价值100似乎是相当恒定的,以我..
duedl0r

19

保持列表未排序。确定是否要插入一个新数字将花费更长的时间,但是插入将为O(1)。


7
我认为,如果没有别的选择,这将为您带来明智的奖励。* 8')
Mark Booth,

4
@Emilio,您在技术上是正确的-当然,这是最好的正确方法……
Gareth

1
但是,您也可以保留100个数字中的最小值,然后再决定是否需要在O(1)中插入。然后,仅当您插入数字时,才需要搜索新的最低数字。但这比决定是否插入要稀有,这对每个新数字都如此。
Andrei Vajna II


9

传递100个数字后,您将为下一个数字产生的最高费用是检查该数字是否在最高100个数字中的费用(我们将其标签为CheckTime)加上将其输入该组并弹出该费用的费用。最低的一个(让我们称之为EnterTime),它是恒定时间(至少对于有界数而言),或者O(1)

Worst = CheckTime + EnterTime

接下来,如果数字分布是随机的,则平均成本会减少,您拥有的数字越多。例如,您必须将第101个数字输入最大集的机会是100/101,第1000个数字的机会是1/10,第n个数字的机会是100 / n。因此,我们的平均成本公式为:

Average = CheckTime + EnterTime / n

因此,当n接近无穷大时,只有CheckTime很重要:

Average = CheckTime

如果数字是绑定的,则CheckTime为常数,因此为 O(1)时间。

如果数字没有限制,则检查时间将随着更多的数字而增加。从理论上讲,这是因为如果最大集中的最小数足够大,则检查时间将更长,因为您将不得不考虑更多的位。这使得它看起来将比恒定时间略高。但是,您也可能会争辩说,随着n接近无穷大,下一个数在最高集合中的机会接近零,因此您需要考虑更多位的机会也接近0,这将是O(1)的一个论点。时间。

我不是很肯定,但是我的直觉说现在是O(log(log(log(n))))时间。这是因为最低位数增加的可能性是对数的,每次检查需要考虑的位数也是对数的。我对其他人对此感兴趣,因为我不确定...


除了列表是任意的以外,如果列表不断增加怎么办?
dan_waterworth 2011年

@dan_waterworth:如果无限列表是任意的,并且碰巧不断增加(其几率将为1 /∞!),则将适合CheckTime + EnterTime每个数字的最坏情况。这才有意义,如果数字是无限的,因此CheckTimeEnterTime由于数字大小的增加将增加双方至少对数。
Briguy37,2011年

1
这些数字不是随机的,而是任意的。谈论赔率没有任何意义。
dan_waterworth 2011年

@dan_waterworth:您已经说了两次,因为数字是任意的。你从哪里得到的?另外,我相信您仍然可以将统计信息应用于从随机情况开始的任意数字,并在您进一步了解仲裁器后提高其准确性。例如,如果您是仲裁人,那么选择不断增长的数字似乎比说我是
仲裁人更有可能

7

如果您知道Binary Heap Trees,那么这很容易。二进制堆支持以平均恒定时间O(1)插入。并让您轻松访问前x个元素。


为什么要存储不需要的元素?(值太低)似乎更像自定义算法。不是说值不高于最低值时您不能“不添加”这些值。
Steven Jeuris 2011年

我不知道,我的直觉告诉我,(某种味道的)堆可以很好地实现这一目标。但这并不意味着他必须保留所有要素。我没有研究它,但它“感觉不错”(TM)。
钻机

3
可以修改堆以丢弃低于第m级的任何对象(对于二进制堆,k = 100,由于节点数= 2 ^ m-1,因此m为7)。这会减慢它的速度,但仍会以固定时间摊销。
Plutor 2011年

3
如果您使用二进制min-heap(因为top是最小值,您一直在检查),并且找到了一个新的数字> min,那么必须先删除top元素,然后才能插入新的。删除顶部(最小)元素将为O(logN),因为您必须遍历树的每个级别一次。因此,从技术上讲,插入的平均数为O(1),因为实际上每次发现数字> min时,插入数仍为O(logN)。
Scott Whitlock,

1
@Plutor,您假设有些保证二进制堆不会给您。将其可视化为二叉树,可能是左分支中的每个元素小于右分支中的任何元素的情况,但是您假设最小的元素最接近根。
彼得·泰勒

6

如果问访员确实是要问“我们可以确保每个入局号码都在恒定时间内得到处理”的问题,那么正如许多人已经指出的那样(例如,参见@ duedl0r的回答),您朋友的解决方案已经是O(1),并且即使他使用了未排序的列表,使用了冒泡排序或其他方法,也是如此。在这种情况下,这个问题没有多大意义,除非这是一个棘手的问题,或者您记错了。

我认为面试官的问题是有意义的,他不是在问如何使某物成为O(1),这显然已经很明显了。

因为质疑算法的复杂性仅在输入的大小无限增加时才有意义,并且此处唯一可以增加的输入是100(列表大小)。我假设真正的问题是“我们可以确保我们获得每个数字的前N个支出O(1)时间(不是您朋友的解决方案中的O(N))吗?”

首先想到的是计数排序,这将以使用O(m)空间的代价为Top-N问题购买每个数字O(1)时间的复杂性,其中m是传入数字范围的长度。是的,这是可能的。


4

使用通过Fibonacci堆实现的最小优先级队列,该队列具有固定的插入时间:

1. Insert first 100 elements into PQ
2. loop forever
       n = getNextNumber();
       if n > PQ.findMin() then
           PQ.deleteMin()
           PQ.insert(n)

4
“操作O(log n)按摊销时间删除和删除最小工作量”,因此这仍将导致要存储的项目数量在O(log k)哪里k
史蒂文·杰里斯

1
这与Emilio的答案没有什么不同,Emilio的答案被称为“智能警报奖”,因为delete minO(log n)中运行(根据Wikipedia)。
妮可(Nicole)

@Renesis Emilio的答案将是O(k)以找到最小值,我的答案是O(log k)
Gabe Moothart 2011年

1
@Gabe公平,我只是原则上的意思。换句话说,如果您不将100设为常数,那么这个答案也不是时间。
妮可,

@Renesis我已经从答案中删除了(不正确的)语句。
·穆阿特

2

任务显然是找到在所需数字列表的长度N中为O(1)的算法。因此,不管您需要前100个数字还是10000个数字,插入时间都应为O(1)。

这里的诀窍是,尽管列表插入提到了O(1)要求,但问题没有说出整数空间中搜索时间的顺序,但是事实证明可以将其设为O(1)也一样 然后的解决方案如下:

  1. 安排一个哈希表,其中键的数字为数字,值的链接列表指针为一对。每对指针是链接列表序列的开始和结束。通常这只是一个元素,然后是下一个。链表中的每个元素都位于编号第二高的元素旁边。因此,链表包含所需编号的排序顺序。保留最低编号的记录。

  2. 从随机流中获取一个新的数字x。

  3. 是否高于最后记录的最低数字?是=>步骤4,否=>步骤2

  4. 用刚刚取得的数字点击哈希表。有条目吗?是=>步骤5。否=>取一个新的数字x-1并重复此步骤(这是一个简单的向下线性搜索,请耐心等待,这可以改进,我将解释如何做)

  5. 使用刚刚从哈希表中获得的list元素,在链接列表中的该元素之后插入新数字(并更新哈希)

  6. 取记录的最低数字l(并将其从哈希/列表中删除)。

  7. 用刚刚取得的数字点击哈希表。有条目吗?是=>步骤8。否=>取一个新的数字l + 1并重复此步骤(这是一个简单的向上线性搜索)

  8. 命中数为正时,该数字将成为新的最低数字。前往步骤2

为了允许重复的值,散列实际上需要维护链表中重复项的开始和结束。因此,在给定键处添加或删除元素会增加或减小指向的范围。

这里的插入是O(1)。我猜提到的搜索是O(数字之间的平均差)。平均差随数字空间的大小而增加,但随数字列表的所需长度而减小。

因此,如果数字空间很大(例如,对于4字节的int类型,0到2 ^ 32-1)并且N = 100,则线性搜索策略非常差。要解决此性能问题,您可以保留并行的哈希表集,在哈希表中将数字四舍五入到更高的幅度(例如1s,10s,100s,1000s)以创建合适的密钥。这样,您可以提高和降低齿轮速度,以更快地执行所需的搜索。我认为性能然后变为O(log numberrange),它是恒定的,即也为O(1)。

为了使这一点更清楚,假设您手头有197。您用“ 190”打了一个10s哈希表,将其四舍五入到最接近的十。有什么事吗 不。因此,您在10秒钟内下降,直到您说出120。然后您可以从1s哈希表中的129开始,然后尝试128、127,直到您击中某些东西。现在,您已经在链接列表中的何处插入了数字197。在将其插入时,还必须使用197条目更新1s哈希表,将数字190更新为10s哈希表,将100替换为100,以此类推。大多数步骤您曾经要做的是数字范围的10倍的对数。

我可能错了一些细节,但是由于这是程序员的交流,而上下文是访谈,所以我希望以上内容可以为这种情况提供一个令人信服的答案。

编辑我在这里添加了一些额外的详细信息来解释并行哈希表方案,以及它如何表示可以用O(1)搜索代替我提到的不良线性搜索。我也意识到当然不需要搜索下一个最低编号,因为您可以通过查找具有最低编号的哈希表并前进到下一个元素来直接进入该编号。


1
搜索必须是插入功能的一部分-它们不是独立的功能。由于搜索为O(n),因此插入函数也为O(n)。
柯克·布罗德赫斯特

否。使用我描述的策略,其中使用更多哈希表来更快地遍历数字空间,它是O(1)。请再次阅读我的答案。
本尼迪克特

1
@Benedict,您的回答很明确地说,它在步骤4和7中具有线性搜索。线性搜索不是O(1)。
彼得·泰勒

是的,确实如此,但我稍后再讨论。您介意请阅读其余内容吗?如有必要,我将编辑答案以使其更加清晰。
本尼迪克特

@Benedict您是正确的-不包括搜索,您的答案是O(1)。不幸的是,如果没有搜索,该解决方案将无法工作。
柯克·布罗德赫斯特

1

我们是否可以假定数字是固定数据类型,例如Integer?如果是这样,则保持每个相加数字相加。这是一个O(1)操作。

  1. 声明一个数组,其中包含尽可能多的元素:
  2. 读取流式传输的每个数字。
  3. 提示数字。如果该数字已经达到100倍,请忽略它,因为您将永远不需要它。这样可以防止溢出无数次地计数。
  4. 从步骤2开始重复。

VB.Net代码:

Const Capacity As Integer = 100

Dim Tally(Integer.MaxValue) As Integer ' Assume all elements = 0
Do
    Value = ReadValue()
    If Tally(Value) < Capacity Then Tally(Value) += 1
Loop

返回列表时,您可以花多长时间。只需从列表末尾开始,然后创建一个记录的最高100个值的新列表。这是一个O(n)操作,但这并不重要。

Dim List(Capacity) As Integer
Dim ListCount As Integer = 0
Dim Value As Integer = Tally.Length - 1
Dim ValueCount As Integer = 0
Do Until ListCount = List.Length OrElse Value < 0
    If Tally(Value) > ValueCount Then
        List(ListCount) = Value
        ValueCount += 1
        ListCount += 1
    Else
        Value -= 1
        ValueCount = 0
    End If
Loop
Return List

编辑:实际上,它是否是固定数据类型并不重要。鉴于对内存(或硬盘)的消耗没有强加的限制,您可以使它适用于任何范围的正整数。


1

一百个数字很容易存储在大小为100的数组中。考虑到手头的任务,任何树,列表或集合都是过大的。

如果输入的数字大于数组中的最小数字(=最后一个数字),请遍历所有条目。一旦找到第一个小于新数字的数字(可以使用奇特搜索进行搜索),遍历数组的其余部分,将每个条目“向下”推一。

由于您从一开始就对列表进行排序,因此根本不需要运行任何排序算法。这是O(1)。


0

您可以使用Binary Max-Heap。您必须跟踪指向最小节点的指针(可能是未知/空)。

首先将前100个数字插入到堆中。最大值将在顶部。完成此操作后,您将始终在其中保留100个号码。

然后,当您获得一个新号码时:

if(minimumNode == null)
{
    minimumNode = findMinimumNode();
}
if(newNumber > minimumNode.Value)
{
    heap.Remove(minimumNode);
    minimumNode = null;
    heap.Insert(newNumber);
}

不幸的findMinimumNode是O(n),您确实要为每个插入花费一次费用(但不会在插入:时发生)。平均而言,删除最小节点并插入新节点为O(1),因为它们倾向于堆的底部。

相反,使用二进制最小堆,最小值在顶部,这对于查找要比较的最小值非常有用,但是当您必须用一个大于最小值的新数字替换最小值时,该值就很糟糕。那是因为您必须删除最小节点(始终为O(logN)),然后插入新节点(平均为O(1))。因此,您仍然拥有比Max-Heap更好的O(logN),但没有O(1)。

当然,如果N为常数,则您始终具有O(1)。:)

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.