算法学习笔记----判断集合S中是否存在有两个其和等于x的元素

题目:请给出一个运行时间为Θ(nlgn)的算法,使之能在给定一个由n个整数构成的集合S和另一个整数x时,判断出S中是否存在有两个其和等于x的元素。

解题思路:
直观的方法是直接计算集合中两两元素的和,然后判断是否存在x,但时间复杂度为Θ(n^2),不符合题目的要求,也不是一个好的解决问题的方法,下面两种方法要好一些:
 第一种是《算法导论》的教师手册上提供的思路,构建一个辅助集合S',通过查找辅助集合S'和原集合合并后的集合中是否有重复的元素来判断,具体步骤如下:
   1)对集合S进行排序
   2)构建辅助集合S',S'={z:z=x-y,y∈S},也就是说S'中的元素是x减去集合S中的元素生成
   3)对集合S'进行排序
   4)移除集合S中重复的元素,只保留一个,也就是说使集合S中的元素唯一。对集合S'中做同样的处理。
   5)合并集合S和S'
   6)当且仅当合并的集合中存在连续的位置上出现相同的的值时,集合S中存在两个数的和为x。(基本直译)
   这个解题思路是有问题,而且如果简单从字面意思理解的话,这个思路是错误的,在某些情况下是不正确的。下面一一列出这个思路存在的问题:
  a. 在生成辅助集合S'之后,才开始将集合S中的重复元素去掉只保留一个,这样S'中也会有同样的重复元素,为什么不在生成辅助结合S'之前做呢?如果在第1步之后做的话,S'中的元素也是唯一的了,减少重复的工作
  b. 第3步完全没有必要,因为S在第1步中已经排好序了,所以生成的S'集合也是排好序的了,只是排序的方式不同。如果集合S是升序排列,则集合S'是降序排列。所以没有必要再对集合S'排序,只需在合并的时候稍作处理即可。
  c. 第6步中的描述原文是”There exist two elements in S whose sum is exactly x if and only if the same value appears in consecutive positions in the merged output“,如果 从字面意思理解的话,就是只要合并的集合中有重复的元素就证明结合S中存在两个数的和为x。但是如果这么理解的话,是不对的,比如集合S={2,3,5, 6},x=4,       则S'={2, 1,-1,-2},合并后的集合为{-2, -1, 1, 2, 2, 3, 5, 6},合并后的结合中存在重复的元素{2, 2},位置连续并且和为x,但是集合S中并没有两个数的和        为x。所以第6步的表述是有问题,要么是真的错了,要么是语音差异理解的有问题。那要怎么才能正确地确定呢?就是在合并的集合中必须至少有两个重复的元素,这时      才能肯定集合S中存在两个数的和为x。可以证明一下,假设w∈S, y∈S,则集合S'中也存在w∈S,y∈S,所以合并的集合中会有两个重复的元素w、y。如果有多对解        的话,重复的元素个数会更多。
d. 第4步中将重复的元素唯一化,如果重复的元素的和刚好是x呢?这时岂不是反而弄巧成拙了?比如集合S={2,2,5, 6},x=4,去除重复的元素,反而错失了找到两个数的和为x的机会,而且题目中也没有要求两个和为x的元素不能重复。
  还有一点需要注意的是,这个思路只是用来确定集合S中是否有两个元素的和为x,不需要确定是哪两个元素的和为x。
  所以个人认为正确的思路应该是这样:
  1)对集合S进行排序
  2)检查集合S中是否有重复的元素,如果有则判断重复的元素乘以2(就是两个相加)是否为x,如果是的话,就找到了,无需做后面的处理;否则移除重复的元素,使集合S           中的元素唯一。
  3)构建辅助集合S',S'={z:z=x-y,y∈S},也就是说S'中的元素是x减去集合S中的元素生成
  4)合并集合S和S'
  5)当合并的集合中有两个或两个以上的重复元素时,集合S中存在两个元素的和为x。
  接下来确定上面的思路的算法复杂度。第1步的使用归并排序来完成,时间复杂度为Θ(nlg(n)),第2、3、4、5步的时间复杂度为Θ(n),合并起来为Θ(nlg(n))。符合题目的要求。

  其代码实现如下所示:

#include <stdio.h>
#include <errno.h>

#ifndef INT_MAX
#define INT_MAX		((int)(~0U>>1))
#endif

#define ARRAY_SIZE(__s) (sizeof(__s) / sizeof(__s[0]))

static void merge(int *a, int start, int mid, int end)
{
    int nl = mid - start + 1;
    int nr = end - mid;
    int sentinel = INT_MAX;
    int left[nl + 1], right[nr + 1];
    int i, j, k = start;
    
    for (i = 0; i < nl; ++i) {
        left[i] = a[k++];
    }
    /* Set sentinel */
    left[i] = sentinel;
    
    for (j = 0; j < nr; ++j) {
        right[j] = a[k++];
    }
    /* Set sentinel */
    right[j] = sentinel;
    
    i = j = 0;
    for (k = start; k <= end; ++k) {
        if (left[i] <= right[j]) {
            a[k] = left[i++];
        } else {
            a[k] = right[j++];
        }
    }
}

static void merge_sort(int *a, int start, int end)
{
    int mid;
    
    if ((start >= 0) && (start < end)) {
        mid = (start + end) /2 ;
        
        merge_sort(a, start, mid);
        merge_sort(a, mid + 1, end);
        
        merge(a, start, mid, end);
    }
}

static int check_exist_x(int *a, int len, int x)
{
    int i, j, k;
    int last;
    /* Just for test, should avoid */
    int tmp[len];
    int collection[2 * len];
    int repeats;
    
    if (len < 1) {
        fprintf(stderr, "Too few elements.\n");
        return -EINVAL;
    }
    
    merge_sort(a, 0, len - 1);
    
    last = 0;
    /* Remove repeat elements */
    for (i = 1; i < len; ++i) {
        if (a[last] == a[i]) {
            if ((a[last] << 1) == x) {
                /* Found */
                return 0;
            }
            continue;
        }
        a[++last] = a[i];
    }
    ++last;
    
    /* Form tmp set */
    for (i = 0; i < last; ++i) {
        tmp[i] = x - a[i];
    }
    
    i = 0;
    j = last - 1;
    k = 0;
    /* Merge */
    while ((i < last) && (j >= 0)) {
        if (a[i] < tmp[j]) {
            collection[k++] = a[i++];
        } else {
            collection[k++] = tmp[j--];
        }
    }
    while (i < last) {
        collection[k++] = a[i++];
    }
    while (j >= 0) {
        collection[k++] = tmp[j--];
    }
    
    repeats = 0;
    /* Check the number of repeat elements */
    for (i = 1, j = 0; i < k; ++i, ++j) {
        if (collection[i] == collection[j]) {
            ++repeats;
        }
        
        if (repeats >= 2) {
            return 0;
        }
    }
    
    return -1;
}

int main(void)
{
    int source[] = { 7, 5, 2, 4, 6, 1, 5, 3};
    int ret;
    int x = 13;
    
    ret = check_exist_x(source, ARRAY_SIZE(source), x);
    
    printf("If there are two elements whose sum equals to x? %s.\n",
        ret < 0 ? "No" : "Yes");
    return 0;
}

  第二种是使用排序+二分查找,具体步骤如下:
    1)对集合S进行排序
    2)从集合S中选择一个元素S(i),计算x与S(i)的差值y=x-S(i)。在集合S中查找除S(i)之外的元素中是否存在y,如果存在,则返回。
    3)检查是否全部元素已遍历,如果没有跳到第2步。

 接下来确定该思路的复杂度。第1步使用归并排序来排序,时间复杂度为Θ(nlg(n));二分查找的时间复杂度为Θ(lg(n)),第2、3步需要遍历的次数为n,因此第2、3步的时间复杂度为Θ(nlg(n)),因此总的时间复杂度为Θ(nlg(n)),符合题目的要求。

  代码实现如下所示:

#include <stdio.h>
#include <errno.h>

#ifndef INT_MAX
#define INT_MAX		((int)(~0U>>1))
#endif

#define ARRAY_SIZE(__s) (sizeof(__s) / sizeof(__s[0]))

static void merge(int *a, int start, int mid, int end)
{
    int nl = mid - start + 1;
    int nr = end - mid;
    int sentinel = INT_MAX;
    int left[nl + 1], right[nr + 1];
    int i, j, k = start;
    
    for (i = 0; i < nl; ++i) {
        left[i] = a[k++];
    }
    /* Set sentinel */
    left[i] = sentinel;
    
    for (j = 0; j < nr; ++j) {
        right[j] = a[k++];
    }
    /* Set sentinel */
    right[j] = sentinel;
    
    i = j = 0;
    for (k = start; k <= end; ++k) {
        if (left[i] <= right[j]) {
            a[k] = left[i++];
        } else {
            a[k] = right[j++];
        }
    }
}

static void merge_sort(int *a, int start, int end)
{
    int mid;
    
    if ((start >= 0) && (start < end)) {
        mid = (start + end) /2 ;
        
        merge_sort(a, start, mid);
        merge_sort(a, mid + 1, end);
        
        merge(a, start, mid, end);
    }
}

static int binary_search(int *a, int len, int expect, int target)
{
    int left = 0, right = len - 1, mid;
    
    do {       
        if (left == expect) {
            ++left;
        }
        
        if (right == expect) {
            --right;
        }
        
        mid = (left + right) / 2;
        
        if ((mid != expect) && (a[mid] == target)) {
            return 0;
        } else if (a[mid] > target) {
            right = --mid;
        } else {
            left = ++mid;
        }
    } while (left <= right);
    
    return -1;
}

static int check_exist_x(int *a, int len, int x)
{
    int i;
    
    if (!a || len < 2) {
        fprintf(stderr, "Too few elements.\n");
        return -1;
    }
    
    merge_sort(a, 0, len - 1);
    
    for (i = 0; i < len; ++i) {
        if (!binary_search(a, len, i, x - a[i])) {
            return 0;
        }
    }
    
    return -1;
}

int main(void)
{
    int source[] = { 7, 5, 2, 4, 6, 1, 5, 3};
    int x = 13;
    int ret;

    ret = check_exist_x(source, ARRAY_SIZE(source), x);
    printf("If there are two elements whose sum equals to x? %s.\n",
        ret < 0 ? "No" : "Yes");

    return 0;
}

你可能感兴趣的:(算法学习笔记----判断集合S中是否存在有两个其和等于x的元素)