1. 题目来源
链接:两数之和 II - 输入有序数组
来源:LeetCode
2. 题目说明
给定一个已按照升序排列 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1
和 index2
,其中 index1
必须小于 index2
。
说明:
返回的下标值(index1 和 index2)不是从零开始的。
你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
示例 :
输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
3. 思维启迪
3.1 暴力解法
对于这个题目来讲题意就很明确,就是在给定数组中找两个数字,它们的相加之和等于给定值。那么很直观的暴力解法就是两个 for()
循环遍历,首先固定一个数的值,通过遍历数组的方式来确定下一个数是否存在。那么这个方法就太过于暴力致使它的时间复杂度达到了
的级别,这个就不如人意了。
3.2 双指针解法
设置两个指针 left
、right
,left
指向数组第一个元素,right
指向最后一个元素。然后进行 left + right
的操作,就会产生下面几种情况:
- 若
left + right > target
说明right
指向的数字太大了,就--right
再判断 - 若
left + right < target
说明left
指向的数字太小了,就++left
再判断 - 若
left ≥ right
就说明没找到
这个思路很直观也很简单,一个 while()
循环,几个 if()
语句,简简单单几行搞定。但是我们思考一下,这个做法为什么是正确的,或是转换一个思维方式来考虑这个问题。
3.3 杨氏矩阵
杨氏矩阵结构如下图所示:
下面产生一个问题:请在杨氏矩阵中查找 target
。
当然我们可以采用暴力解法,那就是
的解法了,这里的 n
、m
就是杨氏矩阵的行和列了。显然效率不尽人意,关键是没有利用到杨氏矩阵特特性。
如果我们换种思维方式,从数字 9 开始查找,如果 target > 9
那么就要往下走,这是不是相当于砍掉了杨氏矩阵的一行?如果 target < 9
那么就要往左走,这是不是相当于砍掉了杨氏矩阵的一列? 那么这个效率就显著的提升上来了,为
。算是一个满意的时间复杂度了。
3.4 杨氏矩阵深度剖析
思考一个问题:我们选择在右上角的数字 9 开始进行查找,那么为什么一定是数字 9 呢?它具备什么样的特点呢?
观察杨氏矩阵图我们可以发现,右上角的数字 9 是第一行的最大值,并且是最后一列的最小值,所以我们可以从右上角走。 这就是杨氏矩阵的特性。
那么分析得到了杨氏矩阵的特性之后,杨氏矩阵中满足该特性的点还有一个,就是左下角的数字 12。其道理和右上角数字 9 相同就不在赘述。
那么经过了 [有趣的算法思维] 1. 链表思维与快乐数(单链表思维、链表带环判断) 的有趣思维,单链表就只是单链表吗?二叉树就只是二叉树吗?这个杨氏矩阵就只是杨氏矩阵吗?
当然不是,下面我们就针对杨氏矩阵进行思维方式的转换!
结论:杨氏矩阵其实代表着一个有序集合的笛卡尔积。
举个简单的例子说明这个问题,给定两个有序矩阵,若同时为 [2, 3, 5]
,分别按照笛卡尔积方式,即下图方式进行相加,并产生一个相加后的矩阵即为:
这样就产生了一个杨氏矩阵。那么回到我们一开始的问题:在有序数组中找两个不同的数组元素相加之和等于 target
,那不就相当于在所对应的杨氏矩阵中查找 target
是否存在吗?按照杨氏矩阵查找数字的方法,选定右上或者左下角的值便可以很快的查找到 target
是否存在。
右上角的数字 7 就等于原序列的第一个值加上最后一个值,而我们向下走一步就意味着 left
向后移动一位,这刚好就是当前位置小于 target
的情况。同理,向左走一步就意味着 right
向前移动一步,也就是当前位置大于 target
的情况。
至此,我们就证明了双指针解法的正确性。当我们具备杨氏矩阵思维的时候,就能够轻松理解在一维序列前后双指针查找两数和的做法为什么是正确的了。
我们再来考虑一个问题,为什么当我们理解了杨氏矩阵的思维后,再理解前后双指针查找两数和的方法就很直观明确呢?其实根本原因在于它的维度,杨氏矩阵是个二维的问题,而一维数组自然是一维的问题,当我们用二维的结论来理解一维的知识的时候就相当简单了,而我们若想用一维的知识反推得到二维的结论那可就很费劲了。
3.5 代码展示
// 执行用时 :4 ms, 在所有 C++ 提交中击败了96.15%的用户
// 内存消耗 :12 MB, 在所有 C++ 提交中击败了5.16%的用户
class Solution {
public:
vector<int> twoSum(vector<int>& num, int target) {
vector<int> ret;
int p = 0, q = num.size() - 1;
while (num[p] + num[q] != target) {
if (num[p] + num[q] < target) p += 1;
else q -= 1;
}
ret.push_back(p + 1);
ret.push_back(q + 1);
return ret;
}
};
4. 总结
一定要把数据结构和算法学成一种思维方式,而不是教科书上的一段代码!
我们通过[有趣的算法思维] 1. 链表思维与快乐数(单链表思维、链表带环判断) 和这个问题的解决能够发现,其实快乐数讲了很多单链表,两数之和 II - 输入有序数组讲了很多的杨氏矩阵,其实单单看代码的话能发现里面蕴藏着这么多的奥秘吗?这就是题目里面所蕴藏的算法本质啊,这就是算法思维!算法之美!