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

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 twoSum(vector &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 twoSum(vector &nums, int target) {
		// write your code here
		vector 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 > threeSum(vector &A) {
		// write your code here
		vector<vector > 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 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=0. 满足要求的四元组集合为:

(-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 > fourSum(vector A, int tar) {
		// write your code here
		vector<vector > 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 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 &v)const{
			string s;
			for (int i=0; i<v.size(); s+=" "+v[i++]);
			return hash()(s);
		}
	};
	vector<vector> fourSum(vector 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,hashvec> cans;    // test duplication
		vector<vector > 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 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

时间: 2024-10-25 11:34:04

[LintCode/LeetCode]——两数和、三数和、四数和的相关文章

两数之和,三数之和,最接近的三数之和,四数之和

LeetCode有一系列做法套路相同的题目,N数之和就可以算一个 两数之和 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标. 你可以假设每种输入只会对应一个答案.但是,你不能重复利用这个数组中同样的元素. 示例: 给定 nums = [2, 7, 11, 15], target = 9 因为 nums[0] + nums[1] = 2 + 7 = 9 所以返回 [0, 1] 第一个解决办法,简单暴力,堆for循环就是,但

[leetcode数组系列]2三数之和

前言 秋招的结束,面试了大大小小的公司,最大的问题在于算法上.所以打算坚持在leetcode打卡,看看到底能不能行,如果你想见证,那我来开车,你坐稳,一起走向更好的远方. 在学习今天内容之前,先学习上一篇的两数之和会更好哟 leetcode两数之和求解 一 题目 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满

LeetCode 两数相加

LeetCode 两数相加 给出两个 非空 的链表用来表示两个非负的整数.其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字. 如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和. 您可以假设除了数字 0 之外,这两个数都不会以 0 开头. 示例: 输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)输出:7 -> 0 -> 8原因:342 + 465 = 807 知识点: strrev(); // 实现字符

求Fibonacci数的三种方法和时间复杂度解析

题目: 定义Fibonacci数列如下: f(0)=1 f(1)=1 f(n)=f(n-1)+f(n-2), n>=2 输入n,用最快的方法求该数列的第n项. 解答一: 直接用公式写递归函数.很简单,很低效,就不写了.时间复杂度T(N) = T(N-1) + T(N-2); 也是f(n)本身,2^(n/2)<f(n)<2^n. 解答二: 用循环求,也很直接,效率很高了,时间复杂度是O(n). int f(int n) { if(n <= 1) return 1; int f0=1,

数模三天乐

我们学校有个很不错的历史传统,就是五一三天假会有一个大型的娱乐项目,"数模三天乐",简直把人爽得不行不行的.一般地说是"自愿报名",但是苦逼的数学系出身还有各种各样的原因吧,最终还是不得不报名打一把数模. 数模和ACM是两个完全不同的竞赛.从过程上和准备上讲,数模对于大家来说都是三天决定结果,但是ACM就不是5个小时的发挥决定结果了,毫不夸张地说,数模三天,ACM三年.从结果展现来讲,数模是论文优劣定胜负.可能你的成果或者你的答案和评委们手上的"最佳答案&

[LeetCode] 454. 4Sum II 四数之和II

Given four lists A, B, C, D of integer values, compute how many tuples (i, j, k, l) there are such that A[i] + B[j] + C[k] + D[l] is zero. To make problem a bit easier, all A, B, C, D have same length of N where 0 ≤ N ≤ 500. All integers are in the r

Leetcode(18)-四数之和

给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组. 注意: 答案中不可以包含重复的四元组. 示例: 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0. 满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ] 思路:

LeetCode:四数之和【18】

LeetCode:四数之和[18] 题目描述 给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组. 注意: 答案中不可以包含重复的四元组. 示例: 给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0. 满足要求的四元组集合为:[ [-1, 0, 0, 1], [-2, -1, 1, 2]

js中斐波拉切数的三种写法;

js中斐波拉切数的三种写法: function factorial(num){ if(num <=1){ return 1; }else{ return num* factorial(num-1); } } console.log(factorial(5));//120 面这个函数的执行与函数名紧紧耦合在了一起,可以使用arguments.callee可以消除函数解耦 第二种(在严格模式下,访问这个属性会抛出TypeError错误) function factorial(num){ if(num