<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个数出现1次1,共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次1(10~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的次数是多少?百位为1的数:100~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(可通过逐位验证公式正确性)