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

Posted

技术标签:

【中文标题】更快的算法来找到两个数组之间的唯一元素?【英文标题】:Faster algorithm to find unique element between two arrays? 【发布时间】:2013-10-12 18:55:35 【问题描述】:

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

注意:这个问题最初是伪代码和使用列表。我已经将它改编为 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)...对吗?

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

【问题讨论】:

使用官方计算“顺序”的规则,从表面上看,似乎不可能做得比 O(m+n) 更好。可以将两个循环合并为一个(对较短的长度进行,然后对较长的进行一次外部“迭代”),但是将执行相同数量的数组索引操作等——只有一点循环开销将被保存。 请试着让你的老师告诉你更快的解决方案,然后告诉我们 - 我真的不明白你怎么能比只看每个元素一次更快。 我想不出比您的解决方案更快的方法了。 o_O....当然,我无法证明您的解决方案是最快的,但老实说,我认为您有最快的方法来做到这一点而不会作弊(例如预先存储部分答案或将底层“集合”存储在内存中数组)。所以是的,我想支持@G.Bach 的评论。请让您的老师给出一个“更快”的解决方案,该解决方案不会通过预先计算的东西来作弊。编辑:Hot Licks 提出了一个很好的观点,即您可以压缩循环,但它本质上仍然是相同的算法。 如果数组包含负数,老师的解决方案不起作用。 我真的怀疑有什么方法可以提高算法的复杂性。从m = n + 1 开始,然后是O(n+m) --&gt; O(2n+1) --&gt; O(n)。由于n 是Big-O 表示法中的输入长度,算法的复杂度不能低于O(n),除非它们有一些预处理输入或数据结构可以使用。另一方面,很可能优化或改进 代码 效率,尽管我认为您的方法可能接近最佳。 【参考方案1】:

这可能是您在 Java 中使用 cmets 中 HotLick 的建议可以做到的最快速度。它假设 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 的最快方法。这意味着它完全依赖于语言实现,您必须进行一些测试和尝试才能在您使用的任何实现中获得真正“最快”的解决方案,因为整体算法不会改变。

【讨论】:

当然,这与原始的数组索引操作数和^操作数相同。仅减少了循环开销。 但作业减少了 50%。 +1 我(也想到了这个)认为它会是最快的。使用 XOR 而不是加法的重要一点是您不必处理整数溢出(如果元素是大数)。您可能会发现长手ret = ret ^ A[i] ^ B[i]; 更快。两者并不完全等价。 到目前为止,我最喜欢这个答案。这只是一个很小的改进,但即使是这样。出于好奇@Bohemian,速记的优势是什么?我认为 ^= 在解释/编译时无论如何都会扩展到普通的。 不同之处在于投射。简写扩展为对变量类型的强制转换。它允许您在没有显式转换的情况下使用混合类型的速记,甚至更大的类型(例如 long)。尽管编译器可能(希望)省略强制转换,因为类型相同。我还没有检查字节码。但速记不会变慢。【参考方案2】:

好的,我们开始...向任何期待更快解决方案的人道歉。原来我的老师和我玩得很开心,我完全没有理解他所说的意思。

我应该首先澄清我的意思:

他暗示有一种更更快的方法

我们谈话的要点是这样的:他说我的 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 个元素,那么我们的算法只检查长度为 2n - 1c 子数组怎么样?这仍然是线性复杂性,但如果我们能做到这一点,那么也许我们可以进一步改进它。

所以,让我们完全随机选择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 访问,也没有足够的信息来解决问题。所以最优解是线性的。

当然,真正的证明会使用归纳法而不是逐例证明,但我会把它留给其他人 :)

【讨论】:

reduce 解决方案做得很好。 :) 单线解决方案看起来总是很整洁。 如果其中一个数组具有唯一值,则您正在寻找恰好出现一次的元素,而不仅仅是奇数次。最快的方法是所有 O(n) 方法集合中具有最小系数因子的成员。现在你有可能找到一个 O(n-1) 的情况。看看你是否能发现这样一个案例或一组案例。 :)【参考方案3】:

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

【讨论】:

值得注意的是,(哈希)映射的开销可能远远超过好处(我们知道abs(m-n) = 1,并且n 映射插入很可能比2n 算术运算慢) . 当然,存储计数需要一个与源数组元素的最大值一样大的数组,而哈希映射很少是真正的 O(n) 插入。 嗯...这无论如何都需要使用 O(m+n) 开销进行结构修改。这不是一个真正的解决方案。我相当肯定你在这方面不能比 O(m+n) 做得更好。【参考方案4】:

有点快一点:

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

-阿尔。

【讨论】:

糟糕——这只是 Hot Licks 所说的加长版。【参考方案5】:

假设只添加了一个元素,并且数组一开始是相同的,你可以达到 O(log(base 2) n)。

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

剩下的就是基本的二分查找。检查中间元素,确定哪个部门必须有答案,然后对该部门进行子搜索。

【讨论】:

我认为给出的示例使您的假设无效,即除了附加元素之外,数组元素具有相同的顺序。 @StephenC 感谢您的观察。我同意这个例子不适合算法;但是,如果教授真的相信有更快的方法,那么教授的例子可能与学生的例子不同。 我的信念是要么“老师”是对调优 Java 代码没有太多实际理解的人,或者 OP误解了老师所说的……“快得多”真的“可能快一点”。鉴于整个上下文(问题、约束、示例解决方案),对输入进行预排序确实没有意义。【参考方案6】:

假设有两个未排序的整数数组 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。

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

【讨论】:

由于整数除法,比率不会为 1 吗?你不应该得到7吗?我不确定我明白为什么会这样。 7.我修改了一些细节。 哦,我明白了。但这不是因为巨大的内存需求而导致效率低下吗? @templatetypedef: 因为它只是一个trick .. 一个trick 的问题带来了一个trick 的答案。 @templatetypedef:顺便说一下,问题中没有提到内存分配的限制。对于算法问题,我想这就是OP的老师希望他发现的。【参考方案7】:

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

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

你所能做的就是减少渐近常数。几乎没有改进的空间,因为显而易见的解决方案已经非常有效。 (10)中的单循环可能很难被击败。通过避免分支,展开它应该(略微)改进。

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

【讨论】:

【参考方案8】:

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

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

【讨论】:

O(n+m) 在这种情况下等于 O(n),所以 O(nlogn) 会慢很多。【参考方案9】:

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

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

因此,从数学的角度来看,没有更快的算法。可能会有一些代码优化,但它们在大规模上并不重要,因为运行时会随着数组的长度线性增长。

【讨论】:

以上是关于更快的算法来找到两个数组之间的唯一元素?的主要内容,如果未能解决你的问题,请参考以下文章

ArrayIndexOutOfBoundsException,同时找到数组中两个连续元素之间的最大差异

分而治之以找到二维数组中两个有序元素之间的最大差异

在非常大的数组中找到 N 个唯一随机数的最佳算法

分而治之 - 在包含唯一元素的两个大小相等的数组之间找到中位数?

C ++:2个数组之间的差异

最大和最小差(贪心算法)