在数组中查找三个元素之和最接近给定数字
Posted
技术标签:
【中文标题】在数组中查找三个元素之和最接近给定数字【英文标题】:Finding three elements in an array whose sum is closest to a given number 【发布时间】:2011-01-05 10:27:19 【问题描述】:给定一个整数数组,A1, A2, ..., An,包括负数和正数,以及另一个整数 S。现在我们需要在数组中找到三个不同的整数,它们的和最接近给定的整数 S。如果存在多个解,则任何一个都可以。
您可以假设所有整数都在 int32_t 范围内,并且在计算总和时不会发生算术溢出。 S 没什么特别的,只是一个随机选择的数字。
除了蛮力搜索之外,有没有其他有效的算法来找到三个整数?
【问题讨论】:
如果您正在寻找等于一个数字(而不是最接近的数字)的总和,这将是 the 3SUM problem。 【参考方案1】:除了蛮力搜索之外,还有什么有效的算法可以找到三个整数吗?
是的;我们可以在 O(n2) 时间内解决这个问题!首先,考虑到您的问题P
可以用稍微不同的方式等效地表述,从而消除对“目标值”的需要:
原始问题
P
: 给定一个数组A
的n
整数和一个目标值S
,是否存在来自A
的三元组和@ 987654327@?修改问题
P'
: 给定一个数组A
n
整数,是否存在来自A
的三元组总和为零?
请注意,您可以通过从A
中的每个元素中减去您的 S/3 来从 P
中解决此版本的问题 P'
,但现在您不再需要目标值了。
显然,如果我们简单地测试所有可能的 3 元组,我们将在 O(n3) 内解决问题——这就是蛮力基线。有没有可能做得更好?如果我们以更智能的方式选择元组会怎样?
首先,我们花费一些时间对数组进行排序,这使我们的初始损失为 O(n log n)。现在我们执行这个算法:
for (i in 1..n-2)
j = i+1 // Start right after i.
k = n // Start at the end of the array.
while (k >= j)
// We got a match! All done.
if (A[i] + A[j] + A[k] == 0) return (A[i], A[j], A[k])
// We didn't match. Let's try to get a little closer:
// If the sum was too big, decrement k.
// If the sum was too small, increment j.
(A[i] + A[j] + A[k] > 0) ? k-- : j++
// When the while-loop finishes, j and k have passed each other and there's
// no more useful combinations that we can try with this i.
该算法通过将三个指针i
、j
和k
放置在数组中的不同点来工作。 i
从头开始,慢慢地走到最后。 k
指向最后一个元素。 j
指向 i
开始的位置。我们反复尝试对各自索引处的元素求和,每次发生以下情况时:
j
移近末尾以选择下一个最大的数字。
总和太大了。将k
移近开头以选择下一个最小的数字。
对于每个i
,j
和k
的指针会逐渐靠近。最终它们会互相通过,此时我们不需要为i
尝试其他任何东西,因为我们会以不同的顺序对相同的元素求和。在那之后,我们尝试下一个i
并重复。
最终,我们要么穷尽有用的可能性,要么找到解决方案。您可以看到这是 O(n2),因为我们执行了外循环 O(n) 次,而我们执行了内循环 O(n) 次。如果您真的很喜欢,可以通过将每个整数表示为位向量并执行快速傅立叶变换,以二次方的方式执行此操作,但这超出了此答案的范围。
注意:因为这是一道面试题,所以我这里有点作弊:这个算法允许多次选择同一个元素。也就是说,(-1, -1, 2) 和 (0, 0, 0) 一样是有效的解决方案。如标题所述,它还只找到 exact 答案,而不是最接近的答案。作为对读者的练习,我将让您弄清楚如何使其仅使用不同的元素(但这是一个非常简单的更改)和确切的答案(这也是一个简单的更改)。
【讨论】:
该算法似乎只能找到 等于 到 S 的 3 元组,而不是 最接近 到 S。 ZelluX:正如我在笔记中提到的,我不想透露太多,因为这是一个面试问题。不过,希望您能看到如何修改它,以便它为您提供最接近的答案。 (提示:一种方法是跟踪迄今为止最接近的答案,如果找到更好的答案,则将其覆盖。) 如果我们不修改问题陈述会怎样,而是搜索 aj 和 ak 的总和为 ai +S。 @ZelluX:这类似于合并排序的工作方式(这就是我第一次点击它的方式)。内部循环试图做的是证明 A[j] 或 A[k] 不能成为 any 令人满意的解决方案的一部分。任何时候的问题是:“是否有任何对 j' >= j 和 k' 每个 k' ... 而第二项 (A[k']) 将与 A[k] 相同甚至更低。所以在这种情况下,我们已经证明 A[j] 不能参与 any 满足的总和——所以我们不妨丢弃它!我们通过设置 j = j+1 并重新开始来做到这一点(尽管考虑以递归方式解决较小的子问题可能会有所帮助)。同样,如果总和 A[j] + A[k] 太高,那么我们知道 A[j'] + A[k] 对于 every j' >= j 也一定太高,因为 A[j'] 必须至少和 A[j] 一样大,而且我们已经太高了。这意味着我们可以通过设置 k = k-1 并重新开始来安全地丢弃 A[k]。【参考方案2】:当然这是一个更好的解决方案,因为它更容易阅读,因此更不容易出错。唯一的问题是,我们需要添加几行代码来避免一个元素的多选。
另一个 O(n^2) 解决方案(通过使用哈希集)。
// K is the sum that we are looking for
for i 1..n
int s1 = K - A[i]
for j 1..i
int s2 = s1 - A[j]
if (set.contains(s2))
print the numbers
set.add(A[i])
【讨论】:
缺点是 O(N) 存储,而不是就地存储。 使用哈希集并不是严格的 O(n^2),因为哈希集在极少数情况下会退化,导致最多线性查找时间。 @Charles - John 的解决方案也需要 O(N) 空间,因为您在排序时更改了原始数组。这意味着调用者在使用该函数之前可能需要一个防御性副本。 我认为你的算法有错误。s2
可能是已选择的元素。例如。如果数组是0,1,2
而K
是2
,则不应该有答案。我认为你的算法会输出0,1,1
,这显然是不正确的。【参考方案3】:
John Feminella 的 解决方案存在错误。
排队
if (A[i] + A[j] + A[k] == 0) return (A[i], A[j], A[k])
我们需要检查 i,j,k 是否都是不同的。否则,如果我的目标元素是 6
并且我的输入数组包含 3,2,1,7,9,0,-4,6
。如果我打印出总和为 6 的元组,那么我也会得到 0,0,6
作为输出。为了避免这种情况,我们需要以这种方式修改条件。
if ((A[i] + A[j] + A[k] == 0) && (i!=j) && (i!=k) && (j!=k)) return (A[i], A[j], A[k])
【讨论】:
John Feminella 解决方案只是提出解决问题的算法,他还指定他的解决方案不适用于不同的数字条件,您必须稍微修改上面的代码,他留给读者. 实际上,i 永远不会是 j,因为你总是从 j = i + 1 开始。你应该检查的唯一真实条件是是否 j == k。但是,通过将 while 循环设置为 j 这似乎不是对问题的回答,而是对 John Feminella 回答的评论。【参考方案4】:这样的事情怎么样,O(n^2)
for(each ele in the sorted array)
ele = arr[i] - YOUR_NUMBER;
let front be the pointer to the front of the array;
let rear be the pointer to the rear element of the array.;
// till front is not greater than rear.
while(front <= rear)
if(*front + *rear == ele)
print "Found triplet "<<*front<<","<<*rear<<","<<ele<<endl;
break;
else
// sum is > ele, so we need to decrease the sum by decrementing rear pointer.
if((*front + *rear) > ele)
decrement rear pointer.
// sum is < ele, so we need to increase the sum by incrementing the front pointer.
else
increment front pointer.
这将确定 3 个元素的总和是否完全等于您的数字。如果您想要最接近,您可以修改它以记住最小的增量(当前三元组数量之间的差异),并在最后打印与最小增量相对应的三元组。
【讨论】:
如果你想找到k个元素来得到总和,复杂度是多少?你是怎么处理的? 使用这种方法,k >= 2 时,k 个元素的复杂度为 O(n^(k-1))。您需要为每个额外的求和添加一个外循环。【参考方案5】:请注意,我们有一个排序数组。此解决方案类似于 John 的解决方案,只是它查找总和并且不重复相同的元素。
#include <stdio.h>;
int checkForSum (int arr[], int len, int sum) //arr is sorted
int i;
for (i = 0; i < len ; i++)
int left = i + 1;
int right = len - 1;
while (right > left)
printf ("values are %d %d %d\n", arr[i], arr[left], arr[right]);
if (arr[right] + arr[left] + arr[i] - sum == 0)
printf ("final values are %d %d %d\n", arr[i], arr[left], arr[right]);
return 1;
if (arr[right] + arr[left] + arr[i] - sum > 0)
right--;
else
left++;
return -1;
int main (int argc, char **argv)
int arr[] = -99, -45, -6, -5, 0, 9, 12, 16, 21, 29;
int sum = 4;
printf ("check for sum %d in arr is %d\n", sum, checkForSum(arr, 10, sum));
【讨论】:
需要计算a[r] + a[l] + a[i] - sum
的绝对差。试试arr = [-1, 2, 1, -4] sum = 1
。【参考方案6】:
这是 C++ 代码:
bool FindSumZero(int a[], int n, int& x, int& y, int& z)
if (n < 3)
return false;
sort(a, a+n);
for (int i = 0; i < n-2; ++i)
int j = i+1;
int k = n-1;
while (k >= j)
int s = a[i]+a[j]+a[k];
if (s == 0 && i != j && j != k && k != i)
x = a[i], y = a[j], z = a[k];
return true;
if (s > 0)
--k;
else
++j;
return false;
【讨论】:
【参考方案7】:非常简单的 N^2*logN 解决方案:对输入数组进行排序,然后遍历所有对 Ai、Aj(N^2 次),并为每对检查 (S - Ai - Aj) 是否在数组中(logN 次)。
另一个 O(S*N) 解决方案使用经典的dynamic programming 方法。
简而言之:
创建一个二维数组 V[4][S + 1]。以这样的方式填写:
V[0][0] = 1, V[0][x] = 0;
V1[Ai]= 1 对于任何 i,V1[x] = 0 对于所有其他 x
V[2][Ai + Aj]= 1,对于任何 i、j。对于所有其他 x,V[2][x] = 0
V[3][任意 3 个元素的总和] = 1。
要填充它,遍历 Ai,对于每个 Ai 从右到左遍历数组。
【讨论】:
对第一个算法稍作改动。如果元素不存在,那么在二分查找结束时,我们必须查看左边、当前和右边的元素看看哪一个给出了最接近的结果。 数组太大,不是 O(s*N) 。这一步是 O(N^2 ) : V[2][Ai + Aj]= 1,对于任何 i, j。 V[2][x] = 0 对于所有其他 x 。【参考方案8】:这可以在 O(n log (n)) 中有效地解决,如下所示。 我正在给出解决方案,它告诉任何三个数字的总和是否等于给定的数字。
import java.util.*;
public class MainClass
public static void main(String[] args)
int[] a = -1, 0, 1, 2, 3, 5, -4, 6;
System.out.println(((Object) isThreeSumEqualsTarget(a, 11)).toString());
public static boolean isThreeSumEqualsTarget(int[] array, int targetNumber)
//O(n log (n))
Arrays.sort(array);
System.out.println(Arrays.toString(array));
int leftIndex = 0;
int rightIndex = array.length - 1;
//O(n)
while (leftIndex + 1 < rightIndex - 1)
//take sum of two corners
int sum = array[leftIndex] + array[rightIndex];
//find if the number matches exactly. Or get the closest match.
//here i am not storing closest matches. You can do it for yourself.
//O(log (n)) complexity
int binarySearchClosestIndex = binarySearch(leftIndex + 1, rightIndex - 1, targetNumber - sum, array);
//if exact match is found, we already got the answer
if (-1 == binarySearchClosestIndex)
System.out.println(("combo is " + array[leftIndex] + ", " + array[rightIndex] + ", " + (targetNumber - sum)));
return true;
//if exact match is not found, we have to decide which pointer, left or right to move inwards
//we are here means , either we are on left end or on right end
else
//we ended up searching towards start of array,i.e. we need a lesser sum , lets move inwards from right
//we need to have a lower sum, lets decrease right index
if (binarySearchClosestIndex == leftIndex + 1)
rightIndex--;
else if (binarySearchClosestIndex == rightIndex - 1)
//we need to have a higher sum, lets decrease right index
leftIndex++;
return false;
public static int binarySearch(int start, int end, int elem, int[] array)
int mid = 0;
while (start <= end)
mid = (start + end) >>> 1;
if (elem < array[mid])
end = mid - 1;
else if (elem > array[mid])
start = mid + 1;
else
//exact match case
//Suits more for this particular case to return -1
return -1;
return mid;
【讨论】:
我认为这行不通。当然,当中间的所有元素都严格小于或大于您想要的数字时,您有两个简单的案例来说明如何推进leftIndex
或 rightIndex
。但是当二分搜索在中间某处停止时的情况呢?您需要检查 both 分支(其中rightIndex--
和leftIndex++
)。在您的解决方案中,您只需忽略这种情况。但我认为没有办法克服这个问题。【参考方案9】:
减少:我认为@John Feminella 解决方案 O(n2) 是最优雅的。我们仍然可以减少在其中搜索元组的 A[n]。通过观察 A[k] 使得所有元素都在 A[0] - A[k] 中,当我们的搜索数组很大并且 SUM (s) 非常小时。
A[0] 是最小值:- 升序排序数组。
s = 2A[0] + A[k] :给定 s 和 A[],我们可以在 log(n) 时间内使用二进制搜索找到 A[k]。
【讨论】:
【参考方案10】:这是java中的程序,O(N^2)
import java.util.Stack;
public class GetTripletPair
/** Set a value for target sum */
public static final int TARGET_SUM = 32;
private Stack<Integer> stack = new Stack<Integer>();
/** Store the sum of current elements stored in stack */
private int sumInStack = 0;
private int count =0 ;
public void populateSubset(int[] data, int fromIndex, int endIndex)
/*
* Check if sum of elements stored in Stack is equal to the expected
* target sum.
*
* If so, call print method to print the candidate satisfied result.
*/
if (sumInStack == TARGET_SUM)
print(stack);
for (int currentIndex = fromIndex; currentIndex < endIndex; currentIndex++)
if (sumInStack + data[currentIndex] <= TARGET_SUM)
++count;
stack.push(data[currentIndex]);
sumInStack += data[currentIndex];
/*
* Make the currentIndex +1, and then use recursion to proceed
* further.
*/
populateSubset(data, currentIndex + 1, endIndex);
--count;
sumInStack -= (Integer) stack.pop();
else
return;
/**
* Print satisfied result. i.e. 15 = 4+6+5
*/
private void print(Stack<Integer> stack)
StringBuilder sb = new StringBuilder();
sb.append(TARGET_SUM).append(" = ");
for (Integer i : stack)
sb.append(i).append("+");
System.out.println(sb.deleteCharAt(sb.length() - 1).toString());
private static final int[] DATA = 4,13,14,15,17;
public static void main(String[] args)
GetAllSubsetByStack get = new GetAllSubsetByStack();
get.populateSubset(DATA, 0, DATA.length);
【讨论】:
不错的方法,但我无法理解将结果数量限制为三元组的地步。例如,考虑输入:[1,11,3,4,5,6,7,8,2] 和总和 12,从您的解决方案看来 [1, 11] [4,8] [1,4, 5,2] 等都可以。【参考方案11】:通过扩展 2-sum 问题并稍作修改,可以在 O(n^2) 内解决该问题。A 是包含元素的向量,B 是所需的总和。
int Solution::threeSumClosest(vector &A, int B)
sort(A.begin(),A.end());
int k=0,i,j,closest,val;int diff=INT_MAX;
while(k<A.size()-2)
i=k+1;
j=A.size()-1;
while(i<j)
val=A[i]+A[j]+A[k];
if(val==B) return B;
if(abs(B-val)<diff)
diff=abs(B-val);
closest=val;
if(B>val)
++i;
if(B<val)
--j;
++k;
return closest;
【讨论】:
【参考方案12】:这是 Python3 代码
class Solution:
def threeSumClosest(self, nums: List[int], target: int) -> int:
result = set()
nums.sort()
L = len(nums)
for i in range(L):
if i > 0 and nums[i] == nums[i-1]:
continue
for j in range(i+1,L):
if j > i + 1 and nums[j] == nums[j-1]:
continue
l = j+1
r = L -1
while l <= r:
sum = nums[i] + nums[j] + nums[l]
result.add(sum)
l = l + 1
while l<=r and nums[l] == nums[l-1]:
l = l + 1
result = list(result)
min = result[0]
for i in range(1,len(result)):
if abs(target - result[i]) < abs(target - min):
min = result[i]
return min
【讨论】:
【参考方案13】:另一种提前检查并失败的解决方案:
public boolean solution(int[] input)
int length = input.length;
if (length < 3)
return false;
// x + y + z = 0 => -z = x + y
final Set<Integer> z = new HashSet<>(length);
int zeroCounter = 0, sum; // if they're more than 3 zeros we're done
for (int element : input)
if (element < 0)
z.add(element);
if (element == 0)
++zeroCounter;
if (zeroCounter >= 3)
return true;
if (z.isEmpty() || z.size() == length || (z.size() + zeroCounter == length))
return false;
else
for (int x = 0; x < length; ++x)
for (int y = x + 1; y < length; ++y)
sum = input[x] + input[y]; // will use it as inverse addition
if (sum < 0)
continue;
if (z.contains(sum * -1))
return true;
return false;
我在这里添加了一些单元测试:GivenArrayReturnTrueIfThreeElementsSumZeroTest。
如果集合占用了太多空间,我可以轻松使用 java.util.BitSet,它会使用 O(n/w) space。
【讨论】:
【参考方案14】:获取这三个元素的程序。我刚刚对数组/列表进行了排序,它们根据每个三元组更新了minCloseness
。
public int[] threeSumClosest(ArrayList<Integer> A, int B)
Collections.sort(A);
int ansSum = 0;
int ans[] = new int[3];
int minCloseness = Integer.MAX_VALUE;
for (int i = 0; i < A.size()-2; i++)
int j = i+1;
int k = A.size()-1;
while (j < k)
int sum = A.get(i) + A.get(j) + A.get(k);
if (sum < B)
j++;
else
k--;
if (minCloseness > Math.abs(sum - B))
minCloseness = Math.abs(sum - B);
ans[0] = A.get(i); ans[1] = A.get(j); ans[2] = A.get(k);
return ans;
【讨论】:
【参考方案15】:我在 n^3 中做了这个,我的伪代码如下;
//创建一个hashMap,key为Integer,value为ArrayList //使用for循环遍历列表,对于列表中的每个值从下一个值开始再次迭代;
for (int i=0; i<=arr.length-1 ; i++)
for (int j=i+1; j<=arr.length-1;j++)
//如果 arr[i] 和 arr[j] 的总和小于期望的总和,那么就有可能找到第三个数字,所以再做一个 for 循环
if (arr[i]+arr[j] < sum)
for (int k= j+1; k<=arr.length-1;k++)
//在这种情况下,我们现在正在寻找第三个值;如果总和 arr[i] 和 arr[j] 和 arr[k] 是所需的总和,然后将它们添加到 HashMap,方法是将 arr[i] 设为键,然后将 arr[j] 和 arr[k] 添加到 ArrayList 中该键的值
if (arr[i]+arr[j]+arr[k] == sum)
map.put(arr[i],new ArrayList<Integer>());
map.get(arr[i]).add(arr[j]);
map.get(arr[i]).add(arr[k]);
在此之后,您现在有一个字典,其中包含表示三个值的所有条目添加到所需的总和。使用 HashMap 函数提取所有这些条目。这非常有效。
【讨论】:
以上是关于在数组中查找三个元素之和最接近给定数字的主要内容,如果未能解决你的问题,请参考以下文章
用Java编写,在给出的数字里面找三个数字的和等于或者最接近513.91的。在线等,用上数组最好。