剑指 Offer 57. 和为s的两个数字 - 力扣(LeetCode) (leetcode-cn.com)
目录
情况1:x[i - θ1] + x[j - θ2] == target.
情况2:x[i + θ1] + x[j + θ2] == target
方案1:基本方法
思路
首先把数组所有元素放进哈希表里,然后遍历哈希表,对于每个元素x,查找s-x是否也在哈希表。
复杂度
时间复杂度为O(N),空间复杂度为O(N).
代码
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_set<int> iset(nums.begin(), nums.end());
for (auto x : iset) {
int y = target - x;
if (x != y && iset.find(y) != iset.end()) return {x, y};
}
return {};
}
};
运行结果
这种方案使用STL的unordered_set,时间常数比较大,当测试用例规模比较小的时候,在性能上体现不出优势。
方案2:对基本方法的改进
思路
遍历数组,对于数组的每个元素x,首先查找x是否在哈希表里,如果x不在哈希表里,那么把s - x放进哈希表里;直到在哈希表中找到某个x。
复杂度
时间复杂度低于O(N),空间复杂度低于O(N),二者均取决于测试用例。
代码
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int s) {
unordered_set<int> iset;
for (auto x : nums) {
int y = s - x;
if (y != x && iset.find(x) != iset.end()) return {x, y};
else iset.emplace(y);
}
return {};
}
};
运行结果
同样的问题:维护STL哈希表的时间常数较大,在数据量较小的时候表现不出性能优势。
方案3:二分查找
思路
遍历递增数列:x[1], x[2], ……, x[n],对于某个x[i],设y[i] = s - x[i],如果y[i] > x[n],那么进行下一个;如果y[i] <= x[n],那么在x[i+1]~x[n]中查找y[i]。
复杂度
我们可以构造一个数列和一个和,从第一个元素开始,每次都需要查找,直到最后才找到,这时有最坏时间复杂度:O( log2(n) + log2(n-1) + …… + log2(1) = log2(n!) )< O( n*log2(n) )
空间复杂度为O(1).
代码
class Solution {
public:
vector<int> twoSum(vector<int>& x, int s) {
vector<int> ans;
int N = x.size();
for (int i = 0; i != N; ++i) {
int y = s - x[i];
if (y <= x[N - 1]) {
int beg = i + 1, end = N;
while (beg != end) {
int mid = beg + (end - beg) / 2;
if (x[mid] == y) return { x[i], y };
x[mid] > y ? end = mid : beg = mid + 1;
}
}
//if (y <= x[N - 1] && Search(x, i + 1, N, y)) return { x[i], y };
}
return {};
}
/*
bool Search(vector<int>& x, int beg, int end, int target) {
while (beg != end) {
int mid = beg + (end - beg) / 2;
if (x[mid] == target) return true;
x[mid] > target ? end = mid : beg = mid + 1;
}
return false;
}
*/
};
运行结果
方案4:从两边,向中间
思路
数列:x[1], x[2], ……, x[n]。iter 从前向后,riter 从后向前,设s = x[i] + x[j],如果s < target,那么正向迭代器后移,即++iter,否则反向迭代器前移,即--riter,直到s == target。
代码
class Solution {
public:
vector<int> twoSum(vector<int>& x, int target) {
int i = 0, j = x.size() - 1;
while (i < j) {
int s = x[i] + x[j];
if (s == target) return { x[i], x[j] };
s < target ? ++i : --j;
}
return {};
}
};
正确性证明
直接证明
从iter = 1,riter = n开始,始终保持iter < riter.
- 如果s(iter, riter) = x[iter] + x[riter] < target的话,那么从s(iter, iter+1)到s(iter, riter-1)所有的这些和均小于target,因此可以排除它们—— ++iter.
- 如果s(iter, riter) = x[iter] + x[riter] > target的话,那么从s(iter + 1, riter)到s(riter-1, riter)所有的这些和均大于target,因此可以排除它们—— --riter.
因此,我们可以保证从第一步开始,每一步排除的都是错误答案,走一遍完成的流程其实就是排除所有错误答案的过程;因为我们每一步都是正确的,所以整个流程是正确的。
反证法
假设当前正进行到 i 和 j 。如果要错过正确答案,只能有两种情况:
-
情况1:x[i - θ1] + x[j - θ2] == target.
此时:x[i] > x[i - θ1],x[j] > x[j - θ2] x[i] + x[j] > target
由于 i 只能增大,不能减小,因此而错过正确答案。
分析1
在正向迭代器iter达到 i 之前,必然会经过 i - θ1,此时反向迭代器riter在 j - θ2之后,x[riter] > x[j-θ2],则x[i - θ1] + x[riter] > target,根据规则,此时应该反向迭代器前移,即--riter,正向迭代器iter不可能达到 i。
-
情况2:x[i + θ1] + x[j + θ2] == target
此时x[i] < x[i + θ1],x[j] < x[j + θ2] x[i] + x[j] < target
由于 j 只能减小,不能增大,因此而错过正确答案。
分析2
在反向迭代器riter达到 j 之前,必然会经过j + θ2,此时正向迭代器iter在i + θ1之前,x[iter] < x[i + θ1],则x[iter] + x[j + θ2] < target,根据规则,此时应该正向迭代器后移,即++iter,反向迭代器riter不可能达到 j 。
复杂度
时间复杂度O(N),空间复杂度O(1)。
运行结果