剑指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/(i 10)写成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;
}
}
class Solution {
public int countDigitOne ( int n) {
int res= 0 ;
for ( int i= 1 ; i<= n; i= i* 10 ) {
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 ;
}
if ( i> Integer. MAX_VALUE/ 10 )
break ;
}
return res;
}
}
笔记
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
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;
}
}
第二次做,理顺了最优解,要理解并记住注释中的分析过程
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) {
if ( n< 1 )
return 0 ;
char [ ] chN = Integer. toString ( n) . toCharArray ( ) ;
return Core ( chN) ;
}
public int Core ( char [ ] chN) {
int len = chN. length;
if ( len== 1 && chN[ 0 ] == '0' )
return 0 ;
if ( len== 1 && chN[ 0 ] != '0' )
return 1 ;
int num1AboutTop = 0 ;
int num1AboutOth = 0 ;
if ( chN[ 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) {
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) {
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 ) ;
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 {
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 ;
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;
if ( current == 0 )
count += before* i;
else if ( current == 1 )
count += before * i + after + 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) {
if ( n< 1 )
return 0 ;
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;
}
}