【问题标题】:Dealing with M occurrences among N处理 N 中的 M 次出现
【发布时间】:2011-04-27 04:16:34
【问题描述】:

我在面试时被问到的问题。我接近解决方案,但不幸的是没有解决它。

假设我们有一个序列,其中包含 Nlong 类型的数字。我们确定在这个序列中,每个数字都恰好出现 n 次,除了一个数字恰好出现 m 次(0 m n)。我们如何通过 O(N) 操作和 O(1) 额外内存找到该数字?

对于最简单的情况(当 n = 2m = 1) 我们应该做的只是对序列中的每个数字执行累积xor。结果将等于所需的数字。但是我在尝试处理任意 mn 时遇到了困难。

我希望有一个实际的 C++ 解决方案。


编辑:我们先验地知道 mn 的实际值。

示例。 我们知道 n = 3 和 m = 2 . 序列 (N = 8) 是

5 11 5 2 11 5 2 11

在这种特殊情况下,正确的答案是 2,因为它只出现了两次。

【问题讨论】:

  • 我只想说这是一个糟糕的面试问题。
  • 你能定义一个像long域大小的数组这样的奇幻结构吗?其中大多数是可代表的。
  • 那么也许这个通常不好的问题适合你的情况。
  • 哇,我通常采访的程序员都很难解决 Fizzbuz 测试。真的有人可以解决这样的问题,当他们很紧张并且几个面试官正在看着他们/在他们的脖子上呼吸时?在哪里可以找到这些人?
  • 面试是一门艰苦的学科,一方面你想围绕候选人的水平提出问题,另一方面你也想让优秀的候选人发光。这些人显然选择了一些最困难的东西,而且在这之上给的时间太少了。无论如何,带上它,这比人们问如何用 jQuery 完成一些简单的任务要有趣得多。

标签: c++ arrays algorithm


【解决方案1】:

您进行 64 次求和,每个位一个,对于您计算 sum mod n 的每个总和,此计算为结果中应该设置的每个位返回 m,不应该设置的每个位返回 0 .

示例:
n = 3,m = 2。列表 = [5 11 5 2 11 5 2 11]

              5  11   5   2  11   5   2  11
sum of bit 0: 1 + 1 + 1 + 0 + 1 + 1 + 0 + 1 = 6   6 % 3 = 0
sum of bit 1: 0 + 1 + 0 + 1 + 1 + 0 + 1 + 1 = 5   5 % 3 = 2
sum of bit 2: 1 + 0 + 1 + 0 + 0 + 1 + 0 + 0 = 3   3 % 3 = 0
sum of bit 3: 0 + 1 + 0 + 0 + 1 + 0 + 0 + 1 = 3   3 % 3 = 0

所以只设置了第 1 位,也就是说结果是 2。

优化实施:
(对实际问题也有用的技巧和注意事项)
值得注意的是,在迭代数组时,执行速度在一定程度上会受到内存访问的限制,如果需要对每个元素执行多个操作,通常一次对一个元素执行所有操作是最快的,因此处理器只需要从内存中加载每个元素一次。 Interesting blog post on memory and cache.

可以对单个整数中的多个位求和,而不是应用 64 个不同的位掩码来单独获取每个位,例如,可以仅使用 4 个位掩码,每个位掩码提取 16 位,每个位之间有 3 位空间,如只要不发生溢出,正常的加法运算将与处理 16 个 4 位整数一样工作,因此该方法适用于 15 个数字。以这种方式处理 15 个数字后,结果必须添加到能够容纳更大整数的存储中(可以是 8 个 64 位整数,每个整数都容纳 8 个 8 位整数,当然它们必须依次清空成更大的整数等。 )。
结果是,不需要对每个值执行 64 位掩码、63 位移位和 64 次加法,而是只需执行 4 次位掩码、3 次移位和 4 次加法,每 15 个值加上 8 位掩码、4 次移位和 8 次加法,再加上每 255 个值16 位掩码、8 位移位和 16 次加法等。

可视化:
(使用 16 位整数对 4x4 位整数求和)

1000 1000 1000 1000 +
1000 0000 0000 1000 +
0000 0000 0000 1000 +
1000 1000 0000 0000 +
1000 0000 1000 0000 +
0000 0000 1000 1000 =
0010 0100 1100 0010

无论您认为这是 4 列 4 位整数还是 1 列 16 位整数,结果都是一样的,只要 4 位整数不溢出,这才是正确的。

【讨论】:

  • 哇,这是最简单、最清晰优雅的解决方案!它works 完美。
  • 优雅。这也可以被认为是泛化异或,只是使用模 m 而不是模 2。
  • 这就是他们告诉他整数的确切大小的原因。很好的解决方案。
  • 很好的解决方案。严格来说,这使用了 O(N log N) 时间和 O(log N) 个内存字(与 Nabb 一样)。标准假设是 O(log n) 位的内存是 O(1) 个字,对这些字的操作每个都需要 O(1) 时间。我在下面发布了一个使用 O(1) 个内存字的解决方案,并且运行时间与此相同。
  • @jonderry 严格来说是 O([int size] * N) 时间和 O([int size]) 内存。当然,可以通过一次计算一个位值来减少内存使用中的几个字节并将其降低到 O(1),尽管这意味着对每个位迭代一次列表。
【解决方案2】:

edit) 好的,这种方法并不像我最初想象的那样可靠。 eBusiness 的解决方案要简单得多,并且适用于 n=4、m=2 等情况。

我们可以将 xor 方法推广到任意 mn。我们首先需要选择一个基础 b 使得 gcd(n, b) = bgcd(m, b) 。由于奇数 n/偶数 m 对满足基数 2 的要求,因此标准二进制异或适用于这些对。

首先我们定义 (a^^n) 来表示 (a^a^...^a) 代表 na ,具有基 b 的广义异或。例如,使用标准二进制异或,a^^2 = 0.

我们需要定义我们的广义异或。我们想要的性质和加法基本相同(交换性、结合性),我们需要a^^b = 0。明显的解决方案是 (x^y) = (x+y)%b 用于基本 b 表示中的每个数字(说服自己这可行,并且与基数为 2) 的二进制异或。然后,我们简单地将其应用于序列中的所有数字,并以 result = s^^(m%b) 结尾,其中 s 是特殊数字。
最后,我们需要将我们的'xor'ed base b数字恢复为预期的数字。我们可以简单地计算 i=0..b-1i^^(m%b),然后在 s 结果中的每个数字。

这个算法是 O(N)。对于列表中的每个数字,我们有固定数量的操作要做,因为我们最多有 64 位数字。对于大 b,最后恢复是最坏的 O(N)。我们可以通过为每个数字的所有 i 计算 i^^(m%b) 在恒定空间中完成这最后一步(同样,我们有一个恒定的数字位数) .


示例:

n = 3, m = 2. list = [5 11 5 2 11 5 2 11]

首先我们选择一个基础b。显然我们必须选择基数 3。

xor 表供参考:

  0|1|2
0|0|1|2
1|1|2|0
2|2|0|1

计算:

  5     11      5      2     11      5      2     11
0^0=0. 0^1=1. 1^0=1. 1^0=1. 1^1=2. 2^0=2. 2^0=2. 2^1=0.
0^1=1. 1^0=1. 1^1=2. 2^0=2. 2^0=2. 2^1=0. 0^0=0. 0^0=0.
0^2=2. 2^2=1. 1^2=0. 0^2=2. 2^2=1. 1^2=0. 0^2=2. 2^2=1.

m % b = 2.

因此我们有 s^^2 = [001]。我们为每个数字 i 生成一个 i^^2 的表,然后进行反向查找。

   i | 0 | 1 | 2 |
i^^2 | 0 | 2 | 1 |

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

我们最后将结果转换回二进制/十进制。 [002] = 2.

【讨论】:

  • 这是正确的解决方案,但如果我在阅读您的回复之前没有自己想通,我认为我无法理解它。
  • 这真的是太棒了! 最后在采访中我发现了一个想法,即非二进制基础表示可能非常有用。但毕竟我完全糊涂了。
  • 然而,当折磨结束时,面试官告诉我,存在一个更容易的解决方案,因为它错过了非二进制基表示的魔法。这个事实完全让我大吃一惊。
  • 对不起,它与我的解决方案不一样,虽然看起来很像,但是当 n 和 m 不是内部主要时,此解决方案会失败,请尝试使用 n=4 和 m=2 和你会得到一个全零的结果。
  • @jonderry:由于序列中的值是长整数,所以我们最多有64 log(2) / log(b) 个数字,所以我们有 O(1) 个数字。每个数字的操作都是 O(1),所以我们总共有 O(1)。
【解决方案3】:

您最简单的情况可以更一般,您可以使用与奇数 m 和偶数 n 相同的技术。

【讨论】:

  • 但上述问题可能有 m 和 n 都是偶数或都是奇数,或者是 m 偶数和 n 奇数。您只介绍了 4 种可能性中的 1 种。
【解决方案4】:

这是一个与电子商务具有相同运行时间的解决方案(我认为实际上是 O(N log N)),但真正使用 O(1) 个单词的内存。它假设 m 不是 n 的倍数。它还假设有两个辅助函数,它们严格计算其参数上方和下方的元素数量。

int divider = 0;

for (int i = 63; i >= 0; i--) {
  divider |= 1 << i;
  int a = countAbove(divider);
  int b = countBelow(divider);
  if (a % n == 0 && b % n == 0) return divider;
  else if (a % n == 0) divider ^= 1 << i;
}

【讨论】:

  • 看起来你的辅助函数会让这个 O(N Log N) (离开我不那么敏锐的头脑)。
  • 没错。其他解决方案,包括 eBusiness',也是 O(N log N)。然而,与其他解决方案不同的是,该解决方案使用的内存只是固定数量的单词。
【解决方案5】:
  • 如果您对从 0 到 (N/n) + 1 的整数集有一个一对一的哈希,那么您可以通过 N 次迭代 + N/n 次迭代和 N 次内存使用来解决它。但是没有一对一的映射

  • 如果没有对内存的限制,它必须是常量,您可以定义一个大小为 long 域的数组,然后您可以在 2N 中解决问题,并使用恒定的大量内存。对于 N 中的每个 x,您只需添加到 BIGARRY[x],然后通过 BIGARRY 循环查找 m。它可怕且不可实施,但符合要求,大多数面试问题都是思想实验。

【讨论】:

    【解决方案6】:

    如果列表已排序,那么这将变得非常简单,因为您只需依次检查每个批次以查看其长度是否为 m。

    如果列表未排序,那么我认为不可能使用 O(1) 额外内存。

    【讨论】:

    • 面试官发誓这是可能的。 :)
    【解决方案7】:

    我相信你不能只使用 O(1) 额外的空间。

    这是我的理由:给你:

    • n
    • x_1 x_2 .. x_N

    由于 x_i 值之间存在重复,我们将 U 定义为唯一值的集合。 U 中的所有元素出现 n 次,其中一个元素在 x_i 系列中出现 m 次。让我们将出现频率较低的元素标记为 u_0,将 U_1 标记为集合 U - { u_0 }。

    令 S 为所有 x_i 的总和。 S可以写成:

    sum(x_i) = n * sum(U_1) + m * u_0 = n * sum(U) + (m - n) * u_0
    

    解决这个问题相当于找到一个序列中唯一元素的总和,而你不能在 O(1) 额外的空间中做到这一点,因为你需要一个带有链接元素的数组或哈希表 - 空间实际上是 O(N)

    【讨论】:

      【解决方案8】:

      解决方案类似于查找 k 阶统计量的过程

      by dividing the sequence into 2 sub-seqs
      (calculate the size of sub-seqs during the procedure)
      while (sizeof(sub-seq) mod n != 0)
        do the same porcess on this sub-seq(dividing)
      

      O(N) 次操作,例如求 k 阶统计量。

      【讨论】:

        猜你喜欢
        • 2018-05-02
        • 1970-01-01
        • 2020-10-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-01-29
        • 2013-09-28
        相关资源
        最近更新 更多