algorithm-base/animation-simulation/剑指offer/1的个数.md

263 lines
9.9 KiB
Java
Raw Normal View History

2021-04-25 09:28:14 +00:00
# 我太喜欢这个题了
> 如果阅读时发现错误或者动画不可以显示的问题可以添加我微信好友 **[tan45du_one](https://raw.githubusercontent.com/tan45du/tan45du.github.io/master/个人微信.15egrcgqd94w.jpg)** ,备注 github + 题目 + 问题 向我反馈
>
> 感谢支持该仓库会一直维护希望对各位有一丢丢帮助
>
2021-07-20 13:13:10 +00:00
> 另外希望手机阅读的同学可以来我的 <u>[**公众号袁厨的算法小屋**](https://raw.githubusercontent.com/tan45du/test/master/微信图片_20210320152235.2pthdebvh1c0.png)</u> 两个平台同步,想要和题友一起刷题,互相监督的同学,可以在我的小屋点击<u>[**刷题小队**](https://raw.githubusercontent.com/tan45du/test/master/微信图片_20210320152235.2pthdebvh1c0.png)</u>进入。
2021-04-25 09:28:14 +00:00
今天我们来看一道贼棒的题目题目不长很经典也很容易理解我们一起来看一哈吧
大家也可能做过这道题那就再复习一下如果没做过的话可以看完文章自己去 AC 一下不过写代码的时候要自己完全写出来这样才能有收获下面我们看题目吧
## leetcode 233. 数字 1 的个数
**题目描述**
给定一个整数 n计算所有小于等于 n 的非负整数中数字 1 出现的个数
示例 1
> 输入n = 13
> 输出6
示例 2
> 输入n = 0
> 输出0
太喜欢这种简洁的题目啦言简意赅就是让咱们找出**小于等于 n 的非负整数中数字 1 出现的个数**
大家看到这个题目的第一印象可能会这样想让我们求 1 的个数
呐我们直接逐位遍历每个数的每一位当遇到 1 的时候计数器 +1不就行了
很棒的方法可惜会超时我试了
或者说我们可以先将所有数字拼接起来然后再逐位遍历这样仍会超时我也试了
大家再思考一下还有没有别的方法呢
既然题目让我们统计小于等于 n 的非负整数中数字 1 出现的个数
那我们可以不可这样统计
我们假设 n = abcd某个四位数
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/1的次数1.1s5l5k3qy3y8.png)
那我们完全可以统计每一位上 1 出现的次数个数上 1 出现的次数十位上 1 出现的次数百位 千位
也就是说**小于等于 n 的所有数字中**个位上出现 1 的次数 + 十位出现 1 的次数 + 最后得到的就是总的出现次数
见下图
我们假设 n = 13 (用个小点的数比较容易举例)
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/1的次数2.1horkktykr7k.png)
我们需要统计小于等于 13 的数中出现 1 的次数
通过上图可知个位上 1 出现 2 十位上 1 出现 4
那么总次数为 2 + 4 = 6
> 另外我们发现 11 这个数会被统计 2 它的十位和个位都为 1
>
> 而我们这个题目是要统计 1 出现的次数而不是统计包含 1 的整数所以上诉方法不会出现重复统计的情况
>
我们题目已经有大概思路啦下面的难点就是如何统计每一位中 1 出现的次数呢
我们完全可以通过遍历 n 的每一位来得到总个数见下图
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/1的次数3.21nr01qnlz40.png)
假设我们想要得到十位上 1 出现的次数当前我们指针指向十位
我们称之为当前位num 则代表当前位的位因子当前位为个位时 num = 1十位时为 10百位时为 100....
那我们将**当前位左边的定义为高位****当前位右边的定义位低位**
2021-07-20 13:13:10 +00:00
> n = 1004 此时指针指向十位当前位num = 10高位为百位千位低位为个位
2021-04-25 09:28:14 +00:00
而且我们某一位的取值范围为 0 ~ 9,那么我们可以将这 10 个数分为 3 小于 1 当前位数字为 0 等于 1当前位数字为 1 大于 1当前位上数字为 2 ~ 9下面我们就来分别考虑三种情况
> **我们进行举例的 n 100410141024重点讨论十位上 3 种不同情况**大家阅读下方文字之前先想象自己脑子里有一个行李箱的滚轮密码锁我们固定其中的某一位然后可以随意滑动其他位这样可以帮助大家理解
>
> 该比喻来自与网友 ryan0414看到的时候不禁惊呼可太贴切了
### **n = 1004**
我们想要计算出**小于等于 1004 的非负整数中**十位上出现 1 的次数
也就是当前位为十位数字为 0 十位上出现 1 的次数
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/位数1.2x7xcbxtkjo0.png)
> 解析为什么我们可以直接通过高位数字 * num得到 1 出现的次数
>
> 因为我们高位为 10可变范围为 0 ~ 10但是我们的十位为 0 所以高位为 10 的情况取不到所以共有 10 种情况
>
> 又当前位为十位低位共有 1 可选范围为 0 ~ 9 共有 10 种情况所以直接可以通过 10 * 10 得到
其实不难理解我们可以设想成行李箱的密码盘在一定范围内也就是上面的 0010 ~ 0919 固定住一位为 1 只能移动其他位看共有多少种组合
好啦这个情况我们已经搞明白啦下面我们看另一种情况
### n = 1014
我们想要计算出**小于等于 1014 的非负整数中**十位上出现 1 的次数
也就是当前位为十位数字为 1 十位上出现 1 的次数
我们在小于 1014 的非负整数中十位上为 1 的最小数字为 10最大数字为 1014所以我们需要在 10 ~ 1014 这个范围内固定住十位上的 1 移动其他位
其实然后我们可以将 1014 看成是 1004 + 10 = 1014
则可以将 10 ~ 1014 拆分为两部分 0010 ~ 0919 小于 1004 1010 ~ 1014
见下图
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/次数为1十位.4e6s2zqwtsw0.png)
> 解析为什么我们可以直接通过 高位数字 * num + 低位数字 + 1 10 * 10 + 4 + 1
>
> 得到 1 出现的次数
>
> 高位数字 * num 是得到第一段的次数第二段为 低位数字 + 1求第二段时我们高位数字和当前位已经固定
>
> 我们可以改变的只有低位
可以继续想到密码盘求第二段时把前 3 位固定只能改变最后一位最后一位最大能到 4 那么共有几种情况
2021-07-20 13:13:10 +00:00
### n = 1024
2021-04-25 09:28:14 +00:00
我们想要计算出**小于等于 1024 的非负整数中**十位上出现 1 的次数
也就是当前位为十位数字为 2 ~ 9 十位上出现 1 的次数其中最小的为 0010最大的为 1019
我们也可以将其拆成两段 0010 ~ 09191010 ~ 1019
![](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/高位.1wn8di6g1t6.png)
> 解析为什么我们可以直接通过高位数字 * num + num 10 * 10 + 10 得到 1 出现的次数
>
> 第一段和之前所说一样第二段的次数我们此时已经固定了高位和当前位当前位为 1低位可以随意取值上诉例子中当前位为 10低位为位数为 1则可以取值 0 ~ 9 的任何数则共有 10 (num) 种可能
好啦这个题目大家应该理解的差不多啦
下面我们通过动画模拟一下是怎样一步一步的计算出小于等于 1014 的数中 1 出现的次数
> 蓝色高位橙色当前位绿色低位
>
> 初始化low = 0 cur = n % 10, num = 1, count = 0, high = n / 10;
![1的个数](https://cdn.jsdelivr.net/gh/tan45du/photobed@master/1的个数.5yccejufzc80.gif)
好啦下面我们看一下题目代码吧
下方代码没有简写也都标有注释大家可以结合动画边思考边阅读
**题目代码**
```java
class Solution {
public int countDigitOne(int n) {
//高位
int high = n;
//低位
int low = 0;
//当前位
int cur = 0;
int count = 0;
int num = 1;
while (high != 0 || cur != 0) {
cur = high % 10;
high /= 10;
2021-07-20 13:13:10 +00:00
//这里我们可以提出 high * num 因为我们发现无论为几,都含有它
2021-04-25 09:28:14 +00:00
if (cur == 0) count += high * num;
2021-07-20 13:13:10 +00:00
else if (cur == 1) count += high * num + 1 + low;
2021-04-25 09:28:14 +00:00
else count += (high + 1) * num;
//低位
2021-07-20 13:13:10 +00:00
low = cur * num + low;
2021-04-25 09:28:14 +00:00
num *= 10;
}
return count;
}
}
```
2021-07-19 15:02:05 +00:00
Swift Code:
```swift
class Solution {
func countDigitOne(_ n: Int) -> Int {
var high = n, low = 0, cur = 0, count = 0, num = 1
while high != 0 || cur != 0 {
cur = high % 10
high /= 10
2021-07-20 13:13:10 +00:00
//这里我们可以提出 high * num 因为我们发现无论为几,都含有它
2021-07-19 15:02:05 +00:00
if cur == 0 {
count += high * num
2021-07-20 13:13:10 +00:00
} else if cur == 1 {
2021-07-19 15:02:05 +00:00
count += high * num + 1 + low
} else {
count += (high + 1) * num
}
low = cur * num + low
num *= 10
}
return count
}
}
```
2021-04-25 09:28:14 +00:00
时间复杂度 : O(logn) 空间复杂度 O(1)
2021-07-20 13:13:10 +00:00
C++ Code:
```C++
class Solution
{
public:
int countDigitOne(int n)
{
// 高位, 低位, 当前位
int high = n, low = 0, cur = 0;
int count = 0, num = 1;
//数字是0的时候完全没必要继续计算
while (high != 0)
{
cur = high % 10;
high /= 10;
//这里我们可以提出 high * num 因为我们发现无论为几,都含有它
if (cur == 0)
count += (high * num);
else if (cur == 1)
count += (high * num + 1 + low);
else
count += ((high + 1) * num);
//低位
low = cur * num + low;
//提前检查剩余数字, 以免溢出
if (high != 0)
num *= 10;
}
return count;
}
};
```