容易的面试问题变得更加困难:给定数字1..100,在恰好缺少k的情况下,找到缺失的数字


1146

前一段时间我有一次有趣的面试经历。这个问题开始很容易:

Q1:我们有包含数字的袋子123,..., 100。每个数字仅出现一次,因此有100个数字。现在,从袋子中随机抽取一个号码。查找丢失的号码。

我当然已经听过这个面试问题,所以我很快就回答了以下问题:

A1:好吧,数字的总和1 + 2 + 3 + … + N(N+1)(N/2)(请参阅Wikipedia:算术级数的总和)。因为N = 100,总和是5050

因此,如果袋子中所有数字都存在,则总和为5050。由于缺少一个数字,所以总和小于这个数字,而差就是那个数字。因此,我们可以找到O(N)时间和O(1)空间上缺少的数字。

在这一点上,我认为我做得不错,但是突然之间,这个问题突然发生了变化:

Q2:是的,但是现在如果缺少两个数字,您将如何处理?

我之前从未见过/听过/考虑过这种变化,所以我感到惊慌,无法回答这个问题。面试官坚持要知道我的思维过程,所以我提到也许我们可以通过与预期产品进行比较来获得更多信息,或者也许在从第一遍收集到一些信息之后再进行第二遍,等等,但是我真的只是在拍摄在黑暗中,而不是真正找到解决方案的清晰途径。

面试官的确通过说第二个方程式确实是解决问题的一种方式来鼓励我。在这一点上,我有点不高兴(因为事先不知道答案),并询问这是否是一种通用的(读作:“有用的”)编程技术,还是仅仅是一个技巧/陷阱。

面试官的回答让我感到惊讶:您可以推广该技术以找到3个缺失的数字。实际上,您可以对其进行概括以找到k个缺失数字。

Qk:如果袋子中恰好缺少k个数字,您将如何有效地找到它?

这是几个月前,但我仍然不知道这种技术是什么。显然存在一个Ω(N)时间下限,因为我们必须至少扫描一次所有数字,但是访调员坚持认为求解技术的TIMESPACE复杂度(减去O(N)时间输入扫描)以k而非N定义。

所以这里的问题很简单:

  • 您将如何解决Q2
  • 您将如何解决Q3
  • 您将如何解决Qk

澄清说明

  • 通常,从1 .. N开始N个数字,而不仅仅是1..100。
  • 我不是在寻找明显的基于集合的解决方案,例如,使用位集,通过指定位的值编码每个数字的存在/不存在,因此O(N)在其他空间中使用位。我们无法承受与N成正比的任何额外空间。
  • 我也不在寻找明显的排序优先方法。这种方法和基于集合的方法在采访中值得一提(它们易于实现,并且取决于N,可能非常实用)。我正在寻找“圣杯”解决方案(可能实现或可能不实际,但仍具有所需的渐近特性)。

因此,当然,您必须再次扫描中的输入O(N),但是您只能捕获少量信息(用k而不是N定义),然后必须以某种方式找到k个缺失的数字。


7
@polygenelubricants谢谢您的澄清。“我正在寻找一种使用O(N)时间和O(K)空间的算法,其中K是不存在的数字的计数”从一开始就很清楚;-)
DaveO。2010年

7
您应该在Q1的陈述中指出您不能按顺序访问数字。这对于您来说似乎很明显,但是我从未听说过这个问题,“ bag”一词(也意味着“ multiset”)有点令人困惑。
杰里米

7
请阅读作为提供答案这里是可笑如下:stackoverflow.com/questions/4406110/...

18
求和的解决方案需要log(N)空间,除非您认为无界整数的空间要求为O(1)。但是,如果允许无限制的整数,那么只有一个整数就可以拥有所需的空间。
Udo Klein

3
顺便说一句,Q1的一个很好的替代解决方案可以是计算XOR1到的所有数字n,然后将结果与给定数组中的所有数字进行异或。最后,您缺少电话号码。在此解决方案中,您不需要像总结中那样关心溢出。
sbeliakov 2015年

Answers:


590

这是Dimitris Andreou的 链接的摘要。

记住第i次幂的总和,其中i = 1,2,..,k。这减少了求解方程组的问题

a 1 + a 2 + ... + a k = b 1

a 1 2 + a 2 2 + ... + a k 2 = b 2

...

a 1 k + a 2 k + ... + a k k = b k

使用牛顿的身份,知道b 可以计算

c 1 = a 1 + a 2 + ... a k

c 2 = a 1 a 2 + a 1 a 3 + ... + a k-1 a k

...

c k = a 1 a 2 ... a k

如果展开多项式(xa 1)...(xa k),则系数将恰好为c 1,...,c k-参见Viète公式。由于每个多项式因子都是唯一的(多项式的环是一个欧几里德域),因此这意味着a i是唯一确定的,直到置换。

这样就证明了记忆力足以恢复数字。对于常数k,这是一个好方法。

然而,当k变化时,直接计算c 1,...,c k的方法是非常昂贵的,因为例如,c k是所有缺失数n 1 /(nk)1的乘积。为了克服这个问题,请在Z q字段中执行计算,其中q是质数,使得n <= q <2n-根据Bertrand的假设存在。由于公式仍然成立,并且多项式的因式分解仍然是唯一的,因此无需更改证明。您还需要一种用于对有限域进行因式分解的算法,例如BerlekampCantor-Zassenhaus提出的算法

常数k的高级伪代码:

  • 计算给定数字的第i次幂
  • 减去可得到未知数的第i次幂。求和b i
  • 使用牛顿的恒等式从b i计算系数;称他们为c i。基本上,c 1 = b 1;c 2=(c 1 b 1 -b 2)/ 2;有关详细公式,请参阅Wikipedia
  • 分解多项式x k -c 1 x k-1 + ... + c k
  • 多项式的根是所需的数字a 1,...,a k

对于变化的k,使用例如Miller-Rabin求素数n <= q <2n,并执行所有以q为模的数字简化的步骤。

编辑:此答案的先前版本指出,代替Z q,其中q是质数,可以使用特征2(q = 2 ^(log n))的有限域。事实并非如此,因为牛顿公式需要除以不超过k的数字。


6
您不必使用素数字段,也可以使用q = 2^(log n)。(您是如何制作上标和下标的?!)
Heinrich Apfelmus,2010年

49
+1这真的非常聪明。同时,这是否值得付出努力还是一个人为的问题的解决方案(的一部分)是否可以以其他方式重用是一个问题。即使这是一个现实世界的问题,在许多平台上O(N^2),即使是相当高的解决方案,最简单的解决方案也可能会胜过这种美感N。让我想到了这一点:tinyurl.com/c8fwgw尽管如此,很棒的工作!我本来就不会耐心地进行所有的数学学习:)
back2dos 2010年

167
我认为这是一个很好的答案。我认为这也说明了采访问题将遗漏的数字扩大到一个之外是多么糟糕。即使是第一个,也有点奇怪,但是它很常见,它基本上表明“您做了一些面试准备”。但是,期望CS专业人士知道超过k = 1(尤其是在面试中“当场”)是有点愚蠢的。
corsiKa 2011年

5
这实际上是对输入进行Reed Solomon编码。
David Ehrmann 2014年

78
我敢打赌,在中输入所有数字,hash set然后1...N使用查找来确定是否丢失数字,对套件进行遍历,这将是最通用,k变体平均速度最快,可调试性最强且易于理解的解决方案。当然,数学方法令人印象深刻,但在此过程中,您需要成为工程师而不是数学家。特别是在涉及业务时。
v.oddou 2014年

243

您可以通过阅读Muthukrishnan-数据流算法:难题1:找到缺失的数字的几页找到它。它精确地显示了您要寻找的概括。可能这是您的面试官阅读的内容以及他提出这些问题的原因。

现在,如果只有人们开始删除Muthukrishnan的待遇所包含或取代的答案,并使此文本更易于查找。:)


另请参见sdcvvc的直接相关答案,该答案还包括伪代码(欢呼!无需阅读那些棘手的数学公式:))(谢谢,干得好!)。


哦,这很有趣。我必须承认我对数学有些困惑,但是我还是在偷看它。可能会保留它以供以后查看。:)和+1可以使此链接更容易找到。;-)
克里斯

2
Google图书链接对我不起作用。这里是一个更好的版本 [PostScript文件]。
海因里希·阿普菲尔姆斯

9
哇。我没想到这会受到谴责!上次我发布解决方案的参考文献(在这种情况下是Knuth的解决方案),而不是自己尝试解决,但实际上
却被否决了

@Apfelmus,请注意,这是草稿。(我当然不怪你,在找本书之前,我将草案的真实情况弄混了将近一年)。顺便说一句,如果链接不起作用,则可以转到books.google.com并搜索“ Muthukrishnan数据流算法”(不带引号),这是第一个弹出的窗口。
Dimitris Andreou 2010年

2
请阅读作为提供答案这里是可笑如下:stackoverflow.com/questions/4406110/...

174

我们可以通过将数字本身和平方和相加来求解Q2

然后我们可以将问题减少到

k1 + k2 = x
k1^2 + k2^2 = y

哪里xy是资金多远低于预期值。

替换给我们:

(x-k2)^2 + k2^2 = y

然后我们可以解决以确定丢失的数字。


7
+1; 我已经尝试过在Maple中选择数字的公式,并且可以正常工作。但是,我仍然无法说服自己为什么要这样做。
polygenelubricants

4
@polygenelubricants:如果你想证明的正确性,你首先表明它总是提供一个正确的解决方案(即,它总是产生一对数字,其从该组删除它们时,会导致该组的其余部分有观察到的总和和平方和)。从那里开始,证明唯一性就像显示它只产生一对这样的数字一样简单。
Anon。

5
方程的性质意味着您将从该方程中得到k2的两个值。但是,从用于生成k1的第一个方程式中,您可以看到k2的这两个值将意味着k1是另一个值,因此您有两个解法,它们的编号相反,而数字相同。如果您任意声明k1> k2,那么您对二次方程式只有一个解,因此也就只有一个解。显然,根据问题的性质,答案始终存在,因此始终有效。
克里斯,2010年

3
对于给定的和k1 + k2,有许多对。我们可以将这些对写为K1 = a + b和K2 = ab,其中a =(K1 + k2 / 2)。对于给定的和,a是唯一的。平方和(a + b)** 2 +(ab)** 2 = 2 *(a 2 + b 2)。对于给定的和K1 + K2,a 2项是固定的,并且由于b 2项,我们看到平方和将是唯一的。因此,值x和y对于一对整数是唯一的。
phkahler 2010年

8
这太棒了。@ user3281743这是一个示例。令缺失的数字(k1和k2)为4和6。Sum(1-> 10)= 55且Sum(1 ^ 2-> 10 ^ 2)=385。现在让x = 55-(Sum(所有剩余数字))和y = 385-(总和(所有余数的平方)),因此x = 10且y =52。按所示替换,这样我们得到:(10-k2)^ 2 + k2 ^ 2 = 52简化为:2k ^ 2-20k + 48 =0。求解二次方程式,您得到4和6的答案。
AlexKoren

137

正如@j_random_hacker所指出的,这与在O(n)时间和O(1)空间中查找重复项非常相似那里对我的答案进行修改也可以在这里工作。

假设“ bag”由一个基于1 A[]的size 数组表示N - k,我们可以及时求解Qk O(N)O(k)增加空间。

首先,我们A[]通过k元素扩展数组,使其现在为size N。这是O(k)额外的空间。然后,我们运行以下伪代码算法:

for i := n - k + 1 to n
    A[i] := A[1]
end for

for i := 1 to n - k
    while A[A[i]] != A[i] 
        swap(A[i], A[A[i]])
    end while
end for

for i := 1 to n
    if A[i] != i then 
        print i
    end if
end for

第一个循环将k额外的条目初始化为与数组中的第一个条目相同(这只是一个方便的值,我们知道数组中已经存在该值-在此步骤之后,初始大小数组中缺少的所有条目N-k是扩展数组中仍然缺少)。

第二个循环置换扩展数组,以便如果元素x至少存在一次,则这些条目之一将位于A[x]

请注意,尽管它具有嵌套循环,但仍会O(N)按时运行-仅在存在i诸如that这样的情况下才会发生交换A[i] != i,并且每次交换都设置至少一个诸如that这样的元素A[i] == i,而以前这是不正确的。这意味着交换的总数(以及while循环体的执行总数)最多为N-1

第三个循环将打印数组i中未被该值占用的索引i-这意味着i必须已丢失该索引。


4
我想知道为什么很少有人投票赞成这个答案,甚至没有将其标记为正确答案。这是Python中的代码。它运行时间为O(n),需要额外的空间O(k)。 pastebin.com/9jZqnTzV
wall-e

3
@caf这与设置位并计算位为0的位置非常相似。而且我认为在创建整数数组时会占用更多内存。
Fox

5
“设置位并计算位为0的位置”需要O(n)额外空间,此解决方案说明了如何使用O(k)额外空间。
caf 2013年

7
不能将流用作输入并修改输入数组(尽管我非常喜欢它,并且这个想法很有用)。
comco 2014年

3
@ v.oddou:不,很好。交换将更改A[i],这意味着下一次迭代将不会比较与前一个相同的两个值。新的A[i]将与上一个循环相同A[A[i]],但新的A[A[i]]将是新的值。试试看。
caf 2014年

128

我请一个4岁的孩子解决这个问题。他对数字进行了排序,然后进行了计数。它的空间要求为O(厨房地板),并且工作原理很简单,但是缺少许多球。


20
;)您4岁的年龄必须接近5岁或/并且是天才。我4岁的女儿甚至还算不上4岁。公平的说,她刚刚勉强整合了“ 4”的存在。否则直到现在她总是会跳过它。“ 1,2,3,5,6,7”是她通常的计数顺序。我要求她将铅笔加在一起,她将通过从头开始重新编号来管理1 + 2 = 3。我真的很担心...:'(meh ..
v.oddou

简单而有效的方法。
PabTorre 2015年

6
O(厨房地板)哈哈-但是那不是O(n ^ 2)吗?

13
O(m²)我猜:)
维克多·梅尔格伦(Miklgren)

1
@phuclv:答案表明“此空间要求为O(厨房地板)”。但是无论如何,在这种情况下,可以O(n)时间内完成排序---请参阅此讨论
Anthony Labarre '19

36

不知道这是否是最有效的解决方案,但是我会遍历所有条目,并使用一个位集记住设置了哪些数字,然后测试0位。

我喜欢简单的解决方案-甚至我相信,这可能比计算总和或平方和等速度要快。


11
我确实提出了这个明显的答案,但这不是面试官想要的。我在问题中明确表示这不是我要找的答案。另一个明显的答案:首先排序。无论是O(N)计数排序也O(N log N)比较排序就是我要找的,虽然都是很简单的解决方案。
polygenelubricants

@polygenelubricants:我找不到您在问题中所说的话。如果您认为该位集是结果,则没有第二遍。复杂度为(如果如访问员所说,如果我们认为N是常数,则复杂度是“用k定义,而不是N”)O(1),并且如果您需要构造更“干净”的结果,则获得O(k),这是您可以获得的最好结果,因为您始终需要O(k)才能产生干净的结果。
克里斯·勒彻

“请注意,我不是在寻找明显的基于集合的解决方案(使用位设置例如,”从原来的问题倒数第二段。
hrnt

9
@hmt:是的,问题是在几分钟前编辑的。我只是给出答案,这是我希望从受访者那里得到的……人为地构建次优解决方案(无论您做什么,您都无法击败O(n)+ O(k)时间)对我来说没有任何意义-除非您负担不起O(n)的额外空间,但问题尚不明确。
克里斯·勒彻

3
我再次编辑了问题以进一步阐明。我非常感谢您的反馈/答复。
polygenelubricants

33

我还没有检查数学,但是我怀疑Σ(n^2)在与计算相同的过程中进行计算Σ(n)将提供足够的信息来获得两个缺失的数字,Σ(n^3)如果有三个则同样执行,依此类推。


15

基于数字和的解决方案存在的问题是它们没有考虑存储和处理具有大指数的数字的成本...在实践中,因为它适用于非常大的n,所以将使用大数字库。我们可以分析这些算法的空间利用率。

我们可以分析sdcvvc和Dimitris Andreou算法的时间和空间复杂性。

存储:

l_j = ceil (log_2 (sum_{i=1}^n i^j))
l_j > log_2 n^j  (assuming n >= 0, k >= 0)
l_j > j log_2 n \in \Omega(j log n)

l_j < log_2 ((sum_{i=1}^n i)^j) + 1
l_j < j log_2 (n) + j log_2 (n + 1) - j log_2 (2) + 1
l_j < j log_2 n + j + c \in O(j log n)`

所以 l_j \in \Theta(j log n)

已使用的总存储空间: \sum_{j=1}^k l_j \in \Theta(k^2 log n)

使用的空间:假设计算a^j需要ceil(log_2 j)时间,总时间为:

t = k ceil(\sum_i=1^n log_2 (i)) = k ceil(log_2 (\prod_i=1^n (i)))
t > k log_2 (n^n + O(n^(n-1)))
t > k log_2 (n^n) = kn log_2 (n)  \in \Omega(kn log n)
t < k log_2 (\prod_i=1^n i^i) + 1
t < kn log_2 (n) + 1 \in O(kn log n)

总使用时间: \Theta(kn log n)

如果这个时间和空间令人满意,则可以使用简单的递归算法。令b!i为袋中的第i个条目,n为移除前的数字,k为移除数。用Haskell语法...

let
  -- O(1)
  isInRange low high v = (v >= low) && (v <= high)
  -- O(n - k)
  countInRange low high = sum $ map (fromEnum . isInRange low high . (!)b) [1..(n-k)]
  findMissing l low high krange
    -- O(1) if there is nothing to find.
    | krange=0 = l
    -- O(1) if there is only one possibility.
    | low=high = low:l
    -- Otherwise total of O(knlog(n)) time
    | otherwise =
       let
         mid = (low + high) `div` 2
         klow = countInRange low mid
         khigh = krange - klow
       in
         findMissing (findMissing low mid klow) (mid + 1) high khigh
in
  findMising 1 (n - k) k

使用的存储空间:O(k)用于列表,O(log(n))用于堆栈:O(k + log(n)) 此算法更直观,具有相同的时间复杂度并且使用较少的空间。


1
+1,看起来不错,但是您在代码段1中从第4行转到第5行让我迷失了-您能进一步解释吗?谢谢!
j_random_hacker 2010年

isInRangeO(log n),而不是O(1):它比较范围1..n中的数字,因此必须比较O(log n)位。我不知道此错误在多大程度上影响了其余的分析。
jcsahnwaldt说GoFundMonica

14

等一下。如问题所述,袋子里有100个数字。无论k有多大,都可以在固定时间内解决该问题,因为您可以使用一个集合并最多在一个循环中进行100-k次迭代从集合中删除数字。100是常数。剩余数字集就是您的答案。

如果将解决方案推广到从1到N的数字,则除N不是常数外,什么都没有改变,所以我们处于O(N-k)= O(N)时间。例如,如果使用位集,则在O(N)时间中将位设置为1,迭代数字,然后将位设置为0(O(Nk)= O(N)),然后有答案。

在我看来,面试官在问您如何在O(k)时间而不是O(N)时间中打印最终集的内容。显然,在设置了某个位后,您必须遍历所有N个位以确定是否应打印该数字。但是,如果您更改实现集合的方式,则可以在k次迭代中打印出数字。这是通过将数字放入要存储在哈希集和双链表中的对象中来完成的。从哈希集中删除对象时,也将从列表中删除它。答案将留在长度为k的列表中。


9
这个答案太简单了,我们都知道简单的答案是行不通的!;)严重地,原始问题可能应该强调O(k)空间要求。
DK。

问题不是那么简单,而是您必须为映射使用O(n)额外的内存。这个问题我的胸围在固定时间和固定的内存解决
魔RISIN

3
我敢打赌,您可以证明最小的解决方案至少是O(N)。因为数量较少,这意味着您甚至没有看过某些数字,并且由于未指定顺序,因此必须查看所有数字。
v.oddou

如果我们将输入视为流,并且n太大而无法保留在内存中,则O(k)内存要求是有意义的。但是,我们仍然可以使用散列:只制作k ^ 2个存储桶,并对每个存储桶使用简单的sum算法。只有k ^ 2的内存,可以使用更多的存储桶来获得高成功率。
Thomas Ahle

8

要解决2(和3)个缺失数字的问题,您可以修改quickselectO(n)如果就地进行分区,则平均可以运行并使用常量内存。

  1. 相对于随机枢轴p将集合划分为分区l,该分区包含小于枢轴的数字,和r包含大于枢轴的数字。

  2. 通过将枢轴值与每个分区(p - 1 - count(l) = count of missing numbers in ln - count(r) - p = count of missing numbers in r)的大小进行比较,确定2个缺失数字所在的分区

  3. a)如果每个分区缺少一个数字,则使用总和差法查找每个丢失的数字。

    (1 + 2 + ... + (p-1)) - sum(l) = missing #1((p+1) + (p+2) ... + n) - sum(r) = missing #2

    b)如果一个分区同时缺少两个数字,并且该分区为空,则缺少的数字是(p-1,p-2)(p+1,p+2) 取决于哪个分区缺少数字。

    如果一个分区缺少2个数字但不为空,则递归到该分区。

只有2个缺失的数字,该算法总是丢弃至少一个分区,因此保留 O(n)了快速选择的平均时间复杂度。同样,对于3个丢失的数字,该算法每次通过还会丢弃至少一个分区(因为2个丢失的数字,最多只有1个分区将包含多个丢失的数字)。但是,我不确定添加更多丢失的数字后性能会下降多少。

这是一个使用就地分区的实现,因此此示例不满足空间要求,但确实说明了算法的步骤:

<?php

  $list = range(1,100);
  unset($list[3]);
  unset($list[31]);

  findMissing($list,1,100);

  function findMissing($list, $min, $max) {
    if(empty($list)) {
      print_r(range($min, $max));
      return;
    }

    $l = $r = [];
    $pivot = array_pop($list);

    foreach($list as $number) {
      if($number < $pivot) {
        $l[] = $number;
      }
      else {
        $r[] = $number;
      }
    }

    if(count($l) == $pivot - $min - 1) {
      // only 1 missing number use difference of sums
      print array_sum(range($min, $pivot-1)) - array_sum($l) . "\n";
    }
    else if(count($l) < $pivot - $min) {
      // more than 1 missing number, recurse
      findMissing($l, $min, $pivot-1);
    }

    if(count($r) == $max - $pivot - 1) {
      // only 1 missing number use difference of sums
      print array_sum(range($pivot + 1, $max)) - array_sum($r) . "\n";
    } else if(count($r) < $max - $pivot) {
      // mroe than 1 missing number recurse
      findMissing($r, $pivot+1, $max);
    }
  }

演示版


对集合进行分区就像使用线性空间。至少它在流媒体设置中不起作用。
Thomas Ahle

@ThomasAhle参见en.wikipedia.org/wiki/Selection_algorithm#Space_complexity。将集合分割就位仅需要O(1)额外空间-不需要线性空间。在流设置中,将有O(k)个额外空间,但是,原始问题没有提到流。
FuzzyTree 2016年

不直接,但他确实写了“您必须扫描O(N)中的输入,但是您只能捕获少量信息(以k而不是N定义)”,这通常是流的定义。除非您有一个大小为N的数组,否则实际上不可能移动所有数字进行分区。只是这个问题有很多答案,女巫似乎忽略了这个约束。
托马斯·阿勒

1
但是正如您所说,性能可能会随着添加更多数字而下降?我们还可以使用线性时间中值算法来始终获得完美的剪切,但是如果k个数很好地分布在1,...,n中,则在修剪之前,您不必将logk级别“变深”有分公司吗?
Thomas Ahle

2
最坏的运行时间确实是nlogk,因为您需要在最多logk的时间处理整个输入,然后是一个几何序列(一个序列最多以n个元素开头)。使用简单递归实现时,会记录空间要求,但是可以通过运行实际的quickselect并确保每个分区的正确长度将它们设置为O(1)。
mu

7

这是一个使用k位额外存储的解决方案,没有任何巧妙的技巧,也很简单。执行时间O(n),额外空间O(k)。只是为了证明可以解决这一问题,而无需先阅读解决方案或成为天才:

void puzzle (int* data, int n, bool* extra, int k)
{
    // data contains n distinct numbers from 1 to n + k, extra provides
    // space for k extra bits. 

    // Rearrange the array so there are (even) even numbers at the start
    // and (odd) odd numbers at the end.
    int even = 0, odd = 0;
    while (even + odd < n)
    {
        if (data [even] % 2 == 0) ++even;
        else if (data [n - 1 - odd] % 2 == 1) ++odd;
        else { int tmp = data [even]; data [even] = data [n - 1 - odd]; 
               data [n - 1 - odd] = tmp; ++even; ++odd; }
    }

    // Erase the lowest bits of all numbers and set the extra bits to 0.
    for (int i = even; i < n; ++i) data [i] -= 1;
    for (int i = 0; i < k; ++i) extra [i] = false;

    // Set a bit for every number that is present
    for (int i = 0; i < n; ++i)
    {
        int tmp = data [i];
        tmp -= (tmp % 2);
        if (i >= even) ++tmp;
        if (tmp <= n) data [tmp - 1] += 1; else extra [tmp - n - 1] = true;
    }

    // Print out the missing ones
    for (int i = 1; i <= n; ++i)
        if (data [i - 1] % 2 == 0) printf ("Number %d is missing\n", i);
    for (int i = n + 1; i <= n + k; ++i)
        if (! extra [i - n - 1]) printf ("Number %d is missing\n", i);

    // Restore the lowest bits again.
    for (int i = 0; i < n; ++i) {
        if (i < even) { if (data [i] % 2 != 0) data [i] -= 1; }
        else { if (data [i] % 2 == 0) data [i] += 1; }
    }
}

你要(data [n - 1 - odd] % 2 == 1) ++odd;吗?
2014年

2
您能解释一下这是如何工作的吗?我不明白
Teepeemm 2014年

如果我可以将(n + k)个布尔数组用于临时存储,则解决方案将非常非常简单,但这是不允许的。因此,我重新排列了数据,将偶数放在数组的开头,而奇数放在数组的结尾。现在,这n个数字中的最低位可以用于临时存储,因为我知道有多少个偶数和奇数,并且可以重构最低位!这n个位和k个额外的位正好是我需要的(n + k)个布尔值。
gnasher729 2014年

2
如果数据太大而无法保存在内存中,并且您仅将其视为流,则此方法将无效。虽然美味可笑:)
Thomas Ahle

空间复杂度可以为O(1)。在第一遍中,您完全通过此算法处理所有<(n-k)个数字,而无需使用“额外”。在第二遍中,再次清除奇偶校验位,并将前k个位置用于索引数字(nk)..(n)。
mu

5

你能检查每个数字是否存在吗?如果是,您可以尝试以下方法:

S =袋子中所有数字的总和(S <5050)
Z =缺少数字5050-S的总和

如果丢失的号码是xy,则:

x = Z-y和
max(x)= Z-1

因此,您检查从1到的范围max(x)并找到数字


1
什么max(x)时候x是数字?
Thomas Ahle

2
他可能表示这组数字中的最大值
-JavaHopper

如果我们有两个以上的数字,则此解决方案将被
废除

4

可能此算法可以解决问题1:

  1. 预计算前100个整数的异或(val = 1 ^ 2 ^ 3 ^ 4 .... 100)
  2. 对元素保持来自输入流的异或(val1 = val1 ^ next_input)
  3. 最终答案= val ^ val1

甚至更好:

def GetValue(A)
  val=0
  for i=1 to 100
    do
      val=val^i
    done
  for value in A:
    do
      val=val^value 
    done
  return val

实际上,可以将该算法扩展为两个缺失的数字。第一步保持不变。当我们用两个缺失数字调用GetValue时,结果将是a1^a2两个缺失数字。可以说

val = a1^a2

现在要从val中筛选出a1和a2,我们可以获取val中的任何设置位。假设该ith位设置为val。这意味着a1和a2 ith在位位置具有不同的奇偶校验。现在我们对原始数组进行另一个迭代,并保留两个xor值。一个代表第i位,第2个未i位。现在,我们有两个存储桶,其保证金a1 and a2将位于不同的存储桶中。现在,重复我们在每个存储桶中找到一个缺失元素的操作。


这只能解决的问题k=1,对吧?但是我喜欢使用xor总和,这似乎更快。
托马斯·阿勒

@ThomasAhle是的。我在回答中已经指出了这一点。
bashrc

对。您是否知道对于k = 2,“二阶”异或可能是什么?类似于使用平方求和,我们可以对“异或”“平方”吗?
Thomas Ahle

1
@ThomasAhle对其进行了修改,可处理2个缺失的数字。
bashrc

这是我最喜欢的方式:)
罗伯特

3

如果您拥有两个列表的总和以及两个列表的乘积,则可以求解Q2。

(l1是原始列表,l2是修改后的列表)

d = sum(l1) - sum(l2)
m = mul(l1) / mul(l2)

我们可以对此进行优化,因为算术级数的总和是第一项和最后一项的平均值的n倍:

n = len(l1)
d = (n/2)*(n+1) - sum(l2)

现在我们知道(如果a和b是删除的数字):

a + b = d
a * b = m

因此,我们可以重新安排为:

a = s - b
b * (s - b) = m

并乘以:

-b^2 + s*b = m

并重新排列,使右侧为零:

-b^2 + s*b - m = 0

然后我们可以用二次方程式求解:

b = (-s + sqrt(s^2 - (4*-1*-m)))/-2
a = s - b

示例Python 3代码:

from functools import reduce
import operator
import math
x = list(range(1,21))
sx = (len(x)/2)*(len(x)+1)
x.remove(15)
x.remove(5)
mul = lambda l: reduce(operator.mul,l)
s = sx - sum(x)
m = mul(range(1,21)) / mul(x)
b = (-s + math.sqrt(s**2 - (-4*(-m))))/-2
a = s - b
print(a,b) #15,5

我不知道sqrt,reduce和sum函数的复杂性,所以我无法解决该解决方案的复杂性(如果有人知道,请在下面评论)。


它需要多少时间和内存来计算x1*x2*x3*...
Thomas Ahle

@ThomasAhle在列表的长度上是O(n)-时间,在列表上是O(1)-空间,但实际上,更多的是乘法(至少在Python中)是O(n ^ 1.6)-time。该数字和数字的长度为O(log n)-空间。
Tuomas Laakkonen

@ThomasAhle否,log(a ^ n)= n * log(a),因此您将拥有O(l log k)空间来存储数字。因此,给定一个长度为l的列表和长度为k的原始数,您将拥有O(l)空间,但常数因子(log k)会比仅将它们全部写出要低。(我认为我的方法不是回答问题的特别好方法。)
Tuomas Laakkonen 2016年

3

对于Q2,这是一个效率不高的解决方案,但仍具有O(N)运行时并占用O(k)空间。

这个想法是运行原始算法两次。在第一个中,您会得到一个缺少的总数,这将为您提供缺失数字的上限。我们叫这个号码N。您知道丢失的两个数字将加起来N,因此第一个数字只能在间隔中,[1, floor((N-1)/2)]而第二个将在间隔中[floor(N/2)+1,N-1]

因此,您将再次循环所有数字,并丢弃第一个间隔中未包括的所有数字。是的,您跟踪它们的总和。最后,您将知道遗漏的两个数字之一,并进一步扩展第二个数字。

我感觉这种方法可以推广,也许在一次输入遍历过程中多个搜索可以“并行”运行,但是我还没有弄清楚该怎么做。


啊哈,是的,这是我为第二季度想出的相同解决方案,只是再次对所有小于N / 2的数字取负数来计算总和,但这更好!
xjcl

2

我认为无需任何复杂的数学方程式和理论即可完成此操作。以下是就地和O(2n)时间复杂度解决方案的建议:

输入表单假设:

袋中的数字数= n

缺失数字的数量= k

袋子中的数字由长度为n的数组表示

算法的输入数组的长度= n

数组中缺少的条目(从包装袋中取出的数字)将替换为数组中第一个元素的值。

例如。最初的包看起来像[2,9,3,7,8,6,4,5,1,10]。如果取出4,则4的值将变为2(数组的第一个元素)。因此,在取出4个袋子之后,袋子看起来像[2,9,3,7,8,6,2,5,1,10]

该解决方案的关键是在遍历数组时,通过否定该索引的值来标记访问的数字的索引。

    IEnumerable<int> GetMissingNumbers(int[] arrayOfNumbers)
    {
        List<int> missingNumbers = new List<int>();
        int arrayLength = arrayOfNumbers.Length;

        //First Pass
        for (int i = 0; i < arrayLength; i++)
        {
            int index = Math.Abs(arrayOfNumbers[i]) - 1;
            if (index > -1)
            {
                arrayOfNumbers[index] = Math.Abs(arrayOfNumbers[index]) * -1; //Marking the visited indexes
            }
        }

        //Second Pass to get missing numbers
        for (int i = 0; i < arrayLength; i++)
        {                
            //If this index is unvisited, means this is a missing number
            if (arrayOfNumbers[i] > 0)
            {
                missingNumbers.Add(i + 1);
            }
        }

        return missingNumbers;
    }

这会占用过多的内存。
Thomas Ahle

2

有一种通用的方法可以概括这样的流算法。这个想法是使用一些随机化来希望将k元素“散布” 为独立的子问题,我们的原始算法可以在其中为我们解决问题。除其他事项外,该技术还用于稀疏信号重建。

  • 制作一个a大小为的数组u = k^2
  • 挑选任何通用散列函数h : {1,...,n} -> {1,...,u}。(像乘移
  • 对于每一个i1, ..., n增加a[h(i)] += i
  • 对于x输入流中的每个数字,递减a[h(x)] -= x

如果所有缺失的数字都已散列到不同的存储桶中,则数组的非零元素现在将包含缺失的数字。

特定对发送到同一存储桶的概率小于1/u通用哈希函数的定义。由于大约有一k^2/2对,所以我们有最大的错误概率k^2/2/u=1/2。就是说,我们成功的可能性至少为50%,如果u我们增加成功的机会,我们的机会就会增加。

请注意,该算法占用k^2 logn空间logn位(每个数组存储桶需要位)。这与@Dimitris Andreou的答案所需的空间匹配(特别是多项式因式分解的空间要求,该空间恰好也是随机的。)该算法也具有常数每次更新的时间,而不是k总和的时间。

实际上,通过使用注释中描述的技巧,我们甚至可以比幂和方法更有效率。


注意:如果在我们的计算机上速度更快,我们也可以xor在每个存储区中使用,而不是sum
托马斯·阿勒

有趣,但我认为这仅在以下情况下遵守空间限制k <= sqrt(n):至少在以下情况下u=k^2?假设k = 11和n = 100,那么您将拥有121个存储桶,并且该算法最终将类似于具有100位的数组,您在从流中读取每个#时将其检查。增加u可以提高成功的机会,但是在超出空间限制之前可以增加多少是有限制的。
FuzzyTree

1
我认为,这个问题的范围n要大得多k,但我认为,实际上,您可以k logn使用与所述哈希非常相似的方法来缩小空间,同时仍保持恒定的时间更新。它在gnunet.org/eppstein-set-reconciliation中进行了描述,就像幂和方法一样,但基本上,您可以通过列表散列之类的强大散列函数将其散列到“两个k”存储桶中,以确保某些存储桶中只有一个元素。要进行解码,请确定该存储桶并从其两个存储桶中删除该元素,这(有可能)释放另一个存储桶,依此类推
Thomas Ahle 2016年

2

Q2的一个非常简单的解决方案,我很惊讶没有人回答。使用Q1中的方法来找到两个遗漏数字的总和。让我们用S表示它,然后缺失的数字之一小于S / 2,另一个大于S / 2(duh)。将所有从1到S / 2的数字求和,然后将其与公式的结果进行比较(类似于Q1中的方法),以找到缺失数字之间的较低者。从S减去它以找到更大的遗漏号。


我认为这与Svalorzen的答案相同,但是您用更好的语言解释了它。有任何想法如何将其推广到Qk吗?
John McClane

很抱歉错过其他答案。我不确定是否可以将其概括为$ Q_k $,因为在这种情况下,您无法将最小的缺失元素绑定到某个范围。您确实知道某些元素必须小于$ S / k $,但是对于多个元素来说可能是正确的
Gilad Deutsch

1

非常好的问题。我会为Qk使用设置差异。许多编程语言甚至都支持它,例如Ruby:

missing = (1..100).to_a - bag

这可能不是最有效的解决方案,但是如果我在这种情况下面临这样的任务(已知边界,低边界),我将在现实生活中使用它。如果数量的集合非常大,那么我当然会考虑一种更有效的算法,但是在那之前,简单的解决方案对我来说就足够了。


1
这会占用太多空间。
托马斯·阿勒

@ThomasAhle:为什么要在第二个答案中添加无用的注释?您使用太多空间是什么意思?
DarkDust

因为这个问题说:“我们不能负担与N成正比的任何额外空间。” 该解决方案正是这样做的。
Thomas Ahle

1

您可以尝试使用布隆过滤器。将购物袋中的每个数字插入花色中,然后遍历完整的1-k集,直到报告未找到的每个数字为止。这可能无法在所有情况下都找到答案,但是可能是一个很好的解决方案。


还有计数布隆过滤器,它允许删除。然后,您只需添加所有数字,然后删除您在信息流中看到的数字即可。
托马斯·阿勒

哈哈,这可能是更实际的答案之一,但很少引起注意。
ldog

1

对于该问题,我将采用不同的方法,并向面试官调查有关他正在尝试解决的更大问题的更多详细信息。根据问题和围绕它的要求,显而易见的基于集合的解决方案可能是正确的事,而生成列表并通过事后选择的方法则可能不正确。

例如,面试官可能要发送n消息并需要了解。之后,该集合将包含缺少的元素的列表,并且无需进行其他处理。k没有导致答复的消息,并且需要在之后的尽可能短的挂钟时间内知道消息n-k答复到达。我们也可以说,消息通道的本质是,即使全速运行,也有足够的时间在消息之间进行一些处理,而不会影响最后一个答复到达后产生最终结果所花费的时间。可以花时间将每个已发送消息的某些识别方面插入集合中,并在每个相应答复到达时将其删除。一旦最后一个答复到达,唯一要做的就是从集中删除其标识符,这在典型的实现中需要O(log k+1)k

这肯定不是批量处理预先生成的数字袋的最快方法,因为整个过程都在运行O((log 1 + log 2 + ... + log n) + (log n + log n-1 + ... + log k))。但这确实适用于任何值k(即使事先未知),并且在上面的示例中,它以最小化最关键间隔的方式应用。


如果您只有O(k ^ 2)额外的内存,这可以工作吗?
Thomas Ahle

1

您可以根据对称性(以数学语言表示的组)思考解决方案,从而激发解决方案。无论数字集的顺序如何,答案都应该相同。如果要使用k函数来帮助确定缺少的元素,则应考虑哪些函数具有该属性:对称。该函数s_1(x) = x_1 + x_2 + ... + x_n是对称函数的一个示例,但是还有其他更高阶的函数。特别要考虑基本对称函数。阶数2的基本对称函数为s_2(x) = x_1 x_2 + x_1 x_3 + ... + x_1 x_n + x_2 x_3 + ... + x_(n-1) x_n,两个元素的所有乘积之和。对于阶数为3或更高的基本对称函数也是如此。它们显然是对称的。此外,事实证明它们是所有对称功能的基础。

您可以通过注意建立基本对称函数s_2(x,x_(n+1)) = s_2(x) + s_1(x)(x_(n+1))。进一步的考虑应该使您确信,s_3(x,x_(n+1)) = s_3(x) + s_2(x)(x_(n+1))依此类推,这样就可以一次计算它们。

我们如何知道数组中缺少哪些项目?考虑一下多项式(z-x_1)(z-x_2)...(z-x_n)。评估0是否输入任何数字x_i。展开多项式,得到z^n-s_1(x)z^(n-1)+ ... + (-1)^n s_n。基本对称函数也出现在这里,这实在不足为奇,因为如果我们对根应用任何置换,多项式应该保持不变。

因此,我们可以构建多项式并尝试对其进行分解,以找出哪些数字不在集合中,如其他人所提到的那样。

最后,如果我们担心大量内存溢出(第n个对称多项式为阶数100!),我们可以mod pp素数大于100的情况下进行这些计算。在这种情况下,我们对多项式求值mod p并发现它再次求值0当输入是集合中的数字时为;当输入是集合中的数字时,它将计算为非零值。但是,正如其他人指出的那样,要及时k从不依赖于多项式的多项式中获取值N,我们必须对多项式进行分解mod p


1

另一方法是使用残差图滤波。

假设我们有数字1至4,而缺少3。二进制表示如下:

1 = 001b,2 = 010b,3 = 011b,4 = 100b

我可以创建如下流程图。

                   1
             1 -------------> 1
             |                | 
      2      |     1          |
0 ---------> 1 ----------> 0  |
|                          |  |
|     1            1       |  |
0 ---------> 0 ----------> 0  |
             |                |
      1      |      1         |
1 ---------> 0 -------------> 1

请注意,流程图包含x个节点,而x是位数。边的最大数量为(2 * x)-2。

因此,对于32位整数,它将占用O(32)空间或O(1)空间。

现在,如果我删除了从1,2,4开始的每个数字的容量,则剩下一个残差图。

0 ----------> 1 ---------> 1

最后,我将运行如下所示的循环,

 result = []
 for x in range(1,n):
     exists_path_in_residual_graph(x)
     result.append(x)

现在,结果中result包含的数字也不会丢失(假阳性)。但是当缺少元素时,k <=(结果的大小)<= nk

我将最后一次浏览给定的列表以标记结果是否丢失。

因此,时间复杂度将为O(n)。

最后,可以通过服用节点,以减少误报(和所需的空间)的数量00011110只是不是01


我不明白您的图表。节点,边和数字代表什么?为什么某些边缘是定向的而不是其他?
dain

实际上,我根本不理解您的答案,您能否进一步说明?
dain

1

您可能需要澄清O(k)的含义。

这是任意k的平凡解决方案:对于一组数字中的每个v,累加2 ^ v的总和。最后,将i从1循环到N。如果与2 ^ i按位与的和为零,则i丢失。(或数字,如果除以2的总和的地板^ i为偶数,或sum modulo 2^(i+1)) < 2^i。)

容易吧?O(N)时间,O(1)存储,它支持任意k。

除了您要计算大量的数据以外,实际计算机上的每个数字都需要O(N)空间。实际上,该解决方案与位向量相同。

因此,您可能很聪明,可以计算平方和,平方和与立方和……直至v ^ k的总和,然后进行精美的数学运算以提取结果。但是,这些数字也很大,这引出了一个问题:我们在谈论什么抽象的运营模式?O(1)空间适合多大,需要多少时间才能得出所需大小的数字?


好答案!一件事:“如果求和模2 ^ i为零,那么我不见了”是不正确的。但是很明显是什么意思。我认为“如果求和模2 ^(i + 1)小于2 ^ i,那么我就丢失了”将是正确的。(当然,在大多数编程语言中,我们将使用位移而不是模运算。有时编程语言比通常的数学符号更具表现力。:
jcsahnwaldt说GoFundMonica

1
谢谢,您完全正确!已修复,尽管我很懒惰并且被数学符号迷惑了……哦,我也弄糟了。再次修复...
sfink

1

这是一个不像sdcvvc / Dimitris Andreou的答案那样依赖复杂数学的解决方案,不像caf和Panic Panic那样改变输入数组,并且不使用Chris Lercher,JeremyP和许多其他人做到了。基本上,我从Svalorzen / Gilad Deutch关于Q2的想法开始,将其推广到常见情况Qk,并在Java中实现以证明该算法有效。

这个主意

假设我们有一个任意间隔I,我们仅知道它包含至少一个缺失的数字。一次通过输入阵列后,只能看着从数字,我们可以得到的总和都小号和数量Q从失踪人数的。我们通过简单地递减做到这一点我的每一个我们遇到从一些时间的长短(获得Q),并通过降低所有数字的预先计算和的每一次(获取所遇到数小号)。

现在我们来看看小号Q。如果Q = 1,则意味着然后只包含丢失的号码中的一个,并且这个数字显然š。我们将I标记为已完成(在程序中称为“明确”),并不再考虑。在另一方面,如果Q> 1,我们可以计算出平均A = S / Q丢失号码包含在。由于所有的数字是不同的,这样的数字中的至少一个不是严格少和至少一个是严格大于。现在,我们分开分成两个较小的间隔,每个间隔至少包含一个缺失的数字。请注意,这并不重要,以该区间的我们指定一个的情况下,它是一个整数。

我们进行下一个数组遍历,分别为每个间隔(但在同一遍中)计算SQ,之后标记间隔为Q = 1并拆分间隔为Q> 1。我们继续此过程,直到没有新的“模棱两可”的时间间隔为止,也就是说,我们没有任何可分割的内容,因为每个时间间隔恰好包含一个缺失的数字(并且我们总是知道这个数字,因为我们知道S)。我们从包含所有可能数字的唯一“整个范围”间隔开始(例如问题中的[1..N])。

时空复杂度分析

在过程停止之前,我们需要通过的总次数p决不会大于缺失数k。不等式p <= k可以得到严格证明。另一方面,还有一个经验上界p <log 2 N + 3对于大k值很有用。我们需要对输入数组的每个数字进行二进制搜索,以确定它所属的间隔。这将log k乘数添加到时间复杂度上。

总的来说,时间复杂度为O(N᛫min(k,log N)᛫log k)。注意,对于较大的k,这明显优于sdcvvc / Dimitris Andreou的方法,即O(N k)

对于其工作,该算法需要O(k)个额外的空间来存储最多k个间隔,这比“位集”解决方案中的O(N)更好。

Java实现

这是实现上述算法的Java类。它总是返回缺少数字的排序数组。除此之外,它不需要遗漏数字计数k,因为它是在第一遍计算的。数字的整个范围由minNumbermaxNumber参数给定(例如,问题中的第一个示例为1和100)。

public class MissingNumbers {
    private static class Interval {
        boolean ambiguous = true;
        final int begin;
        int quantity;
        long sum;

        Interval(int begin, int end) { // begin inclusive, end exclusive
            this.begin = begin;
            quantity = end - begin;
            sum = quantity * ((long)end - 1 + begin) / 2;
        }

        void exclude(int x) {
            quantity--;
            sum -= x;
        }
    }

    public static int[] find(int minNumber, int maxNumber, NumberBag inputBag) {
        Interval full = new Interval(minNumber, ++maxNumber);
        for (inputBag.startOver(); inputBag.hasNext();)
            full.exclude(inputBag.next());
        int missingCount = full.quantity;
        if (missingCount == 0)
            return new int[0];
        Interval[] intervals = new Interval[missingCount];
        intervals[0] = full;
        int[] dividers = new int[missingCount];
        dividers[0] = minNumber;
        int intervalCount = 1;
        while (true) {
            int oldCount = intervalCount;
            for (int i = 0; i < oldCount; i++) {
                Interval itv = intervals[i];
                if (itv.ambiguous)
                    if (itv.quantity == 1) // number inside itv uniquely identified
                        itv.ambiguous = false;
                    else
                        intervalCount++; // itv will be split into two intervals
            }
            if (oldCount == intervalCount)
                break;
            int newIndex = intervalCount - 1;
            int end = maxNumber;
            for (int oldIndex = oldCount - 1; oldIndex >= 0; oldIndex--) {
                // newIndex always >= oldIndex
                Interval itv = intervals[oldIndex];
                int begin = itv.begin;
                if (itv.ambiguous) {
                    // split interval itv
                    // use floorDiv instead of / because input numbers can be negative
                    int mean = (int)Math.floorDiv(itv.sum, itv.quantity) + 1;
                    intervals[newIndex--] = new Interval(mean, end);
                    intervals[newIndex--] = new Interval(begin, mean);
                } else
                    intervals[newIndex--] = itv;
                end = begin;
            }
            for (int i = 0; i < intervalCount; i++)
                dividers[i] = intervals[i].begin;
            for (inputBag.startOver(); inputBag.hasNext();) {
                int x = inputBag.next();
                // find the interval to which x belongs
                int i = java.util.Arrays.binarySearch(dividers, 0, intervalCount, x);
                if (i < 0)
                    i = -i - 2;
                Interval itv = intervals[i];
                if (itv.ambiguous)
                    itv.exclude(x);
            }
        }
        assert intervalCount == missingCount;
        for (int i = 0; i < intervalCount; i++)
            dividers[i] = (int)intervals[i].sum;
        return dividers;
    }
}

为了公平起见,此类接收NumberBag对象形式的输入。NumberBag不允许修改数组和随机访问,并且还计算请求数组进行顺序遍历的次数。Iterable<Integer>与之相比,它还更适合用于大型数组测试,因为它避免了对原始int值进行装箱,并允许包装大块的一部分int[]以方便测试准备。如果需要,可以NumberBag通过将签名中的两个for循环更改为foreach 来替换int[]Iterable<Integer>键入find签名,这并不难。

import java.util.*;

public abstract class NumberBag {
    private int passCount;

    public void startOver() {
        passCount++;
    }

    public final int getPassCount() {
        return passCount;
    }

    public abstract boolean hasNext();

    public abstract int next();

    // A lightweight version of Iterable<Integer> to avoid boxing of int
    public static NumberBag fromArray(int[] base, int fromIndex, int toIndex) {
        return new NumberBag() {
            int index = toIndex;

            public void startOver() {
                super.startOver();
                index = fromIndex;
            }

            public boolean hasNext() {
                return index < toIndex;
            }

            public int next() {
                if (index >= toIndex)
                    throw new NoSuchElementException();
                return base[index++];
            }
        };
    }

    public static NumberBag fromArray(int[] base) {
        return fromArray(base, 0, base.length);
    }

    public static NumberBag fromIterable(Iterable<Integer> base) {
        return new NumberBag() {
            Iterator<Integer> it;

            public void startOver() {
                super.startOver();
                it = base.iterator();
            }

            public boolean hasNext() {
                return it.hasNext();
            }

            public int next() {
                return it.next();
            }
        };
    }
}

测验

下面给出了演示这些类用法的简单示例。

import java.util.*;

public class SimpleTest {
    public static void main(String[] args) {
        int[] input = { 7, 1, 4, 9, 6, 2 };
        NumberBag bag = NumberBag.fromArray(input);
        int[] output = MissingNumbers.find(1, 10, bag);
        System.out.format("Input: %s%nMissing numbers: %s%nPass count: %d%n",
                Arrays.toString(input), Arrays.toString(output), bag.getPassCount());

        List<Integer> inputList = new ArrayList<>();
        for (int i = 0; i < 10; i++)
            inputList.add(2 * i);
        Collections.shuffle(inputList);
        bag = NumberBag.fromIterable(inputList);
        output = MissingNumbers.find(0, 19, bag);
        System.out.format("%nInput: %s%nMissing numbers: %s%nPass count: %d%n",
                inputList, Arrays.toString(output), bag.getPassCount());

        // Sieve of Eratosthenes
        final int MAXN = 1_000;
        List<Integer> nonPrimes = new ArrayList<>();
        nonPrimes.add(1);
        int[] primes;
        int lastPrimeIndex = 0;
        while (true) {
            primes = MissingNumbers.find(1, MAXN, NumberBag.fromIterable(nonPrimes));
            int p = primes[lastPrimeIndex]; // guaranteed to be prime
            int q = p;
            for (int i = lastPrimeIndex++; i < primes.length; i++) {
                q = primes[i]; // not necessarily prime
                int pq = p * q;
                if (pq > MAXN)
                    break;
                nonPrimes.add(pq);
            }
            if (q == p)
                break;
        }
        System.out.format("%nSieve of Eratosthenes. %d primes up to %d found:%n",
                primes.length, MAXN);
        for (int i = 0; i < primes.length; i++)
            System.out.format(" %4d%s", primes[i], (i % 10) < 9 ? "" : "\n");
    }
}

大型阵列测试可以通过以下方式执行:

import java.util.*;

public class BatchTest {
    private static final Random rand = new Random();
    public static int MIN_NUMBER = 1;
    private final int minNumber = MIN_NUMBER;
    private final int numberCount;
    private final int[] numbers;
    private int missingCount;
    public long finderTime;

    public BatchTest(int numberCount) {
        this.numberCount = numberCount;
        numbers = new int[numberCount];
        for (int i = 0; i < numberCount; i++)
            numbers[i] = minNumber + i;
    }

    private int passBound() {
        int mBound = missingCount > 0 ? missingCount : 1;
        int nBound = 34 - Integer.numberOfLeadingZeros(numberCount - 1); // ceil(log_2(numberCount)) + 2
        return Math.min(mBound, nBound);
    }

    private void error(String cause) {
        throw new RuntimeException("Error on '" + missingCount + " from " + numberCount + "' test, " + cause);
    }

    // returns the number of times the input array was traversed in this test
    public int makeTest(int missingCount) {
        this.missingCount = missingCount;
        // numbers array is reused when numberCount stays the same,
        // just Fisher–Yates shuffle it for each test
        for (int i = numberCount - 1; i > 0; i--) {
            int j = rand.nextInt(i + 1);
            if (i != j) {
                int t = numbers[i];
                numbers[i] = numbers[j];
                numbers[j] = t;
            }
        }
        final int bagSize = numberCount - missingCount;
        NumberBag inputBag = NumberBag.fromArray(numbers, 0, bagSize);
        finderTime -= System.nanoTime();
        int[] found = MissingNumbers.find(minNumber, minNumber + numberCount - 1, inputBag);
        finderTime += System.nanoTime();
        if (inputBag.getPassCount() > passBound())
            error("too many passes (" + inputBag.getPassCount() + " while only " + passBound() + " allowed)");
        if (found.length != missingCount)
            error("wrong result length");
        int j = bagSize; // "missing" part beginning in numbers
        Arrays.sort(numbers, bagSize, numberCount);
        for (int i = 0; i < missingCount; i++)
            if (found[i] != numbers[j++])
                error("wrong result array, " + i + "-th element differs");
        return inputBag.getPassCount();
    }

    public static void strideCheck(int numberCount, int minMissing, int maxMissing, int step, int repeats) {
        BatchTest t = new BatchTest(numberCount);
        System.out.println("╠═══════════════════════╬═════════════════╬═════════════════╣");
        for (int missingCount = minMissing; missingCount <= maxMissing; missingCount += step) {
            int minPass = Integer.MAX_VALUE;
            int passSum = 0;
            int maxPass = 0;
            t.finderTime = 0;
            for (int j = 1; j <= repeats; j++) {
                int pCount = t.makeTest(missingCount);
                if (pCount < minPass)
                    minPass = pCount;
                passSum += pCount;
                if (pCount > maxPass)
                    maxPass = pCount;
            }
            System.out.format("║ %9d  %9d  ║  %2d  %5.2f  %2d  ║  %11.3f    ║%n", missingCount, numberCount, minPass,
                    (double)passSum / repeats, maxPass, t.finderTime * 1e-6 / repeats);
        }
    }

    public static void main(String[] args) {
        System.out.println("╔═══════════════════════╦═════════════════╦═════════════════╗");
        System.out.println("║      Number count     ║      Passes     ║  Average time   ║");
        System.out.println("║   missimg     total   ║  min  avg   max ║ per search (ms) ║");
        long time = System.nanoTime();
        strideCheck(100, 0, 100, 1, 20_000);
        strideCheck(100_000, 2, 99_998, 1_282, 15);
        MIN_NUMBER = -2_000_000_000;
        strideCheck(300_000_000, 1, 10, 1, 1);
        time = System.nanoTime() - time;
        System.out.println("╚═══════════════════════╩═════════════════╩═════════════════╝");
        System.out.format("%nSuccess. Total time: %.2f s.%n", time * 1e-9);
    }
}

在Ideone上尝试一下


0

我相信我有一个O(k)时间和O(log(k))空间算法,因为您有floor(x)and log2(x)函数可用于任意大整数:

您有一个k-bit的长整数(因此加了log8(k)空格)x^2,其中x是您在书包中找到的下一个数字:s=1^2+2^2+...这需要O(N)时间(这对访问员来说不是问题)。最后j=floor(log2(s)),您会找到想要的最大数量。然后s=s-j,您再次执行上述操作:

for (i = 0 ; i < k ; i++)
{
  j = floor(log2(s));
  missing[i] = j;
  s -= j;
}

现在,通常没有- 2756位整数的floor和log2函数,而是双精度的。所以?简单来说,对于每2个字节(或1、3或4),您可以使用这些函数来获取所需的数字,但这会增加O(N)时间复杂度


0

这听起来可能很愚蠢,但是,在向您提出的第一个问题中,您将必须查看袋子中所有剩余的数字,然后将它们实际加起来以使用该方程式找到缺失的数字。

因此,由于您可以看到所有数字,因此只需查找丢失的数字即可。当缺少两个数字时也是如此。我认为很简单。当您看到袋子中剩余的数字时,使用方程式毫无意义。


2
我认为对它们进行汇总的好处是您不必记住已经看到的数字(例如,没有额外的内存要求)。否则,唯一的选择是保留所有看到的值的集合,然后再次遍历该集合以查找丢失的值。
丹涛2010年

3
通常用O(1)空间复杂度的规定来问这个问题。

前N个数字的总和为N(N + 1)/ 2。对于N = 100,Sum = 100 *(101)/ 2 = 5050;
tmarthal

0

我认为可以这样概括:

将S,M表示为算术级数和乘法之和的初始值。

S = 1 + 2 + 3 + 4 + ... n=(n+1)*n/2
M = 1 * 2 * 3 * 4 * .... * n 

我应该考虑一个公式来计算,但这不是重点。无论如何,如果缺少一个数字,则您已经提供了解决方案。但是,如果缺少两个数字,则用S1和M1表示新的总和和总倍数,如下所示:

S1 = S - (a + b)....................(1)

Where a and b are the missing numbers.

M1 = M - (a * b)....................(2)

由于您知道S1,M1,M和S,因此上述方程式可求解来找到缺失的数字a和b。

现在,对于缺少的三个数字:

S2 = S - ( a + b + c)....................(1)

Where a and b are the missing numbers.

M2 = M - (a * b * c)....................(2)

现在您的未知数是3,而您只有两个方程式可以解决。


但是,乘法运算会变得非常大。.此外,如何归纳为2个以上的缺失数?
托马斯·阿勒

我在N = 3且缺少数字= {1,2}的非常简单的序列上尝试了这些公式。我没有工作,因为我认为该错误在于应读取的公式(2)中M1 = M / (a * b)(请参阅答案)。然后工作正常。
dma_k '16

0

我不知道这是否有效,但我想提出这个解决方案。

  1. 计算100个元素的异或
  2. 计算98个元素的异或(删除2个元素之后)
  3. 现在(结果为1)XOR(结果2)为您提供两个缺失数的异或..ea如果a和b是缺失元素,则XOR b
    4.用您通常的方法获得缺失数之和对公式diff求和,可以说diff为d。

现在运行一个循环以获取可能的对(p,q),它们都位于[1,100]中,并求和为d。

当获得一对时,检查(结果3)XOR p = q,如果是,则完成。

如果我错了,请纠正我,如果正确,还请评论时间复杂度


2
我认为总和与异或不能唯一定义两个数字。运行循环以获取总和为d的所有可能的k元组需要时间O(C(n,k-1))= O(n <sup> k-1 </ sup>),对于k> 2,不好。
Teepeemm 2014年

0

大多数时候,我们可以在O(log n)中执行Q1和Q2

假设我们memory chipn数量为的数组组成test tubesx试管中的数字用x milliliter化学液体表示。

假设我们的处理器是laser light。当我们点亮激光时,它垂直于激光管的长度横穿所有电子管。每次通过化学液体时,光度降低1。并且使光线达到一定的毫升标记是一种操作O(1)

现在,如果我们在试管中间点燃激光并获得光度输出

  • 等于预先计算的值(当没有数字缺失时计算),则缺失的数字大于n/2
  • 如果我们的输出较小,则至少缺少一个小于的数字n/2。我们还可以检查亮度是否降低了12。如果将其减少,1则一个丢失的数字小于,n/2而另一个丢失的数字大于n/2。如果将其减少,2则两个数字均小于n/2

我们可以一次又一次地重复上述过程,缩小我们的问题范围。在每一步中,我们都将域缩小一半。最后我们可以得到我们的结果。

值得一提的并行算法(因为它们很有趣),

  • 通过某种并行算法进行排序,例如,并行合并可以及时完成O(log^3 n)。然后可以通过二进制搜索O(log n)及时找到丢失的号码。
  • 从理论上讲,如果我们有n处理器,则每个进程都可以检查输入之一并设置一些标识该数字的标志(通常在数组中)。并且在下一步中,每个进程都可以检查每个标志,并最终输出未标记的数字。整个过程需要O(1)时间。它具有额外的O(n)空间/内存要求。

注意,上面提供两个并行算法可能需要额外的空间,如注释中所述


尽管“试管激光”方法确实很有趣,但我希望您同意,它不能很好地转换为硬件指令,因此不太可能O(logn)在计算机上使用。
SirGuy

1
至于您的排序方法,这将占用大量的额外空间,而这取决于我们N,而比O(N)时间(就其对的依赖而言N)要多,而我们打算做得更好。
SirGuy

@SirGuy感谢您对试管概念和并行处理内存成本的关注。我的帖子是分享我对这个问题的想法。GPU处理器现在正在进行并行处理。谁知道,试管概念将来是否会面世。
舒瓦'17
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.