• [数字技巧]重复数字统计算法的空间优化


      今天在微博上看到的一道面试题,觉得非常有意思,特记录下来。

      原题是这样的:

          给定数组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
    ",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 }
    View Code

      上述方法采用了额外的空间记录元素出现次数,因而空间复杂度是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
    ",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 }
    View Code

      微博上还有一种思路,是分类法,大致思路与上述改进一是类似的。顺序扫描数组,当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
    ", 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 }
    View Code

    改进二:

      改进二技巧性比较强,通过对数组进行三次处理。假定数组从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
    ",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 }
    View Code

       这个算法的出发点肯定也是要利用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
    ",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 }
    View Code

      这个算法中也涉及到一个"大数"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

      写得不好,如有错误或表述不清楚的,希望大家指出。

  • 相关阅读:
    自动化测试如何解析excel文件?
    Unittest加载执行用例的方法总结
    pytest进阶之配置文件
    [编程题] 把二叉树打印成多行
    [编程题]求1+2+3+....n
    [编程题]-[位运算技巧系列]不用加减乘除做加法
    [编程题]数值的整数次方
    [编程题]构建乘积数组
    [编程题]变态跳台阶
    [编程题][剑指 Offer 10- II. 青蛙跳台阶问题]
  • 原文地址:https://www.cnblogs.com/codershell/p/3288013.html
Copyright © 2020-2023  润新知