编辑:对于这个问题的新手,我已经发布了一个答案,说明发生了什么。接受的答案是我认为最能回答我最初发布的问题的答案,但有关详细信息,请参阅我的答案。
注意:此问题最初是伪代码和使用的列表。我已将它改编为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)......对吧?
那么有更快的方法吗?如果是这样,它是什么?
答案 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)
让我们从问题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个元素怎么样,以便我们的算法只检查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)差。
因此,从数学的角度来看,没有更快的算法。可能会有一些代码优化,但它们无关紧要,因为运行时将随着数组的长度呈线性增长。