确定 Set S 中是不是存在两个元素之和正好为 x - 正确解?
Posted
技术标签:
【中文标题】确定 Set S 中是不是存在两个元素之和正好为 x - 正确解?【英文标题】:Determine whether or not there exist two elements in Set S whose sum is exactly x - correct solution?确定 Set S 中是否存在两个元素之和正好为 x - 正确解? 【发布时间】:2011-01-11 10:24:10 【问题描述】:取自算法简介
描述一个 Θ(n lg n) 时间算法 即,给定一个包含 n 个整数的集合 S 和 另一个整数 x,确定是否 S中是否存在两个元素 其和正好是 x。
这是迄今为止我用 Java 实现的最佳解决方案:
public static boolean test(int[] a, int val)
mergeSort(a);
for (int i = 0; i < a.length - 1; ++i)
int diff = (val >= a[i]) ? val - a[i] : a[i] - val;
if (Arrays.binarySearch(a, i, a.length, diff) >= 0)
return true;
return false;
现在我的第一个问题是:这是一个正确的解决方案吗?根据我的理解,mergeSort 应该在 O(n lg n) 中执行排序,循环应该采用 O(n lg n) (n 进行迭代乘以 O(lg n) 进行二分搜索,得到 O(2n lg n),所以应该是正确的。
我的第二个问题是:有没有更好的解决方案?对数组进行排序是否必要?
【问题讨论】:
你可以使用哈希集吗?如果是这样,那么你可以得到 O(n),见下文。(val >= a[i]) ? val - a[i] : a[i] - val
这是Math.abs()
的用途:)
是的,Math.abs
就是为此,但我想说的更多:这里不需要。事实上是错误的。 diff
必须始终赋值为 val-a[i]
,无论此差异是正数还是负数。
正如下面一些答案中所建议的那样,在对输入进行一些假设的情况下,有一个更好的 O(n) sol'n。
是的,排序似乎是必要的,但是一旦你有了一个排序列表,你就可以改进。您可以考虑以下方法,而不是天真地使用二进制搜索:将指针 l,r 初始化为列表的开头和结尾。检查 a[l]+a[r] = f;如果 f > x 则 r--;否则如果 f您的解决方案似乎很好。是的,您需要排序,因为它是二进制搜索的先决条件。您可以对您的逻辑稍作修改,如下所示:
public static boolean test(int[] a, int val)
Arrays.sort(a);
int i = 0; // index of first element.
int j = a.length - 1; // index of last element.
while(i<j)
// check if the sum of elements at index i and j equals val, if yes we are done.
if(a[i]+a[j] == val)
return true;
// else if sum if more than val, decrease the sum.
else if(a[i]+a[j] > val)
j--;
// else if sum is less than val, increase the sum.
else
i++;
// failed to find any such pair..return false.
return false;
【讨论】:
【参考方案2】:还有另一种非常快速的解决方案:想象一下,您必须在 Java 中解决大约 10 亿个整数的问题。你知道在 Java 中整数从 -2**31+1
到 +2**31
。
使用2**32
十亿位创建一个数组(500 MB,在当今的硬件上微不足道)。
遍历你的集合:如果你有一个整数,将对应的位设置为 1。
O(n) 到目前为止。
再次遍历您的集合:对于每个值,检查您是否在“当前 val - x”处设置了位。
如果有,则返回 true。
当然,它需要 500 MB 内存。
但是,如果你有 10 亿个整数来解决这个问题,这将绕过任何其他 O(n log n) 解决方案。
O(n)。
【讨论】:
所以您分配并清零 500 MB 内存来解决 n=10000 的问题? 我引用这个答案:“想象一下,你必须在 Java 中解决这个问题大约 10 亿个整数”。我认为很明显,当 OldEnthusiast 说“非常快”时,他的意思是对于大问题来说非常快,即低复杂性。对于 n=1,这不是最有效的解决方案:return false;
是 ;-)
-1。这不适用于所有输入,对于小输入,隐藏常数很大。
@Moron:这个问题也没有说明内存限制,所以在列表中搜索最大的n
,然后创建一个该大小的数组并运行这个算法。那是O(k)
(其中 k 是最大值),由于数组的归零。诸如此类的算法被称为伪多项式 (en.wikipedia.org/wiki/Pseudo-polynomial_time),虽然在现实世界中的某些 (许多) 情况下很有帮助,但它并不是解决此问题的方法,因为 k
是与n
无关。
这行不通。假设 X 是 10,而您正在查看数字 5。10-5 = 5,这意味着数组中存在 5,但是,您只有一个 5。【参考方案3】:
这是正确的;您的算法将在 O(n lg n) 时间内运行。
有一个更好的解决方案:您计算差异的逻辑不正确。不管a[i]
大于还是小于val
,你仍然需要diff为val - a[i]
。
【讨论】:
是的!我正在阅读这个帖子,想知道是否没有人注意到差异必须始终为va - a[i]
。【参考方案4】:
这是一个使用哈希集的 O(n) 解决方案:
public static boolean test(int[] a, int val)
Set<Integer> set = new HashSet<Integer>();
// Look for val/2 in the array
int c = 0;
for(int n : a)
if(n*2 == val)
++c
if(c >= 2)
return true; // Yes! - Found more than one
// Now look pairs not including val/2
set.addAll(Arrays.asList(a));
for (int n : a)
if(n*2 == val)
continue;
if(set.contains(val - n))
return true;
return false;
【讨论】:
@meriton 我不关注你。出于所有实际目的(尤其是在这类问题中),哈希表查找可以被视为 O(1)。如果我们谈论的是确切时间,那么您提出的点数可能是可行的,但如果 n 足够大,O(n) 算法最终将击败 O(nlogn)。 如果我现在回复说你的评论也没有意义怎么办?你认为这个讨论会成功吗?澄清一下:HashSet 提供 O(1) 查找的证明假设哈希值分布良好,这取决于您的输入。如果输入中的所有数字都在 hashSet 中获得相同的存储桶,则 HashSet 将非常慢(java.util.HashSet 使用 LinkedList 来保存存储桶的内容)。通常,您的输入会很好地分配,但并非总是如此。因此,hashSet 不能保证恒定的最坏情况查找复杂度。 @Itay: 是的,hashCode(i)==i
,但是 HashSet 不使用 2^32 个桶,它使用的比这个少。所以真正使用的散列函数是桶数的模数。 AFAIK Java HashSet 重新散列策略不能保证每个桶的最坏情况 O(1) 个元素(除了在微不足道的意义上,几乎所有对有界大小整数所做的事情都是 O(1)),因为它基于负载仅考虑因素,而不是表中最差存储桶的占用率。
@OldEnthusiast:功勋是对的,这是不是 O(n)
,因为哈希表查找不是最坏情况O(1)
(这意味着它们不是最坏的-案例Θ(1)
),它们是普通案例Θ(k/n)
(见en.wikipedia.org/wiki/Hash_table#Performance_analysis)。当一个问题说“找到一个 O(something)-time 算法”时, 它总是意味着最坏的情况。但是,我不会给出 -1,因为虽然它不是这个特定问题的答案,但它是如果我在现实世界中遇到这个问题时我会使用的解决方案。跨度>
@danben:我明白;但是当他们没有在问题中声明 "worst-case," "average-case," 或 "best-case" 时一个算法类,它总是隐含地理解问题是要求“最坏情况”。这个答案在一般情况下非常好,但在最坏情况下却不是,这就是混淆的地方来自,因为 OP 没有明确说明其中之一。【参考方案5】:
一个简单的解决方案是,在排序后,将指针从数组的两端向下移动,寻找总和为 x 的对。如果总和太高,则减少右指针。如果太低,增加左边的一个。如果指针交叉,答案是否定的。
【讨论】:
【参考方案6】:我确实认为我在您的实现中发现了一个小错误,但测试应该很快就会发现。
该方法看起来有效,并且会达到预期的性能。您可以通过将迭代二进制搜索替换为对数组的扫描来简化它,实际上将二进制搜索替换为线性搜索,该线性搜索在先前线性搜索停止的地方恢复:
int j = a.length - 1;
for (int i = 0; i < a.length; i++)
while (a[i] + a[j] > val)
j--;
if (a[i] + a[j] == val)
// heureka!
这一步是O(n)。 (证明这一点留给你练习。)当然,整个算法仍然需要 O(n log n) 来进行归并排序。
【讨论】:
heureka 是启发式和 eureka 的混合体吗? @Bishiboosh:不,它是希腊词的德语音译。我不知道英文音译掉了H。你学到的堆栈溢出的东西...... :-)【参考方案7】:您的分析是正确的,是的,您必须对数组进行排序,否则二进制搜索不起作用。
【讨论】:
【参考方案8】:这是另一种解决方案,通过在合并排序中添加更多条件。
public static void divide(int array[], int start, int end, int sum)
if (array.length < 2 || (start >= end))
return;
int mid = (start + end) >> 1; //[p+r/2]
//divide
if (start < end)
divide(array, start, mid, sum);
divide(array, mid + 1, end, sum);
checkSum(array, start, mid, end, sum);
private static void checkSum(int[] array, int str, int mid, int end, int sum)
int lsize = mid - str + 1;
int rsize = end - mid;
int[] l = new int[lsize]; //init
int[] r = new int[rsize]; //init
//copy L
for (int i = str; i <= mid; ++i)
l[i-str] = array[i];
//copy R
for (int j = mid + 1; j <= end; ++j)
r[j - mid - 1] = array[j];
//SORT MERGE
int i = 0, j = 0, k=str;
while ((i < l.length) && (j < r.length) && (k <= end))
//sum-x-in-Set modification
if(sum == l[i] + r[j])
System.out.println("THE SUM CAN BE OBTAINED with the values" + l[i] + " " + r[j]);
if (l[i] < r[j])
array[k++] = l[i++];
else
array[k++] = r[j++];
//left over
while (i < l.length && k <= end)
array[k++] = l[i++];
//sum-x-in-Set modification
for(int x=i+1; x < l.length; ++x)
if(sum == l[i] + l[x])
System.out.println("THE SUM CAN BE OBTAINED with the values" + l[i] + " " + l[x]);
while (j < r.length && k <= end)
array[k++] = r[j++];
//sum-x-in-Set modification
for(int x=j+1; x < r.length; ++x)
if(sum == r[j] + r[x])
System.out.println("THE SUM CAN BE OBTAINED with the values" + r[j] + " " + r[x]);
但是这个算法的复杂度还是不等于THETA(nlogn)
【讨论】:
以上是关于确定 Set S 中是不是存在两个元素之和正好为 x - 正确解?的主要内容,如果未能解决你的问题,请参考以下文章
实现一个函数, // 判断一个给定整数数组中是否存在某两个元素之和恰好等于一个给定值 k, // 存在则返回 true,否则返回 false。