zoukankan      html  css  js  c++  java
  • 程序员编程艺术:第五章、寻找满足和为定值的两个或多个数

    作者:July,yansha,zhouzhenren。
        致谢:微软100题实现组,编程艺术室。
        微博:http://weibo.com/julyweibo   。
        出处:http://blog.csdn.net/v_JULY_v  。
        wiki:http://tctop.wikispaces.com/
    ------------------------------

    前奏

        希望此编程艺术系列能给各位带来的是一种方法,一种创造力,一种举一反三的能力。本章依然同第四章一样,选取比较简单的面试题,恭祝各位旅途愉快。同样,有任何问题,欢迎不吝指正。谢谢。


    第一节、寻找和为定值的两个数
    第14题(数组):
    题目:输入一个数组和一个数字,在数组中查找两个数,使得它们的和正好是输入的那个数字。
    要求时间复杂度是O(n)。如果有多对数字的和等于输入的数字,输出任意一对即可。
    例如输入数组1、2、4、7、11、15和数字15。由于4+11=15,因此输出4和11。

    分析

    咱们试着一步一步解决这个问题(注意阐述中数列有序无序的区别):

    1. 直接穷举,从数组中任意选取两个数,判定它们的和是否为输入的那个数字。此举复杂度为O(N^2)。很显然,我们要寻找效率更高的解法。
    2. 题目相当于,对每个a[i],然后查找判断sum-a[i]是否也在原始序列中,每一次要查找的时间都要花费为O(N),这样下来,最终找到两个数还是需要O(N^2)的复杂度。那如何提高查找判断的速度列?对了,二分查找,将原来O(N)的查找时间提高到O(logN),这样对于N个a[i],都要花logN的时间去查找相对应的sum-a[i]是否在原始序列中,总的时间复杂度已降为O(N*logN),且空间复杂度为O(1)。(如果有序,直接二分O(N*logN),如果无序,先排序后二分,复杂度同样为O(N*logN+N*logN)=O(N*logN),空间总为O(1))。
    3. 有没有更好的办法列?咱们可以依据上述思路2的思想,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)的空间存储第二个数组(@飞羽:要达到O(N)的复杂度,第一个数组以一指针i 从数组最左端开始向右扫描,第二个数组以一指针j 从数组最右端开始向左扫描,首先初始i指向元素1,j指向元素0,谁指的元素小,谁先移动,由于1(i)>0(j),所以i不动,j向左移动。然后j移动到元素4发现大于元素1,故而停止移动j,开始移动i,直到i指向4,这时,i指向的元素与j指向的元素相等,故而判断4是满足条件的第一个数;然后同时移动i,j再进行判断,直到它们到达边界)。
    4. 当然,你还可以构造hash表,正如编程之美上的所述,给定一个数字,根据hash映射查找另一个数字是否也在数组中,只需用O(1)的时间,这样的话,总体的算法通上述思路3 一样,也能降到O(N),但有个缺陷,就是构造hash额外增加了O(N)的空间,此点同上述思路 3。不过,空间换时间,仍不失为在时间要求较严格的情况下的一种好办法。
    5. 如果数组是无序的,先排序(n*logn),然后用两个指针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*logn+n)=O(n*logn),若原数组是有序的,则不需要事先的排序,直接O(n)搞定,且空间复杂度还是O(1),此思路是相对于上述所有思路的一种改进。(如果有序,直接两个指针两端扫描,时间O(N),如果无序,先排序后两端扫描,时间O(N*logN+N)=O(N*logN),空间始终都为O(1))。(与上述思路2相比,排序后的时间开销由之前的二分的n*logn降到了扫描的O(N))。

    总结

    • 不论原序列是有序还是无序,解决这类题有以下三种办法:1、二分(若无序,先排序后二分),时间复杂度总为O(n*logn),空间复杂度为O(1);2、扫描一遍X-S[i]  映射到一个数组或构造hash表,时间复杂度为O(n),空间复杂度为O(n);3、两个指针两端扫描(若无序,先排序后扫描),时间复杂度最后为:有序O(n),无序O(n*logn+n)=O(n*logn),空间复杂度都为O(1)。
    • 所以,要想达到时间O(N),空间O(1)的目标,除非原数组是有序的(指针扫描法),不然,当数组无序的话,就只能先排序,后指针扫描法或二分(时间n*logn,空间O(1)),或映射或hash(时间O(n),空间O(n))。时间或空间,必须牺牲一个,自个权衡吧。
    • 综上,若是数组有序的情况下,优先考虑两个指针两端扫描法,以达到最佳的时(O(N)),空(O(1))效应。否则,如果要排序的话,时间复杂度最快当然是只能达到N*logN,空间O(1)则是不在话下。

    代码:

    ok,在进入第二节之前,咱们先来实现思路5(这里假定数组已经是有序的),代码可以如下编写(两段代码实现):

    1. //代码一  
    2. //O(N)  
    3. Pair findSum(int *s,int n,int x)     
    4. {     
    5.     //sort(s,s+n);   如果数组非有序的,那就事先排好序O(N*logN)     
    6.       
    7.     int *begin=s;     
    8.     int *end=s+n-1;     
    9.       
    10.     while(begin<end)    //俩头夹逼,或称两个指针两端扫描法,很经典的方法,O(N)    
    11.     {     
    12.         if(*begin+*end>x)     
    13.         {     
    14.             --end;     
    15.         }     
    16.         else if(*begin+*end<x)     
    17.         {     
    18.             ++begin;     
    19.         }     
    20.         else    
    21.         {     
    22.             return Pair(*begin,*end);     
    23.         }     
    24.     }     
    25.       
    26.     return Pair(-1,-1);     
    27. }     
    28.   
    29. //或者如下编写,  
    30. //代码二  
    31. //copyright@ zhedahht && yansha  
    32. //July、updated,2011.05.14。  
    33. bool find_num(int data[], unsigned int length, int sum, int& first_num, int& second_num)  
    34. {     
    35.     if(length < 1)  
    36.         return true;  
    37.       
    38.     int begin = 0;  
    39.     int end = length - 1;  
    40.       
    41.     while(end > begin)  
    42.     {  
    43.         long current_sum = data[begin] + data[end];  
    44.           
    45.         if(current_sum == sum)  
    46.         {  
    47.             first_num = data[begin];  
    48.             second_num = data[end];  
    49.             return true;  
    50.         }  
    51.         else if(current_sum > sum)  
    52.             end--;  
    53.         else  
    54.             begin++;  
    55.     }  
    56.     return false;  
    57. }  

    扩展:
    1、如果在返回找到的两个数的同时,还要求你返回这两个数的位置列?
    2、如果把题目中的要你寻找的两个数改为“多个数”,或任意个数列?(请看下面第二节)
    3、二分查找时: left <= right,right = middle - 1;left < right,right = middle;

    //算法所操作的区间,是左闭右开区间,还是左闭右闭区间,这个区间,需要在循环初始化,
    //循环体是否终止的判断中,以及每次修改left,right区间值这三个地方保持一致,否则就可能出错.

    //二分查找实现一
    int search(int array[], int n, int v)
    {
        int left, right, middle;
     
        left = 0, right = n - 1;
     
        while (left <= right)
        {
            middle = left + (right-left)/2;   
            if (array[middle] > v)
            {
                right = middle - 1;
            }
            else if (array[middle] < v)
            {
                left = middle + 1;
            }
            else
            {
                return middle;
            }
        }
     
        return -1;
    }

    //二分查找实现二
    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;
    }


    第二节、寻找和为定值的多个数
    第21题(数组)
    2010年中兴面试题
    编程求解:
    输入两个整数 n 和 m,从数列1,2,3.......n 中 随意取几个数,
    使其和等于 m ,要求将其中所有的可能组合列出来。

    解法一
    我想,稍后给出的程序已经足够清楚了,就是要注意到放n,和不放n个区别,即可,代码如下:

    1. // 21题递归方法  
    2. //copyright@ July && yansha  
    3. //July、yansha,updated。  
    4. #include<list>  
    5. #include<iostream>  
    6. using namespace std;  
    7.   
    8. list<int>list1;  
    9. void find_factor(int sum, int n)   
    10. {  
    11.     // 递归出口  
    12.     if(n <= 0 || sum <= 0)  
    13.         return;  
    14.       
    15.     // 输出找到的结果  
    16.     if(sum == n)  
    17.     {  
    18.         // 反转list  
    19.         list1.reverse();  
    20.         for(list<int>::iterator iter = list1.begin(); iter != list1.end(); iter++)  
    21.             cout << *iter << " + ";  
    22.         cout << n << endl;  
    23.         list1.reverse();      
    24.     }  
    25.       
    26.     list1.push_front(n);      //典型的01背包问题  
    27.     find_factor(sum-n, n-1);   //放n,n-1个数填满sum-n  
    28.     list1.pop_front();  
    29.     find_factor(sum, n-1);     //不放n,n-1个数填满sum   
    30. }  
    31.   
    32. int main()  
    33. {  
    34.     int sum, n;  
    35.     cout << "请输入你要等于多少的数值sum:" << endl;  
    36.     cin >> sum;  
    37.     cout << "请输入你要从1.....n数列中取值的n:" << endl;  
    38.     cin >> n;  
    39.     cout << "所有可能的序列,如下:" << endl;  
    40.     find_factor(sum,n);  
    41.     return 0;  
    42. }  

    解法二
    @zhouzhenren:
    这个问题属于子集和问题(也是背包问题)。本程序采用 回溯法+剪枝
    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值。

    代码编写如下:

    1. //copyright@ 2011 zhouzhenren  
    2.   
    3. //输入两个整数 n 和 m,从数列1,2,3.......n 中 随意取几个数,  
    4. //使其和等于 m ,要求将其中所有的可能组合列出来。  
    5.   
    6. #include <stdio.h>  
    7. #include <stdlib.h>  
    8. #include <memory.h>  
    9.   
    10. /**  
    11.  * 输入t, r, 尝试Wk 
    12.  */  
    13. void sumofsub(int t, int k ,int r, int& M, bool& flag, bool* X)  
    14. {  
    15.     X[k] = true;   // 选第k个数  
    16.     if (t + k == M) // 若找到一个和为M,则设置解向量的标志位,输出解  
    17.     {  
    18.         flag = true;  
    19.         for (int i = 1; i <= k; ++i)  
    20.         {  
    21.             if (X[i] == 1)  
    22.             {  
    23.                 printf("%d ", i);  
    24.             }  
    25.         }  
    26.         printf("/n");  
    27.     }  
    28.     else  
    29.     {   // 若第k+1个数满足条件,则递归左子树  
    30.         if (t + k + (k+1) <= M)  
    31.         {  
    32.             sumofsub(t + k, k + 1, r - k, M, flag, X);  
    33.         }  
    34.         // 若不选第k个数,选第k+1个数满足条件,则递归右子树  
    35.         if ((t + r - k >= M) && (t + (k+1) <= M))  
    36.         {  
    37.             X[k] = false;  
    38.             sumofsub(t, k + 1, r - k, M, flag, X);  
    39.         }  
    40.     }  
    41. }  
    42.   
    43. void search(int& N, int& M)  
    44. {  
    45.     // 初始化解空间  
    46.     bool* X = (bool*)malloc(sizeof(bool) * (N+1));  
    47.     memset(X, falsesizeof(bool) * (N+1));  
    48.     int sum = (N + 1) * N * 0.5f;  
    49.     if (1 > M || sum < M) // 预先排除无解情况  
    50.     {  
    51.         printf("not found/n");  
    52.         return;  
    53.     }  
    54.     bool f = false;  
    55.     sumofsub(0, 1, sum, M, f, X);  
    56.     if (!f)  
    57.     {  
    58.         printf("not found/n");  
    59.     }  
    60.     free(X);  
    61. }  
    62.   
    63. int main()  
    64. {  
    65.     int N, M;  
    66.     printf("请输入整数N和M/n");  
    67.     scanf("%d%d", &N, &M);  
    68.     search(N, M);  
    69.     return 0;  
    70. }  

    扩展:

    1、从一列数中筛除尽可能少的数使得从左往右看,这些数是从小到大再从大到小的(网易)。

    2、有两个序列a,b,大小都为n,序列元素的值任意整数,无序;
    要求:通过交换a,b中的元素,使[序列a元素的和]与[序列b元素的和]之间的差最小。
    例如:  
    var a=[100,99,98,1,2, 3];
    var b=[1, 2, 3, 4,5,40];(微软100题第32题)。

        @well:[fairywell]:
    给出扩展问题 1 的一个解法:
    1、从一列数中筛除尽可能少的数使得从左往右看,这些数是从小到大再从大到小的(网易)。
    双端 LIS 问题,用 DP 的思想可解,目标规划函数 max{ b[i] + c[i] - 1 }, 其中 b[i] 为从左到右, 0 ~ i 个数之间满足递增的数字个数; c[i] 为从右到左, n-1 ~ i 个数之间满足递增的数字个数。最后结果为 n - max + 1。其中 DP 的时候,可以维护一个 inc[] 数组表示递增数字序列,inc[i] 为从小到大第 i 大的数字,然后在计算 b[i] c[i] 的时候使用二分查找在 inc[] 中找出区间 inc[0] ~ inc[i-1] 中小于 a[i] 的元素个数(low)。
    源代码如下:

    1. /** 
    2. * The problem: 
    3. * 从一列数中筛除尽可能少的数使得从左往右看,这些数是从小到大再从大到小的(网易)。 
    4. * use binary search, perhaps you should compile it with -std=c99 
    5. * fairywell 2011 
    6. */  
    7. #include <stdio.h>  
    8.   
    9. #define MAX_NUM    (1U<<31)  
    10.   
    11. int  
    12. main()  
    13. {  
    14.     int i, n, low, high, mid, max;  
    15.       
    16.     printf("Input how many numbers there are: ");  
    17.     scanf("%d/n", &n);  
    18.     /* a[] holds the numbers, b[i] holds the number of increasing numbers 
    19.     * from a[0] to a[i], c[i] holds the number of increasing numbers 
    20.     * from a[n-1] to a[i] 
    21.     * inc[] holds the increasing numbers 
    22.     * VLA needs c99 features, compile with -stc=c99 
    23.     */  
    24.     double a[n], b[n], c[n], inc[n];  
    25.       
    26.     printf("Please input the numbers:/n");  
    27.     for (i = 0; i < n; ++i) scanf("%lf", &a[i]);  
    28.       
    29.     // update array b from left to right  
    30.     for (i = 0; i < n; ++i) inc[i] = (unsigned) MAX_NUM;  
    31.     //b[0] = 0;  
    32.     for (i = 0; i < n; ++i) {  
    33.         low = 0; high = i;  
    34.         while (low < high) {  
    35.             mid = low + (high-low)*0.5;  
    36.             if (inc[mid] < a[i]) low = mid + 1;  
    37.             else high = mid;  
    38.         }  
    39.         b[i] = low + 1;  
    40.         inc[low] = a[i];  
    41.     }  
    42.       
    43.     // update array c from right to left  
    44.     for (i = 0; i < n; ++i) inc[i] = (unsigned) MAX_NUM;  
    45.     //c[0] = 0;  
    46.     for (i = n-1; i >= 0; --i) {  
    47.         low = 0; high = i;  
    48.         while (low < high) {  
    49.             mid = low + (high-low)*0.5;  
    50.             if (inc[mid] < a[i]) low = mid + 1;  
    51.             else high = mid;  
    52.         }  
    53.         c[i] = low + 1;  
    54.         inc[low] = a[i];  
    55.     }  
    56.       
    57.     max = 0;  
    58.     for (i = 0; i < n; ++i )  
    59.         if (b[i]+c[i] > max) max = b[i] + c[i];  
    60.         printf("%d number(s) should be erased at least./n", n+1-max);  
    61.         return 0;  
    62. }  

    @yansha:fairywell的程序很赞,时间复杂度O(nlogn),这也是我能想到的时间复杂度最优值了。不知能不能达到O(n)。

    扩展题第2题

    当前数组a和数组b的和之差为
        A = sum(a) - sum(b)

    a的第i个元素和b的第j个元素交换后,a和b的和之差为
        A' = sum(a) - a[i] + b[j] - (sum(b) - b[j] + a[i])
               = sum(a) - sum(b) - 2 (a[i] - b[j])
               = A - 2 (a[i] - b[j])

    设x = a[i] - b[j],得
        |A| - |A'| = |A| - |A-2x|

        假设A > 0,

        当x 在 (0,A)之间时,做这样的交换才能使得交换后的a和b的和之差变小,x越接近A/2效果越好,
        如果找不到在(0,A)之间的x,则当前的a和b就是答案。

    所以算法大概如下:
        在a和b中寻找使得x在(0,A)之间并且最接近A/2的i和j,交换相应的i和j元素,重新计算A后,重复前面的步骤直至找不到(0,A)之间的x为止。 

    接上,@yuan:
    a[i]-b[j]要接近A/2,则可以这样想,
    我们可以对于a数组的任意一个a[k],在数组b中找出与a[k]-C最接近的数(C就是常数,也就是0.5*A)
    这个数要么就是a[k]-C,要么就是比他稍大,要么比他稍小,所以可以要二分查找。

    查找最后一个小于等于a[k]-C的数和第一个大于等于a[k]-C的数,
    然后看哪一个与a[k]-C更加接近,所以T(n) = nlogn。

    除此之外,受本文读者xiafei1987128启示,有朋友在stacoverflow上也问过一个类似的题,:-),见此:http://stackoverflow.com/questions/9047908/swap-the-elements-of-two-sequences-such-that-the-difference-of-the-element-sums。感兴趣的可以看看。

    本章完。


    程序员面试题狂想曲-tctop(the crazy thinking of programers)的修订wiki(http://tctop.wikispaces.com/)已建立,我们急切的想得到读者的反馈,意见,建议,以及更好的思路,算法,和代码优化的建议。所以,

    •如果你发现了狂想曲系列中的任何一题,任何一章(http://t.cn/hgVPmH)中的错误,问题,与漏洞,欢迎告知给我们,我们将感激不尽,同时,免费赠送本blog内的全部博文集锦的CHM文件1期;
    •如果你能对狂想曲系列的创作提供任何建设性意见,或指导,欢迎反馈给我们,并真诚邀请您加入到狂想曲的wiki修订工作中;
    •如果你是编程高手,对狂想曲的任何一章有自己更好的思路,或算法,欢迎加入狂想曲的创作组,以为千千万万的读者创造更多的价值,更好的服务。
    Ps:狂想曲tctop的wiki修订地址为:http://tctop.wikispaces.com/
    。欢迎围观,更欢迎您加入到狂想曲的创作或wiki修订中。 

    版权所有,本人对本blog内所有任何内容享有版权及著作权。实要转载,请以链接形式注明出处。

  • 相关阅读:
    Firemonkey 控件设定字型属性及颜色
    ListView 使用 LiveBindings 显示超过 200 条记录
    Firemonkey ListView 获取项目右方「>」(Accessory) 事件
    XE7 Update 1 选 iOS 8.1 SDK 发布 iPhone 3GS 实机测试
    Firemonkey Bitmap 设定像素颜色 Pixel
    Firemonkey 移动平台 Form 显示使用 ShowModal 范例
    XE7 提交 App(iOS 8)提示「does not contain the correct beta entitlement」问题修复
    XE7 Android 中使用 MessageDlg 范例
    导出 XE6 预设 Android Style (*.style) 档案
    修正 Memo 設定為 ReadOnly 後, 無法有複製的功能
  • 原文地址:https://www.cnblogs.com/mfryf/p/3081015.html
Copyright © 2011-2022 走看看