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++,现在先来看会不会出现 k > p即i跑过头的情况,因为nums[p] + nums[q] == target,现在已经有j >= q, 则必然nums[j] >= nums[q],所以i走到p时必然有nums[p] + nums[j] >= target,故i必然在p之前就停下来了,假设停下来的位置为k,则必有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,在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的移动不会越界,同时没到目的地也不会停下,一直一步一步向两个点靠近,一旦有一个点到了答案点(例如上例的i到了p)则立马会停下来,下面全部是j在往左走,即不会出现之前引出问题的情景:在i的左侧可能还有解。上面两条可以归结为:
① i和j在还没到目标位置时,会一直逐渐靠近,不会出现提前停下来的情况
② 一旦到达了目标位置,当前点就不会再动,只会等着另一端达到目标点。
=== > 不会停,不会越界,刚刚好。
对于target对应的点对不唯一的情况,其实是上面的简化版本,即在i和j到达其中一个目的地之前已经满足了nums[i] + nums[j] == target,直接返回。换句话说,该策略找到的第一对结果即为"最外层"的目标点对。
所以排序+双指针的方法是正确。
当然,这里插个话,对于target对应的点对不唯一的情况,如果真要探究,如现在有一道题要返回所有符合条件的点对,则可以先分析点对的位置关系入手,其实可以看到,如果存在两个点对满足和为target,则他们一定满足点对包围的情况:
如下这种点对交叉的情况根本不可能存在:
只要交叉,设其中一个和target,则其余之和必然为大于或小于target,道理很简单:两个数都大于/小于其余两个数,其和必然大于/或小于另外两个数之和。
那么如下的点对包围情况该怎么解呢:
如上图点对包围的情况,有两个点对满足和为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的数字组合。