[2021校招必看之Java版《剑指offer》-31] 整数中1出现的次数

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、解题心得

  本题难度中等,可以不难地分析出需要遍历每一个位,从草稿中发现需要分解成两个问题,即求最大值,和得到最大值之后的操作。
  优化法虽然代码量少,但是不直观,适合对本题熟练度高的人在笔试中做,统计法会耗费不少的写代码时间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值