题目:请给出一个运行时间为Θ(nlgn)的算法,使之能在给定一个由n个整数构成的集合S和另一个整数x时,判断出S中是否存在有两个其和等于x的元素。
思考:
1. 本能的反应是,这个需要两重遍历这个集合,以获取每两个元素的组合是否满足要求,那么运行时间就是Θ(n*n)。就算可以像下面的方式减去一半左右的运行时间,但是依然是n平方的时间复杂度。
for (i=0; i<size(S); i++)
for (j=i; j<size(S); j++)
if (S[i] + S[j] == x)
return FOUND;
return NOT FOUND
2. 考虑要求是nlgn的时间复杂度,通常应当采取分治的策略,简单看一下,我们也许可以把S平均分成两部分,每部分含有一半多的原S集合中的元素,那么现在的题目就变成了求解两个更小集合内是否存在两个其和等于x的元素,以及是否存在这种情况:分别从这两个集合内取出一个数使其和等于x。这里的麻烦在于后面这种情况,我们依然不得不进行两重循环来遍历,因此其复杂度依然是n平方(其实是二分之n取平方,Θ运算会去掉常数参数)
3. 既然刚学习过一个时间复杂度为nlgn的排序算法,那么我们可以考虑是否能借助排序来达到要求,因为先对S的元素进行一次排序的时间复杂度是满足需求的。在进行升序排序之后得到的集合记做S1,现在我们可以考虑以下算法:
for (i=0,j=sizeof(S1);i!=j;)
if (S1[i]+S1[j] == x)
return FOUND;
else if (S1[i]+S1[j] > x)
j--;
else if (S1[i]+S1[j] < x)
i++
endif
return FALSE;
我们需要证明一下这个算法的正确性:
1) 如果S只有1个元素,那么i=0, j=0 , 循环不运行,返回FALSE
2) 如果S只有2个元素,那么i=0, j=1 ,循环将运行1次,如果正好两个元素之和为x,返回TRUE,如果不为x,那么或者i自增,或者j自减,将使得i和j相等,循环退出,返回FALSE
3) 对于大于2个元素的情况
假设对于某时刻的i和j而言,这个算法是正确的,我们现在需要证明接下来的操作也是正确的。
对于此刻的i和j,进入这一状态的前提可能有两个,即S1[i-1]+S1[j] < x ,或者S1[i]+S1[j+1] > x
如果是前者即S1[i-1]+S1[j] < x,那么进入这一状态的前提又可能有两个,即S1[i-2]+S1[j] < x ,或者S1[i-1]+S1[j+1] > x
此推理可以继续直到S1[0]+S1[j] < x ,或者S1[1]+S1[j+1] > x
而如果S1[0]+S1[j] < x ,进入此状态的唯一条件就是S1[0]+S1[j+1] > x
由此我们可知对于任意m<=i , S1[m]+S1[j+1] != x
若S1[i]+S1[j] < x ,此时对于任意S1[m] (m<i) ,显然S1[m]+S1[j] < x ,所以我们可以通过i++来寻找一个稍大的元素而不是j++,因为j+1的对应元素我们已经在之前的算法中遍历过了,对于任意S1[m] (m<=i) ,必然有S1[m]+S1[j+1] > x ,由于S1是有序集合因此其实对于任意S1[m] 都有S1[m]+S1[j+1] > x
同理可知如果S1[i]+S1[j] > x ,我们必然有对于任意S1[m] 都有S1[i]+S1[m] < x ,故此时我们为了挑选一个更小的元素,只能取 j-- 而不是i++
由此我们证明了在当前i和j的取值条件下如果算法是正确的,那么接下来的一个循环内这个算法也是正确的。这个遍历的时间复杂度是Θ(n)
由于此算法依赖于一个排序,所以算法时间复杂度不低于排序算法的时间复杂度,也就是nlgn,是满足题目要求的。
4. 另一个角度思考,如果我们可以把求和转化为查找两个相等的元素,这就是一个更加常见的有通用算法的题目,依然借助于排序,可以转化为查找是否有相邻两元素相等,显然其时间复杂度依然依赖于排序。或者根据编程珠玑的讨论,对于特定输入集合(整数取值范围不太大),我们甚至可以使用位图的方法,不需要排序,由此获得Θ(n)的时间复杂度。
那么如何转化到查找两个相等的元素呢?
考虑假如存在两元素i和j使得i+j == x,我们对式子进行一下变形,考虑到要使运算结果依然是整数,我们可以这样:
2(i+j) == 2x
2i + 2j == 2x
2i-x + 2j-x == 0
abs(2i-x) == abs(2j-x)
也就是说,我们先遍历一次集合S,并且对每个元素进行乘2再减x的操作,然后求绝对值,得到一个新的集合S1,那么如果S1[i] == S1[j] ,则可能存在S[i] + S[j] == x ,此时再验算S[i] + S[j]的结果就可以知道是否存在这样的数据对了,只是这种方法一定要在排序的时候让两个集合的下标保持同步。位图算法此时虽然可以知道是否S1中存在相等的元素,但是较难反向查找到对应的S元素,必须根据S1[i]的结果反向算出两个数据再查找S中是否存在这两个数据,每次都要遍历整个数组,因此最差情况下时间复杂度反而会是n平方。