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

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 给定一个数组An 整数和一个目标值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.

该算法通过将三个指针ijk 放置在数组中的不同点来工作。 i 从头开始​​,慢慢地走到最后。 k 指向最后一个元素。 j 指向 i 开始的位置。我们反复尝试对各自索引处的元素求和,每次发生以下情况时:

总和完全正确!我们找到了答案。 总和太小了。将j 移近末尾以选择下一个最大的数字。 总和太大了。将k 移近开头以选择下一个最小的数字。

对于每个ijk的指针会逐渐靠近。最终它们会互相通过,此时我们不需要为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,2K2,则不应该有答案。我认为你的算法会输出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;


【讨论】:

我认为这行不通。当然,当中间的所有元素都严格小于或大于您想要的数字时,您有两个简单的案例来说明如何推进 leftIndexrightIndex。但是当二分搜索在中间某处停止时的情况呢?您需要检查 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寻找任意元素之和最接近指定数值的程序

C++:查找数组中最接近的值

用Java编写,在给出的数字里面找三个数字的和等于或者最接近513.91的。在线等,用上数组最好。

在排序数组列表中查找2个最接近的先前值和2个最接近的下一个值

Bailian4134 查找最接近的元素二分查找

LeetCode第十六题-找出数组中三数之和最接近目标值的答案