1、题目描述
【JZ31】如何求出1-13的整数中1出现的次数,并算出100-1300的整数中1出现的次数?ACMer为此特别数了一下1~13中包含1的数字有1、10、11、12、13因此1共出现6次,但是对于后面问题他就没辙了。ACMer希望你们帮帮他,并把问题更加普遍化,可以很快的求出任意非负整数区间中1出现的次数(从1 到 n 中1出现的次数)。
知识点:查找,数学
难度:☆☆☆
2、解题思路
2.1 统计法
题意:1 - n 中,11 出现 2 次 1 , 13 出现 1 次 1 ,求出 1 - n 的序列中,总共出现了多少次 1。
解题思路为把数字的每一位(个、十、百、…)进行遍历。
将 1 ~ n 的个位、十位、百位、…的 1 出现次数相加,即为 1 出现的总次数。
比如输入的数字为11,那么0-11当中,带1的分别是1、10、11,所以个位总共有2个1,十位有2个1,总共4个1。
以下内容说的“位”,指的是个位、十位、百位等等的位。
--------------------分界线--------------------
以n=21035为例:
1、个位
个位为1的取值范围是:00001-21031
个位前面的高位取值范围是:0000-2103,总共有2104个可能。
因此个位的1总共有2104个。
2、十位
十位为1的取值范围是:00010-21019
十位前面的高位取值范围是:000-210,总共有211个可能。
十位后面的低位取值范围是:0-9,总共10个可能。
因此个位的1总共有211×10=2110个。
3、百位
百位为1的取值范围是:00100-20199
百位前面的高位取值范围是:00-20,总共有21个可能。
百位后面的低位取值范围是:00-99,总共100个可能。
因此个位的1总共有21×100=2100个。
4、千位
千位为1的取值范围是:01000-21035
千位前面的高位取值范围是:0-2,总共有3个可能。
当高位为0-1时,千位后面的低位取值范围是:000-999,1000个可能。
当高位为2时,千位后面的低位取值范围是:000-035,总共36个可能。
因此千位的1总共有2×1000+36=2036个。
4、万位
万位为1的取值范围是:10000-19999
万位后面的低位取值范围是:0000-9999,总共10000个可能。
因此万位的1总共有10000个。
合计1的个数为2104+2110+2100+2036+10000=18350。
--------------------分界线--------------------
因此,我们把问题分解为两个子问题:
1、已知数字n,求某位为1且不超过n的最大整数;
2、已知数字n和每一个位为1时所对应的不超过n的最大整数maxS,求1-n中,该位为1的数字的个数。
下面就根据两个问题逐步分析。
--------------------分界线--------------------
问题1:已知数字n,求某位为1且不超过n的最大整数;
算法步骤:
1、拷贝该数字n为target,把n的目标位设置为1,把目标位的低位全部设置为9。比如,当n为21035,求百位为1且不超过它的最大整数。先复制n,target变为21035,百位设置为1,target=21135,百位低位全设置为9,target=21199。
2、校验第1步得到的target是否满足要求,即target是否大于n,如果小于n,则得到结果,如果大于n,则需要进行调整。比如:21199就大于21035,需要进行调整,先用下面的策略一调整,策略一不行就策略二。
3、调整策略一:把该位的低位全部恢复成n的原样,然后再和n比较,是不是满足要求,如果满足,则得到结果,否则回到第一步结束的状态,使用调整策略二。比如target从21199变成21135,但是21135还是大于21035,需要换一个调整策略。
4、调整策略二:策略一调整后还不符合要求的情况下使用,此时target为步骤1的结果,当第3步没发生过,如例子的target=21199。如果该位的低位调整为原样还是超过n,说明是该位肯定为0,变为1后,肯定是超过n。因此,如果要把该位设为1,那么这个1只能是从高位借过来的,因此高位需要减1,target从21199变为20199。调整后再和n比较,肯定是小于n,返回结果。
--------------------分界线--------------------
问题2:已知数字n和每一个位为1时所对应的不超过n的最大整数maxS,求1-n中,该位为1的数字的个数。
算法步骤:
1、定义一个变量count记录1的出现次数,根据每一个位对应的maxS,从个位开始,依次计算每一个位中1出现的个数。
2、如果是个位,则获取maxS的高位front,count += front + 1。
3、如果是最高为,则获取maxS的低位rear,count += rear + 1。
4、如果既不是个位也不是最高位的中间位,则获取maxS该位的高位front和该位的低位rear,判断一下rear是不是都是9:
4.1 rear不全是9,则count += front × 10i + rear + 1。其中i表示该位的位因子,个位i=0,十位i=10,百位i=100,以此类推。
4.2 rear全是9,则count += ( front + 1 ) × ( rear + 1 )
例如,21035的百位对应的maxS为20199,则front为20,rear为99,因此 count += ( 20 + 1 ) × ( 99 + 1 ),即:count += 2100,说明 1 - 21035 当中,百位的1出现了2100次。
2.2 统计法优化
统计法不仅分成两个子问题,中间存在大量的判断、字符串截取和字符串和整形的转换等等,代码繁多。
假设数字n是x位数,记 n 的第i位为 ni ,则可将n写为:nxnx-1…n1n1
称 ni 为当前位,记为cur;
将 ni-1ni-2…n1n1 称为低位,记为low;
将 nxnx-1…ni+2ni+1 称为高位,记为high;
记 10i 为位因子,记为 digit ,个位i = 0,十位i = 10,百位i = 100,以此类推。
某位中 1 出现次数的计算方法:
根据当前位 cur 值的不同,分为以下三种情况:
1、当 cur = 0 时,此位 1 的出现次数只由高位 high 决定,计算公式为:
high × digit
如下图所示,以 n = 2304 为例,求 digit = 10(即十位)的 1出现次数。
2、当 cur = 1 时: 此位 1 的出现次数由高位 high 和低位 low 决定,计算公式为:
high × digit + low + 1
如下图所示,以 n = 2314 为例,求 digit = 10(即十位)的 1 出现次数。
3、当 cur=2,3,⋯,9 时: 此位 1 的出现次数只由高位 high 决定,计算公式为:
( high + 1 ) × digit
如下图所示,以 n = 2324 为例,求 digit = 10(即十位)的 1 出现次数。
变量递推公式:
设计按照 “个位、十位、…” 的顺序计算,则high/cur/low/digit 应初始化为:
high = n // 10
cur = n % 10
low = 0
digit = 1
# 个位
因此,从个位到最高位的变量递推公式为:
while(high != 0 || cur != 0)
当 high 和 cur 同时为 0 时,说明已经越过最高位,因此跳出。
low += cur * digit
将 cur 加入 low ,组成下轮 low
cur = high % 10
下轮 cur 是本轮 high 的最低位
high /= 10
将本轮 high 最低位删除,得到下轮 high
digit *= 10
位因子每轮 × 10
3、解题代码
3.1 统计法
package pers.klb.jzoffer.medium;
/**
* @program: JzOffer2021
* @description: 从1到n整数中1出现的次数(统计法)
* @author: Meumax
* @create: 2020-07-05 15:09
**/
public class NumberOf1Between1AndN {
public int NumberOf1Between1AndN_Solution(int n) {
// n是10以内
if (n == 0) {
return 0;
} else if (n <= 9) {
return 1;
}
int count = 0; // 统计 n 大于10时的1出现的次数
StringBuilder ns = new StringBuilder(String.valueOf(n));
int length = ns.length();
// 遍历 个位、十位、百位...
for (int i = 0; i < length; i++) {
String maxS = calculateMax(n, i);
int index = length - i - 1; // 右到左的索引转成左到右的索引
if (index == 0) { // 1xyz 的格式
String rear = maxS.substring(index + 1); // 后半截
count += Integer.parseInt(rear) + 1;
} else if (index == length - 1) { // xyz1 的格式
String front = maxS.substring(0, index); // 前半截
count += Integer.parseInt(front) + 1;
} else { // xyz1abc 的格式
String front = maxS.substring(0, index); // 前半截
String rear = maxS.substring(index + 1); // 后半截
if (isAllNine(rear)) {
count += (Integer.parseInt(front) + 1) * (Integer.parseInt(rear) + 1);
} else {
count += Integer.parseInt(front) * Math.pow(10, i) + Integer.parseInt(rear) + 1;
}
}
}
return count;
}
/**
* 计算从右到左第 index 位为1且不超过 num 的最大值
* 例如:num = 1234,index=2,则返回 1199
*
* @param num 数字上限
* @param index 第 index 位为 1
* @return 最小值
*/
public String calculateMax(int num, int index) {
StringBuilder numSB = new StringBuilder(String.valueOf(num));
int length = numSB.length();
// 从右到左的索引改为从左到右,方便下面计算
// 比如,长度为6,右到左的第2位等于左到右的第3位
index = length - index - 1;
StringBuilder target = new StringBuilder(String.valueOf(num));
// 第一道工序:index设为1,1前面的数字不变,1后面的数字设置为99
for (int i = index; i < length; i++) {
if (i == index) {
target.setCharAt(i, '1');
} else {
target.setCharAt(i, '9');
}
}
// 第二道加工:判断初步生成的target是否超出了num
if (Integer.parseInt(target.toString()) > num) {
String temp = target.toString();
// 把index后面的所有9恢复成原来的样子
for (int j = index + 1; j < length; j++) {
target.setCharAt(j, numSB.charAt(j));
}
// 再判断是不是还是超出num
if (Integer.parseInt(target.toString()) > num) {
// 还超出,说明num的index位置是0,需要从高位借1给index位,低位依旧为一堆数字9
target = new StringBuilder(temp);
int i = index - 1;
while (target.charAt(i) == '0') {
target.setCharAt(i, '9');
i--;
}
target.setCharAt(i, (char) (target.charAt(i) - 1));
}
}
return target.toString();
}
/**
* 判断字符串是否只由字符 ‘9’ 组成
*
* @param str
* @return
*/
public boolean isAllNine(String str) {
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) != '9') {
return false;
}
}
return true;
}
}
3.2 统计法优化
package pers.klb.jzoffer.medium;
/**
* @program: JzOffer2021
* @description: 从1到n整数中1出现的次数(统计法优化)
* @author: Meumax
* @create: 2020-07-05 15:09
**/
public class NumberOf1Between1AndN {
public int NumberOf1Between1AndN_Solution(int n) {
int digit = 1, res = 0;
int high = n / 10, cur = n % 10, low = 0;
while (high != 0 || cur != 0) {
if (cur == 0) res += high * digit;
else if (cur == 1) res += high * digit + low + 1;
else res += (high + 1) * digit;
low += cur * digit;
cur = high % 10;
high /= 10;
digit *= 10;
}
return res;
}
}
时间复杂度 O(logN) ,循环次数为数字N 的位数,即log10N,因此循环使用 O(logN)时间。
空间复杂度 O(1)
4、解题心得
本题难度中等,可以不难地分析出需要遍历每一个位,从草稿中发现需要分解成两个问题,即求最大值,和得到最大值之后的操作。
优化法虽然代码量少,但是不直观,适合对本题熟练度高的人在笔试中做,统计法会耗费不少的写代码时间。