我建议将此算法从以 2 为基数调整为以 10 为基数:
Number of 1s in the two's complement binary representations of integers in a range
得到的算法是 O(log N)。
方法是编写一个简单的递归函数count(n),从1 到n 计数零。
关键的观察是,如果 N 以 9 结尾,例如:
123456789
您可以将 0 到 N 的数字分成 10 个大小相等的组。第 0 组是以 0 结尾的数字。第 1 组是以 1 结尾的数字。第 2 组是以 2 结尾的数字。以此类推,一直到第 9 组,即所有以 9 结尾的数字。
除了第 0 组之外的每个组都将count(N/10) 的数字贡献到总数中,因为它们都没有以零结尾。第 0 组贡献了count(N/10)(计算除最后一位以外的所有数字)加上N/10(计算最后一位数字中的零)。
由于我们是从 1 到 N,而不是从 0 到 N,因此对于一位数 N,这个逻辑会失效,所以我们只是将其作为一种特殊情况来处理。
[更新]
什么鬼,让我们将count(n, d) 概括并定义为数字d 在从1 到n 的数字中出现的次数。
/* Count how many d's occur in a single n */
unsigned
popcount(unsigned n, unsigned d) {
int result = 0;
while (n != 0) {
result += ((n%10) == d);
n /= 10;
}
return result;
}
/* Compute how many d's occur all numbers from 1 to n */
unsigned
count(unsigned n, unsigned d) {
/* Special case single-digit n */
if (n < 10) return (d > 0 && n >= d);
/* If n does not end in 9, recurse until it does */
if ((n % 10) != 9) return popcount(n, d) + count(n-1, d);
return 10*count(n/10, d) + (n/10) + (d > 0);
}
n < 10 的丑陋再次来自 1 到 n 的范围,而不是 0 到 n... count 为 1,除非 d 为零。
将此解决方案转换为非递归循环是 (a) 微不足道的,(b) 不必要的,并且 (c) 留给读者作为练习。
[更新 2]
最后的 (d > 0) 术语也来自 1 到 n 而不是 0 到 n 的范围。当n以9结尾时,1到n之间有多少个数字有最后一位d?好吧,当d 为零时,答案是n/10;当d 不为零时,它会比它大一,因为它本身包含值d。
例如,如果n 是 19,d 是 0,则只有一个以 0 结尾的较小数字(即 10)。但是如果n 是 19 而d 是 2,则有两个以 2 结尾的较小数字(即 2 和 12)。
感谢@Chan 指出 cmets 中的这个错误;我已经在代码中修复了它。