【问题标题】:O(n) algorithm to find the odd-number-out in array of consecutive integers from 1 to n(not odd numbers)O(n) 算法在从 1 到 n(非奇数)的连续整数数组中找到奇数输出
【发布时间】:2013-10-22 14:36:51
【问题描述】:

我在试图解决这个问题时遇到了很多麻烦,而这个麻烦的根源是创建了一个O(n) 复杂度的算法。这是我正在努力解决的问题:

长度为n 的数组A 包含[0, .., n - 1] 范围内的整数。但是,它只包含 n - 1 不同的数字。因此,其中一个数字丢失,另一个数字重复。编写一个以A为输入参数并返回缺失数字的Java方法;该方法应该在O(n) 中运行。

例如A = [0, 2, 1, 2, 4]时,oddOneOut()应该返回3;当A = [3, 0, 0, 4, 2, 1]oddOneOut() 应该返回5

显然这是一个使用O(n<sup>2</sup>) 算法很容易解决的问题,(很可能是O(n),我只是没看到!)。我试图用各种方法解决它,但无济于事。我正在尝试用 Java 来解决它,但如果你更愿意用 Python 来解决它,那也可以。

提前谢谢你...

【问题讨论】:

  • 您可以使用的空间是否也有限制?我将遍历数组一次并获得最小值和最大值;然后创建一个大小范围的数组。然后再次循环遍历数组,将每个元素放入其槽中。然后,您需要在桶排序数组中找到丢失的项目。
  • 感谢您的快速回复!问题中所述的使用空间没有任何限制。我对 bucker-sort 不熟悉,但是从我刚刚查到的情况来看,这不是 o(n logn) 吗?
  • @Turtle-in-a-bash-shell 那是因为它实际上不是桶排序。它相当于一个集合/布尔成员数组,它只需要 O(n) 时间和 O(n) 空间来创建和 O(n) 时间来通过。但无论如何,我建议查看 user1952500 给出的 O(1) 空间解决方案

标签: java arrays algorithm time-complexity big-o


【解决方案1】:

假设缺少的数字是x,而重复的数字是y。如果将所有数字相加,总和将为:

(n - 1) * n / 2 - x + y

从上面可以找到(x - y).....(1)

同样,对数字的平方求和。总和将是:

(n - 1) * n * (2 * n - 1) / 6 - x<sup>2</sup> + y<sup>2</sup>

从上面你得到(x<sup>2</sup> - y<sup>2</sup>)....(2)

(2) / (1) gives (x + y).....(3)

(1) + (3) 给出2 * x,因此您可以找到xy

请注意,在此解决方案中存在 O(1) 额外存储,并且是 O(n) 时间复杂度。上面的其他解决方案是不必要的O(n) 额外存储空间。

混合 C/C++ 中的代码更清晰:

#include <stdio.h>

int findDup(int *arr, int n, int& dup, int& missing)
{
    int sum = 0;
    int squares = 0;

    for (int i = 0; i < n; i++) {
        sum += arr[i];
        squares += arr[i] * arr[i];
    }

    sum = (n - 1) * n / 2 - sum; // x - y

    squares = (n - 1) * n * (2 * (n - 1) + 1) / 6 - squares; // x^2 - y^2

    if (sum == 0) {
        // no duplicates
        missing = dup = 0;
        return -1;
    }
    missing = (squares / sum + sum) / 2; // ((x^2 - y^2) / (x - y) + (x - y)) / 2 = ((x + y) + (x - y)) / 2 = x

    dup = missing - sum; // x - (x - y) = y

    return 0;
}


int main(int argc, char *argv[])
{
    int dup = 0;
    int missing = 0;

    int a[] = {0, 2, 1, 2, 4};

    findDup(a, sizeof(a) / sizeof(int), dup, missing);
    printf("dup = [%d], missing = [%d]\n", dup, missing);

    int b[] = {3, 0, 0, 4, 2, 1};
    findDup(b, sizeof(b) / sizeof(int), dup, missing);
    printf("dup = [%d], missing = [%d]\n", dup, missing);

    return 0;
}

输出:

dup = [2], missing = [3]
dup = [0], missing = [5]

一些python代码:

def finddup(lst):
    sum = 0
    sumsq = 0
    missing = 0
    dup = 0
    for item in lst:
        sum = sum + item
        sumsq = sumsq + item * item
    n = len(a)
    sum = (n - 1) * n / 2 - sum
    sumsq = (n - 1) * n * (2 * (n - 1) + 1) / 6 - sumsq
    if sum == 0:
        return [-1, missing, dup]
    missing = ((sumsq / sum) + sum) / 2
    dup = missing - sum
    return [0, missing, dup]

found, missing, dup = finddup([0, 2, 1, 2, 4])
if found != -1:
    print "dup = " + str(dup) + " missing = " + str(missing)

print finddup([3, 0, 0, 4, 2, 1])

输出:

dup = 2 missing = 3
[-1, 0, 0]

【讨论】:

  • 很好的答案,虽然它确实溢出了long(因此需要使用BigInteger 或手动溢出计数器)在n = 2^22 附近的某个地方,这比内存中的容量要少合适的电脑。尽管如此,任何解决方案最终都会耗尽一些资源。
  • 如果你小心,它是否溢出并不重要;分析可以适应模运算。
  • 当之无愧的最佳解决方案。 +1
  • 那么我将如何将其作为算法实现呢?这是我在 python 中测试的内容,但我不知道它是如何工作的: A = [0, 2, 1, 2, 4] B = [3, 0, 0, 4, 2, 1] defoddOneout (list): sum = 0 squares = 0 for i in list: sum = sum + ((i-1)*i/2) for i in list: squares = squares + ((i - 1) * i * (2 * i - 1) / 6) print(sum) print(squares) oddOneout(A)
  • 添加了不进行排序或其他任何操作的代码,如您所见。它在 C++ 中,但应该易于理解并可移植到 java / python 中。这个想法是你正在寻找 x 和 y。您计算数组的总和,并且有 [0..(n-1)] 的总和和 [0..(n-1)] 的平方和的众所周知的公式。因此,您可以轻松计算 x 和 y。
【解决方案2】:

这可能很有趣,尽管我不确定它在什么条件下(如果有的话)表现最好。我们的想法是,我们要将每个元素移动到数组中的正确位置(0 到索引 0 等),直到清楚什么是缺失的,什么是多余的。

def findmissing(data):
    upto = 0
    gap = -1
    while upto < len(data):
        #print data, gap
        if data[upto] == upto:
            upto += 1
            continue
        idx = data[upto]
        if idx is None:
            upto += 1
            continue
        data[upto], data[idx] = data[idx], data[upto]
        if data[upto] == data[idx]:
            print 'found dupe, it is', data[upto]
            data[upto] = None
            gap = upto
            upto += 1
        elif data[upto] is None:
            gap = upto
    return gap

if __name__ == '__main__':
    data = range(1000)
    import random
    missing = random.choice(data)
    print missing
    data[missing] = data[0]
    data[0] = random.choice(data[1:])
    random.shuffle(data)
    print 'gap is', findmissing(data)

这是 O(n),因为每一步 要么 递增 upto 将一个值移动到数组中的“正确”位置,并且这些东西中的每一个都可以只发生n 次。

【讨论】:

  • 好主意。如果您需要保留数据的副本,则需要 O(n) 额外的存储空间,但如果不需要,它应该是最简单的实现方式。平均速度是原来的两倍?
  • @clwhisk:它可能比 user1952500 的回答慢(因为它对数据进行了单次顺序传递)并且很可能是 Martijn 的(对位集进行非顺序访问在第一遍,但我的对更大的输入数组进行非顺序访问,因此可能会引发更多的缓存未命中)。我只是觉得这很有趣。
【解决方案3】:

使用散列集并单次通过来检测哪个数字是重复的。在同一迭代中,跟踪所有数字的累积和。

如果所有数字都不同,现在计算预期总数:n * (n - 1) / 2。减去你找到的总数。您将留下“缺失”的数字减去重复的数字。将副本添加回来以获得您的答案。

由于哈希表访问是恒定的时间并且我们使用单次传递,因此这是O(n)。 (请注意,单次传递并不是绝对必要的:Martijn 指出固定次数的传递仍然是线性复杂度是正确的。)

【讨论】:

  • 求和是一种优雅的方式。 +1
  • 这很好找到,但我担心使用一个集合会使它超过 O(n)。我不擅长计算 Big-Oh,但我认为它类似于 O(n^2)。
  • 再看一遍。假设您每次重新散列时将容器大小加倍,则插入和探测散列映射/集是摊销的常数时间。所有标准实现都可以。
  • @MartijnCourteaux 不,这是不对的。 Java 中的HashSet 保证O(1) add
  • @MartijnCourteaux 这可能会有所帮助:cs.cornell.edu/courses/cs312/2008sp/lectures/lec20.html
【解决方案4】:

遍历数组两次:仍然是 O(n)。创建一个临时的布尔数组(或 Java BitSet)来保存你得到的数字。第二次执行循环时,检查布尔数组中是否有空洞。

【讨论】:

  • 我试试这个。在研究这个问题时,我认为 Java 位集可能是一种可能的途径,但由于我从未使用过它而犹豫不决;看起来我没有其他选择。感谢您的回复!
  • @Turtle-in-a-bash-shell:找到解决问题的问题后,您可以单击答案旁边的勾号接受它。
  • 我想我仍然对如何使用布尔数组来保存我拥有的数字感到困惑?为什么第二个循环会在数组中显示一个洞?
  • 另外,我将如何创建一个 BitSet,为什么会这样?我想我不明白它背后的概念。
  • @Turtle-in-a-bash-shell:就我们的目的而言,BitSet 只是boolean 值的空间优化向量。因此,在第一遍(在输入数组上)中,当您看到值 i 时,将索引 i 设置为 true。然后在第二遍(通过 BitSet)中,您寻找仍然是 false 的一个索引。
猜你喜欢
  • 1970-01-01
  • 2012-09-26
  • 2011-05-11
  • 2014-12-28
  • 2020-10-02
  • 1970-01-01
  • 1970-01-01
  • 2012-08-07
  • 1970-01-01
相关资源
最近更新 更多