更快的算法来找到两个数组之间的唯一元素?
Posted
技术标签:
【中文标题】更快的算法来找到两个数组之间的唯一元素?【英文标题】:Faster algorithm to find unique element between two arrays? 【发布时间】:2013-10-12 18:55:35 【问题描述】:编辑:对于这个问题的新手,我已经发布了一个答案来澄清发生了什么。接受的答案是我认为最能回答我最初发布的问题的答案,但有关更多详细信息,请参阅我的答案。
注意:这个问题最初是伪代码和使用列表。我已经将它改编为 Java 和数组。因此,尽管我希望看到任何使用 Java 特定技巧(或任何语言中的技巧!)的解决方案,但请记住,原始问题与语言无关。
问题
假设有两个未排序的整数数组a
和b
,允许元素重复。它们是相同的(就包含的元素而言)除了其中一个数组有一个额外的元素。举个例子:
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) --> O(2n+1) --> 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)
一个非常非正式的“证明”
让我们从问题中的原始两个数组开始,a
和 b
:
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 - 1
的 c
子数组怎么样?这仍然是线性复杂性,但如果我们能做到这一点,那么也许我们可以进一步改进它。
所以,让我们完全随机选择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,同时找到数组中两个连续元素之间的最大差异