题目来源:56. 从 1 到 n 整数中 1 出现的次数
与 LeetCode 233. 数字 1 的个数 相同。
输入一个整数 n,求从 1 到 n 这 n 个整数的十进制表示中 1 出现的次数。
最直接的想法是枚举从 1 到 n 这 n 个整数,计算每一个数中 1 出现的次数,全部累加起来。
但这样做会超时。
代码:
class Solution {
public:
int numberOf1Between1AndN_Solution(int n) {
int number=0;
for(int i=1;i<=n;i++)
number+=numberOfOne(i);
return number;
}
// 辅函数
int numberOfOne(int x)
{
int count=0;
while(x)
{
if(x%10==1)
count++;
x/=10;
}
return count;
}
};
复杂度分析:
时间复杂度:O(n)。
空间复杂度:O(1)。
举个例子: n = 2304 。答案为四个部分之和:
用第 2 部分来举例,也就是计算所有小于等于 2304 的正整数中,十位出现 1 的次数。
为了帮助理解,我们先想象有一个“密码锁”,一共有 4 位,每一位可单独滚动。为了计算十位出现 1 的次数,我们考虑三种情况:
第一种情况:n 中的十位为 0,即 n = 2304。
我们先锁住十位,强行让十位变成 1,剩下三位可以随意滚动,“密码锁”形式为 XX1X。
那么求十位出现 1 的个数等同于可以滚出多少种密码组合(前提是密码要小于等于 n,且十位锁死为 1,不能更改)。
不难发现,我们能滚出的最大数是 2219,最小数是 0010。
那么 0010 到 2219 之间有多少种十位为 1 的密码呢?
我们去掉十位,得到 000 和 229。一共就是 229 - 000 + 1 = 230 种,即 n 的千位和百位构成的数(23) * 10。
第二种情况:n 中的十位为 1,即 n = 2314。
我们先锁住十位,强行让十位变成 1,剩下三位可以随意滚动,“密码锁”形式为 XX1X。
那么求十位出现 1 的个数等同于可以滚出多少种密码组合(前提是密码要小于等于 n,且十位锁死为 1,不能更改)。
不难发现,我们能滚出的最大数是 2314,最小数是 0010。
那么 0010 到 2314 之间有多少种十位为 1 的密码呢?
我们去掉十位,得到 000 和 234,一共就是 23 * 10 + 4 + 1 = 235 种,即 n 的千位和百位构成的数(23) * 10 + n 个位的数字(4) + 1。
第三种情况:n 中的十位为 2~9 中任意数字
我们用 n = 2324 举例,其他情况都一样。
我们先锁住十位,强行让十位变成 1,剩下三位可以随意滚动,“密码锁”形式为 XX1X。
那么求十位出现 1 的个数等同于可以滚出多少种密码组合(前提是密码要小于等于 n,且十位锁死为 1,不能更改)。
不难发现,我们能滚出的最大数是 2319,最小数是 0010。
那么 0010 到 2319 之间有多少种十位为 1 的密码呢?
我们去掉十位,得到 000 和 239,一共就是 239 - 000 + 1 = 240 种,即(n 的千位和百位构成的数 + 1)(23 + 1) * 10。
如果我们定义十位左边的数为高位 high ,例如 2304 的高位为 23,十位右边的数为低位 low,例如 2304 的低位为 4,那么以上规律就可以写成高位和低位的规律。我们分别对 2304 的每一位做一次分析,并将四部分结果相加就得到了答案。
代码:
/*
* @lc app=leetcode.cn id=233 lang=cpp
*
* [233] 数字 1 的个数
*/
// @lc code=start
class Solution
{
public:
int countDigitOne(int n)
{
int res = 0;
long digit = 1;
int high = n / 10, cur = n % 10, low = 0;
while (high != 0 || cur != 0)
{
if (cur == 0)
res += high * digit;
else if (cur == 1)
res += high * digit + low + 1;
else
res += (high + 1) * digit;
low += cur * digit;
cur = high % 10;
high /= 10;
digit *= 10;
}
return res;
}
};
// @lc code=end
复杂度分析:
时间复杂度:O(digit),其中 digit 是 n 的位数。
空间复杂度:O(1)。
书上的思路:
以 21345 为例,分为 2 段:1 ~ 1345 和 1346 ~ 21345。
我们先看 1346 ~ 21345 中 1 的出现次数。1 的出现分为两种情况。首先分析 1 出现在最高位(本例子是万位)的情况。
在 1346 ~ 21345 的数字中,1 出现在 10000 ~ 19999 这 10000 个数字的万位上,一共出现了 10000 次。
值得注意的是,并不是对所有 5 位数而言万位上出现 1 的次数是 10000次。对于万位是 1 的数字,比如 12345,1 只出现在 10000 ~ 12345 的万位,次数为 2346 次,也就是除去最高位后剩下的数字 + 1。
接下来分析 1 出现在除了最高位之外的其他 4 位数中的情况。例子中 1346 ~ 21345 这 20000 个数字中后 4 位中 1 出现的次数是 8000 次。由于最高位是 2,我们可以再把 1346 ~ 21345 分成两段:1346 ~ 11345 和 11346 ~ 21345。每一段剩下的数字中,选择其中一位是 1,其余三位可以在 0 ~ 9 这 10 个数字中任意选择,因此总共出现的次数是 2*4*103 次。
至于在 1 ~ 1235 中 1 的出现次数,我们可以递归求得。
为了编程方便,我们先把数字转换成字符串。
代码:
class Solution
{
public:
int numberOf1Between1AndN_Solution(int n)
{
if (n <= 0)
return 0;
string strN = to_string(n);
return numberOf1(strN);
}
int numberOf1(string strN)
{
if (strN.empty())
return 0;
int first = strN[0] - '0';
int length = strN.size();
if (length == 1)
{
if (first == 0)
return 0;
else
return 1;
}
// 假设 strN 是 "21345"
// numFirstDigit 是数字 10000~19999 的第一位中 1 的数目
int numFirstDigit = 0;
if (first > 1)
numFirstDigit = PowerBase10(length - 1);
else if (first == 1)
numFirstDigit = stoi(strN.substr(1)) + 1;
// numOtherDigits 是 1346~21345 除第一位之外的数位中 1 的数目
int numOtherDigits = first * (length - 1) * PowerBase10(length - 2);
// numRecursive 是 1~1235 中 1 的数目
int numRecursive = numberOf1(strN.substr(1));
return numFirstDigit + numOtherDigits + numRecursive;
}
// 辅函数 - 计算 pow(10, n)
int PowerBase10(int n)
{
int res = 1;
for (int i = 0; i < n; i++)
res *= 10;
return res;
}
};