经典算法题之(七)------ n数之和问题

1.0 寻找二数之和

两数之和问题--leetcode

即在数组nums[]中寻找和为target的元素下标。

题本身的解法很经典:

解法一:暴力解法,判断所有元素对的组合:空间:O(l),时间:O(N(N-1)/2) = O(N^2)

解法二:使用Map,遍历时判断,target-nums[i]在不在现有的Map中,如果存在,则已经找到,直接返回;否则将当前元素添加进Map中。空间:O(N),时间:O(N)。

关于这两种方法的代码我的leetcode上都有,这里就不再讨论了。但现在考虑另外一种解法:现将nums[i]排序,再使用以下策略找到元素对:设2个指针:i,j分别指向nums[]首尾,

若nums[i] + nums[j] > target,则 j--

若nums[i] + nums[j] < target,则 i++

若nums[i] + nums[j] == target,则返回。

这里有个问题可能使人迷惑,考虑数组 int[] nums = {6,5, -1, 7, 9}, 排好序后为 {-1, 5, 6, 7,9}, 当我们寻找target = 11这个数时,假设我们已经寻找到了nums[i] = 5, nums[j] = 7的位置(前面的过程不再赘述),5+7 > 11, 此时我们按照之前说的策略应该令 j--,但是有个问题:当前之和大于target,即需要nums[i] 或者nums[j]变小,那么其实有两个选择,i--或者j--,为什么不能让j不动,i--呢?或者说,你采取一旦大了就令nums[j]减小,一旦小了就令nums[i]增大,这样会不会“漏掉”一些本应该能组合而成的两数之和呢。举个例子,当前nums[i] + nums[j] > target,令j--,会不会nums[j-1] 一下子减的太小,令nums[i]  + nums[j-1] < target了,而其实存在nums[i-1] + nums[j] == target的情况。

这里就来证明排序+双指针法的正确性。

为了证明充分起见,假设target唯一一对点对nums[p] 与nums[q] 之和,见下图:

假设一开始始终有nums[i] + nums[j] < target,故一直i++,一直到 i= k时,nums[i] + nums[j] > target。现在先来看会不会出现 k > p即i跑过头的情况,因为nums[p] + nums[q] == target,现在已经有j >= q, 所有i走到p时必然有nums[p] + nums[j]  >= target,故i不会加过头,必然在p之前就停下来了,即k <= p。

因为nums[k] + nums[j] > target,故开始减小j,假设当j = r时,出现nums[k] + nums[r] > target,又要开始增大i,这里再看一下j会不会跑过头:因为k <= p, 故在j = q时已经有:nums[k] + nums[q] <= target,即j翻不过q。不会过头。

因为target是唯一的nums[p]与nums[q]之和,所以在i < p, j > q时,nums[i] + nums[j] == target总是不成立的,如果小于,则i++,如果大于则j--。i和j分别向p和q靠近,且在i = p或者j = q之前会一直进行下去。

假设现在i已经到达p,则必然有nums[p] + nums[j] > target,故i++的动作停止,开始j--,一直到j = q,刚好等式成立,结束。

综上,如果nums[]中存在一个点对之和为target,其值唯一,则i和j的移动不会越界,同时没到目的地也不会停下,一直一步一步向两个点靠近,最后总会到达相应的位置返回。

对于target对应的点对不唯一的情况,其实是上面的简化版本,即在i和j到达其中一个目的地之前已经满足了nums[i] + nums[j] == target,直接返回。 策略同样返回了正确的结果。

所以排序+双指针的方法是正确。

当然,这里插个话,对于target对应的点对不唯一的情况,如果真要探究,如现在有一道题要返回所有符合条件的点对,则可以先分析点对的位置关系入手,其实可以看到,如果存在两个点对满足和为target,则他们一定满足点对包围的情况:

因为如下这种点对交叉的情况根本不可能存在:

只要交叉,设其中一个和target,则其余之和必然为大于或小于target(相等元素的情况下,q,n必然是最外围的两个点,隶属于点对包围的情况,不会出现这种交叉的情况,见下分析)。

如上图点对包围的情况,有两个点对满足和为target,其一对被另外一个对包围。则因为i和j翻越不了p,q,故会访问p,q点对,此时直接return了,但是如果现在条件该为了“返回所有符合条件的点对”,则不能直接return,而是存下p,q值,令i++ && j--(在不允许使用重复元素的条件下),继续进行,直到i和j相遇为止。

现在来看看复杂度:

空间:O(l),时间:O(NlogN+N) = O(NlogN)

相较于hash法的O(N)和O(N)在空间上更优,但在时间上更劣。当前,这里详细介绍这个方法其实是为了下面解决三数之和的问题做铺垫。

 

2.0 寻找三数之和

三数之和问题--leetcode

和两数之和问题类似,能想到的解法有两种:

解法一:暴力,判断所有的三数组合之和满不满足条件,枚举的复杂度为:空间:O(l), 时间:O(N^3)

解法二:hash法,不同的是,这里在检查元素nums[i]时,需要将所有元素能组成的两数之和塞进map中(这里先不讨论如何存储下标的问题,只做策略上的思考),而通过1.0两数之和的暴力解法我们已经知道,列举N个元素之和的复杂度为O(N^2)(即使有重复之和不用put,但是判断同样需要O(l)时间,故还是要O(N^2)时间),put两数之和后,还要遍历一次nums[](这里都没考虑一个元素不能重复使用的问题了),故总的复杂度为O(N^3),并不可行。

此时让我们再回看1.0的排序+双指针方法。

本题我们可以先排序,然后遍历nums[i], 对于每个nums[i],我们使用双指针法判断 num[i+1]到nums[N-1]的序列中有没有和为target - nums[i]的,只要找到就成功返回。一直到nums[N-3]也没找到,则就不存在。

具体程序见leetcode。

当然这题也有变式,如:

最接近的三数之和 ---- leetcode

当然,直接用同样的方法遍历即可。更新最新的距离。此时就能更加直观地感受排序+双指针和暴力的复杂度区别:

O(NlogN)(排序) + O(N^2)(遍历N元素) = O(N^2) < O(N^3)

 

3.0 寻找四数之和

四数之和问题--leetcode

其实和三数之和一个道理了,只是此时需要先定下来两个数,剩下的两个用排序+双指针法寻找。解答可见leetcode。

另外注意,这类题目要仔细审题:是判断是否至少存在一个,还是返回任意一个,还是返回所有符合条件的元组。就例如本题在leetcode上对应的三数之和的题目,要求:1.所有符合条件的元组  2. 不重复。

另外,也有可能在target做手脚,target大于等于0,则可以直接在for循环里先判断nums[i] > target ,如果成立,则后面不用寻找了,因为枢纽本身大于target,则枢纽后面的必然大于 > 0, 故不用查找。

但是对于target < 0 则不能这么干,因为例如target为-6, 数组为 {-4,-2,-1,0},虽然nums[0] = -4 > -6,但是nums[1] = -2,越加越小,故还是有可能的:nums[0] + nums[1] = -6满足条件。

如leetcode上对应的四数之和问题,有可能target小于0的情况。

 

4.0 组合数之和

组合数之和问题 ---- leetcode

即不限定个数,求所有和能凑成target的数字组合。