更快的算法找到两个数组之间的唯一元素?

时间:2013-10-05 23:44:28

标签: java arrays algorithm

编辑:对于这个问题的新手,我已经发布了一个答案,说明发生了什么。接受的答案是我认为最能回答我最初发布的问题的答案,但有关详细信息,请参阅我的答案。

注意:此问题最初是伪代码和使用的列表。我已将它改编为Java和数组。因此,虽然我很想看到任何使用Java特定技巧的解决方案(或任何语言的技巧!),但请记住原始问题与语言无关。

问题

假设有两个未排序的整数数组ab,允许元素重复。它们是相同的(关于包含的元素)除了其中一个数组有一个额外的元素。举个例子:

int[] a = {6, 5, 6, 3, 4, 2};
int[] b = {5, 7, 6, 6, 2, 3, 4};

设计一种算法,将这两个数组作为输入并输出单个唯一整数(在上例中为7)。

解决方案(迄今为止)

我想出了这个:

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    for (int i = 0; i < a.length; i++) {
        ret ^= a[i];
    }
    for (int i = 0; i < b.length; i++) {
        ret ^= b[i];
    }
    return ret;
}

课堂上提出的“官方”解决方案:

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    for (int i = 0; i < a.length; i++) {
        ret += a[i];
    }
    for (int i = 0; i < b.length; i++) {
        ret -= b[i];
    }
    return Math.abs(ret);
}

所以,两者在概念上做同样的事情。鉴于a的长度为m且b的长度为n,则两个解的运行时间均为O(m + n)。

问题

我后来与老师谈话,他暗示有一种甚至更快的方式。老实说,我不知道怎么样;要确定元素是否是唯一的,您似乎必须至少查看每个元素。那至少是O(m + n)......对吧?

那么有更快的方法吗?如果是这样,它是什么?

9 个答案:

答案 0 :(得分:28)

使用HotLick在评论中的建议,这可能是你用Java做的最快的。它假设b.length == a.length + 1所以b是具有额外“唯一”元素的较大数组。

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    int i;
    for (i = 0; i < a.length; i++) {
        ret = ret ^ a[i] ^ b[i];
    }
    return ret ^ b[i];
}

即使无法做出假设,您也可以轻松扩展它,以包括a或b可以是具有唯一元素的较大数组的情况。它仍然是O(m + n),只减少了循环/分配开销。

编辑:

由于语言实现的细节,这仍然(令人惊讶地)是在CPython中实现它的最快方式。

def getUniqueElement1(A, B):
    ret = 0
    for a in A: ret = ret ^ a
    for b in B: ret = ret ^ b
    return ret

我已使用timeit模块对此进行了测试,并发现了一些有趣的结果。事实证明,Python中的缩写ret = ret ^ a确实比速记ret ^= a更快。迭代循环元素比迭代索引然后在Python中进行下标操作要快得多。这就是为什么这段代码比我之前尝试复制Java的方法快得多的原因。

我想这个故事的寓意是没有正确答案,因为这个问题无论如何都是假的。正如OP在下面的另一个答案中指出的那样,事实证明你不能真的比O(m + n)更快,而他的老师只是拉着他的腿。因此,问题减少到找到迭代两个数组中所有元素并累积所有元素的XOR的最快方法。这意味着它完全依赖于语言实现,你必须做一些测试并在你正在使用的任何实现中获得真正的“最快”解决方案,因为整体算法不会改变。

答案 1 :(得分:14)

好的,我们去......向任何期待更快解决方案的人道歉。事实证明我的老师和我一起玩得很开心,我完全错过了他说的话。

我应该先澄清一下我的意思:

  

他暗示有一种甚至更快的做法

我们谈话的要点是这样的:他说我的XOR方法很有意思,我们谈了一段时间我是如何找到解决方案的。他问我是否认为我的解决方案是最佳的。我说我做了(因为我在问题中提到的原因)。然后他问我:“你肯定?”看着他的脸,我只能形容为“自鸣得意”。我犹豫不决但是说是的。他问我是否能想出更好的办法。我非常喜欢,“你的意思是有更快的方法吗?”但他没有给我一个直接的回答,而是告诉我要考虑一下。我说我愿意。

所以我想到了,确定我的老师知道我没有的东西。在一天没有提出任何事情之后,我来到了这里。

我的老师实际上希望我做的是保护我的解决方案是最佳的,尝试找到更好的解决方案。正如他所说:创建一个好的算法是很容易的部分,困难的部分证明它是有效的(并且它是最好的)。他认为我花了很多时间在Find-A-Better-Way Land上而不是制作一个可以花费相当少时间的O(n)的简单证明是非常有趣的(我们最终这样做了,见下面的你有兴趣)。

所以我想,这里学到了很多教训。我会接受Shashank Gupta的答案,因为我认为确实能够回答原来的问题,即使这个问题存在缺陷。

我会给你们留下一个我在打字证明时发现的整齐的小Python单线程。这不是更有效但我喜欢它:

def getUniqueElement(a, b):
    return reduce(lambda x, y: x^y, a + b)

非常非正式的“证明”

让我们从问题ab中的原始两个数组开始:

int[] a = {6, 5, 6, 3, 4, 2};
int[] b = {5, 7, 6, 6, 2, 3, 4};

我们在这里说较短的数组的长度为n,那么较长的数组的长度必须为n + 1。证明线性复杂性的第一步是将数组附加到第三个数组(我们称之为c):

int[] c = {6, 5, 6, 3, 4, 2, 5, 7, 6, 6, 2, 3, 4};

,其长度为2n + 1。为什么这样?那么,现在我们完全有另一个问题:找到在c中发生奇数次的元素(从这里开始“奇数次”和“唯一”被认为是相同的事情)。这实际上是一个pretty popular interview question,显然是我老师对他的问题有所了解,所以现在我的问题有一些实际意义。万岁!

假设比O(n)更快的算法,例如O(log n)。这意味着它只会访问 c元素的某些。例如,O(log n)算法可能只需要检查示例数组中的log(13)~4个元素来确定唯一元素。我们的问题是,这可能吗?

首先让我们看看我们是否可以删除任何元素(通过“删除”,我的意思是不必访问它)。如果我们删除2个元素怎么样,以便我们的算法只检查c长度为2n - 1的子数组?这仍然是线性复杂性,但如果我们能做到这一点,那么我们可以进一步改进它。

所以,让我们随机选择c的两个元素来删除。实际上有几件事情可以在这里发生,我将总结为案例:

// Case 1: Remove two identical elements
{6, 5, 6, 3, 4, 2, 5, 7, 2, 3, 4};

// Case 2: Remove the unique element and one other element
{6, 6, 3, 4, 2, 5, 6, 6, 2, 3, 4};

// Case 3: Remove two different elements, neither of which are unique
{6, 5, 6, 4, 2, 5, 7, 6, 6, 3, 4};

我们的阵列现在是什么样的?在第一种情况下,7仍然是唯一的元素。在第二种情况下,有一个 new 独特的元素,5。在第三种情况下,现在有3个独特的元素......是的,那里一团糟。

现在我们的问题变成:我们可以通过查看这个子阵列来确定c的唯一元素吗?在第一种情况下,我们看到7是子阵列的唯一元素,但我们不能确定它也是c的唯一元素;两个被移除的元素也可以是7和1.类似的论点适用于第二种情况。在案例3中,有3个独特元素,我们无法告知c中哪两个是非唯一的。

很明显,即使使用2n - 1次访问,也没有足够的信息来解决问题。因此,最佳解决方案是线性解决方案。

当然,真正的证据会使用归纳而不是使用示例,但我会将其留给其他人:)

答案 2 :(得分:7)

您可以将每个值的计数存储在集合(如数组或哈希映射)中。 O(n)然后您可以检查其他集合的值,并在您知道未命中时立即停止。这可能意味着您只能平均搜索第二个数组的一半。

答案 3 :(得分:3)

这是位更快:

public static int getUniqueElement(int[] a, int[] b) {
    int ret = 0;
    int i;
    for (i = 0; i < a.length; i++) {
        ret += (a[i] - b[i]);
    }
    return Math.abs(ret - b[i]);
}

这是O(m),但订单并不能说明整个故事。 “官方”解决方案的循环部分大约有3 * m + 3 * n个操作,稍快的解决方案有4 * m。

(将循环“i ++”和“i&lt; a.length”计为每个操作)。

-Al。

答案 4 :(得分:1)

假设只添加了一个元素,并且数组与开头相同,则可以命中O(log(base 2)n)。

基本原理是任何数组都需要搜索二进制O(log n)。除非在这种情况下您没有在有序数组中搜索值,否则您将搜索第一个不匹配的元素。在这种情况下,[n] == b [n]意味着你太低了,而[n]!= b [n]意味着你可能太高了,除非[n-1] == b [N-1]。

其余的是基本的二进制搜索。检查中间元素,确定哪个部门必须有答案,并对该部门进行子搜索。

答案 5 :(得分:1)

  

假设有两个未排序的整数数组a和b,允许元素重复。 它们是相同的(关于所包含的元素)除了其中一个数组有额外的元素 ..

您可能会注意到我在原始问题中强调了两点,并且我添加了额外的假设,即值非零

在C#中,你可以这样做:

int[, , , , ,] a=new int[6, 5, 6, 3, 4, 2];
int[, , , , , ,] b=new int[5, 7, 6, 6, 2, 3, 4];
Console.WriteLine(b.Length/a.Length);

请参阅?无论额外元素是什么,您只需将它们的长度分开即可了解它。

使用这些语句,我们不会将给定的整数序列作为值存储到数组中,而是存储为维度

无论给出的较短的整数序列如何,较长的整数应该只有一个额外的整数。因此,无论整数的顺序如何,没有额外的整数,这两个多维数组的总大小是相同的。额外的维度乘以较长的大小,并除以较短的大小,我们知道什么是额外的整数。

这个解决方案仅适用于我从你的问题中引用的这个特殊情况。您可能希望将其移植到Java。

这只是一个技巧,因为我认为问题本身就是一招。我们绝对不会将其视为生产解决方案。

答案 6 :(得分:1)

注意,使用O(n + m)表示法是错误的。只有一个大小参数是n(在渐近意义上,n和n + 1相等)。你应该说O(n)。 [对于m> n + 1,问题不同,也更具挑战性。]

正如其他人所指出的,这是最佳的,因为您必须阅读所有值。

你所能做的就是减少渐近常数。由于明显的解决方案已经非常有效,因此几乎没有改进的余地。 (10)中的单循环可能很难被击败。稍微展开它应该通过避免分支(稍微)来改善。

如果您的目标是纯粹的性能,那么您应该转向非便携式解决方案,例如矢量化(使用AXV指令,一次8个整数)以及多核或GPGPU上的并行化。在旧的脏C和64位处理器中,您可以将数据映射到64位整数的数组,并且xor元素一次映射两对;)

答案 7 :(得分:0)

我认为这与Matching nuts and bolts problem类似。

你可以在O(nlogn)中实现这一点。在这种情况下,不确定是否小于O(n + m)。

答案 8 :(得分:0)

根本没有更快的算法。问题中提出的是O(n)。解决此问题的任何算术“技巧”都需要至少读取两个数组的每个元素,因此我们保持在O(n)(或更差)。

O(n)的实际子集中的任何搜索策略(如O(log n))都需要排序数组或其他一些预构建排序结构(二叉树,哈希)。人类已知的所有排序算法平均至少为O(n * log n)(Quicksort,Hashsort),其比O(n)差。

因此,从数学的角度来看,没有更快的算法。可能会有一些代码优化,但它们无关紧要,因为运行时将随着数组的长度呈线性增长。