【问题标题】: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

标签: arrays algorithm


【解决方案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 函数提取所有这些条目。这非常有效。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 2012-12-27
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2021-08-14
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多