0x01.问题
输入一个整数
n
,求1~n
这n
个整数的十进制表示中1出现的次数。
例如,输入12,1~12这些整数中包含1 的数字有1、10、11和12,1一共出现了5次。
1 <= n < 2^31
C++函数形式: int countDigitOne(int n)
0x02.详细分析
初看这个问题感觉比较简单,但细细想想,越想发现越难,因为这个数可能很大,暴力枚举肯定不能解决问题了。所以我们一定要找其他办法解决。
我刚开始傻傻的数出了1-99,1-999,的1的个数,因为我总感觉里面存在某些制约关系,或者存在一些规律,后来一想,我的确傻啊,我为啥不写一段代码专门测试一下,看是否存在规律,于是我写了下面这段测试代码:
#include<iostream>
using namespace std;
int main() {
while (1) {
int n,m, ans = 0;
cout << "请输入区间左端点:";
cin >> n;
cout << "请输入区间右端点:";
cin >> m;
for (int i = n; i <= m; i++) {
int temp = i;
while (temp) {
int w = temp % 10;
if (w == 1) ans++;
temp /= 10;
}
}
cout <<"该区间 1 的个数是 "<< ans << endl;
}
}
发现这段代码还是真香,于是,我开始了我的测试:
怎么样,发现规律了嘛,原来在一个指定位数的区间是有规律的,这样,我们发现,心中瞬间有底了。
我们就可以开始假设一些情况了,假如数字是19989
,是个5位数,这样,我们1-9999
就不用计算了,因为我们已经知道了,接下来,只要考虑后面的数字的情况就行了。
后面的数字我们就产生疑惑了,如果第一个数字是2
,或者其它不是1
的数呢?很明显产生的1
的个数不同,这怎么办呢?
于是我又开始去找找规律:
咦,原来1
开头的空间也都是有规律的,于是我接着找不是1
开头的数:
神奇的事又来了,原来除了1
开头的,其它的整区间都是相等的。
我们现在把发现的规律整理一下:
- 位数之间的大区间(也就是10,100,100这样的区间)含
1
的个数都是
位数*10^(位数-1)
1
开头的数字,在当前位数(不包含低位)含1
的个数规律是:100-199
是120
,1000-1999
是1300
,10000-19999
是14000
,依此类推。- 不是
1
开头的数字,在当前位数含1
的个数规律是:三位的都是20
,四位的都是300
,五位的都是4000
,依次类推。
理清上面的规律后,我们可以开始想办法去解决问题了。
我们再来看19989
,我们要计算这个的含1
个数,是不是只要四位数的含1
数加上,本位含1
数,而开头是1
,所以肯定是多了9989
个1
的,然后就只要计算9989
这个四位数了,依次类推,最后就只要计算到各位就行了,这其实就是递归的思路了,但我发现,这个写出递归代码似乎有点复杂,于是我就仿造了一个递归代码,就是写多个分位数计算的函数,之间相互调用,哈哈,这也是一种解决办法。而且我们会发现,最大数字是2^31
,也就是2,147,483,648
,十位数,所以我们可以把这些情况都枚举出来。
我们的整体思路是:
-
先判断
n
的位数,然后选择调用不同的函数。 -
对于一个指定位数的计算,需要分多步计算:
- 先加上低位数的全部
1
的个数。 - 如果最高位等于
1
,那么ans
先加1,目的是包含最高位的最小数字(如:19987,这个步骤的含义是加上10000的1
的个数),然后ans
加上低位数字,这是最高位产生的1
,再把低一位的传给上一个计算这个低位的计算。
-如果最高位不等于1
,ans
先加上相应的1
作为最高位产生的个数,再加上之前几个最高位产生的个数(如,计算5676,这个步骤就是加上2000-4999的个数),最后把低一位的传给相应的函数计算。
- 先加上低位数的全部
-
最后返回得到答案
ans
。
举例:98789
- 先加上四位数的个数
4000
,然后最高位不是1
,加上10000-19999
产生的14000
,然后加上20000-89999
产生的,也就是(8-2)*4000
,最后将8789
传给相应的函数计算。
0x03.解决代码–最高效率
class Solution {
public:
int f1=1;
int f2=20;
int f3=300;
int f4=4e3;
int f5=5e4;
int f6=6e5;
int f7=7e6;
int f8=8e7;
int f9=9e8;
int basicCounter1(int n){
return n!=0?1:0;
}
int basicCounter2(int n){
if(n<10) return basicCounter1(n);
int ans=0;
ans+=f1;//1-9
int ten=n/10;
if(ten==1){
ans++;//10
ans+=n-10+basicCounter1(n-10);
}
else{
ans+=11;//10-19
ans+=ten-2;
ans+=basicCounter1(n-ten*10);
}
return ans;
}
int basicCounter3(int n){
if(n<100) return basicCounter2(n);
int ans=0;
ans+=f2;//1-99
int hundred=n/100;
if(hundred==1){
ans++;//100
ans+=(n-100)+basicCounter2(n-100);//二位数
}
else{
ans+=120;//100-199
ans+=(hundred-2)*f2;//前面几个百位的值
ans+=basicCounter2(n-hundred*100);//二位数
}
return ans;
}
int basicCounter4(int n){
if(n<1000) return basicCounter3(n);
int ans=0;
ans+=f3;//1-999
int q=n/1000;
if(q==1){
ans++;//1000
ans+=(n-1000)+basicCounter3(n-1000);//三位数
}
else{
ans+=1300;//1000-1999
ans+=(q-2)*f3;//前面几个前位值
ans+=basicCounter3(n-q*1000);//三位数
}
return ans;
}
int basicCounter5(int n){
if(n<1e4) return basicCounter4(n);
int ans=0;
ans+=f4;
int w=n/1e4;
if(w==1){
ans++;
ans+=(n-1e4)+basicCounter4(n-1e4);
}
else{
ans+=14e3;
ans+=(w-2)*f4;
ans+=basicCounter4(n-w*1e4);
}
return ans;
}
int basicCounter6(int n){
if(n<1e5) return basicCounter5(n);
int ans=0;
ans+=f5;
int sw=n/1e5;
if(sw==1){
ans++;
ans+=(n-1e5)+basicCounter5(n-1e5);
}
else{
ans+=15e4;
ans+=(sw-2)*f5;
ans+=basicCounter5(n-sw*1e5);
}
return ans;
}
int basicCounter7(int n){
if(n<1e6) return basicCounter6(n);
int ans=0;
ans+=f6;
int bw=n/1e6;
if(bw==1){
ans++;
ans+=(n-1e6)+basicCounter6(n-1e6);
}
else{
ans+=16e5;
ans+=(bw-2)*f6;
ans+=basicCounter6(n-bw*1e6);
}
return ans;
}
int basicCounter8(int n){
if(n<1e7) return basicCounter7(n);
int ans=0;
ans+=f7;
int qw=n/1e7;
if(qw==1){
ans++;
ans+=(n-1e7)+basicCounter7(n-1e7);
}
else{
ans+=17e6;
ans+=(qw-2)*f7;
ans+=basicCounter7(n-qw*1e7);
}
return ans;
}
int basicCounter9(int n){
if(n<1e8) return basicCounter8(n);
int ans=0;
ans+=f8;
int y=n/1e8;
if(y==1){
ans++;
ans+=(n-1e8)+basicCounter8(n-1e8);
}
else{
ans+=18e7;
ans+=(y-2)*f8;
ans+=basicCounter8(n-y*1e8);
}
return ans;
}
int basicCounter10(int n){
int ans=0;
ans+=f9;
int sy=n/1e9;
if(sy==1){
ans++;
ans+=(n-1e9)+basicCounter9(n-1e9);
}
else{
ans+=19e8;
ans+=(sy-2)*f9;
ans+=basicCounter9(n-sy*1e9);
}
return ans;
}
int countDigitOne(int n) {
int ans=0,len=0;
string s=to_string(n);
len=s.size();
switch(len){
case 1:
ans=basicCounter1(n);
break;
case 2:
ans=basicCounter2(n);
break;
case 3:
ans=basicCounter3(n);
break;
case 4:
ans=basicCounter4(n);
break;
case 5:
ans=basicCounter5(n);
break;
case 6:
ans=basicCounter6(n);
break;
case 7:
ans=basicCounter7(n);
break;
case 8:
ans=basicCounter8(n);
break;
case 9:
ans=basicCounter9(n);
break;
case 10:
ans=basicCounter10(n);
break;
}
return ans;
}
};
0x04.简要说明
这个算法本来的暴力解法是O(N)
的时间复杂度,但是我们使用这种思想后,时间和空间都大幅减少了。
我们来看一下神奇之处,别看这好像是个递归,但其实最多执行11
次,这是常数级别的!!!
而且在每个函数执行的过程中,都只是简单的加法运算,所以时间复杂度也是常数级别的!!!
时间和空间都达到了最高的常数级别!!!
一般会有时间换空间,或者空间换时间这一说法,但,这是代码换空间和时间,就是代码多写了一点,将可能的十位数都列举出来了,但效率惊人!!!
ATFWUS --Writing By 2020–03–26~