关于统计数字问题的算法

 更新时间:2021年11月02日 09:37:40   作者:jcwKyl  
本文介绍了统计数字问题的算法,计算出书的全部页码中分别用到多少次数字0,1,2,3,.....9,并有每一步的解题思路,需要的朋友可以参考下

一本书的页码从自然数1开始顺序编码直到自然数n。书的页码按照通常的习惯编排,每个页码都不含多余的前导数字0。例如第6页用6表示而不是06或006。数字统计问题要求对给定书的总页码,计算出书的全部页码中分别用到多少次数字0,1,2,3,.....9。

这个题目有个最容易想到的n*log10(n)的算法。这是自己写的复杂度为O(n*log10(n))的代码:

void statNumber(int n) {
  int i, t;
  int count[10] = {0};
  for(i = 1; i <= n; i++) {
  t = i;
  while(t) {
    count[t%10]++;
    t/=10;
  }
  }
  for(i = 0; i < 10; i++) {
  printf("%d/n", count[i]);
  }
}

仔细考虑m个n位十进制数的特点,在一个n位十进制数的由低到高的第i个数位上,总是连续出现10^i个0,然后是10^i个1……一直到10^i个9,9之后又是连续的10^i个0,这样循环出现。找到这个规律,就可以在常数时间内算出第i个数位上每个数字出现的次数。而在第i个数位上,最前面的10^i个0是前导0,应该把它们减掉。

这样,可以只分析给定的输入整数n的每个数位,从面可以得到一个log10(n)的算法,代码如下:

void statNumber(int n) {
  int m, i, j, k, t, x, len = log10(n);
  char d[16];
  int pow10[12] = {1}, count[10] = {0};
  for(i = 1; i < 12; i++) {
  pow10[i] = pow10[i-1] * 10;
  }
  sprintf(d, "%d", n);
  m = n+1;
  for(i = 0; i <= len; i++) {
  x = d[i] - '0';
  t = (m-1) / pow10[len-i]; 
  
  count[x] += m - t * pow10[len-i]; 
  
  t /= 10;
  j = 0;
  while(j <= x-1) {
    count[j] += (t + 1) * pow10[len-i];
    j++;
  }
  while(j < 10) {
    count[j] += t * pow10[len - i];
    j++;
  }
  count[0] -= pow10[len-i]; /* 第i个数位上前10^i个0是无意义的 */
  }
  for(j = 0; j < 10; j++) {
  printf("%d/n", count[j]);
  }
}

通过对随机生成的测试数据的比较,可以验证第二段代码是正确的。
对两段代码做效率测试,第一次随机产生20万个整数,结果在我的电脑上,第二段代码执行1.744秒。第一段代码等我吃完钣回来看还是没反应,就强行关了它。
第二次产生了1000个整数,再次测试,结果第一段代码在我的电脑上执行的时间是
10.1440秒,而第二段代码的执行时间是0.0800秒。

其原因是第一段代码时间复杂度为O(n*log10(n)),对m个输入整数进行计算,则需要的时间为 1*log10(1) + 2*log10(2) + ... + m*log10(m), 当n > 10时,有n*log10(n) > n,所以上式的下界为11+12+....+m,其渐近界为m*m。对于20万个测试数据,其运行时间的下界就是4*10^10。

同样可得第二段代码对于n个输入数据的运行时间界是n*log10(n)的。

上面的代码中有个pow10数组用来记录10^i,但10^10左右就已经超过了2^32,但是题目给定的输入整数的范围在10^9以内,所以没有影响。

原著中给出的分析如下:

考察由0,1,2...9组成的所有n位数。从n个0到n个9共有10^n个n位数。在这10^n个n位数中,0,1,2.....9第个数字使用次数相同,设为f(n)。f(n)满足如下递推式:

n>1:
f(n) = 10f(n-1)+10^(n-1)
n = 1:
f(n) =1

由此可知,f(n) = n*10^(n-1)。
据此,可从高位向低位进行统计,再减去多余的0的个数即可。
著者的思想说的更清楚些应该是这样:
对于一个m位整数,我们可以把0到n之间的n+1个整数从小到大这样来排列:
000......0
.............
199......9
200......0
299......9
.........
这样一直排到自然数n。对于从0到199......9这个区间来说,抛去最高位的数字不看,其低m-1位恰好就是m-1个0到m-1个9共10^(m-1)个数。利用原著中的递推公式,在这个区间里,每个数字出现的次数(不包括最高位数字)为(m-1)*10^(m-2)。假设n的最高位数字是x,那么在n之间上述所说的区间共有x个。那么每个数字出现的次数x倍就可以统计完这些区间。再看最高位数字的情况,显然0到x-1这些数字在最高位上再现的次数为10^(m-1),因为一个区间长度为10^(m-1)。而x在最高位上出现次数就是n%10^(m-1)+1了。接下来对n%10^(m-1),即n去掉最高位后的那个数字再继续重复上面的方法。直到个位,就可以完成题目要求了。

比如,对于一个数字34567,我们可以这样来计算从1到34567之间所有数字中每个数字出现的次数:

从0到9999,这个区间的每个数字的出现次数可以使用原著中给出的递推公式,即每个数字出现4000次。

从10000到19999,中间除去万位的1不算,又是一个从0000到9999的排列,这样的话,从0到34567之间的这样的区间共有3个。所以从00000到29999之间除万位外每个数字出现次数为3*4000次。然后再统计万位数字,每个区间长度为10000,所以0,1,2在万位上各出现10000次。而3则出现4567+1=4568次。

之后,抛掉万位数字,对于4567,再使用上面的方法计算,一直计算到个位即可。

下面是自己的实现代码: 

void statNumber_iterative(int n) {
  int len, i, k, h, m;
  int count[10] = {0};
  int pow10[12] = {1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000};
  char d[16];
  len = log10(n);   /* len表示当前数字的位权 */
  m = len;
  sprintf(d, "%d", n);
  k = 0;     /* k记录当前最高位数字在d数组中的下标 */
  h = d[k] - '0';   /* h表示当前最高位的数字 */
  n %= pow10[len];    /* 去掉n的最高位 */
  while(len > 0) {
  if(h == 0) {
    count[0] += n + 1;
    h = d[++k] - '0';
    --len;
    n %= pow10[len];
    continue;
  }
  for(i = 0; i < 10; i++) {
    count[i] += h * len * pow10[len-1];
  }
  for(i = 0; i < h; i++) {
    count[i] += pow10[len];
  }
  count[h] += n + 1;
  --len;
  h = d[++k] - '0';
  n %= pow10[len];
  }
  for(i = 0; i <= h; i++) {
  count[i] += 1;
  }
  /* 减去前导0的个数 */
  for(i = 0; i <= m; i++) { 
  count[0] -= pow10[i];
  }
  for(i = 0; i < 10; i++) {
  printf("%d/n", count[i]);
  }
}

以上就是本文的全部内容,希望对大家的学习有所帮助。

相关文章

  • C语言关于自定义数据类型之枚举和联合体详解

    C语言关于自定义数据类型之枚举和联合体详解

    枚举顾名思义就是把所有的可能性列举出来,像一个星期分为七天我们就可以使用枚举,联合体是由关键字union和标签定义的,和枚举是一样的定义方式,不一样的是,一个联合体只有一块内存空间,什么意思呢,就相当于只开辟最大的变量的内存,其他的变量都在那个变量占据空间
    2021-11-11
  • 详解C语言之柔性数组

    详解C语言之柔性数组

    这篇文章主要介绍了C语言柔性数组,通过实例分析了不完整类型、结构体及柔性数组等概念,需要的朋友可以参考下
    2021-11-11
  • 详解C语言处理算经中著名问题百钱百鸡

    详解C语言处理算经中著名问题百钱百鸡

    古代的很多数学问题都可以用现代的编程语言去尝试解决,就如本篇,将会带你通过C语言来解决算经中百钱百鸡问题,感兴趣的朋友来看看吧
    2022-02-02
  • C++实现图书馆系统

    C++实现图书馆系统

    这篇文章主要为大家详细介绍了C++实现图书馆系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-03-03
  • Matlab实现生成箭头坐标轴详解

    Matlab实现生成箭头坐标轴详解

    这篇文章主要介绍了如何利用Matlab实现生成箭头坐标轴,为坐标轴增添箭头,文中的示例代码讲解详细,对我们学习Matlab有一定帮助,需要的可以参考一下
    2022-03-03
  • C++中strlen函数的三种实现方法

    C++中strlen函数的三种实现方法

    在C语言中我们要获取字符串的长度,可以使用strlen 函数,strlen 函数计算字符串的长度时,直到空结束字符,但不包括空结束字符,因为strlen函数时不包含最后的结束字符的,因此一般使用strlen函数计算的字符串的长度会比使用sizeof计算的字符串的字节数要小
    2022-05-05
  • fcntl函数的使用详解

    fcntl函数的使用详解

    本篇文章是对fcntl函数的使用进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • C++中的函数返回值问题

    C++中的函数返回值问题

    这篇文章主要介绍了C++中的函数返回值问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-09-09
  • C++和python实现单链表及其原理

    C++和python实现单链表及其原理

    这篇文章主要介绍了C++和python实现单链表及其原理,单链表是链表家族中的一员,每个节点依旧由数据域(data)和指针域(next)组成,链表的具体概念下面文章将详细介绍,需要的小伙伴可以参考一下
    2022-03-03
  • 详解c++中的trait与policy模板技术

    详解c++中的trait与policy模板技术

    trait模板和policy模板技术是把模板的trait和policy这两个针对不同具体类型有变化的方面抽离出来形成两个独立的模板。由于trait和policy本身是模板,它的行为是可配置的,在模板中通过组合或者以模板实参传进来的方式使用trait和policy,就可以配置出不同的具体实现
    2021-06-06

最新评论