在数组中查找其和最接近给定数字的三个元素

给定一个整数数组,A1,A2,... ,AN,包括负数和正数,以及另一个整数 S。现在我们需要在数组中找到三个不同的整数,它们的和最接近给定的整数 S。如果存在多个解决方案,则其中任何一个都可以。

你可以假设所有的整数都在 int32 _ t 范围内,并且在计算和的时候不会出现任何算术溢出。S 没什么特别的,只是一个随机选择的数字。

除了蛮力搜索,还有什么有效的算法可以找到这三个整数吗?

134804 次浏览

非常简单的 N ^ 2 * logN 解决方案: 对输入数组进行排序,然后遍历所有对 A,AJ(N ^ 2时间) ,并对每个对检查(S-A-AJ)是否在数组中(logN 时间)。

另一个 O (S * N)解使用经典的 动态程序设计方法。

简而言之:

创建一个2-d 数组 V [4][ S + 1]。按照这样的方式填充它:

V [0][0] = 1,V [0][ x ] = 0;

对于任意 i,V1[ A] = 1; 对于所有其他 x,V1[ x ] = 0

V [2][ A + AJ] = 1,对于任意 i,j.V [2][ x ] = 0对于所有其他 x

V [3][任意3个元素之和] = 1。

要填充它,需要从右到左遍历 A,对于每个 A遍历数组。

除了蛮力搜索,还有什么有效的算法可以找到这三个整数吗?

是的; 我们可以在 O (n2)时间内解决这个问题!首先,考虑到你的问题 P可以用一种略有不同的方式来表达,这样就不需要“目标值”了:

原始问题 P: 给定一个由 n整数组成的数组 A和一个目标值 S,是否存在一个从 A和到 S的3元组?

修改后的问题 P': 给定一个 n整数的数组 A,是否存在一个从 A到零的3元组?

注意,您可以通过从 A中的每个元素中减去 S/3,从 P中减去问题 P'的这个版本,但是现在您不再需要目标值了。

显然,如果我们简单地测试所有可能的3-元组,我们就可以在 O (n3)中解决问题——这是蛮力基线。有可能做得更好吗?如果我们以更聪明的方式选择元组呢?

首先,我们投入一些时间对数组进行排序,这使我们的初始损失为 O (n logn)。现在我们执行这个算法:

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.
}

该算法通过在数组中的不同点放置三个指针 ijk来工作。i从一开始就开始,慢慢地走到最后。k指向最后一个元素。j指向 i开始的位置。我们迭代地尝试将元素按照它们各自的索引求和,每次都会发生下列情况之一:

  • 总数是正确的! 我们已经找到了答案。
  • 总和太小。将 j移到靠近末尾的位置,选择下一个最大的数字。
  • 总和太大了。将 k移到靠近开头的位置,选择下一个最小的数字。

对于每个 ijk的指针会逐渐靠近。最终它们会相互传递,到那时我们就不需要为那个 i尝试任何其他操作了,因为我们将以不同的顺序求和相同的元素。在这一点之后,我们尝试下一个 i并重复。

最终,我们要么穷尽有用的可能性,要么找到解决办法。可以看到这是 O (n2) ,因为我们执行外部循环 O (n)次,执行内部循环 O (n)次。如果你非常喜欢的话,可以通过将每个整数表示为一个位向量并执行一个快速傅里叶变换来实现次二次函数,但这超出了这个答案的范围。


注意: 因为这是一个面试问题,所以我在这里作了一点小弊: 这个算法允许多次选择相同的元素。也就是说,(- 1,-1,2)将是一个有效的解决方案,(0,0,0)也是如此。它还发现只有 一模一样答案,而不是最接近的答案,正如标题所提到的。作为对读者的一个练习,我将让你弄清楚如何使它只与不同的元素(但这是一个非常简单的变化)和准确的答案(这也是一个简单的变化)。

比如这个,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个元素的总和正好等于你的数字。如果希望最接近,可以修改它以记住最小 delta (当前三元组数目之间的差异) ,并在最后打印对应于最小 delta 的三元组。

注意,我们有一个排序的数组。这个解与约翰的解类似,只是它寻找和而不重复相同的元素。

#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));
}

还原: 我认为@John Feminella 溶液 O (n2)是最优雅的。我们仍然可以减少搜索 tuple 的 A [ n ]。通过观察 A [ k ] ,当我们的搜索数组很大而 SUM (s)很小时,所有元素都在 A [0]-A [ k ]中。

A [0]是最小值:-升序排序数组。

S = 2A [0] + A [ k ] : 给定 s 和 A [] ,我们可以在 log (n)时间内使用二进制搜索找到 A [ k ]。

当然,这是一个更好的解决方案,因为它更容易阅读,因此不容易出错。唯一的问题是,我们需要添加几行代码来避免多次选择一个元素。

另一个 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])

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])

下面是 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;
}

另一个提前检查并失败的解决方案是:

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;
}

我在这里添加了一些单元测试: 给定数组返回 TrueIfThree 元素 SumZero 测试

如果集合使用了太多的空间,我可以很容易地使用 java.util. BitSet,它将使用 O (n/w) 空间

我在 n ^ 3中做了这个,我的伪代码在下面;

//创建一个 hashMap,键为 Integer,值为 ArrayList //使用 for 循环遍历 list,对于 list 中的每个值,从下一个值开始再次遍历;

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 ]是所需的和,然后将 arr [ i ]作为键,然后将 arr [ j ]和 arr [ k ]作为键的值添加到 ArrayList 中,从而将它们添加到 HashMap 中

          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 中的程序,它是 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);
}
}

这可以在 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;
}
}

这个问题可以在 O (n ^ 2)中通过扩展2-和问题来解决,只需稍作修改。 A 是包含元素的向量,B 是所需的和。

Int 解: : three SumClosest (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;

获取这三个元素的程序。我刚刚排序的数组/列表第一,他们更新了基于每个三元组的 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;
}

下面是 Python 3代码

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