判断数组中是否存在两个元素使得其和等于给定值X

题目:

给定一个有序数组a,长度为len,和一个数X,判断A数组里面是否存在两个数,他们的和为X,bool judge(int *a, int len, int x),存在返回true,不存在返回false。(人人网校招笔试题)

解:  注意仔细研究题目中的条件,比如关键的一句,有序数组a,这是一个有序的数组,这一点十分重要!

思路1:遍历,时间复杂度为O(N2),很显然,这种方法没什么实际意义,而且题目中说了这个一个有序数组,采用遍历的方法是你想不出其他解法时的最坏选择而已!(在这代码不给出了)

思路2:根据有序数组的特点,要么递增要么递减,这个时候我们不要凭空想象,拿出纸和笔,举一个实例来分析是最好的方法。

算法的思路通过上图可以清晰的表现出来,这里再简单叙述一下:申请一个与原数组a[N]一样长度的内存空间arr[N],用给定的值X减去原数组中的元素,对应的放到申请的内存空间arr[N]中,设置两个指针p和q,分别指向原数组a[N]的最后一个元素和arr[N]的第一个元素,指针移动满足条件:

指针移动必须满足条件:p和q指针没有越界,越界则跳出

若指针指向的值相等且i不等于j(添加了条件:i不等于j,避免出现8 = 4 + 4的情况),则返回true;

若原数组是升序的,那么每次移动指向的值较大的指针;

若原数组是降序的,那么每次移动指向的值较小的指针;

移动结束,跳出移动指针的循环,说明不存在,返回false。

代码:

#include <iostream>

using namespace std;

bool judge(int *a, int len, int x);

int main(void)
{
    int a[] = {1, 2, 3, 4};
    int len = sizeof(a) / sizeof(a[0]);
    int x = 6;
    bool ret = judge(a, len, x);
    if(ret)
        cout<<"存在"<<endl;
    else
        cout<<"不存在"<<endl;

    return 0;
}

bool judge(int *a, int len, int x)
{
    bool ret = false;
    int *tmp = new int[len];
    for(int i = 0; i < len; i++)
        tmp[i] = x - a[i];

    int j = 0;
    int k = len - 1;
    while(j < len && k >= 0)
    {
        if(tmp[j] == a[k] && j != k)
        {
            ret = true;
            break;    
        }
        else if(tmp[j] == a[k])
        {
            j++;
            k--;
        }
        
        if(j < len && k >= 0&&tmp[j] > a[k])
            j++;
        if(j < len && k >= 0&&tmp[j] < a[k])
            k--;
            
    }

    
    
    return ret;
}

结果:

QQ截图20140303220856

 

思路3:跟思路2有点相似,但不用申请另外同样一个的内存空间,直接在原来数组上操作。(假设升序)前后2个指针(指向第一个和最后一个),第一个与最后一个相加。若结果大于n,则后指针往前移一位。反之前指针往后移。若前后2个指针相遇,则为false。

代码一:

bool judge(int *a, int len, int x)
{
    int begin = 0;
    int end = len - 1;
    int order = 0;       /*0-升序 1-降序*/
     
    /*判断升序还是降序*/
    if(a[0] < a[len - 1])
    {
        order = 0;
    }
    else
    {
        order = 1;
    }
     
    while(begin < end)
    {
        if(a[begin] + a[end] > x)
        {
            if(order == 0)
                --end;
            else
                ++begin;
        }
        else if(a[begin] + a[end] == x)
        {
            return true;
        }
        else
        {
            if(order == 0)
                ++begin;
            else
                --end;
        }
    }
 
    return false;
}

或代码二:

bool judge(int *a, int len, int x)
{
    int begin = 0;
    int end = len - 1;
    int order = 0;       /*0-升序 1-降序*/
     
    /*判断升序还是降序*/
    if(a[0] < a[len - 1])
        order = 0;
    else
        order = 1;
     
    if(order == 0)
    {
        while(begin < end)
        {
            if(a[begin] + a[end] > x)
                --end;
            else if(a[begin] + a[end] == x)
                return true;
            else
                ++begin;
        }
    }
    else
    {
        while(begin < end)
        {
            if(a[begin] + a[end] > x)
                ++begin;
            else if(a[begin] + a[end] == x)
                return true;
            else
                --end;
        }
    }
 
    return false;
}

各种解题思路:

  1. 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)则是不在话下。
posted @ 2014-03-03 21:43  mickole  阅读(1480)  评论(0)    收藏  举报