[LintCode/LeetCode]——两数和、三数和、四数和

时间:2022-12-09 19:19:14

LintCode有大部分题目来自LeetCode,但LeetCode比较卡,下面以LintCode为平台,简单介绍我AC的几个题目,并由此引出一些算法基础。

1)两数之和(two-sum)

题目编号:56,链接:http://www.lintcode.com/zh-cn/problem/two-sum/

题目描述:

给一个整数数组,找到两个数使得他们的和等于一个给定的数 target

你需要实现的函数twoSum需要返回这两个数的下标, 并且第一个下标小于第二个下标。注意这里下标的范围是 1 到 n,不是以 0 开头。

注意:你可以假设只有一组解。

样例:给出 numbers = [2, 7, 11, 15], target = 9, 返回 [1, 2],即数字2,7

代码接口:

class Solution {
public:
/*
* @param numbers : An array of Integer
* @param target : target = numbers[index1] + numbers[index2]
* @return : [index1+1, index2+1] (index1 < index2)
*/
vector<int> twoSum(vector<int> &nums, int target) {
// write your code here
}
};

常见的思路是:两层for循环,任意两个数组合求其和,判断是否等于给定的target。但这样太慢,需要O(n^2)的时间,O(1)的额外空间。可以反过来思考,假如当前选择了一个数字a,那么为了满足条件,另一个数字b必须满足:b=targe-a,即在数组中寻找是否存在b。

如何快速寻找数组中是否存在一个数字b?假如数组是有序的,可以使用二分查找方法,其查找时间复杂度是O(logn)。然而题目并没给定这个条件。如果对数组排序,首先就要O(nlogn)的时间进行排序,并且排序后,数字的原始下标也要保存,显然需要O(nlogn)的时间以及O(n)的空间,并不是最好的方法。

如何对一个数组进行快速查找一个元素?算法中提供了一种方法——哈希(Hash),即对数组中的每个元素按照某种方法(hash function)计算其“唯一”值id(称为哈希值),存储在新的数组A中(一般称为哈希数组),并且其下标就是这个“唯一”值。那么如果访问A[id]存在,则这个元素存在,否则,原始数组中不存在该元素。由于数组是顺序存储的支持随机访问,所以查找一个元素是否在数组中,只需要O(1)的时间,但是在初始化哈希数组时,需要O(n)的时间和O(n)的空间。对于某些特定应用中,需要快速的时间,而对空间要求不苛刻时,哈希数组是一个非常好的方法。为了能够满足各种应用场景,又衍生出容量大小可以动态增长的哈希集合(hash set)、哈希映射(hash map),STL提供了关于哈希的两个类:unordered_set和unordered_map,前者只存储元素,后者可以再增加额外的标志信息。详细的内容,请自行补充。

由于构造的哈希数组,其元素的下标已经改变了,所以需要额外存储元素原始的下标,因此此题使用unordered_map<int,int>,其存储的内容为<元素值,元素原始下标>,详细代码:

class Solution {
public:
/*
* @param numbers : An array of Integer
* @param target : target = numbers[index1] + numbers[index2]
* @return : [index1+1, index2+1] (index1 < index2)
*/
/* Tips: find any pair is ok not all pairs.
* using hash map to store the num value and index
* notice: if the target is 4 and the answer expection num 2 + 2,
* only the one num 2 is stored in hash map, but also work ok!
* because must have the other num 2 is not in hash map!
* */
vector<int> twoSum(vector<int> &nums, int target) {
// write your code here
vector<int> v(2,0);
unordered_map<int,int> hash;// val+id
// we can search num and insert it to hash map at same time
// and current num must be not in hash map
for(int i=nums.size(); i--; hash[nums[i]]=i){
if (hash.find(target-nums[i]) == hash.end()) continue;
v[0] = 1 + i; // the index from 1 to n not 0 to n-1
v[1] = 1 + hash[target-nums[i]];
return v;
}
return v; // no answer return {0,0}
}
};

需要注意的是:哈希无法存储相同元素,因为相同元素有相同的哈希值。如果数组{2,5,6},待求值target=4,没有解;而数组{2,2,5,6},target=4则有解。如何处理这种情况?可以反向遍历,初始hash为空,逐渐将已经遍历过的元素加入到哈希中。

2)三数和(3 sum)

题目编号:57,链接:http://www.lintcode.com/zh-cn/problem/3sum/

题目描述:给出一个有n个整数的数组S,在S中找到三个整数a, b, c,找到所有使得a + b + c = 0的三元组。在三元组(a, b, c),要求a <= b <= c。结果不能包含重复的三元组。

样例:如S = {-1 0 1 2 -1 -4}, 你需要返回的三元组集合的是:(-1, 0, 1),(-1, -1, 2)

这个题目难度增加不少,首先变成3个数的和,然后要求找出所有结果,并且不能重复。但是,返回的只是三元组,并不是原始的下标,如果再使用哈希,那么三个数,需要已知两个数,即要两层for循环,那么时间复杂度O(n^2),并且辅助空间也要O(n^2)。有没有更好地方法?在两数之和时,曾考虑过排序,然后二分查找。三数和不用返回原始下标,那么用排序+二分查找可否?

首先按升序排序;然后定义下标变量i,j,k,因为是三元组,所以要三个变量。如果简单的遍历,那么跟是否有序没有关系,其时间复杂度将达到O(n^3)。仔细想想:如果当前选择了a、b、c三个数,如果其和小于目标target,那么需要将其中一个数用更大的数替换;反之亦然。但究竟替换三个数中的哪个数?无法确定就只能先固定两个变量,让其第三个变化(替换)。一种办法是:固定前两个数i,j,然后让k在一个范围中二分变化(二分查找思想),核心代码如下:

	for (int i=0; i<n; ++i){
for (int j=i+1; j<n; ++j){
for (int left=j+1, right=n-1;left!=right;){
int k = (right+left)/2;
int sum = A[i]+A[j]+A[k];
if (sum>target) right = k;
else if (sum<target) left=k;
else {insert(A[i],A[j],A[k]);break;}
}
}
}

抛开一些细节之外,这种方法时间复杂度仍然很大,为O(n^2logn)。仔细观察发现,k值不是连续变化的,而是两边跳跃的。那么可以只固定一个变量i,让j和k变化。当前值小于target时,可以让j增加;否则,k减小。完整代码如下:

class Solution {
public:
/**
* @param numbers : Give an array numbers of n integer
* @return : Find all unique triplets in the array
* which gives the sum of zero.
* each triplet in non-descending order
*/
vector<vector<int> > threeSum(vector<int> &A) {
// write your code here
vector<vector<int> > vs;
int target = 0;
sort(A.begin(),A.end()); // sort A in ascending order
for(int i=0; i<A.size(); ++i){
if (i>0 && A[i-1]==A[i]) continue; // skip duplication
for(int j=i+1, k=A.size()-1; j<k;){
if (j>i+1 && A[j-1]==A[j]){
++j;
continue; // skip duplication
}
if (k<A.size()-1 && A[k]==A[k+1]){
--k;
continue; // skip duplication
}
int sum = A[i]+A[j]+A[k];
if (sum > target) --k;
else if (sum < target) ++j;
else{ // find a triplet
vector v(3,A[i]);
v[1] = A[j++];
v[2] = A[k--];
vs.push_back(v);
}
}
}
return vs;
}
};

注意去除重复的结果。设一个满足条件的三元组<a,b,c>,如果有重复的三元组与之相同,则说明a,b,c中至少有一个元素的值在数组中出现至少两次。假如a的值2,在数组中出现多次,则其必然是连续的(数组已经排序),因此可以使用如上的方法去除重复的三元组。该方法时间复杂度O(nlogn)+O(n^2),空间复杂度为O(1)。

3)最接近的三数和(3sum closest)

题目编号:59,题目链接:http://www.lintcode.com/zh-cn/problem/3sum-closest/

题目描述:给一个包含 n 个整数的数组 S, 找到和与给定整数 target 最接近的三元组,返回这三个数的和。只需返回最接近的三数和,不需要三个数。

样例:例如 S = [-1, 2, 1, -4] and target = 1. 和最接近 1 的三元组是 -1 + 2 + 1 = 2.

只需寻找三数和,无需去除重复,显然,此题比2)简单得多。可以使用类似的方法,并且实时更新最接近的三数和,这里不再详述,一种实现代码:

class Solution {
public:
/**
* @param numbers: Give an array numbers of n integer
* @param target: An integer
* @return: return the sum of the three integers
* the sum closest target.
*/
int threeSumClosest(vector<int> nums, int tar) {
// write your code here
sort(nums.begin(),nums.end());
int ans = INT_MAX;
for(int i=0; i<nums.size(); ++i){
for(int j=i+1, k=nums.size()-1; j<k;){
int sum = nums[i]+nums[j]+nums[k];
// update the closest answer
ans = (abs(tar-sum)<abs(tar-ans) ? sum:ans);
if (sum > tar) --k;
else if (sum < tar) ++j;
else return sum; // sum equal to target
}
}
return ans;
}
};

4)四数和(4 sum)

题目编号:58,题目链接:http://www.lintcode.com/zh-cn/problem/4sum/

题目描述:给一个包含n个数的整数数组S,在S中找到所有使得和为给定整数target的四元组(a, b, c, d)。四元组(a, b, c, d)中,需要满足a <= b <= c <= d,答案中不可以包含重复的四元组。

样例:例如,对于给定的整数数组S=[1, 0, -1, 0, -2, 2] 和 target=. 满足要求的四元组集合为:

(-1, 0, 0, 1),(-2, -1, 1, 2),(-2, 0, 0, 2)

显然,此题难度大大提高。如果沿用2)的思路,则需要O(n^3)的时间复杂度,但空间为常数级。不妨先试一试:

class Solution {
public:
/**
* @param numbers: Give an array numbersbers of n integer
* @param target: you need to find four elements that's sum of target
* @return: Find all unique quadruplets in the array which gives the sum of
* zero.
*/
/*
* Time O(n^3) , Space O(1)
* */
vector<vector<int> > fourSum(vector<int> A, int tar) {
// write your code here
vector<vector<int> > vs;
sort(A.begin(),A.end()); // ascending order
for(int i=0; i<A.size(); ++i){
if (i>0 && A[i-1]==A[i]) continue; // duplication
for(int j=i+1; j<A.size(); ++j){
if (j>i+1 && A[j-1]==A[j]) continue;// duplication
for(int k=j+1, l=A.size()-1; k<l;){
if (k>j+1 && A[k-1]==A[k]){
++k; // duplication
continue;
}
if (l<A.size()-1 && A[l]==A[l+1]){
--l; // duplication
continue;
}
int sum = A[i]+A[j]+A[k]+A[l];
if (sum > tar) --l;
else if (sum < tar) ++k;
else {
vector<int> v(4,A[i]);
v[1]=A[j], v[2]=A[k++], v[3]=A[l--];
vs.push_back(v);
}
}
}
}
return vs;
}
};

很明显,需要定义四个下标变量:i,j,k,l,其中固定i,j,让k和l一个自增,一个自减。同样需要注意去重复,并且保证每个四元组按升序排列。

如果使用1)的方法,首先将任意两个元素组合,计算其两数和并存入哈希;然后再任选两个数a,b,此时去哈希中寻找是否存在target-a-b。但需要注意的是,具有相同和的二元组,可能不唯一,因此需要一个数组存储所有和相同的二元组,因此,使用unordered_map<int,vector<pair<int,int> > > twosum;作为哈希映射存储,其种key表示两数和的值,数组存储具有该值的所有二元组,pair<int,int>为具体的二元组的元素值。因此,构建哈希映射的时间、空间复杂度为O(n^2)。

然后再一次定义两个下标变量i,j,当选择该i,j时,在哈希映射中可能存在多个二元组的和都为target-a-b,设最多有k个和相同的二元组,则整体时间复杂度为O(k*n^2)

如何进行去重复?目前没有很好的办法。回想一下哈希无法存储相同的元素,因此再使用一个哈希存储候选四元组(candidates),对于任意一个满足体题意的四元组,直接到该哈希中检验是否已经存在,从而去重复。下面是一种实现代码:

class Solution {
public:
// hash functional for hashing the vector
struct hashvec{
size_t operator()(const vector<int> &v)const{
string s;
for (int i=0; i<v.size(); s+=" "+v[i++]);
return hash()(s);
}
};
vector<vector<int> > fourSum(vector<int> A, int tar){
// write your code
sort(A.begin(), A.end()); // ascending order
// using hash map to store the sum of A[i]+A[j] and index i,j
unordered_map<int,vector<pair<int,int> > > twosum;
for (int i=0; i<A.size(); ++i){
if (i>0 && A[i-1]==A[i]) continue; // skip duplication
for (int j=i+1; j<A.size(); ++j) {
if (j>i+1 && A[j-1]==A[j]) continue;// skip duplication
twosum[A[i]+A[j]].push_back(make_pair(i,j));
}
} unordered_set<vector<int>,hashvec> cans; // test duplication
vector<vector<int> > res; // results
unordered_map<int,vector<pair<int,int> > >::iterator ts;
vector<pair<int,int> >::iterator it;
for (int i=2; i<A.size(); ++i) {
for (int j=i+1; j<A.size(); ++j) {
ts = twosum.find(tar-A[i]-A[j]);
if (ts == twosum.end()) continue; // can't find sum
for (it=ts->second.begin(); it!=ts->second.end(); ++it){
if (it->second >= i) continue; // skip duplication
vector<int> v(4, A[it->first]);
v[1]=A[it->second], v[2]=A[i], v[3]=A[j];
// add the v to both cans and res if cans has no v
if (cans.find(v) == cans.end()){
cans.insert(v);
res.push_back(v);
}
}
}
}
return res;
}
};

其中,struct hashvec是一个哈希仿函数。所谓仿函数,其实质并不是函数,只是表现出来像一个函数一样。其作用是计算哈希值。因为STL提供的unordered_map只能对基本数据类型进行计算哈希值,哈希值是元素的“唯一”识别码,之所以带引号,是因为并不存在一个哈希函数能对任意一个元素计算出唯一的识别码,当有两个不同的元素,经过哈希函数计算后,其哈希值相同,那么就需要解决冲突(通常有线性探测和十字链表方法,这里不做详细介绍)。一个好的哈希函数,可以提高哈希的查找速度。对于任意一个四元组,STL并没有相应的哈希函数进行计算,但是STL对字符串提供了简单的哈希函数,因此可以利用这一点,将四元组转化成字符串,从而利用STL自带的哈希函数hash<string>()进行计算,如上面代码3~9行。

因此,这种方法虽然额外占用了O(n^2)的空间,但在时间上大大减少,为O(k*n^2),所以对于某些应用还是有参考意义的。

注意:尽管可以使用哈希映射cans进行去重复,但是在生成两数和的哈希映射twosum时,最好还是进行两数和的去重复,否则哈希映射太大,也会影响性能。

注:本文涉及的代码:

two sum:https://git.oschina.net/eudiwffe/lintcode/blob/master/C++/two-sum.cpp

3 sum:https://git.oschina.net/eudiwffe/lintcode/blob/master/C++/3sum.cpp

3 sum closest:https://git.oschina.net/eudiwffe/lintcode/blob/master/C++/3sum-closest.cpp

4 sum:https://git.oschina.net/eudiwffe/lintcode/blob/master/C++/4sum.cpp