算法设计与分析-统计数字问题(暴力、分治递归)(通俗易懂,附源码和图解,含时间复杂度分析)(c++)

这篇博客介绍了两种方法来计算一本书页码中数字0到9的出现次数。方法一是暴力遍历,从1到n依次拆解页码并统计;方法二是递归计算,利用数学规律减少计算量。复杂度分析显示递归方法的时间复杂度更低,为O(nlogn),而暴力方法的时间复杂度为O(n^2)。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1-1统计数字问题

(一)题目

问题描述

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

算法设计

给定表示书的总页码的十进制整数 n ( 1 ≤ n ≤ 1 0 9 ) n(1\leq n\leq10^{9}) n(1n109),计算书的全部页 码中分别用到多少次数字 0,1,2,……,9。 数据输入:输入数据由文件名为input.txt的文本文件提供。每个文件只有1行, 给出表示书的总页码的整数 n n n

结果输出

将计算结果输出到文件 output.txt。输出文件共 10 行,在第 k ( k = 1 , 2 , … … , 10 ) k(k=1,2,……,10) k(k=1,2,,10)行输出页码中用到数字$ k-1$ 的次数。

(二)解答

方法1.暴力
算法思路

从 0 到 n n n依次遍历每一页的页码,把页码从低位到高位用循环依次拆开,记录每个数字的出现次数。

举例

在这里插入图片描述

源代码
#include<iostream>
#include<cstdio>
#include<fstream>

using namespace std;

//读取
int read(); 
//写入
void write(int a[]);
//计算总页码0-9出现的次数
void solution(int n, int a[]);

int main()
{
    int a[10] = {0};
    //从输入文件获取总页数n
    int n = read();
    //计算0-9出现的次数
    solution(n, a);
    //将0-9的出现次数写入输出文件
    write(a);
    return 0;
}

int read()
{
    ifstream ifs;
    //打开输入文件
    ifs.open("G:\\algorithm\\data\\1_1_input.txt", ios::in);
    //读取数据
    int n;
    ifs>>n;
    //关闭输入文件
    ifs.close();
    //返回总页数n;
    return n;
}

void write(int a[])
{
    ofstream ofs;
    //创建输出文件
    ofs.open("G:\\algorithm\\data\\1_1_1out.txt", ios::out);
    //写入数据
    for (int i = 0; i < 10; ++i)
    {
        ofs<<a[i]<<endl;
    }
    //关闭输出文件
    ofs.close();
}

void solution(int n, int a[])
{
    //依次遍历每一页的页码i
    for (int i = 1; i <= n; ++i)
    {
        //用t暂时存储页码i
        int t = i;
        //从右往左依次记录t每一位上的数字的出现次数,直到t=0
        while (t)
        {
            a[t % 10]++;
            t /= 10;
        }
    }
}
方法2.递归
算法思路

对于从 n n n 个 0 到 n n n 个 9 的数,它们中 0-9 出现的次数相同,记为 f ( n ) f(n) f(n)

在这里插入图片描述

通过观察可以得出递归式:
f ( n ) = { 1 ,      n = 1 10 f ( n − 1 ) + 1 0 n − 1 , n > 1 f(n)=\begin{cases} 1,\;\;\qquad \qquad \qquad \quad \qquad n=1\\ 10f(n-1)+10^{n-1},\qquad n>1\\ \end{cases} f(n)={1,n=110f(n1)+10n1,n>1
整理可得:
f ( n ) = n 1 0 n − 1 f(n)=n10^{n-1} f(n)=n10n1
因此,我们可以利用这个规律进行运算:

(1)递归计算 0(包含前导 0)到𝑛各数字出现的次数

设页码数 n n n的位数为 c c c,最高位为 m m m,除最高位外的部分为 r r r。 0(包含前导 0)到 n n n的数由三部分构成:

第一部分是 m m m c − 1 c-1 c1个 0 到 9 的数,这部分 0 到 9 出现的次数相同,均为 m ( c − 1 ) 1 0 c − 2 m(c-1)10^{c-2} m(c1)10c2次;

第二部分为最高位,0 到 m − 1 m-1 m1各出现 1 0 c − 1 10^{c-1} 10c1次, m m m出现 r + 1 r+1 r+1次;

第三部分为 0(包含前导 0)到 r r r 的数,按照同样的方法进行递归,直到 r 的位数为 1,0 到 r r r 各出现一次。

值得注意的是,计算机计算 r r r时是不带前导 0 的,如果 n n n中间出现 0,这部分的 0 将被略去,因此,我们要把这部分的 0 补上。设 r r r的位数为 c r cr cr,则需要补上 ( c − c r − 1 ) ( r + 1 ) (c-cr-1)(r+1) (ccr1)(r+1)个 0。

(2)减去多余的前导 0

对于位数为 c c c的页码数 n n n多余的前导 0 个数为 1 0 0 + ⋯ + 1 0 c − 1 10^0 + ⋯ + 10^{c−1} 100++10c1

举例

n n n=2222为例,先递归计算 0(包含前导 0)到𝑛各数字出现的次数

在这里插入图片描述

注意 n n n的中间有0的情况

例如当 n n n=1003时,计算机计算 r r r的结果为3(不带前导0),但我们知道 r = 003 r=003 r=003(带上前导0)才是正确的结果,中间的0要补上
在这里插入图片描述

然后删去多余的前导0,对于一个位数为 c c c n n n,它的前导0个数是固定的
在这里插入图片描述

将所有位置上的多余前导0加起来删去即可得最终结果

源代码

#include<iostream>
#include<cstdio>
#include<fstream>
#include<cmath>

using namespace std;

//读取
int read(); 
//写入
void write(int a[]);
//求n的位数
int digit(int n);
//计算n个0到n个9中0-9各出现的次数
int f(int n);
//递归计算总页码0-9出现的次数
void solution(int n, int a[]);
//减去多余的0
void substract(int n, int a[]);

int main()
{
    int a[10] = {0};
    //从输入文件获取总页数n
    int n = read();
    //递归计算0-9出现的次数
    solution(n, a);
    //减去多余的0
    substract(n, a);
    //将0-9的出现次数写入输出文件
    write(a);
    return 0;
}

int read()
{
    ifstream ifs;
    //打开输入文件
    ifs.open("G:\\algorithm\\data\\1_1_input.txt", ios::in);
    //读取数据
    int n;
    ifs>>n;
    //关闭输入文件
    ifs.close();
    //返回总页数n;
    return n;
}

void write(int a[])
{
    ofstream ofs;
    //创建输出文件
    ofs.open("G:\\algorithm\\data\\1_1_2out.txt", ios::out);
    //写入数据
    for (int i = 0; i < 10; ++i)
    {
        ofs<<a[i]<<endl;
    }
    //关闭输出文件
    ofs.close();
}

int digit(int n)
{
    return (int)log10(n) + 1;
}

int f(int n)
{
    //n=0时无法通过公式求n的位数
    if (n == 0)
    {
        return 1;
    }
    return n * (int)pow(10.0, n - 1);
}

void solution(int n, int a[])
{
    //求n的位数c,c-1个0到9中0-9出现的次数相同
    int c = digit(n);
    //递归到只剩个位的情况
    if (c == 1)
    {
        for (int i = 0; i <= n; ++i)
        {
            a[i]++;
        }
        return;
    }
    //求n的最高位m,共有m组c-1个0到9的数
    int m = n / (int)pow(10.0, c - 1);
    //求n的最高位以外的数r
    int r = n % (int)pow(10.0, c - 1);
    //求m组c-1个0到9中0-9出现的次数
    for (int i = 0; i <= 9; ++i)
    {
        a[i] += m * f(c - 1);
    }
    //再处理最高位
    for (int i = 0; i < m; ++i)
    {
        a[i] += (int)pow(10.0, c - 1);
    }
    a[m] += r + 1;
    //如果r的位数不是c-1,说明n中间含有0,把这些0补上
    int cr = digit(r);
    if (cr != c - 1)
    {
        a[0] += (c -cr - 1) * (r + 1);
    }
    //递归处理r中0-9出现的次数
    solution(r, a);
}

void substract(int n, int a[])
{
    int c = digit(n);
    for (int i = 0; i < c; ++i)
    {
        a[0] -= (int)pow(10.0, i);
    }
}
结果示例

输入:
在这里插入图片描述
输出:
在这里插入图片描述

(三)总结

对两种方法进行复杂度分析,结果如下:

方法1.暴力
t ( n ) = n × ( ( l o g 10 1 + 1 ) + ( l o g 10 2 + 1 ) + . . . + ( l o g 10 n + 1 ) ) + 10 = n × ( l o g 10 1 + l o g 10 2 + + . . . + l o g 10 n + n ) + 10 < n × ( 1 + 2 + . . . + n + n ) + 10 = O ( n 2 ) \begin{aligned} t(n)&=n\times((log_{10}1+1)+(log_{10}2+1)+...+(log_{10}n+1))+10\\ &=n\times(log_{10}1+log_{10}2++...+log_{10}n+n)+10\\ &<n\times(1+2+...+n+n)+10\\ &=\Omicron(n^2) \end{aligned} t(n)=n×((log101+1)+(log102+1)+...+(log10n+1))+10=n×(log101+log102++...+log10n+n)+10<n×(1+2+...+n+n)+10=O(n2)
方法2.递归
t ( n ) = ⌊ l o g 10 n ⌋ × ( 10 + ⌊ n 1 0 ⌊ l o g 10 ⌋ + 1 ⌋ ) + n M O D 10 + ( ⌊ l o g 10 n ⌋ + 1 ) + 10 = O ( n l o g n ) \begin{aligned} t(n)&=\lfloor log_{10}n\rfloor\times(10+\lfloor\frac{n}{10^{\lfloor log_{10}\rfloor+1}}\rfloor)+n MOD 10+(\lfloor log_{10}n\rfloor+1)+10\\ &=\Omicron(nlogn) \end{aligned} t(n)=log10n×(10+10log10+1n)+nMOD10+(log10n+1)+10=O(nlogn)
可以看到递归的复杂度小于暴力

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值