问题描述
一本书的页码从自然数1开始顺序编码直到自然数n。书的页码按照通常的习惯编码,每个页码都不含有多余的前导数字0。现在给出表示书的总页码的自然数n,计算书的全部页码中分别用到多少次数字0,1,2,3,…, 9。(或者计算说0-9数字出现了多少次)。
例如,给出一个数n=12,则出现的数有1,2,3,4,5,6,7,8,9,10,11,12。这些数中0-9分别出现的次数为:(由上到下分别表示0-9出现的次数,后文程序结果都采用这样的格式)
题目源于:王晓东.《计算机算法设计与分析》.第5版习题1-1
算法思路与实现代码
方法一:暴力遍历法
思路:设置一个长度为10的数组作为0-9每个数出现次数的计数器,将页码从1~n遍历一遍,提取每一个数的每一位,根据每一位数的值给对应数字计数器加1,最后在循环结束后通过计数器内的结果确定每个数字的出现次数。
代码1
#include<stdio.h>
int main(){
int arr[10]={0};//存储0-9各个数的出现次数,初始化为零
int n; scanf("%d",&n);//输入n
for(int i=1;i<=n;i++){
//拆分每一个数的每一位
int num[10]={0};//存每次数的每一位的值
int len=0; //这个数的位数
int m = i;
while(m/10){//采用除10取余的方式,从个位开始获取每一位的值
num[len++] = m%10;
m = m/10;
}
num[len++] = m;
for(int j=0;j<len;j++){//对应计数器+1
arr[ num[j] ]++;
}
}
for(int i=0;i<10;i++){//打印结果
printf("%d\n",arr[i]);
}
return 0;
}
方法二:拆数计算法
思路:统计0-9每个数,分别在每一位上的出现次数。即对于一个总位数为len的数n,依次计算其第一位(个位),第二位(十位),第三位…,第len位上,0-9每个数出现的次数,再统计每次的计算结果。
每一位上各个数字出现的次数与四个条件有关:这一位上的数的值V(Value),这一位数前面一串数的值F(Front),这一位数后面一串数的值R(Rear),这一位数的位置P。以n=12345的第三位(百位)为例,这一位上0-9各个数出现的个数分别与:V=3,F=12,R=45,P=3.有关。
对于输入数n,其任意位都有VFRP(对于最高位,取F=0;对于第一位,取R=0),考虑其任意一位,有如下四种规则:
①
0
−
9
各
数
在
该
为
的
出
现
次
数
至
少
为
:
F
×
1
0
P
−
1
②
对
于
0
−
9
中
大
于
V
的
数
,
出
现
次
数
不
必
额
外
增
加
。
③
对
于
0
−
9
中
等
于
V
的
数
,
出
现
次
数
与
还
R
有
关
,
需
加
上
:
R
+
1
(
0
到
x
,
共
有
x
+
1
个
数
)
④
对
于
1
−
9
中
小
于
V
的
数
,
需
加
上
:
1
0
P
−
1
\begin{aligned} ①&0-9各数在该为的出现次数至少为:F×10^{P-1}\\ ②&对于0-9中大于V的数,出现次数不必额外增加。\\ ③&对于0-9中等于V的数,出现次数与还R有关,需加上:R + 1(0到x,共有x+1个数)\\ ④&对于1-9中小于V的数,需加上:10^{P-1} \end{aligned}
①②③④0−9各数在该为的出现次数至少为:F×10P−1对于0−9中大于V的数,出现次数不必额外增加。对于0−9中等于V的数,出现次数与还R有关,需加上:R+1(0到x,共有x+1个数)对于1−9中小于V的数,需加上:10P−1
第四点中不包括0,是因为0不可出现在首位,即没有0,024,0324等数,例如当n=123,考虑P=2(十位),在已考虑①的前提下,考虑④,显然当1<2,1出现的次数需要额外加10(因为有10,11,12,…,20的过程);而对于0<2,0出现的次数不能加10(因为没有00,01,02,…,10的过程,只有1,2,3,4…,10,在这个过程十位没有出现0)。
所以只需要得到n每一位上的VFRP,再设置一个长度为10的数组作为0-9每个数出现次数的计数器,循环计算并统计各个位上每个数的出现次数,最后通过计数器内的结果确定每个数字的出现次数。
代码2
#include<stdio.h>
int main(){
int arr[10]={0};//保存每一个数出现次数的计数器数组
int pow10[10]={1}; //每一位的权值,即10^(P-1)
for(int i=1;i<10;i++)pow10[i]=10*pow10[i-1];
int n; scanf("%d",&n); //输入n
int num[3][10]={0};
//[0]存每一位的值V
//[1]存每一位前面一串数的值F
//[2]存每一位后面一串数的值R
int len=0; //记录位数 P
while(n/10){ // 拆分n的每一位,获取VFR
num[0][len] = n%10;
num[2][len+1] += n%10*pow10[len] + num[2][len];
n = n/10;
num[1][len++]=n;
}
num[0][len++] = n;
int i = 0;
for(i=0;i<len;i++){
for(int j=0;j<10;j++){
arr[j] += num[1][i]*pow10[i];
//对于0-9中等于V的数,出现次数与还R有关,需加上:R + 1
if(j==num[0][i]){
arr[j] += num[2][i] + 1;
}
// 对于1-9中小于V的数,需加上:10^(P-1)
else if(j<num[0][i] && j!=0){
arr[j] += pow10[i];
}
}
}
for(int i=0;i<10;i++)printf("%d\n",arr[i]);
return 0;
}
代码测试
测试用例1:12345
测试用例2:45678
算法复杂度分析
方法一:
此算法一共包括三个核心循环,最外层大循环1 ~ n 需要循环n次。大循环内有两个小循环顺序执行,一个循环是提取每个数的每一位,循环次数由每次被提取数的位数决定,而被提取数的最大位数不大于
log
10
n
\log_{10} n
log10n;第二个循环是根据提取的每一位,给对应计数器+1,循环次数也由被提取数的位数决定,即不大于
log
10
n
\log_{10} n
log10n,所以程序总的时间复杂度为O (
2
log
10
n
⋅
n
2\log_{10} n · n
2log10n⋅n), 即O (
n
⋅
log
10
n
n·\log_{10}n
n⋅log10n)。
方法二:
此算法一共包括三个核心循环,第一个循环拆分n的每一位,并求各种值,一共循环
log
10
n
\log_{10} n
log10n次;第二个循环逐位计算每一位上出现各种数的次数,共循环
log
10
n
\log_{10} n
log10n次,其内嵌套着第三个循环,计算从0-9各个数的具体出现次数,共循环10次。所以程序总的时间复杂度为O(
log
10
n
+
10
log
10
n
\log_{10} n + 10\log_{10} n
log10n+10log10n ),即O(
log
10
n
\log_{10} n
log10n)。
分享的同时记录自己的学习,感谢阅读,有问题还请交流指正。^ ^