剑指 Offer(第2版)面试题 43:从 1 到 n 整数中 1 出现的次数

剑指 Offer(第2版)面试题 43:从 1 到 n 整数中 1 出现的次数

  • 剑指 Offer(第2版)面试题 43:从 1 到 n 整数中 1 出现的次数
    • 解法1:暴力
    • 解法2:数学

剑指 Offer(第2版)面试题 43:从 1 到 n 整数中 1 出现的次数

题目来源:56. 从 1 到 n 整数中 1 出现的次数

与 LeetCode 233. 数字 1 的个数 相同。

输入一个整数 n,求从 1 到 n 这 n 个整数的十进制表示中 1 出现的次数。

解法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)。

解法2:数学

举个例子: n = 2304 。答案为四个部分之和:

  1. 所有小于等于 2304 的正整数中,个位出现 1 的次数。
  2. 所有小于等于 2304 的正整数中,十位出现 1 的次数。
  3. 所有小于等于 2304 的正整数中,百位出现 1 的次数。
  4. 所有小于等于 2304 的正整数中,千位出现 1 的次数。

用第 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;
	}
};

你可能感兴趣的:(剑指,Offer,C++,剑指Offer)