剑指offer面试题43(java版):1~n整数中1出现的次数

welcome to my blog

剑指offer面试题43(java版):1~n整数中1出现的次数

题目描述

输入一个整数 n ,求1~n这n个整数的十进制表示中1出现的次数。

例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。

 

示例 1:

输入:n = 12
输出:5
示例 2:

输入:n = 13
输出:6
第四次做; 力扣上的测试案例多, 之前在牛客上的代码无法通过, 比如1410065408, 有越界问题; 核心: 1)规律:个位上的1每隔10个数出现1次, 十位数上的1每个100个数出现10次; 2)循环中的i依次表示个位,十位,…最多是十亿, 因为int的最大值的最高位就是十亿位 3)为避免(i10)越界, 需要将n/(i10)写成n/i/10; 如果i等于10亿, 需要将n%(i*10)直接写成n
class Solution {
    public int countDigitOne(int n) {
        int res = 0;
        for(int i=1; i<=n; i*=10){
            res += n/10/i*i;
            //避免越界
            int residual = i>Integer.MAX_VALUE/10? n : n%(i*10);
            if(residual >= 2*i){
                res += i;
            }else if(residual >= i){
                res += residual - i + 1;
            }
            //避免循环条件越界
            if(i>Integer.MAX_VALUE/10){
                break;
            }
        }
        return res;
    }
}


/*
规律
0~9: 个位数上的1, 每隔10个数出现1次
10~99: 十位数上的1, 每隔100个数出现10次
100~999: 百位数上的1, 每隔1000个数出现100次
...

绊脚案例:1410065408
res*10 > max
res > max/10
*/
class Solution {
    public int countDigitOne(int n) {
        int res=0;
        //个, 十, 百, 千...
        for(int i=1; i<=n; i=i*10){
            //完整的10, 100, 1000...
            // res += n/(i*10) * i;
            res += n/i/10 * i;
            //还剩余数
            int cur = i>Integer.MAX_VALUE/10 ? n : n%(i*10);
            if(cur>=2*i){
                res += i;
            }
            else if(cur>=i){
                res += cur - i + 1;
            }
            //如果i等于10亿, 就别进行下一轮循环了, 因为int最高位就是10亿位
            if(i>Integer.MAX_VALUE/10)
                break;
        }
        return res;
    }
}

笔记

  • 一般不要在循环内改变循环变量
// 错误示例, 在for循环内部改变了循环变量i, 导致i始终==1, 进入了死循环
        for(int i=1; i<=n; i++){
            while(i!=0){
                if(i%10==1)
                    i++;
                i = i/10;
            }
        }
  • 十进制数字n, 有多少位? ceiling(lg n)位
  • 最朴素做法的时间复杂度为O(nlogn)

第三次做, 要弄清楚处理的是哪些数, 比如1,2,3,…1555; 10位上的1每个100个出现10次, 1555中有15个完整的100, 分别对应1~~100, 101~~200, 201~~300, 301~~400, 401~~500,…,901~~1000, 1001~~1100, 1101~~1200, 1201~~1300, 1301~~1400, 1401~~1500; 但是还有1501~~1555没有考虑, 就是1555%100=55, 只考虑到十位即可, 对应1~~55

/*
1,2,...n这n个数中
x位上每隔(x*10)个数出现x个1, n中有n/(x*10)个完整的(x*10),以百位为例,就是n中有n/1000个完整的1000, 分别是1~~1000,1001~~2000, 2001~~3000,...(n/1000 - 1)*1000 + 1~~(n/1000)*1000
最后还可能剩下不足1000的部分, 也就是从(n/1000)*1000+1~~n, 百位的情况对应n%1000

不足(x*10)的部分,也就是n%(x*10)的部分, x位上也有可能出现1
如果n%(x*10)<x, x位上没有1
如果n%(x*10)>2*x-1, x位上有x个1
如果n%(x*10)>=x && n%(x*10)<=2*x-1, x位上有n%(x*10) - x + 1个1
*/
public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        int count = 0;
        for(int i=1; i<=n; i*=10){
            count += n/(i*10) * i;
            if(n%(i*10) > 2*i-1)
                count += i;
            else if(n%(i*10)>=i)
                count += n%(i*10) - i + 1;
        }
        return count;
    }
}
第二次做,理顺了最优解,要理解并记住注释中的分析过程
  • 从0分析的话,很难
        /*
        1...N这N个数字中,i位(如个位,十位, 百位...i=1,10,100...)上的1每隔(10*i)个数出现i次,
        那么有几个(10*i)呢? N/(10*i)个,除不尽的余数还需要单独考虑
        (为什么要考虑余数?本质上是因为N/(10*i)个10*i考虑了1...N/(10*i)*(10*i)这些数,
        还剩下N/(10*i)*(10*i)+1...N没有考虑,余数对应的就是这部分!)
        如果N%(10*i)小于i,那么当前这个余数不会使i位上出现1
        如果N%(10*i)大于2*i-1,那么当前这个余数会使i位上出现i个1
        如果N%(10*i)大于等于i,同时N%(10*i)小于等于2*i-1时,当前这个余数会使i位上出现N%(10*i)-i+1个1
        
        上面的分析过程拿N的百位位置套用以下
        1...23987这23987个数字中,百位上的1每隔1000个数出现100次,那么23987中有多少个1000个?23987/(10*100)=23个,再来考虑余数
        (为什么要考虑余数?本质上是因为23987/(10*100)=23个1000考虑的是1...23000这些数,
        还剩下23001...23987没有考虑,23987/(10*100)的余数考虑的就是这部分数字)
        23987%(10*100)=987大于2*100-1=199,那么当前这个余数会使百位上出现100个1
        */
public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        if(n<1)
            return 0;
        int count=0;
        int temp;
        for(int i=1; i<=n; i*=10){
            if(n%(10*i) < i)
                temp = 0;
            else if(n%(10*i) > 2*i-1)
                temp = i;
            else
                temp = n%(10*i) - i + 1;
            count += n/(10*i)*i + temp;
        }
        return count;
    }
}
第二次做,最直接的做法;一般不要在循环内部改变循环条件,容易引起死循环
public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        int count=0,temp;
        for(int i=1; i<=n; i++){
            temp = i;
            while(temp!=0){
                if(temp%10 == 1){
                    count++;
                }
                temp = temp/10;
            }
        }
        return count;
    }
}
最朴素的做法,对每个数字逐位判断
public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        //最朴素的做法, 逐位判断
        int curr, count = 0;
        for(int i=1; i<=n; i++){
            curr = i;
            while(curr!=0){
                if(curr%10==1)
                    count++;
                curr = curr/10;
            }
        }
        return count;
    }
}

剑指offer上的思路

笔记

  • java基础: int 到 char[], 需要以String作为桥梁
  • char[] chN = Integer.toString(n).toCharArray();
  • int nn = Integer.parseInt(new String(chN, 1, chN.length-1));

思路

  • 递归处理,比如当前数字是21345, 在一次递归函数中只处理1346-21345这2万个数字; 再下一次递归函数中处理347-1346这1千个数字; 接着处理48-347这300个数字; 接着处理9-48这40个数字; 最后一轮传入的数字是9,直接返回0即可
  • 可以发现, 每一轮都处理最高位*10^(len-1)个数字
  • 对于当前要处理的最高位*10^(len-1)个数字中(如果最高位是0,那么本次递归中1的出现次数是0)
    • 先找出最高位有多少个1
      • 如果最高位是1, 那么最高位是1的出现次数: 除去最高位后,剩下的数+1; 比如1345,最高位是1的出现次数是345+1=346
      • 如果最高位大于1, 那么最高位1的出现次数: 10^(len-1)个; 比如2345,最高位是1的出现次数是1000
    • 再找出剩下的位中有多少个1
      • 先举个例子, 比如1346-11345这1万个数, 后四位的变化范围是1346-9999, 0000-1345; 合起来正好是0000-9999, 共1万个数, 这也是为什么要这样分段.
      • 如此一来, 后四位中任取一位是1,剩下3位从0-9中任选, 也就是4*10*10*10=4000个1
      • 对于11346-21345这1万个数来说同理, 后四位的变化范围依然是1346-9999,0000-1345 => 0000-9999
      • 所以1346-21345这2万个数字中,后四位中出现1的次数是4000+4000=8000
      • 总结一下规律, 当前的数字分成最高位段(比如21345分成2段),每一段中除去最高位后剩下位数中出现1的次数 = (len-1) * 10^(len-2)个, (len-1)表示1能出现在几个位置上, 10^(len-2)表示某个位置是1时,其余位置有多少种变化. 把所有段的结果合起来 = 最高位*(len-1)*10^(len-2)
  • 递归终止条件
    • 当前数字是个位数
      • 个位数是0时, 1的出现次数是0
      • 个位数是1时, 1的出现次数是1
  • 这个思路很棒, 难点在于很难以递归的方式分析的这么完备, 也很难准确地分段:最高位,除去最高位剩下的位
  • 还有个难点就是, 分析除去最高位的剩下的位时, 如何计算1的出现次数, 巧妙的分段可以直接使用排列得到结果
public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        //input check 
        if(n<1)
            return 0;
        char[] chN = Integer.toString(n).toCharArray();
        return Core(chN);
    }
    public int Core(char[] chN){
        //每一轮递归计算bcd+1 ~ abcd这段范围中1出现的次数
        //recursion finish
        int len = chN.length;
        if(len==1 && chN[0]=='0')
            return 0;
        if(len==1 && chN[0]!='0')
            return 1;
        //recursive body
        //here, chN.length>1
        int num1AboutTop = 0;
        int num1AboutOth = 0;
        if(chN[0]!='0'){ //最高位是0的话就不处理了, 递归进入下一轮
            num1AboutTop = chN[0]=='1'? Integer.parseInt(new String(chN,1,len-1))+1:powBase10(len-1);
            num1AboutOth = (chN[0]-'0') * (len-1) * powBase10(len-2);
        }
        return num1AboutTop + num1AboutOth + Core(new String(chN, 1, len-1).toCharArray());
    }
    
    public int powBase10(int n){ //计算10的n次方
        int res=1;
        for(int i=0; i<n; i++)
            res *= 10;
        return res;
    }
}

以下两个高分答案,本质上是一样的

高分答案1

笔记

  • 不舒服的地方在于, 整数n有几位? 答案是 (int)Math.ceil(Math.log10(n+0.1)); 而不是(int)Math.ceil(Math.log10(n)); n=10; 比如,1这个数有一位,但是lg1=0,所以需要加上一个小数
  • 循环条件可以用while(n/i != 0), 也就是每判断一次去掉一位, 直到n为0

思路(很不错)

  • 如21m45, 当前位是m, top是21, low是45
  • 如果m==0, 在1-21045这个范围中, m是1的出现次数只跟top有关:100-199, 1100-1199, …,20100-20199; 共top*100个
    • 其中100-199这一组在m不是个位数时是一定存在的,跟top是否为0无关, 在找规律时应该发现这一点
  • 如果m==1, 在1-21145这个范围中, m是1的出现次数分别跟top和low有关:100-199, 1100-1199,…,20100-20199;这里是top*100个. 还有一部分是:21100-21145, 共low+1个
  • 如果m>1, 在1-21m45这个范围中, m是1的出现次数只跟top有关:100-199, 1100-1199,…,21100-21199; 共(top+1)*100个
个人版
public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        //execute
        int top,curr,low=0;
        int loop = (int)Math.ceil(Math.log10(n+0.1));
        int count = 0;
        while(loop>0){
            curr = n/(int)Math.pow(10,loop-1) % 10;
            top = n/(int)Math.pow(10,loop);
            low = n - n/(int)Math.pow(10,loop-1)*(int)Math.pow(10,loop-1);
            //当前位是0
            if(curr == 0)
                count = count + top*(int)Math.pow(10,loop-1);
            else if(curr == 1)
                count = count + top*(int)Math.pow(10,loop-1) + low + 1;
            else{//curr > 1
                count = count + (top+1)*(int)Math.pow(10,loop-1);
            }
            loop--;
        }
        return count;
    }
}
高分答案版(循环条件更合理!)
public class Solution {
    public int NumberOf1Between1AndN_Solution(int n) {
        int count = 0;//1的个数
        int i = 1;//当前位
        int current = 0,after = 0,before = 0;
        while((n/i)!= 0){           
            current = (n/i)%10; //当前位数字
            before = n/(i*10); //高位数字
            after = n-(n/i)*i; //低位数字
            //如果为0,出现1的次数由高位决定,等于高位数字 * 当前位数
            if (current == 0)
                count += before*i;
            //如果为1,出现1的次数由高位和低位决定,高位*当前位+低位+1
            else if(current == 1)
                count += before * i + after + 1;
            //如果大于1,出现1的次数由高位决定,//(高位数字+1)* 当前位数
            else{
                count += (before + 1) * i;
            }    
            //前移一位
            i = i*10;
        }
        return count;
    }
}

高分答案2(相当漂亮并简洁的思路)

思路

  • 个位数上的1,每隔10个数出现1次. 0-9, 10-19, 20-29…
    • n/10*1 + n%10>0?1:0; n/10表示n这个数有多少个10,当前位是几
  • 十位数上的1,每隔100次出现10次. 10-19, 110-119,210-219
    • n/100*10 + if(n%100>19) 10 else if(n%100<10) 0 else (n%100)-10 + 1
  • 百位数上的1,每隔1000次出现100次. 100-199, 1100-1199, 2100-2199
    • n/1000*100 + if(n%1000>199) 100 else if(n%1000<100) 0 else (n%1000) - 100 + 1
  • 千位数上的1, 每隔10000次出现1000次. 1000-1999, 11000-11999, 21000-21999
    • n/10000*1000 + if(n%10000>1999) 1000 else if(n%10000<1000) 0 else (n%10000) - 1000 + 1;
  • 再把个位数的情况统一
    • n/10*1 + if(n%10>1) 1 else if(n%10<1) 0 else (n%10) - 1 + 1;
  • 通式,i=1表示个位数, i=10表示十位数, i=100表示百位数
    • n/(i*10)*i + if(n%i*10>2*i-1) i else if(n%i*10<i) 0 else (n%i*10) - i + 1
    • 优化一下通式, 去掉if else, 变成
    • n/(i*10)*i + min(max(n%i*10-i+1,0),i)
public class Solution {
    public static int NumberOf1Between1AndN_Solution(int n) {
        //input check
        if(n<1)
            return 0;
        //execute
        int count = 0;
        for(int i=1; i<=n; i*=10){ //这个循环条件也非常棒
            count += n/(i*10)*i + Math.min(Math.max(n%(i*10) - i + 1, 0), i);
        }
        return count;
    }
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值