【程序员编程艺术】第五章:寻找满足和为定值的两个或多个数
2014-06-21 21:17
585 查看
文章参考:http://blog.csdn.net/v_JULY_v/article/details/6419466
本文是基于上文链接的内容进行裁剪,在自己的理解层次上写出的文章。
第一节:寻找和为定值的两个数
输入一个数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。
要求时间复杂度是O(N)。如果有多对数字的和等于输入的数字,输出任意一对即可。
例如输入数组1、2、4、7、11、15和数字15。由于4+11=15,因此输出4和11。
思路一:穷举法
从数组中任意选取两个数,判定它们的和是否为输入的那个数字。对每个a[i],查找sum-a[i]是否也在原始序列中,每一次要查找的时间都要花费为O(N),这样下来,最终找到两个数还是需要O(N^2)的复杂度。此举复杂度为O(N^2)。现在这是第一步,得到的最大时间复杂度。紧接着是要用各种方式去优化。
思路二:利用排序去优化
当我们对数组进行排序以后,再去查找,就可以利用各种有效的查找算法。这里,可以利用二分查找。可以将O(N)的查找时间提高到O(logN),这样对于N个a[i],都要花logN的时间去查找相对应的sum-a[i]是否在原始序列中,总的时间复杂度已降为O(N*logN),且空间复杂度为O(1)。
二分查找的实现,有两种定义:算法所操作的区间,是左闭右开区间,还是左闭右闭区间,这个区间,需要在循环初始化,循环体是否终止的判断中,以及每次修改left,right区间值这三个地方保持一致,否则就可能出错。
方式一:左闭右闭( left <= right,right
= middle - 1)
方式二:左闭右开(left < right,right = middle;)
思路三:牺牲空间优化
通过构建辅助数组,来便于查找,通常首先应该想到用hash表,如方案二;也可以通过构建其他辅助数组,不过也要通过一定的技巧去遍历,如方案一。
方案一:
根据前面的分析,a[i]在序列中,如果a[i]+a[k]=sum的话,那么sum-a[i](a[k])也必然在序列中。 举个例子,如下: 原始序列:
1、 2、 4、 7、11、15
用输入数字15减一下各个数,得到对应的序列为:
14、13、11、8、4、 0
第一个数组以一指针i 从数组最左端开始向右扫描,第二个数组以一指针j 从数组最右端开始向左扫描,如果第一个数组出现了和第二个数组一样的数,即a[*i]=a[*j],就找出这俩个数来了。 如上,i,j最终在第一个,和第二个序列中找到了相同的数4和11,所以符合条件的两个数,即为4+11=15。 怎么样,两端同时查找,时间复杂度瞬间缩短到了O(N),但却同时需要O(N)的空间存储第二个数组。
方案二:利用哈希表
当题目对时间复杂度要求比较严格时,我们可以考虑下用空间换时间,上述解法一即是此思想,此外,构造hash表也是典型的用空间换时间的处理办法。
即给定一个数字,根据hash映射查找另一个数字是否也在数组中,只需用O(1)的时间,前提是经过O(N)时间的预处理,和用O(N)的空间构造hash表。
思路四:根据结构特征构造巧妙解法
当我们对原数组排序以后,观察数组,是递增结构,可以附设两个指针,一增一减来遍历。
如果数组是无序的,先排序(N log N),然后用两个指针i,j,各自指向数组的首尾两端,令i=0,j=n-1,然后i++,j--,逐次判断a[i]+a[j]?=sum,
如果某一刻a[i]+a[j] > sum,则要想办法让sum的值减小,所以此刻i不动,j--;
如果某一刻a[i]+a[j] < sum,则要想办法让sum的值增大,所以此刻i++,j不动。
所以,数组无序的时候,时间复杂度最终为O(N log N + N)=O(N log N)。
如果原数组是有序的,则不需要事先的排序,直接用两指针分别从头和尾向中间扫描,O(N)搞定,且空间复杂度还是O(1)。
下面,咱们先来实现此思路(这里假定数组已经是有序的),代码可以如下编写:
不论原序列是有序还是无序,解决这类题有以下三种办法:
1、二分(若无序,先排序后二分),时间复杂度总为O(N log N),空间复杂度为O(1);
2、扫描一遍X-S[i] 映射到一个数组或构造hash表,时间复杂度为O(N),空间复杂度为O(N);
3、两个指针两端扫描(若无序,先排序后扫描),时间复杂度最后为:有序O(N),无序O(N log N + N)=O(N log N),空间复杂度都为O(1)。
所以,要想达到时间O(N),空间O(1)的目标,除非原数组是有序的(指针扫描法),不然,当数组无序的话,就只能先排序,后指针扫描法或二分(时间 O(Nlog N),空间O(1)),或映射或hash(时间O(N),空间O(N))。时间或空间,必须牺牲一个,达到平衡。
综上,若是数组有序的情况下,优先考虑两个指针两端扫描法,以达到最佳的时O(N),空O(1)效应。否则,如果要排序的话,时间复杂度最快当然是只能达到O(N log N),空间O(1)则不在话下。
如果在返回找到的两个数的同时,还要求你返回这两个数的位置列?
如果需要输出所有满足条件的整数对呢?
如果把题目中的要你寻找的两个数改为“多个数”,或任意个数列?
1、在二元树中找出和为某一值的所有路径 输入一个整数和一棵二元树,从树的根结点开始往下访问一直到叶结点所经过的所有结点形成一条路径,然后打印出和与输入整数相等的所有路径。 例如输入整数22和如下二元树
/ \
5 12
/ \
4 7
则打印出两条路径:10, 12和10, 5, 7。 其中,二元树节点的数据结构定义为:
2、有一个数组a,设有一个值n。在数组中找到两个元素a[i]和a[j],使得a[i]+a[j]等于n,求出所有满足以上条件的i和j。
3、3-sum问题
给定一个整数数组,判断能否从中找出3个数a、b、c,使得他们的和为0,如果能,请找出所有满足和为0个3个数对。
4、4-sum问题
给定一个整数数组,判断能否从中找出4个数a、b、c、d,使得他们的和为0,如果能,请找出所有满足和为0个4个数对。
第二节:寻找和为定值的多个数
编程求解:
输入两个整数 n 和 m,从数列1,2,3.......n 中 随意取几个数,
使其和等于 m ,要求将其中所有的可能组合列出来。
题目解析:
方案一:
穷举法,就太麻烦了,凡是涉及到数的,可试图去寻找规律,如何遍历更简单。
首先,为了程序的健壮性,应该考虑一下,m和n之间的关系,如果m>n(n+1)/2,那么就不可能了。这个判断,也是在递归的时候,加上去,可以减少遍历时间。
其次,我们该怎么去遍历?从1到n,还是从n到1。显然从n到1比较方便,递归的形式f(n)与f(n-1)一致。那么这个时候,就是n选还是不选的问题。
总结:
注意到取n,和不取n个区别即可,考虑是否取第n个数的策略,可以转化为一个只和前n-1个数相关的问题。
如果取第n个数,那么问题就转化为“取前n-1个数使得它们的和为sum-n”,对应的代码语句就是sumOfkNumber(sum - n, n - 1);
如果不取第n个数,那么问题就转化为“取前n-1个数使得他们的和为sum”,对应的代码语句为sumOfkNumber(sum, n - 1)。
贴出c++代码:
方案二:(没看太懂……还需要研究)
这个问题属于子集和问题(也是背包问题)。本程序采用回溯法+剪枝,其中X数组是解向量,t=∑(1,..,k-1)Wi*Xi, r=∑(k,..,n)Wi,且
若t+Wk+W(k+1)<=M,则Xk=true,递归左儿子(X1,X2,..,X(k-1),1);否则剪枝;
若t+r-Wk>=M && t+W(k+1)<=M,则置Xk=0,递归右儿子(X1,X2,..,X(k-1),0);否则剪枝;
本题中W数组就是(1,2,..,n),所以直接用k代替WK值。
代码编写如下:
总结:网上有个《背包九讲》,回头需要仔细研究一下这个系列。该题目跟背包问题很类似。
两个网址:
http://blog.csdn.net/v_JULY_v/article/details/6419466
https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/02.03.md
这两个链接中,最后有拓展题,需要看看。
本文是基于上文链接的内容进行裁剪,在自己的理解层次上写出的文章。
第一节:寻找和为定值的两个数
题目描述
输入一个数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。要求时间复杂度是O(N)。如果有多对数字的和等于输入的数字,输出任意一对即可。
例如输入数组1、2、4、7、11、15和数字15。由于4+11=15,因此输出4和11。
思路一:穷举法
从数组中任意选取两个数,判定它们的和是否为输入的那个数字。对每个a[i],查找sum-a[i]是否也在原始序列中,每一次要查找的时间都要花费为O(N),这样下来,最终找到两个数还是需要O(N^2)的复杂度。此举复杂度为O(N^2)。现在这是第一步,得到的最大时间复杂度。紧接着是要用各种方式去优化。
思路二:利用排序去优化
当我们对数组进行排序以后,再去查找,就可以利用各种有效的查找算法。这里,可以利用二分查找。可以将O(N)的查找时间提高到O(logN),这样对于N个a[i],都要花logN的时间去查找相对应的sum-a[i]是否在原始序列中,总的时间复杂度已降为O(N*logN),且空间复杂度为O(1)。
二分查找的实现,有两种定义:算法所操作的区间,是左闭右开区间,还是左闭右闭区间,这个区间,需要在循环初始化,循环体是否终止的判断中,以及每次修改left,right区间值这三个地方保持一致,否则就可能出错。
方式一:左闭右闭( left <= right,right
= middle - 1)
int search(int array[], int n, int v) { int left, right, middle; left = 0, right = n - 1;<span style="white-space:pre"> </span>//不同1 while (left <= right)<span style="white-space:pre"> </span>//不同2 { middle = left + (right-left)/2; if (array[middle] > v) { right = middle - 1;<span style="white-space:pre"> </span>//不同3 } else if (array[middle] < v) { left = middle + 1; } else { return middle; } } return -1; }
方式二:左闭右开(left < right,right = middle;)
int search(int array[], int n, int v) { int left, right, middle; left = 0, right = n; while (left < right) { middle = left + (right-left)/2; if (array[middle] > v) { right = middle; } else if (array[middle] < v) { left = middle + 1; } else { return middle; } } return -1; }
思路三:牺牲空间优化
通过构建辅助数组,来便于查找,通常首先应该想到用hash表,如方案二;也可以通过构建其他辅助数组,不过也要通过一定的技巧去遍历,如方案一。
方案一:
根据前面的分析,a[i]在序列中,如果a[i]+a[k]=sum的话,那么sum-a[i](a[k])也必然在序列中。 举个例子,如下: 原始序列:
1、 2、 4、 7、11、15
用输入数字15减一下各个数,得到对应的序列为:
14、13、11、8、4、 0
第一个数组以一指针i 从数组最左端开始向右扫描,第二个数组以一指针j 从数组最右端开始向左扫描,如果第一个数组出现了和第二个数组一样的数,即a[*i]=a[*j],就找出这俩个数来了。 如上,i,j最终在第一个,和第二个序列中找到了相同的数4和11,所以符合条件的两个数,即为4+11=15。 怎么样,两端同时查找,时间复杂度瞬间缩短到了O(N),但却同时需要O(N)的空间存储第二个数组。
方案二:利用哈希表
当题目对时间复杂度要求比较严格时,我们可以考虑下用空间换时间,上述解法一即是此思想,此外,构造hash表也是典型的用空间换时间的处理办法。
即给定一个数字,根据hash映射查找另一个数字是否也在数组中,只需用O(1)的时间,前提是经过O(N)时间的预处理,和用O(N)的空间构造hash表。
思路四:根据结构特征构造巧妙解法
当我们对原数组排序以后,观察数组,是递增结构,可以附设两个指针,一增一减来遍历。
如果数组是无序的,先排序(N log N),然后用两个指针i,j,各自指向数组的首尾两端,令i=0,j=n-1,然后i++,j--,逐次判断a[i]+a[j]?=sum,
如果某一刻a[i]+a[j] > sum,则要想办法让sum的值减小,所以此刻i不动,j--;
如果某一刻a[i]+a[j] < sum,则要想办法让sum的值增大,所以此刻i++,j不动。
所以,数组无序的时候,时间复杂度最终为O(N log N + N)=O(N log N)。
如果原数组是有序的,则不需要事先的排序,直接用两指针分别从头和尾向中间扫描,O(N)搞定,且空间复杂度还是O(1)。
下面,咱们先来实现此思路(这里假定数组已经是有序的),代码可以如下编写:
void TwoSum(int data[], unsigned int length, int sum) { //sort(s, s+n); 如果数组非有序的,那就事先排好序O(N log N) int begin = 0; int end = length - 1; //俩头夹逼,或称两个指针两端扫描法,很经典的方法,O(N) while (begin < end) { long currSum = data[begin] + data[end]; if (currSum == sum) { //题目只要求输出满足条件的任意一对即可 printf("%d %d\n", data[begin], data[end]); //如果需要所有满足条件的数组对,则需要加上下面两条语句: //begin++ //end-- break; } else{ if (currSum < sum) begin++; else end--; } } }
解法总结
不论原序列是有序还是无序,解决这类题有以下三种办法:1、二分(若无序,先排序后二分),时间复杂度总为O(N log N),空间复杂度为O(1);
2、扫描一遍X-S[i] 映射到一个数组或构造hash表,时间复杂度为O(N),空间复杂度为O(N);
3、两个指针两端扫描(若无序,先排序后扫描),时间复杂度最后为:有序O(N),无序O(N log N + N)=O(N log N),空间复杂度都为O(1)。
所以,要想达到时间O(N),空间O(1)的目标,除非原数组是有序的(指针扫描法),不然,当数组无序的话,就只能先排序,后指针扫描法或二分(时间 O(Nlog N),空间O(1)),或映射或hash(时间O(N),空间O(N))。时间或空间,必须牺牲一个,达到平衡。
综上,若是数组有序的情况下,优先考虑两个指针两端扫描法,以达到最佳的时O(N),空O(1)效应。否则,如果要排序的话,时间复杂度最快当然是只能达到O(N log N),空间O(1)则不在话下。
问题扩展
如果在返回找到的两个数的同时,还要求你返回这两个数的位置列?如果需要输出所有满足条件的整数对呢?
如果把题目中的要你寻找的两个数改为“多个数”,或任意个数列?
举一反三
1、在二元树中找出和为某一值的所有路径 输入一个整数和一棵二元树,从树的根结点开始往下访问一直到叶结点所经过的所有结点形成一条路径,然后打印出和与输入整数相等的所有路径。 例如输入整数22和如下二元树10
/ \
5 12
/ \
4 7
则打印出两条路径:10, 12和10, 5, 7。 其中,二元树节点的数据结构定义为:
struct BinaryTreeNode // a node in the binary tree { int m_nValue; // value of node BinaryTreeNode *m_pLeft; // left child of node BinaryTreeNode *m_pRight; // right child of node };
2、有一个数组a,设有一个值n。在数组中找到两个元素a[i]和a[j],使得a[i]+a[j]等于n,求出所有满足以上条件的i和j。
3、3-sum问题
给定一个整数数组,判断能否从中找出3个数a、b、c,使得他们的和为0,如果能,请找出所有满足和为0个3个数对。
4、4-sum问题
给定一个整数数组,判断能否从中找出4个数a、b、c、d,使得他们的和为0,如果能,请找出所有满足和为0个4个数对。
第二节:寻找和为定值的多个数
编程求解:
输入两个整数 n 和 m,从数列1,2,3.......n 中 随意取几个数,
使其和等于 m ,要求将其中所有的可能组合列出来。
题目解析:
方案一:
穷举法,就太麻烦了,凡是涉及到数的,可试图去寻找规律,如何遍历更简单。
首先,为了程序的健壮性,应该考虑一下,m和n之间的关系,如果m>n(n+1)/2,那么就不可能了。这个判断,也是在递归的时候,加上去,可以减少遍历时间。
其次,我们该怎么去遍历?从1到n,还是从n到1。显然从n到1比较方便,递归的形式f(n)与f(n-1)一致。那么这个时候,就是n选还是不选的问题。
总结:
注意到取n,和不取n个区别即可,考虑是否取第n个数的策略,可以转化为一个只和前n-1个数相关的问题。
如果取第n个数,那么问题就转化为“取前n-1个数使得它们的和为sum-n”,对应的代码语句就是sumOfkNumber(sum - n, n - 1);
如果不取第n个数,那么问题就转化为“取前n-1个数使得他们的和为sum”,对应的代码语句为sumOfkNumber(sum, n - 1)。
贴出c++代码:
list<int>list1; void SumOfkNumber(int sum, int n) { // 递归出口 if (n <= 0 || sum <= 0) return; // 输出找到的结果 if (sum == n) { // 反转list list1.reverse(); for (list<int>::iterator iter = list1.begin(); iter != list1.end(); iter++) cout << *iter << " + "; cout << n << endl; } list1.push_front(n); //典型的01背包问题 SumOfkNumber(sum - n, n - 1); //“放”n,前n-1个数“填满”sum-n list1.pop_front(); SumOfkNumber(sum, n - 1); //不“放”n,n-1个数“填满”sum }
方案二:(没看太懂……还需要研究)
这个问题属于子集和问题(也是背包问题)。本程序采用回溯法+剪枝,其中X数组是解向量,t=∑(1,..,k-1)Wi*Xi, r=∑(k,..,n)Wi,且
若t+Wk+W(k+1)<=M,则Xk=true,递归左儿子(X1,X2,..,X(k-1),1);否则剪枝;
若t+r-Wk>=M && t+W(k+1)<=M,则置Xk=0,递归右儿子(X1,X2,..,X(k-1),0);否则剪枝;
本题中W数组就是(1,2,..,n),所以直接用k代替WK值。
代码编写如下:
//输入t, r, 尝试Wk void SumOfkNumber(int t, int k, int r, int& M, bool& flag, bool* X) { X[k] = true; // 选第k个数 if (t + k == M) // 若找到一个和为M,则设置解向量的标志位,输出解 { flag = true; for (int i = 1; i <= k; ++i) { if (X[i] == 1) { printf("%d ", i); } } printf("\n"); } else { // 若第k+1个数满足条件,则递归左子树 if (t + k + (k + 1) <= M) { SumOfkNumber(t + k, k + 1, r - k, M, flag, X); } // 若不选第k个数,选第k+1个数满足条件,则递归右子树 if ((t + r - k >= M) && (t + (k + 1) <= M)) { X[k] = false; SumOfkNumber(t, k + 1, r - k, M, flag, X); } } } void search(int& N, int& M) { // 初始化解空间 bool* X = (bool*)malloc(sizeof(bool)* (N + 1)); memset(X, false, sizeof(bool)* (N + 1)); int sum = (N + 1) * N * 0.5f; if (1 > M || sum < M) // 预先排除无解情况 { printf("not found\n"); return; } bool f = false; SumOfkNumber(0, 1, sum, M, f, X); if (!f) { printf("not found\n"); } free(X); }
总结:网上有个《背包九讲》,回头需要仔细研究一下这个系列。该题目跟背包问题很类似。
两个网址:
http://blog.csdn.net/v_JULY_v/article/details/6419466
https://github.com/julycoding/The-Art-Of-Programming-By-July/blob/master/ebook/zh/02.03.md
这两个链接中,最后有拓展题,需要看看。
相关文章推荐
- 程序员编程艺术:第五章、寻找满足和为定值的两个或多个数
- 程序员编程艺术:第五章、寻找满足和为定值的两个或多个数
- 程序员编程艺术:第五章、寻找满足和为定值的两个或多个数
- 程序员编程艺术:第五章、寻找满足和为定值的两个或多个数
- 程序员编程艺术:第五章、寻找满足和为定值的两个或多个数
- 程序员编程艺术:第五章、寻找满足和为定值的两个或多个数
- 程序员编程艺术:第五章、寻找满足条件的两个或多个数
- 程序员编程艺术学习笔记(五):寻找满足和为定值的两个或多个数
- 程序员编程艺术:第五章、寻找满足条件的两个或多个数
- 程序员编程艺术---5、寻找满足和为定值的两个或多个数
- 程序员编程艺术:第五章、寻找满足条件的两个或多个数
- 2.12快速寻找满足条件的两个数---程序员编程艺术之五
- 第五章:寻找满足和为定值的两个或多个数
- 第五章、寻找满足和为定值的两个或多个数
- 【July程序员编程艺术】之寻找和为定值的两个或者多个数
- 程序员编程艺术:第三章、寻找最小的k个数
- (10)寻找满足和为定值的两个或多个数
- 编程之美 2.12 快速寻找满足条件的两个数 解法三证明 (算法导论 第二版 2.3-7 在n个元素的集合S中找到两个和为x的元素)
- 程序员编程艺术:第三章、寻找最小的k个数
- 程序员编程艺术3:寻找最小的k个数