求小于等于N的所有正整数里面包含的1的个数

这篇博客介绍了如何高效地计算小于等于给定正整数N的所有正整数中1的个数。博主提出了一种时间复杂度更低的算法,通过对N的每一位进行检查,来确定每一位上1出现的总次数,以此来优化传统逐个数字检查的方法。通过举例和详细解释,博主展示了如何应用这个算法,并提供了相应的代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目:

已知一个正整数N,求比N小(包括N)的所有正整数中包含的1的个数。例如N = 12,则包含5个1,分别为1、10、11、12


解答:

  1. 最直观最简单的想法是,从1到N对每一个数检查,得出每个数中包含的一的个数,事件复杂度大概是O(N)
  2. 1时间复杂度比较高,不是最优算法。一个比1快的算法是:对N的每一位进行检查,得出比N小的正数某一位包含的1的个数总和。比如1234,千位包含234 + 1个1(1000 ~ 1234),百位包含 2 * 100个1(1100 ~ 1199, 100 ~ 199),十位包含13 * 10个1(1210 ~ 1219,1110 ~ 1119,。。。。。。,10 ~ 19),个位包含124 * 1个1(1231,1221,1211,1201,。。。。。。,1)。按照这个思路可以写出代码如下。

代码:

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <math.h>
using namespace std;

int getNumAfterI(char *buf, int i, int len)
{
	int sum = 0;
	for(int j = i + 1; j < len; j++)
		sum = sum * 10 + (buf[j] - '0');
	return sum + 1;
}

int NumOf1AfterN(int n)
{
	int sum = 0;
	int num = 0;
	char buf[20];
	sprintf(buf, "%d", n);
	int len = strlen(buf);

	for(int i = 0; i < len; i++)
	{
		if(buf[i] > '1')
			sum += (num + 1) * pow(10, len - i - 1)
<think>我们需要计算小于等于N的正整数1出现的次。例如,当N=13时,1出现的次是6(因为1,10,11,12,13中包含1个数分别是1,1,2,1,1,总共1+1+2+1+1=6)。一种直观的方法是遍历1到N的每个数,然后计算每个数1个数并累加。但是当N非常大时,这种方法会非常慢(时间复杂度为O(N*logN))。我们可以使用更高效的方法,通过分析每一位(个位、十位、百位等)上1出现的次,然后累加。这种方法的时间复杂度为O(logN)。思路:对于每一位(比如当前位为第k位,从个位开始计),我们可以将整分成三部分:高位部分(当前位左边的字)、当前位、低位部分(当前位右边的字)。例如:对于整N=123456,当前位是百位(即k=3),则高位是123,当前位是4,低位是56。当前位上1出现的次由当前位的字决定:1.如果当前位字为0,则当前位出现1的次为:高位部分*10^(k-1)2.如果当前位字为1,则当前位出现1的次为:高位部分*10^(k-1)+低位部分+13.如果当前位字大于1,则当前位出现1的次为:(高位部分+1)*10^(k-1)但是注意,这种分情况讨论是基于当前位在整中的位置。具体步骤:1.初始化计器count=0,以及当前位的权重base=1(从个位开始)。2.设置变量high表示高位,low表示低位,cur表示当前位。初始时,high=N//10,cur=N%10,low=0。3.循环条件:当high不为0或者cur不为0时(即还有位需要处理):ifcur==0:count+=high*baseelseifcur==1:count+=high*base+low+1else:count+=(high+1)*base然后,更新低位:low=low+cur*base(注意:在更新低位时,当前位会变成低位的一部分)更新当前位:cur=high%10更新高位:high=Math.floor(high/10)更新base:base*=104.循环结束后,返回count。但是注意,在更新时,我们实际上是从低位向高位移动。每次迭代后,当前位处理完毕,然后将其加入低位,同时高位去掉最后一位成为新的高位,而高位的最后一位成为新的当前位。举例:N=1234初始:high=123,cur=4,low=0,base=1第一轮(个位):cur=4>1->count+=(123+1)*1=124更新:low=0+4*1=4;high=12;cur=high%10=2;base=10第二轮(十位):cur=2>1->count+=(12+1)*10=130->累计124+130=254更新:low=4+2*10=24;high=1;cur=1;base=100第三轮(百位):cur=1->count+=1*100+24+1=125->累计254+125=379更新:low=24+1*100=124;high=0;cur=0;base=1000第四轮(千位):cur=0->count+=0->累计379更新:high=0,cur=0退出循环。然后验证:1~1234中1个数。个位:每10个数出现11,共124次(0~1234中个位1出现124次,因为1230~1234的个位是0~4,所以1230~1234中个位1出现1次(1231),所以0~1229有123个完整的10,每个10个位出现1次,共123次,加上1230~1234中1次,共124次)。十位:每100个数中,十位出现10次110~19),1234中十位有12个完整的100(0~1199)和35个数1200~1234)。在0~1199中,十位出现1的次12*10=120。在1200~1234中,十位是3(>=2),所以会出现10次(1210~1219),所以十位总共130次?但是上面的算法在第二轮得到130次,实际上:0~99:10次(10~19)100~199:10次(110~119)但是注意,199的十位是9,所以只有10~19这10次?不对,实际上100~199的十位1出现在110~119,所以10次。但是100~199中,十位为1的还有110~119,所以10次。所以0~999:十位出现1的次10*10=100次(因为每100个数出现10次,共10个100)。0~1199:12个100,所以12*10=120次。1200~1234:十位是3,所以十位为1的区间是1210~1219,共10次。所以总共130次。百位:每1000个数中,百位出现100次(100~199)。0~999:100次;1000~1199:200次?不对,应该是1000~1199中,百位为1,所以有200次?但是注意,1000~1199中百位是1,所以每个数百位都是1,共200个数,所以200次。但0~999中百位为1的有100次(100~199)。所以0~1199中百位1出现300次?不对,重新算:0~999:100次(100~199)1000~1199:百位1出现200次(因为1000~1199的百位都是1,每个数百位都是1,共200个数1200~1234:百位是2(大于1),所以不会出现百位1。所以百位1出现300次?但是上面计算中,第三轮我们只加了125次。为什么?重新审视我们的例子:N=1234第三轮(百位):high=1,cur=1,low=24,base=100所以:count+=1*100+24+1=125这是因为我们只计算了1000~1234中百位1出现的次,而0~999中百位1出现的次在之前没有计算过?不对,因为我们是按位独立计算的,百位在0~999中出现的次应该由高位0~9(即0~9)来决定,但是我们处理百位的时候,高位已经变成了1(因为1234的高位是12,然后我们更新后高位变成1,低位24)。所以这里我们只计算了1000~1234的百位1的次。实际上,我们整个算法是逐位计算,每一位独立计算,然后累加。所以对于百位,我们只考虑百位这一位上的1,并且考虑整个数(0~1234)中百位出现1的次。在百位上:百位出现1的情况:100~199,1100~1199(注意1100~1199是1000~1199的一部分)?不对,实际上:百位为1的次:从0到1234,百位出现1的次是多少?百位为1100~199,1100~1199。但是100~199:100个数1100~1199:100个数。总共200个?但是注意1100~1199中的百位1实际上是在千位1的情况下,百位为1。但是我们的算法中,在百位这一轮:high=1(表示高位,即千位是1,但实际上千位是1,百位前面的高位是1?这里的高位实际上是千位,因为我们是分解成:高位(千位)是1,当前位(百位)是2(在第三轮中,当前位是百位,但是我们的例子中,第三轮时cur=1?不对,在第三轮,我们的例子中cur=1(即百位是2?不对,我们重新计算例子N=1234的步骤)。修正例子步骤:初始:N=1234第一轮(个位):high=1234//10=123,cur=1234%10=4,low=0,base=1cur=4>1->count+=(123+1)*1=124更新:low=0+4*1=4high=123//10=12(因为high=123,除以10取整)cur=123%10=3(这里应该是取high的最后一位,即123%10=3)base=10第二轮(十位):high=12,cur=3,low=4,base=10cur=3>1->count+=(12+1)*10=130->累计124+130=254更新:low=4+3*10=34high=12//10=1cur=12%10=2base=100第三轮(百位):high=1,cur=2,low=34,base=100cur=2>1->count+=(1+1)*100=200->累计254+200=454更新:low=34+2*100=234high=1//10=0cur=1%10=0?不对,因为high=1,除以10取整为0,所以cur=1%10=1(但此时high=0,所以cur应该是0?)这里我们重新写更新步骤:更新:high=Math.floor(high/10)->1/10取整为0然后cur=high%10->0%10=0?不对,因为high已经变成0了,所以cur=0所以base=1000第四轮(千位):high=0,cur=0,low=234,base=1000cur=0->count+=0*1000=0更新:low=234+0*1000=234high=0//10=0cur=0%10=0base=10000然后循环条件:high=0且cur=0,退出。所以总次454。但是实际手动计算1~1234中1个数:个位:124个(前面已算)十位:130个(前面已算,0~1199:120个,1200~1234:10个(1210~1219))->130个百位:100~199:100个;1100~1199:100个;共200个千位:1000~1234:每个数千位都是1,共235个(1000~1234有235个数)所以总共:124+130+200+235=689个。这显然不对,因为重复计算了?不对,我们统计的是每一位上1出现的次,包括同一个数多个1的情况。但是实际计算:1000~1234的千位1出现235次(1000~1234共235个数,每个数千位都是1,所以235次)。百位:100~199:100次;1100~1199:100次;共200次。十位:0~99:10次(10~19);100~199:10次(110~119);...900~999:10次;共10*10=100次;然后1000~1199:十位出现1的区间:1010~1019,1110~1119->2*10=20次;1200~1234:1210~1219:10次;所以十位总共100+20+10=130次。个位:0~99:10次(1,11,...,91)共10次;100~199:10次(101,111,...,191)共10次;...1200~1234:个位1出现2次(1201,1211,1221,1231)共4次?不对,1201,1211,1221,1231,确实4次。但是0~1199:每10个数个位出现1次,共120次(0~1199有1200个数120组,每组1次),然后1200~1234:1201,1211,1221,1231共4次,所以个位124次。所以总共:235(千位)+200(百位)+130(十位)+124(个位)=689次。但是我们的算法只计算了454次,显然不对。问题出在哪里?我们重新审视算法步骤:正确的算法步骤(参考《编程之美》或Leetcode题解):设当前位为第k位(从个位开始为第1位),则base=10^(k-1)设高位为a=N//(base*10)(即去掉当前位和低位)设当前位为b=(N//base)%10设低位为c=N%base则当前位出现1的次:ifb==0:count+=a*baseifb==1:count+=a*base+c+1ifb>=2:count+=(a+1)*base然后base乘以10,直到base超过N。注意:这里a,b,c的法。以N=1234,base=1(个位):a=1234//(1*10)=123b=(1234//1)%10=4c=1234%1=0b>1->count=(123+1)*1=124base=10(十位):a=1234//(10*10)=1234//100=12b=(1234//10)%10=123%10=3->注意:1234//10=123,然后123%10=3c=1234%10=4b>1->count+=(12+1)*10=130->累计254base=100(百位):a=1234//(100*10)=1234//1000=1b=(1234//100)%10=12%10=2c=1234%100=34b>1->count+=(1+1)*100=200->累计454base=1000(千位):a=1234//(1000*10)=1234//10000=0b=(1234//1000)%10=1%10=1c=1234%1000=234b==1->count+=0*1000+234+1=235->累计454+235=689所以正确步骤应该是每次更新base,然后重新计算a,b,c。而不是通过迭代更新high,cur,low。因此,我们修改算法:初始化:count=0,base=1当base<=N时循环:a=Math.floor(N/(base*10));b=Math.floor(N/base)%10;c=N%base;if(b===0){count+=a*base;}elseif(b===1){count+=a*base+c+1;}else{count+=(a+1)*base;}base*=10;注意:base可能会溢出(当N很大时,base*10可能会超出整范围?但在JS中,整范围比较大,通常问题不大。如果N非常大,我们可以使用BigInt,但题目是整,一般可以不用。)我们按照这个算法重新计算N=1234,得到689,和手动计算一致。代码实现:注意:base从1开始,每次乘以10,直到base>N时退出循环。但是注意:当base很大时,base*10可能会大于N,但我们需要在base<=N时进入循环。因为当base大于N时,a=0,b=0,c=N,然后b=0,count+=0,所以也可以继续,但base会一直乘下去直到溢出。所以循环条件改为:while(base<=N)或者while(base<=N)但是当N=0时,base=1>0,所以不会进入循环,没问题。实现代码:</think>以下是一个高效计算小于等于 N 的正整数1 出现次的 JavaScript 解决方案。该算法通过逐位分析字的每一位(个位、十位、百位等)来计算 1 出现的次,时间复杂度为 O(log N),远优于暴力遍历的 O(N log N) 方法。 ### 解决方案代码 ```javascript function countDigitOne(n) { if (n <= 0) return 0; let count = 0; let base = 1; while (base <= n) { const higher = Math.floor(n / (base * 10)); // 当前位左侧高位部分 const current = Math.floor(n / base) % 10; // 当前位字 const lower = n % base; // 当前位右侧低位部分 if (current === 0) { count += higher * base; } else if (current === 1) { count += higher * base + lower + 1; } else { count += (higher + 1) * base; } base *= 10; // 移动到下一位(十位、百位等) } return count; } // 测试示例 console.log(countDigitOne(13)); // 输出: 6 (1,10,11,12,13) console.log(countDigitOne(0)); // 输出: 0 console.log(countDigitOne(1234)); // 输出: 689 ``` ### 代码详细解释 1. **初始化**: - `count`:累计 1 出现的总次 - `base`:当前数位的权重(初始为 1,表示个位) 2. **逐位处理**: - **分解字**: - `higher`:当前位左侧的高位部分(如百位处理时,千位以上的字) - `current`:当前位的字(0-9) - `lower`:当前位右侧的低位部分(如百位处理时,十位和个位组成的字) - **计算规则**: - **当前位为 0**:次 = `higher * base` (高位决定周期,每个周期出现 `base` 次) - **当前位为 1**:次 = `higher * base + lower + 1` (高位周期 + 低位部分额外出现的次) - **当前位 ≥ 2**:次 = `(higher + 1) * base` (高位周期多算一次完整的 `base` 次) - **更新权重**:`base *= 10` 处理更高数位(十位 → 百位 → 千位...) 3. **终止条件**: - 当 `base > n` 时,所有数位处理完毕,返回累计结果 ### 示例分析(N=13) | base | higher | current | lower | 公式 | 累计次 | |------|--------|---------|-------|-------------------------|----------| | 1 | 1 | 3 | 0 | (1+1)*1 = 2 | 2 | | 10 | 0 | 1 | 3 | 0*10 + 3 + 1 = 4 | 6 | | 100 | - | - | - | base>13,结束循环 | **6** | ### 算法复杂度 - **时间复杂度**:O(log₁₀ N) 循环次字位决定(如 1000 仅需 4 次循环) - **空间复杂度**:O(1) 仅使用常级额外空间 ### 边界情况处理 - 输入为 0 或负时直接返回 0 - 大处理:JavaScript 字安全范围(±2⁵³)内完全支持 ### 测试验证 - `countDigitOne(13)` → 6(正确:1,10,11,12,13 中共 6 个 1) - `countDigitOne(0)` → 0 - `countDigitOne(1234)` → 689(可通过逐位验证公式正确性)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值