算法面试题(4) - N个数中有一半以上的数都等于x,找到x

问题:

  已知: N个数中有一半以上的数都等于x, 求:x
 
要求: 空间复杂度为O(1), 时间复杂度为O(n).

解答:

  int Find_Common(int [] a, int length) {
    int current = a[0];
    int count = 1;
    for(int i=1; i<length; i++) {
       if(a[i] == current) {
         count++;
       } else {
         count--;
       }
       if(count==0&&i!=length-1) {
         i++;
         current = a[i];
         count = 1;
       }
    }
   return current;
  }

下面用代数的方法证明如下(这个证明有点生搬硬套之嫌), 假设字符串(Xn...Xn+a1C...C)(Xm...Xm+a2C...C)...(Xy...Xy+an-1C...C)Xz...Xz+an,
其中Xn...Xn+a1的count值的取值范围是[1,a1],设为A1, Xn+a1之后有连续的B1个C, 那么B1个C后的count值为(B1-A1)的绝对值, 这时的current则由B1和A1的大小来决定,B1>A1时, current为X(未知);相反,current为C; 同样的道理(Xn...Xn+a1C...C)Xm...Xm+a2的count值为A2-(B1-A1),其中A2的取值范围是[1,a2];假设后面有B2个C, 则count值为(B2-A2)+(B1-A1)的绝对值.以此类推...
(Xn...Xn+a1C...C)(Xm...Xm+a2C...C)...(Xy...Xy+an-1C...C)Xz...Xz+ancount值为An-((Bn-1-An-1)+...+(B2-A2)+(B1-A1))的绝对值, 而由条件可知An+...+A1<Bn+...+B1,所以An<((Bn-1-An-1)+...+(B2-A2)+(B1-A1)), 这样当前的current应为C.

扩展:

如果数组中有2个出现次数超过1/3, 或3个出现次数超过1/4的数字.... 应该怎么找出来呢? 有一个通用的办法, 例如需要找到出现次数超过1/m的数字. 那么在搜索过程中需要一个长度为(m-1)数组, 也就是说空间开销为O(m). 当算法结束时, 数组中的数字就是满足条件的数字(出现次数超过1/m的数字最多也就m-1个).

int [] find_num(int [] a, int length, int m) {
  int [] result = (int []) malloc((m-1)*sizeof(int));
   int i = 0; j = 0;
  Boolean found;
  for(;i<length;i++) {
    found = false;
    for(;j<m-1;j++) {
      if(a[i] == result[j]) {
        count[j]++;
        found = true;
        break;
      }
   }
  if(!found) {
    j = 0;
    for(;j<m-1;j++) {
      if(count[j]==0) {
        result[j] = a[i];
        count[j] = 1;
        found = true;
        break;
     }
    }
  }
  if(!found) {
    j = 0;
    for(;j<m-1;j++) {
      count[j]--;
    }
  }
  return result;

}

道理很简单, 我们从数组a的头部开始扫描, 对每一个碰到的数字进行计数(初始情况下result的槽都为空):
如果遇到的数字在result已经出现了,那么把count值加1,也就是对这个数字进行计数;
如果遇到一个result中中没有的数字, 则分为两种情况:
  如果有空槽, 把它放入一个空槽中并把count值设为1.
  如果没有, 则把每个槽中的数字的count减一, 也就是丢弃(m-1)个已计数的数字 (算上当前数字, 本次丢弃了m个数字)
用上面的算法, 每次丢弃的m个数字都为不相同数字, 而丢弃次数为length/m. 假设有数字K在a中出现次数超过了length/m, 则经过length/m次丢弃(每次丢弃最多丢弃1个K)后, K应该还在result中未被丢弃(count值大于0). 所以result中count值大于0的数字即为所求数.

需要说明的是, 如果题目并未指出是否一定有数字出现次数确实超过了length/m, 那么算法结束时result中count大于0的数字不一定就是所求数字. 我们需要逐一对其进行验证. 比如, "检测是否有数字在数组a中的出现次数超过了1/3,并列出这些数字". 那么在count大于0的前提下, 我们需要对result[0], result[1]中的数字进行验证, 时间复杂度为O(n). 所以并不影响整体的时间复杂度. 如果length, m值都比较大, 那么在不知道是否有数字超过length/m+1次的情况下, 时间复杂度可以认为是O(length*m), 空间复杂度为O(m).

另外对于临界值, 比如验证一个数字出现次数是否大于等于1/2, 那么我们需要对m向上取整. 也就是说[1,1/2)需要1个槽的数组, [1/2, 1/3)需要2个槽的数组....总之这个题目的精髓在于如何用尽量少的内存来记录数字的计数.

posted @ 2009-03-05 18:06  破冰  阅读(1320)  评论(0编辑  收藏  举报