ITPub博客

首页 > Linux操作系统 > Linux操作系统 > 程序员面试题狂想曲:第五章、寻找满足条件的两个或多个数

程序员面试题狂想曲:第五章、寻找满足条件的两个或多个数

原创 Linux操作系统 作者:July___ 时间:2011-05-17 12:28:40 0 删除 编辑
程序员面试题狂想曲:第五章、寻找满足条件的两个或多个数
 
    作者: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]总结:
•不论原序列是有序还是无序,解决这类题有以下三种办法: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))效应。
代码:
ok,在进入第二节之前,咱们先来实现思路5(这里假定数组已经是有序的),代码可以如下编写(两段代码实现):
view plaincopy to clipboardprint?
//代码一  
//O(N)  
Pair findSum(int *s,int n,int x)     
{     
    //sort(s,s+n);   如果数组非有序的,那就事先排好序O(N*logN)     
      
    int *begin=s;     
    int *end=s+n-1;     
      
    while(begin    {     
        if(*begin+*end>x)     
        {     
            --end;     
        }     
        else if(*begin+*end        {     
            ++begin;     
        }     
        else    
        {     
            return Pair(*begin,*end);     
        }     
    }     
      
    return Pair(-1,-1);     
}     
 
//或者如下编写,  
//代码二  
//copyright@ zhedahht && yansha  
//July、updated,2011.05.14。  
bool find_num(int data[], unsigned int length, int sum, int& first_num, int& second_num)  
{     
    if(length < 1)  
        return true;  
      
    int begin = 0;  
    int end = length - 1;  
      
    while(end > begin)  
    {  
        long current_sum = data[begin] + data[end];  
          
        if(current_sum == sum)  
        {  
            first_num = data[begin];  
            second_num = data[end];  
            return true;  
        }  
        else if(current_sum > sum)  
            end--;  
        else 
            begin++;  
    }  
    return false;  

//代码一
//O(N)
Pair findSum(int *s,int n,int x)  
{  
    //sort(s,s+n);   如果数组非有序的,那就事先排好序O(N*logN)  
 
    int *begin=s;  
    int *end=s+n-1;  
 
    while(begin    {  
        if(*begin+*end>x)  
        {  
            --end;  
        }  
        else if(*begin+*end        {  
            ++begin;  
        }  
        else 
        {  
            return Pair(*begin,*end);  
        }  
    }  
 
    return Pair(-1,-1);  
}  
//或者如下编写,
//代码二
//copyright@ zhedahht && yansha
//July、updated,2011.05.14。
bool find_num(int data[], unsigned int length, int sum, int& first_num, int& second_num)
{  
    if(length < 1)
        return true;
 
    int begin = 0;
    int end = length - 1;
 
    while(end > begin)
    {
        long current_sum = data[begin] + data[end];
  
        if(current_sum == sum)
        {
            first_num = data[begin];
            second_num = data[end];
   return true;
        }
        else if(current_sum > sum)
            end--;
        else
            begin++;
    }
    return false;
}
扩展:
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;   //谢谢well
        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;    //谢谢well
 
        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个区别,即可,代码如下:
view plaincopy to clipboardprint?
// 21题递归方法  
//copyright@ July && yansha  
//July、yansha,updated。  
#include  
#include  
using namespace std;  
 
listlist1;  
void find_factor(int sum, int n)   
{  
    // 递归出口  
    if(n <= 0 || sum <= 0)  
        return;  
      
    // 输出找到的结果  
    if(sum == n)  
    {  
        // 反转list  
        list1.reverse();  
        for(list::iterator iter = list1.begin(); iter != list1.end(); iter++)  
            cout << *iter << " + ";  
        cout << n << endl;  
        list1.reverse();      
    }  
      
    list1.push_front(n);      //典型的01背包问题  
    find_factor(sum-n, n-1);   //放n,n-1个数填满sum-n  
    list1.pop_front();  
    find_factor(sum, n-1);     //不放n,n-1个数填满sum   
}  
 
int main()  
{  
    int sum, n;  
    cout << "请输入你要等于多少的数值sum:" << endl;  
    cin >> sum;  
    cout << "请输入你要从1.....n数列中取值的n:" << endl;  
    cin >> n;  
    cout << "所有可能的序列,如下:" << endl;  
    find_factor(sum,n);  
    return 0;  

// 21题递归方法
//copyright@ July && yansha
//July、yansha,updated。
#include
#include
using namespace std;
listlist1;
void find_factor(int sum, int n)
{
    // 递归出口
    if(n <= 0 || sum <= 0)
        return;
   
    // 输出找到的结果
    if(sum == n)
    {
        // 反转list
        list1.reverse();
        for(list::iterator iter = list1.begin(); iter != list1.end(); iter++)
            cout << *iter << " + ";
        cout << n << endl;
        list1.reverse();   
    }
   
 list1.push_front(n);      //典型的01背包问题
 find_factor(sum-n, n-1);   //放n,n-1个数填满sum-n
 list1.pop_front();
 find_factor(sum, n-1);     //不放n,n-1个数填满sum 
}
int main()
{
    int sum, n;
    cout << "请输入你要等于多少的数值sum:" << endl;
    cin >> sum;
    cout << "请输入你要从1.....n数列中取值的n:" << endl;
    cin >> n;
    cout << "所有可能的序列,如下:" << endl;
    find_factor(sum,n);
    return 0;
}
解法二
@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值。
代码编写如下:
view plaincopy to clipboardprint?
//copyright@ 2011 zhouzhenren  
 
//输入两个整数 n 和 m,从数列1,2,3.......n 中 随意取几个数,  
//使其和等于 m ,要求将其中所有的可能组合列出来。  
 
#include   
#include   
#include   
 
/**  
 * 输入t, r, 尝试Wk 
 */ 
void sumofsub(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)  
        {  
            sumofsub(t + k, k + 1, r - k, M, flag, X);  
        }  
        // 若不选第k个数,选第k+1个数满足条件,则递归右子树  
        if ((t + r - k >= M) && (t + (k+1) <= M))  
        {  
            X[k] = false;  
            sumofsub(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;  
    sumofsub(0, 1, sum, M, f, X);  
    if (!f)  
    {  
        printf("not found\n");  
    }  
    free(X);  
}  
 
int main()  
{  
    int N, M;  
    printf("请输入整数N和M\n");  
    scanf("%d%d", &N, &M);  
    search(N, M);  
    return 0;  

//copyright@ 2011 zhouzhenren
//输入两个整数 n 和 m,从数列1,2,3.......n 中 随意取几个数,
//使其和等于 m ,要求将其中所有的可能组合列出来。
#include
#include
#include
/**
 * 输入t, r, 尝试Wk
 */
void sumofsub(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)
        {
            sumofsub(t + k, k + 1, r - k, M, flag, X);
        }
        // 若不选第k个数,选第k+1个数满足条件,则递归右子树
        if ((t + r - k >= M) && (t + (k+1) <= M))
        {
            X[k] = false;
            sumofsub(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;
    sumofsub(0, 1, sum, M, f, X);
    if (!f)
    {
        printf("not found\n");
    }
    free(X);
}
int main()
{
    int N, M;
    printf("请输入整数N和M\n");
    scanf("%d%d", &N, &M);
    search(N, M);
 return 0;
}
扩展:
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)。
源代码如下:
view plaincopy to clipboardprint?
/** 
* The problem: 
* 从一列数中筛除尽可能少的数使得从左往右看,这些数是从小到大再从大到小的(网易)。 
* use binary search, perhaps you should compile it with -std=c99 
* fairywell 2011 
*/ 
#include   
 
#define MAX_NUM    (1U<<31)  
 
int 
main()  
{  
    int i, n, low, high, mid, max;  
      
    printf("Input how many numbers there are: ");  
    scanf("%d\n", &n);  
    /* a[] holds the numbers, b[i] holds the number of increasing numbers 
    * from a[0] to a[i], c[i] holds the number of increasing numbers 
    * from a[n-1] to a[i] 
    * inc[] holds the increasing numbers 
    * VLA needs c99 features, compile with -stc=c99 
    */ 
    double a[n], b[n], c[n], inc[n];  
      
    printf("Please input the numbers:\n");  
    for (i = 0; i < n; ++i) scanf("%lf", &a[i]);  
      
    // update array b from left to right  
    for (i = 0; i < n; ++i) inc[i] = (unsigned) MAX_NUM;  
    //b[0] = 0;  
    for (i = 0; i < n; ++i) {  
        low = 0; high = i;  
        while (low < high) {  
            mid = low + (high-low)*0.5;  
            if (inc[mid] < a[i]) low = mid + 1;  
            else high = mid;  
        }  
        b[i] = low + 1;  
        inc[low] = a[i];  
    }  
      
    // update array c from right to left  
    for (i = 0; i < n; ++i) inc[i] = (unsigned) MAX_NUM;  
    //c[0] = 0;  
    for (i = n-1; i >= 0; --i) {  
        low = 0; high = i;  
        while (low < high) {  
            mid = low + (high-low)*0.5;  
            if (inc[mid] < a[i]) low = mid + 1;  
            else high = mid;  
        }  
        c[i] = low + 1;  
        inc[low] = a[i];  
    }  
      
    max = 0;  
    for (i = 0; i < n; ++i )  
        if (b[i]+c[i] > max) max = b[i] + c[i];  
        printf("%d number(s) should be erased at least.\n", n+1-max);  
        return 0;  

/**
* The problem:
* 从一列数中筛除尽可能少的数使得从左往右看,这些数是从小到大再从大到小的(网易)。
* use binary search, perhaps you should compile it with -std=c99
* fairywell 2011
*/
#include
#define MAX_NUM    (1U<<31)
int
main()
{
    int i, n, low, high, mid, max;
 
    printf("Input how many numbers there are: ");
    scanf("%d\n", &n);
    /* a[] holds the numbers, b[i] holds the number of increasing numbers
 * from a[0] to a[i], c[i] holds the number of increasing numbers
 * from a[n-1] to a[i]
 * inc[] holds the increasing numbers
 * VLA needs c99 features, compile with -stc=c99
 */
    double a[n], b[n], c[n], inc[n];
 
    printf("Please input the numbers:\n");
    for (i = 0; i < n; ++i) scanf("%lf", &a[i]);
 
    // update array b from left to right
    for (i = 0; i < n; ++i) inc[i] = (unsigned) MAX_NUM;
    //b[0] = 0;
    for (i = 0; i < n; ++i) {
        low = 0; high = i;
        while (low < high) {
            mid = low + (high-low)*0.5;
            if (inc[mid] < a[i]) low = mid + 1;
            else high = mid;
        }
        b[i] = low + 1;
        inc[low] = a[i];
    }
 
    // update array c from right to left
    for (i = 0; i < n; ++i) inc[i] = (unsigned) MAX_NUM;
    //c[0] = 0;
    for (i = n-1; i >= 0; --i) {
        low = 0; high = i;
        while (low < high) {
            mid = low + (high-low)*0.5;
            if (inc[mid] < a[i]) low = mid + 1;
            else high = mid;
        }
        c[i] = low + 1;
        inc[low] = a[i];
    }
 
    max = 0;
    for (i = 0; i < n; ++i )
        if (b[i]+c[i] > max) max = b[i] + c[i];
  printf("%d number(s) should be erased at least.\n", n+1-max);
  return 0;
}
@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为止。
本章完。

--------------------------------------------------------------------------------
程序员面试题狂想曲-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内所有任何内容享有版权及著作权。实要转载,请以链接形式注明出处。
 

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/25835657/viewspace-695559/,如需转载,请注明出处,否则将追究法律责任。

上一篇: 没有了~
请登录后发表评论 登录
全部评论

注册时间:2011-05-17

  • 博文量
    14
  • 访问量
    40636