[数字技巧]重复数字统计算法的空间优化
2013-08-29 15:54 庸男勿扰 阅读(792) 评论(0) 编辑 收藏 举报今天在微博上看到的一道面试题,觉得非常有意思,特记录下来。
原题是这样的:
给定数组A,大小为n,数组元素为1到n的数字,不过有的数字出现了多次,有的数字没有出现。请给出算法和程序,统计哪些数字没有出现,哪些数字出现了多少次。能够在O(n)的时间复杂度,O(1)的空间复杂度要求下完成么?
这道题目最大的难点就在于时空限制,确切的说是空间限制,如果没有空间复杂度为O(1)的要求,我们很容易想出用一个hash表来记录元素的出现次数。实现的代码如下:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 5 int a[1001]; 6 int b[1001] 7 int n; 8 void solve4() 9 { 10 for(int i=1; i<=n; i++) 11 { 12 b[a[i]]++; 13 } 14 for(int i=1; i<=n; i++) 15 printf("%d\n",b[i]); 16 } 17 int main() 18 { 19 freopen("1.in","r",stdin); 20 freopen("1.out","w",stdout); 21 22 scanf("%d",&n); 23 for(int i=1; i<=n; i++) 24 { 25 scanf("%d",&a[i]); 26 } 27 solve4(); 28 29 return 0; 30 }
上述方法采用了额外的空间记录元素出现次数,因而空间复杂度是O(n)的。这与题目要求不符。
显然,这就要求我们要充分利用已有的空间即原数组空间来记录。Hash的方法是使用另外一个数组b,通过b[k]来表示k在数组a中出现的次数。那么,如果要在已有的数组上改,就要让a[k]能表示k在a中出现的次数。所以就有了第一种改进思路。
改进一:
通过对原数组进行改造,使得数字k出现在以他为下标的位置上。如数组54131,经过改造后变成11345,然后再统计出现次数,为了与原数组中的数字区分开,使用负数统计,即0代表未出现过,-j代表出现j次。
1 #include <cstdio> 2 #include <cstring> 3 #include <ctime> 4 #include <assert.h> 5 #include <algorithm> 6 #include <cmath> 7 using namespace std; 8 9 int a[1001]; 10 int n; 11 12 void solve2() 13 { 14 for(int i = 1; i <=n; i++){ 15 if(a[i]>0){//当前位置尚未统计过 16 int t = a[i];//保存当前位置值并置0,说明i尚未出现 17 a[i] = 0; 18 while(a[t] > 0){//尚未统计t出现次数 19 int temp = a[t]; 20 a[t] = -1;//t出现一次 21 t = temp;//循环统计原来a[t]值在数组中出现的次数 22 } 23 a[t] --;//已统计过,-1即可 24 } 25 } 26 //绝对值为出现次数 27 for(int i = 1; i <=n; i++){ 28 a[i] = abs(a[i]); 29 printf("%d\n",a[i]); 30 } 31 } 32 int main() 33 { 34 freopen("1.in","r",stdin); 35 freopen("1.out","w",stdout); 36 37 scanf("%d",&n); 38 for(int i=1; i<=n; i++) 39 { 40 scanf("%d",&a[i]); 41 } 42 solve2(); 43 44 return 0; 45 }
微博上还有一种思路,是分类法,大致思路与上述改进一是类似的。顺序扫描数组,当a[i]>0即a[i]尚未被统计过时,根据a[i]与i的大小分类讨论。
1、a[i] = i:说明i出现在正确位置上,且是第一次出现,直接将a[i]置为-1,代表出现过一次;
2、a[i] < i:说明a[i]出现在他应该出现的位置后面,“小数在后”,由于是顺序扫描,他应该出现的那个位置肯定已经被统计过,直接-1即可,即a[a[i]] -= 1;
3、a[i] > i:说明a[i]出现在他应该出现的位置前面,“大数在前”,此时不能直接像2中那样修改,因为,大数应该出现的位置可能没有统计过,此时要再次分类讨论:
3.1、当a[a[i]] == a[i],即a[i]出现了两次,此时可以直接将a[a[i]] = -2;因为在当前位置之前,a[i]这个数肯定未出现过,不然已经被当做情况3讨论过,a[a[i]]应该就是负数了(代表a[i]出现的次数);
3.2、当a[a[i]] < 0,即a[i]已经被考察过了,直接-1,即a[a[i]] -= 1;
3.3、其余情况指,a[a[i]]是另一个尚未统计过的数,此时将a[i]换到正确的位置a[a[i]],继续考察a[i]。
代码如下:
1 #include <cstdio> 2 #include <cstring> 3 #include <ctime> 4 #include <assert.h> 5 #include <algorithm> 6 #include <cmath> 7 using namespace std; 8 9 int a[1001]; 10 int n; 11 void solve5() 12 { 13 for (int i = 1; i <= n; ++i) { 14 if (a[i] <= 0) continue;//尚未被统计过的 15 bool succ = false; 16 while (succ == false) {//尚未被统计过 17 if (a[i] == i) { 18 a[i] = -1;//第一次出现 19 succ = true; 20 } else if (a[i] < i) {//a[i]出现在他应该在的位置的后面 21 a[a[i]] -= 1;//前面肯定已经统计过 ,直接修改即可 22 a[i] = 0; 23 succ = true; 24 } else if (a[i] > i) {//a[i]出现在他应该出现的位置的前面,不能随意修改,会覆盖尚未统计的数 25 if (a[a[i]] == a[i]) {//他应该出现的位置的数也是他本身 26 a[a[i]] = -2;//计数出现2次 27 a[i] = 0; 28 succ = true; 29 }else if (a[a[i]] < 0) {//该数已经统计过 30 a[a[i]] -= 1; 31 a[i] = 0; 32 succ = true; 33 }else { 34 swap(a[i], a[a[i]]);//将a[i]换到他应该出现的位置上,继续考察当前位置上的数 35 } 36 } 37 } 38 } 39 //绝对值 40 for (int i = 1; i <= n; ++i) { 41 printf("%d\n", abs(a[i])); 42 } 43 } 44 int main() 45 { 46 freopen("1.in","r",stdin); 47 freopen("1.out","w",stdout); 48 49 scanf("%d",&n); 50 for(int i=1; i<=n; i++) 51 { 52 scanf("%d",&a[i]); 53 } 54 solve5(); 55 56 return 0; 57 }
改进二:
改进二技巧性比较强,通过对数组进行三次处理。假定数组从1~n。
步骤一:a[i]=a[i]*(n+1)
步骤二:a[a[i]/(n+1)]++
步骤三:输出a[i]%(n+1)即为则依次为i在数组出现的次数。
代码如下:
1 #include <cstdio> 2 #include <cstring> 3 #include <ctime> 4 #include <assert.h> 5 #include <algorithm> 6 #include <cmath> 7 using namespace std; 8 9 int a[1001]; 10 int n; 11 12 void solve1() 13 { 14 int i; 15 for(i=1; i<=n; i++) 16 { 17 a[i] = a[i]*(n+1); 18 } 19 for(i=1; i<=n; i++) 20 { 21 a[a[i]/(n+1)]++; 22 } 23 for(i=1; i<=n; i++) 24 { 25 printf("%d\n",a[i]%(n+1)); 26 } 27 28 } 29 int main() 30 { 31 freopen("1.in","r",stdin); 32 freopen("1.out","w",stdout); 33 34 scanf("%d",&n); 35 for(int i=1; i<=n; i++) 36 { 37 scanf("%d",&a[i]); 38 } 39 solve1(); 40 41 return 0; 42 }
这个算法的出发点肯定也是要利用a[i]来表示i在数组中出现的次数,更形式化一点就是使用a[a[i]]来表示数组中每个数出现的次数,每出现一次就++即可。也就是说原a[i]的"增量"就是i出现的次数。要想得到增量,显然的做法是取余。这是为什么步骤一和步骤二要先乘后除的原因。如果不做这样的操作(或者认为乘除是1),那么取余就都为0了。
搞清楚这一初衷,我们不难理解为什么用来乘除取余的数k是n+1,而不是n或者更小。
原因1:只有k足够大(即大于n)时,在步骤1乘了k之后,步骤二又除以才会还在原来的位置,而不会因为a[i]多次加1改变a[i]/k的值;
原因2:当k为n或者更小时,有可能存在某一个数,他出现的次数也是k,那么其增量为k,与k取余后为0,因此,k的取值应该大于n,即大于出现次数的上界。
对于改进二,还有另一种方法,改进二的思路是,加1得到增量再取余。另一种类似的方法是,加一个“大数”再除以"大数"得到加上“大数”的次数即出现的频率。
代码如下:
1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 #include <cmath> 5 using namespace std; 6 7 int a[1001]; 8 int n; 9 10 void solve3() 11 { 12 int i; 13 for(i=1; i<=n; i++) 14 { 15 a[a[i]%(n+1)] += (n+1); 16 } 17 for(i=1; i<=n; i++) 18 { 19 printf("%d\n",a[i]/(n+1)); 20 } 21 } 22 int main() 23 { 24 freopen("1.in","r",stdin); 25 freopen("1.out","w",stdout); 26 27 scanf("%d",&n); 28 for(int i=1; i<=n; i++) 29 { 30 scanf("%d",&a[i]); 31 } 32 solve1(); 33 34 return 0; 35 }
这个算法中也涉及到一个"大数"k,代码中取值为n+1,事实上,与上面类似,这个值k必须大于n。原因如下:
原因1:只有a[i]<k才能保证a[i] % k是不变的
原因2:最后每一个元素表示为a[i] = x + f*k,其中x<k,并且f就是我们要统计的频率。
其实,这道题的核心思想就是让元素出现在该出现的位置,陈利人老师出过另外一道题是最小没出现的正整数,与这个有点类似。题目如下:
给定一个无序的整数数组,怎么找到第一个大于0,并且不在此数组的最小整数。比如[1,2,0] 返回 3, [3,4,-1,1] 返回 2。最好能O(1)空间和O(n)时间。
CSDN上有人给出详细的题解,有兴趣的可以看看:http://blog.csdn.net/ju136/article/details/8153274
写得不好,如有错误或表述不清楚的,希望大家指出。
出处:http://www.cnblogs.com/codershell
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
如果您觉得对您有帮助,不要忘了推荐一下哦~