计算1至n中数字X出现的次数
描述
计算 1 至 n 中数字 X 出现的次数,其中 $n \ge 1,X \in [0,9]$。
解题思路
这是一道比较简单的题目,举个例子先:假设 $n=11, X=1$,那么就是求 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 这 11 个数字中 1 出现的次数,很容易能看出来结果为 4,在 1 和 10 中各出现了一次,在 11 中出现了两次。
最简单的办法就是依次遍历 1 至 n,再分别求每个数字中 X 出现的次数,代码如下所示:
#include <stdio.h> // 计算数字 X 在 n 中出现的次数。
int countOne(int n, int x) {
int cnt = 0;
for (;n > 0;n /= 10) {
if (n % 10 == x) {
cnt++;
}
}
return cnt;
}
// 计算数字 X 在 1-n 中出现的次数。
int count(int n, int x) {
int cnt = 0;
for (int i = 1;i <= n;i++) {
cnt += countOne(i, x);
}
return cnt;
}
int main() {
printf("%d\n", count(237, 1));
}
这个方法的缺点是时间复杂度太高,countOne 方法的时间复杂度是 $O({\log _{10}}n)$,count 方法的时间复杂度是 $O(n{\log _{10}}n)$。
一个更好的办法是利用数学公式直接计算出最终的结果,该方法是依次求出数字 X 在个位、十位、百位等等出现的次数,再相加得到最终结果。这里的 $X \in [1,9]$,因为 $X=0$ 不符合下列规律,需要单独计算。
首先要知道以下的规律:
- 从 1 至 10,在它们的个位数中,任意的 X 都出现了 1 次。
- 从 1 至 100,在它们的十位数中,任意的 X 都出现了 10 次。
- 从 1 至 1000,在它们的千位数中,任意的 X 都出现了 100 次。
依此类推,从 1 至 $10^i$,在它们的左数第二位(右数第 $i$ 位)中,任意的 X 都出现了 $10^{i-1}$ 次。
这个规律很容易验证,这里不再多做说明。
接下来以 $n=2593, X=5$ 为例来解释如何得到数学公式。从 1 至 2593 中,数字 5 总计出现了 813 次,其中有 259 次出现在个位,260 次出现在十位,294 次出现在百位,0 次出现在千位。
现在依次分析这些数据,首先是个位。从 1 至 2590 中,包含了 259 个 10,因此任意的 X 都出现了 259 次。最后剩余的三个数 2591, 2592 和 2593,因为它们最大的个位数字 3 < X,因此不会包含任何 5。
然后是十位。从 1 至 2500 中,包含了 25 个 100,因此任意的 X 都出现了 $25 \times 10=250$ 次。剩下的数字是从 2501 至 2593,它们最大的十位数字 9 > X,因此会包含全部 10 个 5。最后总计 250 + 10 = 260。
接下来是百位。从 1 至 2000 中,包含了 2 个 1000,因此任意的 X 都出现了 $2 \times 100=200$ 次。剩下的数字是从 2001 至 2593,它们最大的百位数字 5 == X,这时情况就略微复杂,它们的百位肯定是包含 5 的,但不会包含全部 100 个。如果把百位是 5 的数字列出来,是从 2500 至 2593,数字的个数与百位和十位数字相关,是 93+1 = 94。最后总计 200 + 94 = 294。
最后是千位。现在已经没有更高位,因此直接看最大的千位数字 2 < X,所以不会包含任何 5。到此为止,已经计算出全部数字 5 的出现次数。
总结一下以上的算法,可以看到,当计算右数第 $i$ 位包含的 X 的个数时:
- 取第 $i$ 位左边(高位)的数字,乘以 $10^{i-1}$,得到基础值 $a$。
- 取第 $i$ 位数字,计算修正值:
- 如果大于 X,则结果为 $a + 10^{i-1}$。
- 如果小于 X,则结果为 $a$。
- 如果等 X,则取第 $i$ 位右边(低位)数字,设为 $b$,最后结果为 $a + b + 1$。
相应的代码非常简单,效率也非常高,时间复杂度只有 $O({\log _{10}}n)$。
// 计算数字 X 在 1-n 中出现的次数。
int count(int n, int x) {
int cnt = 0, k;
for (int i = 1;k = n / i;i *= 10) {
// k / 10 为高位的数字。
cnt += (k / 10) * i;
// 当前位的数字。
int cur = k % 10;
if (cur > x) {
cnt += i;
} else if (cur == x) {
// n - k * i 为低位的数字。
cnt += n - k * i + 1;
}
}
return cnt;
}
当 X = 0 时,规律与上面给出的规律不同,需要另行考虑。
最主要的区别是,最高位中永远是不会包含 0 的,因此,从个位累加到左起第二位就要结束,需要将上面代码中 for 循环的判断条件改为 k / 10 != 0。
其次是,第 $i$ 位的基础值不是高位数字乘以 $10^{i-1}$,而是乘以 $10^{i-1}-1$。以 1 至 102 为例,千位中实际包含 3 个 0,但这三个 0 是来自于个位 2 计算得到的修正值,而非来自于基础值。千位的基础值是 0,因为不存在数字 01, 02, 03, ..., 09,即数字前是没有前导 0 的。解决办法就是将上面代码中第 6 行改为 cnt += (k / 10 - 1) * i。
经过综合与化简,得到了以下代码:
// 计算数字 0 在 1-n 中出现的次数。
int countZero(int n) {
int cnt = 0, k;
// k / 10 为高位的数字。
for (int i = 1;(k = n / i) / 10;i *= 10) {
cnt += (k / 10) * i;
// k % 10 为当前位的数字。
if (k % 10 == 0) {
// n - k * i 为低位的数字。
cnt += n - k * i + 1 - i;
}
}
return cnt;
}
主要是将一些步骤进行了合并,令代码比较简练。
将上面两段代码进行合并,可以得到以下代码,对 X 从 0 到 9 都有效:
// 计算数字 X 在 1-n 中出现的次数。
int count(int n, int x) {
int cnt = 0, k;
for (int i = 1;k = n / i;i *= 10) {
// 高位的数字。
int high = k / 10;
if (x == 0) {
if (high) {
high--;
} else {
break;
}
}
cnt += high * i;
// 当前位的数字。
int cur = k % 10;
if (cur > x) {
cnt += i;
} else if (cur == x) {
// n - k * i 为低位的数字。
cnt += n - k * i + 1;
}
}
return cnt;
}
计算1至n中数字X出现的次数的更多相关文章
- 计算1至n中数字X出现的次数【math】
转自: nailperry 一.1的数目 编程之美上给出的规律: 1. 如果第i位(自右至左,从1开始标号)上的数字为0,则第i位可能出现1的次数由更高位决定(若没有高位,视高位为0),等于更高位数字 ...
- (算法)从0到n整数中数字2出现的次数
题目: 数出0到n(含)中数字2出现了几次. 思路: 1.暴力方法,数出每个数字包含几个2,然后累加起来. 2.分析:分别考虑数字n每一位出现2的次数,如123123: 从左往右考虑4123123: ...
- 求一个区间[a,b]中数字1出现的次数
问题来源:http://ac.jobdu.com/problem.php?pid=1373 举例:如果n=10 那么1-10之间的1的个数是2(1,2,3,4,...10) 这其中有一个规律: 挨着看 ...
- 233. Number of Digit One *HARD* -- 从1到n的整数中数字1出现的次数
Given an integer n, count the total number of digit 1 appearing in all non-negative integers less th ...
- 1~n中数字0~9出现的次数
题意:rt 分析: 当然不可能去遍历,应该寻找统计的方法. 如计算 78501 中 "5" 出现的次数. 我们可以枚举“5”出现的位置, 如当“5”位于倒数第2位时,写成 xxx5 ...
- OC特有语法:分类category,给NSString增加方法计算字符串中数字的个数
1:分类的使用场景:想对一个类,扩充一些功能,而又不改变原来类的模型,也不用继承,这时OC中的特有语法:分类可以做到: 当然分类也是一个类,也需要声明和实现,声明在.h文件中,实现在.m文件中,格式如 ...
- 尝试在条件“$(_DeviceSdkVersion) >= 21”中对计算结果为“”而不是数字的“$(_DeviceSdkVersion)
晚上搞xamarin ,运行xamarin项目好好的,不知道怎么回事,一次运行xamarin android项目的时候,部署失败,以前也是遇到这样的错误. 尝试在条件"$(_DeviceSd ...
- 原码,反码,补码,及Java中数字表示方法
原码:原码是符号位加上真值的绝对值, 即用第一位表示符号, 其余位表示值. 如:如果是八位二进制1即用00000001表示,-1即用10000001表示. 反码:正数的反码就是其本身,负数的反码是在其 ...
- Callable接口使用以及计算斐波那契数字的数值总和
一.简单使用 Runnable是执行工作的独立任务,但是它不返回任何值.如果你希望任务完成的时能够返回一个值,那么可以实现一个Callable接口.在Java SE5中引入的Callable是一种具有 ...
随机推荐
- hibernate杂记
1.hibernate中get和load方法有什么不同? (1)执行get方法,会立即加载对象, 执行load方法,若不使用该对象,则不会立即执行查询操作. (2)load 方法可能会抛出 LazyI ...
- Java笔记--泛型总结与详解
泛型简介: 在泛型没有出来之前,编写存储对象的数据结构是很不方便的.如果要针对每类型的对象写一个数据结构, 则当需要将其应用到其他对象上时,还需要重写这个数据结构.如果使用了Object类型, ...
- 淘宝(阿里百川)手机客户端开发日记第六篇 Service详解(一)
public abstract class Service; [API文档关于Service类的介绍] A Service is an application component representi ...
- Powershell学习之道-文件夹共享及磁盘映射
导读 在Linux环境下,我们很轻易就能得心应手地通过命令操作一切事物,在Windows下,Powershell也算是后起之秀,提供大量的cmdlet以及c#的横向拓展.下面将由小编带领大家通过Pow ...
- Class Methods & Variables
When calling an instance method like withdraw_securely, the syntax generally looks something like th ...
- 数学复习 ---- Mathematics Notes: A Programmer's Perspective ---- by Orzer ---- 我是沙茶
今年是好没长进的一年呢..只学了些基本的方法.. 本文记号0] x:p x类型为p1] f(x) 表示一个函数2] (n_1,n_2,...) 表示多元组,特别的,(n)表示一个一元组3] x 表示一 ...
- 能用Shell就别编程-海量文本型数据的处理
对于txt文本类数据,优先采用shell脚本,实在不行才用Python,Java,MySQL 1) Shell命令行或脚本的处理速度极快,比Java快得多. 2) Shell代码量少,几个命令就能完成 ...
- 【leetcode】Best Time to Buy and Sell Stock III
Best Time to Buy and Sell Stock III Say you have an array for which the ith element is the price of ...
- 4.前端笔记之jsdom基础
一.简介 文件对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展标志语言的标准编程接口.DOM编程: DOM 是关于如何获取.修改.添加或删除 HTML ...
- [SVN(Ubuntu)] SVN 查看历史详细信息
转载: http://lee2013.iteye.com/blog/1074457 以下内容,对ubuntu命令行查看代码变化非常有用. SVN 查看历史信息 通过svn命令可以根据时间或修订号去除过 ...