提交 6db9753f 编写于 作者: L lucifer

feat: 精选题解

上级 b5bd9863
# 回炉重铸, 91 天见证不一样的自己(第二期)
力扣加加,一个努力做西湖区最好的算法题解的团队。就在今天它给大家带来了《91 天学算法》,帮助大家摆脱困境,征服算法。
<img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gf2atkdikgj30u70u0tct.jpg" width="50%">
## 初衷
为了让想学习的人能够真正学习到东西, 我打算新开一个栏目《91 天学算法》,在 91 天内来帮助那些想要学习算法,提升自己算法能力的同学,帮助大家建立完整的算法知识体系。
群里每天都会有题目,推荐大家讨论当天的题目。我们会帮助大家规划学习路线,91 天见证不一样的自己。群里会有专门的资深算法竞赛大佬坐阵解答大家的问题和疑问,并且会对前一天的题目进行讲解。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gf2b2zkclnj30xm0b6aat.jpg)
## 活动时间
`2020-10-01``2020-12-30`
> lucifer 我刚好 12.31 生日, 好巧!
## 你能够得到什么?
1. 显著提高你的刷题效率,让你少走弯路
2. 掌握常见面试题的思路和解法
3. 掌握常见套路,了解常见算法的本质,横向对比各种题目
4. 纵向剖析一道题,多种方法不同角度解决同一题目
## 要求
- 🈲️ 不允许经常闲聊
- 🈲️ 不允许发广告,软文(只能发算法相关的技术文章)
- ✅ 一周至少参与一次打卡
> 违反上述条件的人员会被强制清退
## 课程大纲
![](https://tva1.sinaimg.cn/large/007S8ZIlly1giq98aux20j30ju0qt781.jpg)
部分公开的讲义:
- [【91 算法-基础篇】05.双指针](https://lucifer.ren/blog/2020/05/26/91algo-basic-05.two-pointer/)
- [动态规划问题为什么要画表格?](https://lucifer.ren/blog/2020/08/27/91algo-dp-lecture/)
### 基础篇(30 天)
1. 数组,队列,栈
2. 链表
3. 树与递归
4. 哈希表
5. 双指针
### 进阶篇(30 天)
1.
2. 前缀树
3. 并查集
4. 跳表
5. 剪枝技巧
6. RK 和 KMP
7. 高频面试题
...
### 专题篇(31 天)
1. 二分法
2. 滑动窗口
3. 位运算
4. 背包问题
5. 搜索(BFS,DFS,回溯)
6. 动态规划
7. 分治
8. 贪心
...
## 游戏规则
- 每天会根据课程大纲的规划,出一道相关题目。
- 大家可以在指定 https://91.leetcode-solution.cn 中打卡(不可以抄作业哦),对于不会做的题目可以在群里提问。
- 第二天会对前一天的题目进行讲解。
## 奖励
- 对于坚持打卡满一个月的同学,可以参加抽奖,奖品包括`算法模拟面试`,算法相关的图书,科学上网兑换码等
- 连续打卡七天可以获得补签卡一张哦
## 冲鸭
报名开始时间待定。
采用 微信群的方式进行,`前 50 个进群的小伙伴免费哦 ~`,50 名之后的小伙伴采取阶梯收费的形式。
收费标准:
- 前 50 人免费
- 51 - 100 收费 5 元
- 101 - 500 收费 10 元
想要参与的小伙伴加我,发红包拉你进群。
- 微信号:DevelopeEngineer
# 【91算法-基础篇】05.双指针
力扣加加,一个努力做西湖区最好的算法题解的团队。就在今天它给大家带来了《91 天学算法》,帮助大家摆脱困境,征服算法。
<img src="https://tva1.sinaimg.cn/large/007S8ZIlly1gf2atkdikgj30u70u0tct.jpg" width="50%">
## 什么是双指针
顾名思议,双指针就是**两个指针**,但是不同于 C,C++中的指针, 其是一种**算法思想**
如果说,我们迭代一个数组,并输出数组每一项,我们需要一个指针来记录当前遍历的项,这个过程我们叫单指针(index)的话。
```java
for(int i = 0;i < nums.size(); i++) {
输出(nums[i]);
}
```
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gf5w79tciyj30aa0hl77b.jpg)
(图 1)
那么双指针实际上就是有两个这样的指针,最为经典的就是二分法中的左右双指针啦。
```java
int l = 0;
int r = nums.size() - 1;
while (l < r) {
if(一定条件) return 合适的值一般是 l r 的中点
if(一定条件) l++
if(一定条件) r--
}
// 因为 l == r,因此返回 l 和 r 都是一样的
return l
```
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gf5yfe9da7j307504ut8r.jpg)
(图 2)
读到这里,你发现双指针是一个很宽泛的概念,就好像数组,链表一样,其类型会有很多很多, 比如二分法经常用到`左右端点双指针`。滑动窗口会用到`快慢指针和固定间距指针`。 因此双指针其实是一种综合性很强的类型,类似于数组,栈等。 但是我们这里所讲述的双指针,往往指的是某几种类型的双指针,而不是“只要有两个指针就是双指针了”。
> 有了这样一个**算法框架**,或者算法思维,有很大的好处。它能帮助你理清思路,当你碰到新的问题,在脑海里进行搜索的时候,**双指针**这个词就会在你脑海里闪过,闪过的同时你可以根据**双指针**的所有套路和这道题进行**穷举匹配**,这个思考解题过程本来就像是算法,我会在进阶篇《搜索算法》中详细阐述。
那么究竟我们算法中提到的双指针指的是什么呢?我们一起来看下算法中双指针的常见题型吧。
## 常见题型有哪些?
这里我将其分为三种类类型,分别是:
1. 快慢指针(两个指针步长不同)
2. 左右端点指针(两个指针分别指向头尾,并往中间移动,步长不确定)
3. 固定间距指针(两个指针间距相同,步长相同)
> 上面是我自己的分类,没有参考别人。可以发现我的分类标准已经覆盖了几乎所有常见的情况。 大家在平时做题的时候一定要养成这样的习惯,将题目类型进行总结,当然这个总结可以是别人总结好的,也可以是自己独立总结的。不管是哪一种,都要进行一定的消化吸收,把它们变成真正属于自己的知识。
不管是哪一种双指针,只考虑双指针部分的话 ,由于最多还是会遍历整个数组一次,因此时间复杂度取决于步长,如果步长是 1,2 这种常数的话,那么时间复杂度就是 O(N),如果步长是和数据规模有关(比如二分法),其时间复杂度就是 O(logN)。并且由于不管规模多大,我们都只需要最多两个指针,因此空间复杂度是 O(1)。下面我们就来看看双指针的常见套路有哪些。
## 常见套路
### 快慢指针
1. 判断链表是否有环
这里给大家推荐两个非常经典的题目,一个是力扣 287 题,一个是 142 题。其中 142 题我在我的 LeetCode 题解仓库中的每日一题板块出过,并且给了很详细的证明和解答。而 287 题相对不直观,比较难以想到,这道题曾被官方选定为每日一题,也是相当经典的。
- [287. 寻找重复数](https://leetcode-cn.com/problems/find-the-duplicate-number/)
- [【每日一题】- 2020-01-14 - 142. 环形链表 II · Issue #274 · azl397985856/leetcode](https://github.com/azl397985856/leetcode/issues/274)
2. 读写指针。典型的是`删除重复元素`
这里推荐我仓库中的一道题, 我这里写了一个题解,横向对比了几个相似题目,并剖析了这种题目的本质是什么,让你看透题目本质,推荐阅读。
- [80.删除排序数组中的重复项 II](https://github.com/azl397985856/leetcode/blob/master/problems/80.remove-duplicates-from-sorted-array-ii.md)
## 左右端点指针
1. 二分查找。
二分查找会在专题篇展开,这里不多说,大家先知道就行了。
2. 暴力枚举中“从大到小枚举”(剪枝)
一个典型的题目是我之前参加官方每日一题的时候给的一个解法,大家可以看下。这种解法是可以 AC 的。同样地,这道题我也给出了三种方法,帮助大家从多个纬度看清这个题目。强烈推荐大家做到一题多解。这对于你做题很多帮助。除了一题多解,还有一个大招是多题同解,这部分我们放在专题篇介绍。
[find-the-longest-substring-containing-vowels-in-even](https://leetcode-cn.com/problems/find-the-longest-substring-containing-vowels-in-even-counts/solution/qian-zhui-he-zhuang-tai-ya-suo-pythonjava-by-fe-lu/)
3. 有序数组。
区别于上面的二分查找,这种算法指针移动是连续的,而不是跳跃性的,典型的是 LeetCode 的`两数和`,以及`N数和`系列问题。
## 固定间距指针
1. 一次遍历(One Pass)求链表的中点
2. 一次遍历(One Pass)求链表的倒数第 k 个元素
3. 固定窗口大小的滑动窗口
## 模板(伪代码)
我们来看下上面三种题目的算法框架是什么样的。这个时候我们没必要纠结具体的语言,这里我直接使用了伪代码,就是防止你掉进细节。
当你掌握了这种算法的细节,就应该找几个题目试试。一方面是检测自己是否真的掌握了,另一方面是“细节”,”细节“是人类,尤其是软件工程师最大的敌人,毕竟我们都是`差不多先生`
1. 快慢指针
```jsx
l = 0
r = 0
while 没有遍历完
if 一定条件
l += 1
r += 1
return 合适的值
```
2. 左右端点指针
```jsx
l = 0
r = n - 1
while l < r
if 找到了
return 找到的值
if 一定条件1
l += 1
else if 一定条件2
r -= 1
return 没找到
```
3. 固定间距指针
```jsx
l = 0
r = k
while 没有遍历完
自定义逻辑
l += 1
r += 1
return 合适的值
```
## 题目推荐
如果你`差不多`理解了上面的东西,那么可以拿下面的题练练手。Let's Go!
### 左右端点指针
- 16.3Sum Closest (Medium)
- 713.Subarray Product Less Than K (Medium)
- 977.Squares of a Sorted Array (Easy)
- Dutch National Flag Problem
> 下面是二分类型
- 33.Search in Rotated Sorted Array (Medium)
- 875.Koko Eating Bananas(Medium)
- 881.Boats to Save People(Medium)
### 快慢指针
- 26.Remove Duplicates from Sorted Array(Easy)
- 141.Linked List Cycle (Easy)
- 142.Linked List Cycle II(Medium)
- 287.Find the Duplicate Number(Medium)
- 202.Happy Number (Easy)
### 固定间距指针
- 1456.Maximum Number of Vowels in a Substring of Given Length(Medium)
> 固定窗口大小的滑动窗口见专题篇的滑动窗口专题(暂未发布)
## 其他
有时候也不能太思维定式,比如 https://leetcode-cn.com/problems/consecutive-characters/ 这道题根本就没必要双指针什么的。
再比如:https://lucifer.ren/blog/2020/05/31/101.symmetric-tree/
此差异已折叠。
# 你的衣服我扒了 - 《最长公共子序列》
之前出了一篇[穿上衣服我就不认识你了?来聊聊最长上升子序列](https://lucifer.ren/blog/2020/06/20/LIS/),收到了大家的一致好评。今天给大家带来的依然是换皮题 - 最长公共子序列系列。
最长公共子序列是一个很经典的算法题。有的会直接让你求最长上升子序列,有的则会换个说法,但最终考察的还是最长公共子序列。那么问题来了,它穿上衣服你还看得出来是么?
如果你完全看不出来了,说明抽象思维还不到火候。经常看我的题解的同学应该会知道,我经常强调`抽象思维`。没有抽象思维,所有的题目对你来说都是新题。你无法将之前做题的经验迁移到这道题,那你做的题意义何在?
虽然抽象思维很难练成,但是幸好算法套路是有限的,经常考察的题型更是有限的。从这些入手,或许可以让你轻松一些。本文就从一个经典到不行的题型《最长公共子序列》,来帮你进一步理解`抽象思维`
> 注意。 本文是帮助你识别套路,从横向上理清解题的思维框架,并没有采用最优解,所有的题目给的解法可能不是最优的,但是都可以通过所有的测试用例。如果你想看最优解,可以直接去讨论区看。或者期待我的`深入剖析系列`。
## 718. 最长重复子数组
### 题目地址
https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/
### 题目描述
```
给两个整数数组  A  和  B ,返回两个数组中公共的、长度最长的子数组的长度。
示例 1:
输入:
A: [1,2,3,2,1]
B: [3,2,1,4,7]
输出: 3
解释:
长度最长的公共子数组是 [3, 2, 1]。
说明:
1 <= len(A), len(B) <= 1000
0 <= A[i], B[i] < 100
```
### 前置知识
- 哈希表
- 数组
- 二分查找
- 动态规划
### 思路
这就是最经典的最长公共子序列问题。一般这种求解**两个数组或者字符串求最大或者最小**的题目都可以考虑动态规划,并且通常都定义 dp[i][j]`以 A[i], B[j] 结尾的 xxx`。这道题就是:`以 A[i], B[j] 结尾的两个数组中公共的、长度最长的子数组的长度`
> 关于状态转移方程的选择可以参考: [穿上衣服我就不认识你了?来聊聊最长上升子序列](https://lucifer.ren/blog/2020/06/20/LIS/)
算法很简单:
- 双层循环找出所有的 i, j 组合,时间复杂度 $O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。
- 如果 A[i] == B[j],dp[i][j] = dp[i - 1][j - 1] + 1
- 否则,dp[i][j] = 0
- 循环过程记录最大值即可。
**记住这个状态转移方程,后面我们还会频繁用到。**
### 关键点解析
- dp 建模套路
### 代码
代码支持:Python
Python Code:
```py
class Solution:
def findLength(self, A, B):
m, n = len(A), len(B)
ans = 0
dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if A[i - 1] == B[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
ans = max(ans, dp[i][j])
return ans
```
**复杂度分析**
- 时间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。
- 空间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。
> 二分查找也是可以的,不过并不容易想到,大家可以试试。
## 1143.最长公共子序列
### 题目地址
https://leetcode-cn.com/problems/longest-common-subsequence
### 题目描述
给定两个字符串  text1 和  text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的   子序列   是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
示例 1:
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace",它的长度为 3。
示例 2:
输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc",它的长度为 3。
示例 3:
输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0。
提示:
1 <= text1.length <= 1000
1 <= text2.length <= 1000
输入的字符串只含有小写英文字符。
### 前置知识
- 数组
- 动态规划
### 思路
和上面的题目类似,只不过数组变成了字符串(这个无所谓),子数组(连续)变成了子序列 (非连续)。
算法只需要一点小的微调: `如果 A[i] != B[j],那么 dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])`
### 关键点解析
- dp 建模套路
### 代码
> 你看代码多像
代码支持:Python
Python Code:
```py
class Solution:
def longestCommonSubsequence(self, A: str, B: str) -> int:
m, n = len(A), len(B)
ans = 0
dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if A[i - 1] == B[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
ans = max(ans, dp[i][j])
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return ans
```
**复杂度分析**
- 时间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。
- 空间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。
## 1035. 不相交的线
### 题目地址
https://leetcode-cn.com/problems/uncrossed-lines/description/
### 题目描述
我们在两条独立的水平线上按给定的顺序写下  A  和  B  中的整数。
现在,我们可以绘制一些连接两个数字  A[i]  和  B[j]  的直线,只要  A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。
以这种方法绘制线条,并返回我们可以绘制的最大连线数。
示例 1:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1ggbkku13xuj315x0u0abp.jpg)
输入:A = [1,4,2], B = [1,2,4]
输出:2
解释:
我们可以画出两条不交叉的线,如上图所示。
我们无法画出第三条不相交的直线,因为从 A[1]=4 到 B[2]=4 的直线将与从 A[2]=2 到 B[1]=2 的直线相交。
示例 2:
输入:A = [2,5,1,2,5], B = [10,5,2,1,5,2]
输出:3
示例 3:
输入:A = [1,3,7,1,7,5], B = [1,9,2,5,1]
输出:2
提示:
1 <= A.length <= 500
1 <= B.length <= 500
1 <= A[i], B[i] <= 2000
### 前置知识
- 数组
- 动态规划
### 思路
从图中可以看出,如果想要不相交,则必然相对位置要一致,换句话说就是:**公共子序列**。因此和上面的 `1143.最长公共子序列` 一样,属于换皮题,代码也是一模一样。
### 关键点解析
- dp 建模套路
### 代码
> 你看代码多像
代码支持:Python
Python Code:
```py
class Solution:
def longestCommonSubsequence(self, A: str, B: str) -> int:
m, n = len(A), len(B)
ans = 0
dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if A[i - 1] == B[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
ans = max(ans, dp[i][j])
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
return ans
```
**复杂度分析**
- 时间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。
- 空间复杂度:$O(m * n)$,其中 m 和 n 分别为 A 和 B 的 长度。
## 总结
第一道是“子串”题型,第二和第三则是“子序列”。不管是“子串”还是“子序列”,状态定义都是一样的,不同的只是一点小细节。
**只有熟练掌握基础的数据结构与算法,才能对复杂问题迎刃有余。** 基础算法,把它彻底搞懂,再去面对出题人的各种换皮就不怕了。相反,如果你不去思考题目背后的逻辑,就会刷地很痛苦。题目稍微一变化你就不会了,这也是为什么很多人说**刷了很多题,但是碰到新的题目还是不会做**的原因之一。关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。
更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 30K star 啦。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg)
# 穿上衣服我就不认识你了?来聊聊最长上升子序列
最长上升子序列是一个很经典的算法题。有的会直接让你求最长上升子序列,有的则会换个说法,但最终考察的还是最长上升子序列。那么问题来了,它穿上衣服你还看得出来是么?
如果你完全看不出来了,说明抽象思维还不到火候。经常看我的题解的同学应该会知道,我经常强调`抽象思维`。没有抽象思维,所有的题目对你来说都是新题。你无法将之前做题的经验迁移到这道题,那你做的题意义何在?
虽然抽象思维很难练成,但是幸好算法套路是有限的,经常考察的题型更是有限的。从这些入手,或许可以让你轻松一些。本文就从一个经典到不行的题型《最长上升子序列》,来帮你进一步理解`抽象思维`
> 注意。 本文是帮助你识别套路,从横向上理清解题的思维框架,并没有采用最优解,所有的题目给的解法都不是最优的,但是都可以通过所有的测试用例。如果你想看最优解,可以直接去讨论区看。或者期待我的`深入剖析系列`。
## 300. 最长上升子序列
### 题目地址
https://leetcode-cn.com/problems/longest-increasing-subsequence
### 题目描述
```
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:
可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?
```
### 思路
> 美团和华为都考了这个题。
题目的意思是让我们从给定数组中挑选若干数字,这些数字满足: `如果 i < j 则 nums[i] < nums[j]`。问:一次可以挑选最多满足条件的数字是多少个。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfyyu7187bj31ku0igq6f.jpg)
这种子序列求极值的题目,应该要考虑到贪心或者动态规划。这道题贪心是不可以的,我们考虑动态规划。
按照动态规划定义状态的套路,我们有**两种常见**的定义状态的方式:
- dp[i] : 以 i 结尾(一定包括 i)所能形成的最长上升子序列长度, 答案是 max(dp[i]),其中 i = 0,1,2, ..., n - 1
- dp[i] : 以 i 结尾(可能包括 i)所能形成的最长上升子序列长度,答案是 dp[-1] (-1 表示最后一个元素)
容易看出第二种定义方式由于无需比较不同的 dp[i] 就可以获得答案,因此更加方便。但是想了下,状态转移方程会很不好写,因为 dp[i] 的末尾数字(最大的)可能是 任意 j < i 的位置。
第一种定义方式虽然需要比较不同的 dp[i] 从而获得结果,但是我们可以在循环的时候顺便得出,对复杂度不会有影响,只是代码多了一点而已。因此我们**选择第一种建模方式**
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfyyz18gu6j31t40dy77l.jpg)
由于 dp[j] 中一定会包括 j,且以 j 结尾, 那么 nums[j] 一定是其所形成的序列中最大的元素,那么如果位于其后(意味着 i > j)的 nums[i] > nums[j],那么 nums[i] 一定能够融入 dp[j] 从而形成更大的序列,这个序列的长度是 dp[j] + 1。因此状态转移方程就有了:`dp[i] = dp[j] + 1 (其中 i > j, nums[i] > nums[j])`
`[10, 9, 2, 5, 3, 7, 101, 18]` 为例,当我们计算到 dp[5]的时候,我们需要往回和 0,1,2,3,4 进行比较。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfzzp18iyej311i0o8dk8.jpg)
具体的比较内容是:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfzzqeaen1j30um0fwwhd.jpg)
最后从三个中选一个最大的 + 1 赋给 dp[5]即可。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfzzt54n5wj30ys05g74x.jpg)
**记住这个状态转移方程,后面我们还会频繁用到。**
### 代码
```py
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
if n == 0: return 0
dp = [1] * n
ans = 1
for i in range(n):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
ans = max(ans, dp[i])
return ans
```
**复杂度分析**
- 时间复杂度:$O(N ^ 2)$
- 空间复杂度:$O(N)$
## 435. 无重叠区间
### 题目地址
https://leetcode-cn.com/problems/non-overlapping-intervals/
### 题目描述
```
给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。
注意:
可以认为区间的终点总是大于它的起点。
区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。
示例 1:
输入: [ [1,2], [2,3], [3,4], [1,3] ]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
```
### 思路
我们先来看下最终**剩下**的区间。由于剩下的区间都是不重叠的,因此剩下的**相邻区间的后一个区间的开始时间一定是不小于前一个区间的结束时间的**。 比如我们剩下的区间是`[ [1,2], [2,3], [3,4] ]`。就是第一个区间的 2 小于等于 第二个区间的 2,第二个区间的 3 小于等于第三个区间的 3。
不难发现如果我们将`前面区间的结束``后面区间的开始`结合起来看,其就是一个**非严格递增序列**。而我们的目标就是删除若干区间,从而**剩下最长的非严格递增子序列**。这不就是上面的题么?只不过上面是严格递增,这不重要,就是改个符号的事情。 上面的题你可以看成是删除了若干数字,然后剩下**剩下最长的严格递增子序列****这就是抽象的力量,这就是套路。**
如果对区间按照起点或者终点进行排序,那么就转化为上面的最长递增子序列问题了。和上面问题不同的是,由于是一个区间。因此实际上,我们是需要拿**后面的开始时间****前面的结束时间**进行比较。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfyzp8n59cj31000a2jse.jpg)
而由于:
- 题目求的是需要移除的区间,因此最后 return 的时候需要做一个转化。
- 题目不是要求严格递增,而是可以相等,因此我们的判断条件要加上等号。
> 这道题还有一种贪心的解法,其效率要比动态规划更好,但由于和本文的主题不一致,就不在这里讲了。
### 代码
**你看代码多像**
```py
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
n = len(intervals)
if n == 0: return 0
dp = [1] * n
ans = 1
intervals.sort(key=lambda a: a[1])
for i in range(len(intervals)):
for j in range(i - 1, -1, -1):
if intervals[i][0] >= intervals[j][1]:
dp[i] = max(dp[i], dp[j] + 1)
# 由于我事先进行了排序,因此倒着找的时候,找到的第一个一定是最大的数,因此不用往前继续找了。
# 这也是为什么我按照结束时间排序的原因。
break
dp[i] = max(dp[i], dp[i - 1])
ans = max(ans, dp[i])
return n - ans
```
**复杂度分析**
- 时间复杂度:$O(N ^ 2)$
- 空间复杂度:$O(N)$
## 646. 最长数对链
### 题目地址
https://leetcode-cn.com/problems/maximum-length-of-pair-chain/
### 题目描述
```
给出 n 个数对。 在每一个数对中,第一个数字总是比第二个数字小。
现在,我们定义一种跟随关系,当且仅当 b < c 时,数对(c, d) 才可以跟在 (a, b) 后面。我们用这种形式来构造一个数对链。
给定一个对数集合,找出能够形成的最长数对链的长度。你不需要用到所有的数对,你可以以任何顺序选择其中的一些数对来构造。
示例 :
输入: [[1,2], [2,3], [3,4]]
输出: 2
解释: 最长的数对链是 [1,2] -> [3,4]
注意:
给出数对的个数在 [1, 1000] 范围内。
```
### 思路
和上面的`435. 无重叠区间`是换皮题,唯一的区别这里又变成了严格增加。没关系,我们把等号去掉就行了。并且由于这道题求解的是最长的长度,因此转化也不需要了。
> 当然,这道题也有一种贪心的解法,其效率要比动态规划更好,但由于和本文的主题不一致,就不在这里讲了。
### 代码
**这代码更像了!**
```py
class Solution:
def findLongestChain(self, pairs: List[List[int]]) -> int:
n = len(pairs)
dp = [1] * n
ans = 1
pairs.sort(key=lambda a: a[0])
for i in range(n):
for j in range(i):
if pairs[i][0] > pairs[j][1]:
dp[i] = max(dp[i], dp[j] + 1)
ans = max(ans, dp[i])
return ans
```
**复杂度分析**
- 时间复杂度:$O(N ^ 2)$
- 空间复杂度:$O(N)$
## 452. 用最少数量的箭引爆气球
### 题目地址
https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/
### 题目描述
```
在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以y坐标并不重要,因此只要知道开始和结束的x坐标就足够了。开始坐标总是小于结束坐标。平面内最多存在104个气球。
一支弓箭可以沿着x轴从不同点完全垂直地射出。在坐标x处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。
Example:
输入:
[[10,16], [2,8], [1,6], [7,12]]
输出:
2
解释:
对于该样例,我们可以在x = 6(射爆[2,8],[1,6]两个气球)和 x = 11(射爆另外两个气球)。
```
### 思路
把气球看成区间,几个箭可以全部射爆,意思就是有多少不重叠的区间。注意这里重叠的情况也可以射爆。这么一抽象,就和上面的`646. 最长数对链`一模一样了,不用我多说了吧?
> 当然,这道题也有一种贪心的解法,其效率要比动态规划更好,但由于和本文的主题不一致,就不在这里讲了。
### 代码
**代码像不像?**
```py
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
n = len(points)
if n == 0: return 0
dp = [1] * n
cnt = 1
points.sort(key=lambda a:a[1])
for i in range(n):
for j in range(0, i):
if points[i][0] > points[j][1]:
dp[i] = max(dp[i], dp[j] + 1)
cnt = max(cnt, dp[i])
return cnt
```
**复杂度分析**
- 时间复杂度:$O(N ^ 2)$
- 空间复杂度:$O(N)$
## More
其他的我就不一一说了。比如 [673. 最长递增子序列的个数](https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/) (滴滴面试题)。 不就是求出最长序列,之后再循环比对一次就可以得出答案了么?
[491. 递增子序列](https://leetcode-cn.com/problems/increasing-subsequences/) 由于需要找到所有的递增子序列,因此动态规划就不行了,妥妥回溯就行了,套一个模板就出来了。回溯的模板可以看我之前写的[回溯专题](https://github.com/azl397985856/leetcode/blob/master/problems/90.subsets-ii.md "回溯专题")
大家把我讲的思路搞懂,这几个题一写,还怕碰到类似的题不会么?**只有熟练掌握基础的数据结构与算法,才能对复杂问题迎刃有余。** 最长上升子序列就是一个非常经典的基础算法,把它彻底搞懂,再去面对出题人的各种换皮就不怕了。相反,如果你不去思考题目背后的逻辑,就会刷地很痛苦。题目稍微一变化你就不会了,这也是为什么很多人说**刷了很多题,但是碰到新的题目还是不会做**的原因之一。关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。
更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 30K star 啦。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg)
# 一文看懂《最大子序列和问题》
最大子序列和是一道经典的算法题, leetcode 也有原题《53.maximum-sum-subarray》,今天我们就来彻底攻克它。
## 题目描述
求取数组中最大连续子序列和,例如给定数组为 A = [1, 3, -2, 4, -5], 则最大连续子序列和为 6,即 1 + 3 +(-2)+ 4 = 6。
首先我们来明确一下题意。
- 题目说的子数组是连续的
- 题目只需要求和,不需要返回子数组的具体位置。
- 数组中的元素是整数,但是可能是正数,负数和 0。
- 子序列的最小长度为 1。
比如:
- 对于数组 [1, -2, 3, 5, -3, 2], 应该返回 3 + 5 = 8
- 对于数组 [0, -2, 3, 5, -1, 2], 应该返回 3 + 5 + -1 + 2 = 9
- 对于数组 [-9, -2, -3, -5, -3], 应该返回 -2
## 解法一 - 暴力法(超时法)
一般情况下,先从暴力解分析,然后再进行一步步的优化。
### 思路
我们来试下最直接的方法,就是计算所有的子序列的和,然后取出最大值。
记 Sum[i,....,j]为数组 A 中第 i 个元素到第 j 个元素的和,其中 0 <= i <= j < n,
遍历所有可能的 Sum[i,....,j] 即可。
我们去枚举以 0,1,2...n-1 开头的所有子序列即可,
对于每一个开头的子序列,我们都去枚举从当前开始到 n-1 的所有情况。
这种做法的时间复杂度为 O(N^2), 空间复杂度为 O(1)。
### 代码
JavaScript:
```js
function LSS(list) {
const len = list.length;
let max = -Number.MAX_VALUE;
let sum = 0;
for (let i = 0; i < len; i++) {
sum = 0;
for (let j = i; j < len; j++) {
sum += list[j];
if (sum > max) {
max = sum;
}
}
}
return max;
}
```
Java:
```java
class MaximumSubarrayPrefixSum {
public int maxSubArray(int[] nums) {
int len = nums.length;
int maxSum = Integer.MIN_VALUE;
int sum = 0;
for (int i = 0; i < len; i++) {
sum = 0;
for (int j = i; j < len; j++) {
sum += nums[j];
maxSum = Math.max(maxSum, sum);
}
}
return maxSum;
}
}
```
Python 3:
```python
import sys
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n = len(nums)
maxSum = -sys.maxsize
sum = 0
for i in range(n):
sum = 0
for j in range(i, n):
sum += nums[j]
maxSum = max(maxSum, sum)
return maxSum
```
空间复杂度非常理想,但是时间复杂度有点高。怎么优化呢?我们来看下下一个解法。
## 解法二 - 分治法
### 思路
我们来分析一下这个问题, 我们先把数组平均分成左右两部分。
此时有三种情况:
- 最大子序列全部在数组左部分
- 最大子序列全部在数组右部分
- 最大子序列横跨左右数组
对于前两种情况,我们相当于将原问题转化为了规模更小的同样问题。
对于第三种情况,由于已知循环的起点(即中点),我们只需要进行一次循环,分别找出
左边和右边的最大子序列即可。
所以一个思路就是我们每次都对数组分成左右两部分,然后分别计算上面三种情况的最大子序列和,
取出最大的即可。
举例说明,如下图:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gds543yp2cj31400u0myf.jpg)
(by [snowan](https://github.com/snowan))
这种做法的时间复杂度为 O(N\*logN), 空间复杂度为 O(1)。
### 代码
JavaScript:
```js
function helper(list, m, n) {
if (m === n) return list[m];
let sum = 0;
let lmax = -Number.MAX_VALUE;
let rmax = -Number.MAX_VALUE;
const mid = ((n - m) >> 1) + m;
const l = helper(list, m, mid);
const r = helper(list, mid + 1, n);
for (let i = mid; i >= m; i--) {
sum += list[i];
if (sum > lmax) lmax = sum;
}
sum = 0;
for (let i = mid + 1; i <= n; i++) {
sum += list[i];
if (sum > rmax) rmax = sum;
}
return Math.max(l, r, lmax + rmax);
}
function LSS(list) {
return helper(list, 0, list.length - 1);
}
```
Java:
```java
class MaximumSubarrayDivideConquer {
public int maxSubArrayDividConquer(int[] nums) {
if (nums == null || nums.length == 0) return 0;
return helper(nums, 0, nums.length - 1);
}
private int helper(int[] nums, int l, int r) {
if (l > r) return Integer.MIN_VALUE;
int mid = (l + r) >>> 1;
int left = helper(nums, l, mid - 1);
int right = helper(nums, mid + 1, r);
int leftMaxSum = 0;
int sum = 0;
// left surfix maxSum start from index mid - 1 to l
for (int i = mid - 1; i >= l; i--) {
sum += nums[i];
leftMaxSum = Math.max(leftMaxSum, sum);
}
int rightMaxSum = 0;
sum = 0;
// right prefix maxSum start from index mid + 1 to r
for (int i = mid + 1; i <= r; i++) {
sum += nums[i];
rightMaxSum = Math.max(sum, rightMaxSum);
}
// max(left, right, crossSum)
return Math.max(leftMaxSum + rightMaxSum + nums[mid], Math.max(left, right));
}
}
```
Python 3 :
```python
import sys
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
return self.helper(nums, 0, len(nums) - 1)
def helper(self, nums, l, r):
if l > r:
return -sys.maxsize
mid = (l + r) // 2
left = self.helper(nums, l, mid - 1)
right = self.helper(nums, mid + 1, r)
left_suffix_max_sum = right_prefix_max_sum = 0
sum = 0
for i in reversed(range(l, mid)):
sum += nums[i]
left_suffix_max_sum = max(left_suffix_max_sum, sum)
sum = 0
for i in range(mid + 1, r + 1):
sum += nums[i]
right_prefix_max_sum = max(right_prefix_max_sum, sum)
cross_max_sum = left_suffix_max_sum + right_prefix_max_sum + nums[mid]
return max(cross_max_sum, left, right)
```
## 解法三 - 动态规划
### 思路
我们来思考一下这个问题, 看能不能将其拆解为规模更小的同样问题,并且能找出
递推关系。
我们不妨假设问题 Q(list, i) 表示 list 中以索引 i 结尾的情况下最大子序列和,
那么原问题就转化为 Q(list, i), 其中 i = 0,1,2...n-1 中的最大值。
我们继续来看下递归关系,即 Q(list, i)和 Q(list, i - 1)的关系,
即如何根据 Q(list, i - 1) 推导出 Q(list, i)。
如果已知 Q(list, i - 1), 我们可以将问题分为两种情况,即以索引为 i 的元素终止,
或者只有一个索引为 i 的元素。
- 如果以索引为 i 的元素终止, 那么就是 Q(list, i - 1) + list[i]
- 如果只有一个索引为 i 的元素,那么就是 list[i]
分析到这里,递推关系就很明朗了,即`Q(list, i) = Math.max(0, Q(list, i - 1)) + list[i]`
举例说明,如下图:
![53.maximum-sum-subarray-dp.png](https://tva1.sinaimg.cn/large/007S8ZIlly1gds544xidoj30pj0h2wew.jpg)
(by [snowan](https://github.com/snowan))
这种算法的时间复杂度 O(N), 空间复杂度为 O(1)
### 代码
JavaScript:
```js
function LSS(list) {
const len = list.length;
let max = list[0];
for (let i = 1; i < len; i++) {
list[i] = Math.max(0, list[i - 1]) + list[i];
if (list[i] > max) max = list[i];
}
return max;
}
```
Java:
```java
class MaximumSubarrayDP {
public int maxSubArray(int[] nums) {
int currMaxSum = nums[0];
int maxSum = nums[0];
for (int i = 1; i < nums.length; i++) {
currMaxSum = Math.max(currMaxSum + nums[i], nums[i]);
maxSum = Math.max(maxSum, currMaxSum);
}
return maxSum;
}
}
```
Python 3:
```python
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n = len(nums)
max_sum_ending_curr_index = max_sum = nums[0]
for i in range(1, n):
max_sum_ending_curr_index = max(max_sum_ending_curr_index + nums[i], nums[i])
max_sum = max(max_sum_ending_curr_index, max_sum)
return max_sum
```
## 解法四 - 数学分析
### 思路
我们来通过数学分析来看一下这个题目。
我们定义函数 S(i) ,它的功能是计算以 0(包括 0)开始加到 i(包括 i)的值。
那么 S(j) - S(i - 1) 就等于 从 i 开始(包括 i)加到 j(包括 j)的值。
我们进一步分析,实际上我们只需要遍历一次计算出所有的 S(i), 其中 i 等于 0,1,2....,n-1。
然后我们再减去之前的 S(k),其中 k 等于 0,1,i - 1,中的最小值即可。 因此我们需要
用一个变量来维护这个最小值,还需要一个变量维护最大值。
这种算法的时间复杂度 O(N), 空间复杂度为 O(1)。
其实很多题目,都有这样的思想, 比如之前的《每日一题 - 电梯问题》。
### 代码
JavaScript:
```js
function LSS(list) {
const len = list.length;
let max = list[0];
let min = 0;
let sum = 0;
for (let i = 0; i < len; i++) {
sum += list[i];
if (sum - min > max) max = sum - min;
if (sum < min) {
min = sum;
}
}
return max;
}
```
Java:
```java
class MaxSumSubarray {
public int maxSubArray3(int[] nums) {
int maxSum = nums[0];
int sum = 0;
int minSum = 0;
for (int num : nums) {
// prefix Sum
sum += num;
// update maxSum
maxSum = Math.max(maxSum, sum - minSum);
// update minSum
minSum = Math.min(minSum, sum);
}
return maxSum;
}
}
```
Python 3:
```python
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
n = len(nums)
maxSum = nums[0]
minSum = sum = 0
for i in range(n):
sum += nums[i]
maxSum = max(maxSum, sum - minSum)
minSum = min(minSum, sum)
return maxSum
```
## 总结
我们使用四种方法解决了`《最大子序列和问题》`,
并详细分析了各个解法的思路以及复杂度,相信下次你碰到相同或者类似的问题
的时候也能够发散思维,做到`一题多解,多题一解`
实际上,我们只是求出了最大的和,如果题目进一步要求出最大子序列和的子序列呢?
如果要题目允许不连续呢? 我们又该如何思考和变通?如何将数组改成二维,求解最大矩阵和怎么计算?
这些问题留给读者自己来思考。
# 一招吃遍力扣四道题,妈妈再也不用担心我被套路啦~
我花了几天时间,从力扣中精选了四道相同思想的题目,来帮助大家解套,如果觉得文章对你有用,记得点赞分享,让我看到你的认可,有动力继续做下去。
这就是接下来要给大家讲的四个题,其中 1081 和 316 题只是换了说法而已。
- [316. 去除重复字母](https://leetcode-cn.com/problems/remove-duplicate-letters/)(困难)
- [321. 拼接最大数](https://leetcode-cn.com/problems/create-maximum-number/)(困难)
- [402. 移掉 K 位数字](https://leetcode-cn.com/problems/remove-k-digits/)(中等)
- [1081. 不同字符的最小子序列](https://leetcode-cn.com/problems/smallest-subsequence-of-distinct-characters/)(中等)
## 402. 移掉 K 位数字(中等)
我们从一个简单的问题入手,识别一下这种题的基本形式和套路,为之后的三道题打基础。
### 题目描述
```
给定一个以字符串表示的非负整数  num,移除这个数中的 k 位数字,使得剩下的数字最小。
注意:
num 的长度小于 10002 且  ≥ k。
num 不会包含任何前导零。
示例 1 :
输入: num = "1432219", k = 3
输出: "1219"
解释: 移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219。
示例 2 :
输入: num = "10200", k = 1
输出: "200"
解释: 移掉首位的 1 剩下的数字为 200. 注意输出不能有任何前导零。
示例 3 :
输入: num = "10", k = 2
输出: "0"
解释: 从原数字移除所有的数字,剩余为空就是 0。
```
### 前置知识
- 数学
### 思路
这道题让我们从一个字符串数字中删除 k 个数字,使得剩下的数最小。也就说,我们要保持原来的数字的相对位置不变。
以题目中的 `num = 1432219, k = 3` 为例,我们需要返回一个长度为 4 的字符串,问题在于: 我们怎么才能求出这四个位置依次是什么呢?
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfr0o3bz8aj30ya0he75v.jpg)
(图 1)
暴力法的话,我们需要枚举`C_n^(n - k)` 种序列(其中 n 为数字长度),并逐个比较最大。这个时间复杂度是指数级别的,必须进行优化。
一个思路是:
- 从左到右遍历
- 对于每一个遍历到的元素,我们决定是**丢弃**还是**保留**
问题的关键是:我们怎么知道,一个元素是应该保留还是丢弃呢?
这里有一个前置知识:**对于两个数 123a456 和 123b456,如果 a > b, 那么数字 123a456 大于 数字 123b456,否则数字 123a456 小于等于数字 123b456**。也就说,两个**相同位数**的数字大小关系取决于第一个不同的数的大小。
因此我们的思路就是:
- 从左到右遍历
- 对于遍历到的元素,我们选择保留。
- 但是我们可以选择性丢弃前面相邻的元素。
- 丢弃与否的依据如上面的前置知识中阐述中的方法。
以题目中的 `num = 1432219, k = 3` 为例的图解过程如下:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfr3me4mltj30u00xjgp5.jpg)
(图 2)
由于没有左侧相邻元素,因此**没办法丢弃**
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfr3p4idahj30sk116dj7.jpg)
(图 3)
由于 4 比左侧相邻的 1 大。如果选择丢弃左侧的 1,那么会使得剩下的数字更大(开头的数从 1 变成了 4)。因此我们仍然选择**不丢弃**
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfr3rtp1b1j30tk12etcr.jpg)
(图 4)
由于 3 比左侧相邻的 4 小。 如果选择丢弃左侧的 4,那么会使得剩下的数字更小(开头的数从 4 变成了 3)。因此我们选择**丢弃**
。。。
后面的思路类似,我就不继续分析啦。
然而需要注意的是,如果给定的数字是一个单调递增的数字,那么我们的算法会永远**选择不丢弃**。这个题目中要求的,我们要永远确保**丢弃** k 个矛盾。
一个简单的思路就是:
- 每次丢弃一次,k 减去 1。当 k 减到 0 ,我们可以提前终止遍历。
- 而当遍历完成,如果 k 仍然大于 0。不妨假设最终还剩下 x 个需要丢弃,那么我们需要选择删除末尾 x 个元素。
上面的思路可行,但是稍显复杂。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfk7m9z3elj30zk0i01kx.jpg)
(图 5)
我们需要把思路逆转过来。刚才我的关注点一直是**丢弃**,题目要求我们丢弃 k 个。反过来说,不就是让我们保留 $n - k$ 个元素么?其中 n 为数字长度。 那么我们只需要按照上面的方法遍历完成之后,再截取前**n - k**个元素即可。
按照上面的思路,我们来选择数据结构。由于我们需要**保留****丢弃相邻**的元素,因此使用栈这种在一端进行添加和删除的数据结构是再合适不过了,我们来看下代码实现。
### 代码(Python)
```py
class Solution(object):
def removeKdigits(self, num, k):
stack = []
remain = len(num) - k
for digit in num:
while k and stack and stack[-1] > digit:
stack.pop()
k -= 1
stack.append(digit)
return ''.join(stack[:remain]).lstrip('0') or '0'
```
**_复杂度分析_**
- 时间复杂度:虽然内层还有一个 while 循环,但是由于每个数字最多仅会入栈出栈一次,因此时间复杂度仍然为 $O(N)$,其中 $N$ 为数字长度。
- 空间复杂度:我们使用了额外的栈来存储数字,因此空间复杂度为 $O(N)$,其中 $N$ 为数字长度。
> 提示: 如果题目改成求删除 k 个字符之后的最大数,我们只需要将 stack[-1] > digit 中的大于号改成小于号即可。
## 316. 去除重复字母(困难)
### 题目描述
```
给你一个仅包含小写字母的字符串,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证返回结果的字典序最小(要求不能打乱其他字符的相对位置)。
示例 1:
输入: "bcabc"
输出: "abc"
示例 2:
输入: "cbacdcbc"
输出: "acdb"
```
## 前置知识
- 字典序
- 数学
### 思路
与上面题目不同,这道题没有一个全局的删除次数 k。而是对于每一个在字符串 s 中出现的字母 c 都有一个 k 值。这个 k 是 c 出现次数 - 1。
沿用上面的知识的话,我们首先要做的就是计算每一个字符的 k,可以用一个字典来描述这种关系,其中 key 为 字符 c,value 为其出现的次数。
具体算法:
- 建立一个字典。其中 key 为 字符 c,value 为其出现的剩余次数。
- 从左往右遍历字符串,每次遍历到一个字符,其剩余出现次数 - 1.
- 对于每一个字符,如果其对应的剩余出现次数大于 1,我们**可以**选择丢弃(也可以选择不丢弃),否则不可以丢弃。
- 是否丢弃的标准和上面题目类似。如果栈中相邻的元素字典序更大,那么我们选择丢弃相邻的栈中的元素。
还记得上面题目的边界条件么?如果栈中剩下的元素大于 $n - k$,我们选择截取前 $n - k$ 个数字。然而本题中的 k 是分散在各个字符中的,因此这种思路不可行的。
不过不必担心。由于题目是要求只出现一次。我们可以在遍历的时候简单地判断其是否在栈上即可。
代码:
```py
class Solution:
def removeDuplicateLetters(self, s) -> int:
stack = []
remain_counter = collections.Counter(s)
for c in s:
if c not in stack:
while stack and c < stack[-1] and remain_counter[stack[-1]] > 0:
stack.pop()
stack.append(c)
remain_counter[c] -= 1
return ''.join(stack)
```
**_复杂度分析_**
- 时间复杂度:由于判断当前字符是否在栈上存在需要 $O(N)$ 的时间,因此总的时间复杂度就是 $O(N ^ 2)$,其中 $N$ 为字符串长度。
- 空间复杂度:我们使用了额外的栈来存储数字,因此空间复杂度为 $O(N)$,其中 $N$ 为字符串长度。
查询给定字符是否在一个序列中存在的方法。根本上来说,有两种可能:
- 有序序列: 可以二分法,时间复杂度大致是 $O(N)$。
- 无序序列: 可以使用遍历的方式,最坏的情况下时间复杂度为 $O(N)$。我们也可以使用空间换时间的方式,使用 $N$的空间 换取 $O(1)$的时间复杂度。
由于本题中的 stack 并不是有序的,因此我们的优化点考虑空间换时间。而由于每种字符仅可以出现一次,这里使用 hashset 即可。
### 代码(Python)
```py
class Solution:
def removeDuplicateLetters(self, s) -> int:
stack = []
seen = set()
remain_counter = collections.Counter(s)
for c in s:
if c not in seen:
while stack and c < stack[-1] and remain_counter[stack[-1]] > 0:
seen.discard(stack.pop())
seen.add(c)
stack.append(c)
remain_counter[c] -= 1
return ''.join(stack)
```
**_复杂度分析_**
- 时间复杂度:$O(N)$,其中 $N$ 为字符串长度。
- 空间复杂度:我们使用了额外的栈和 hashset,因此空间复杂度为 $O(N)$,其中 $N$ 为字符串长度。
> LeetCode 《1081. 不同字符的最小子序列》 和本题一样,不再赘述。
## 321. 拼接最大数(困难)
### 题目描述
```
给定长度分别为  m  和  n  的两个数组,其元素由  0-9  构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n)  个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。
求满足该条件的最大数。结果返回一个表示该最大数的长度为  k  的数组。
说明: 请尽可能地优化你算法的时间和空间复杂度。
示例  1:
输入:
nums1 = [3, 4, 6, 5]
nums2 = [9, 1, 2, 5, 8, 3]
k = 5
输出:
[9, 8, 6, 5, 3]
示例 2:
输入:
nums1 = [6, 7]
nums2 = [6, 0, 4]
k = 5
输出:
[6, 7, 6, 0, 4]
示例 3:
输入:
nums1 = [3, 9]
nums2 = [8, 9]
k = 3
输出:
[9, 8, 9]
```
### 前置知识
- 分治
- 数学
### 思路
和第一道题类似,只不不过这一次是两个**数组**,而不是一个,并且是求最大数。
最大最小是无关紧要的,关键在于是两个数组,并且要求从两个数组选取的元素个数加起来一共是 k。
然而在一个数组中取 k 个数字,并保持其最小(或者最大),我们已经会了。但是如果问题扩展到两个,会有什么变化呢?
实际上,问题本质并没有发生变化。 假设我们从 nums1 中取了 k1 个,从 num2 中取了 k2 个,其中 k1 + k2 = k。而 k1 和 k2 这 两个子问题我们是会解决的。由于这两个子问题是相互独立的,因此我们只需要分别求解,然后将结果合并即可。
假如 k1 和 k2 个数字,已经取出来了。那么剩下要做的就是将这个长度分别为 k1 和 k2 的数字,合并成一个长度为 k 的数组合并成一个最大的数组。
以题目的 `nums1 = [3, 4, 6, 5] nums2 = [9, 1, 2, 5, 8, 3] k = 5` 为例。 假如我们从 num1 中取出 1 个数字,那么就要从 nums2 中取出 4 个数字。
运用第一题的方法,我们计算出应该取 nums1 的 [6],并取 nums2 的 [9,5,8,3]。 如何将 [6] 和 [9,5,8,3],使得数字尽可能大,并且保持相对位置不变呢?
实际上这个过程有点类似`归并排序`中的**治**,而上面我们分别计算 num1 和 num2 的最大数的过程类似`归并排序`中的**分**
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfruuvyrn5j31mk0i8414.jpg)
(图 6)
代码:
> 我们将从 num1 中挑选的 k1 个数组成的数组称之为 A,将从 num2 中挑选的 k2 个数组成的数组称之为 B,
```py
def merge(A, B):
ans = []
while A or B:
bigger = A if A > B else B
ans.append(bigger[0])
bigger.pop(0)
return ans
```
这里需要说明一下。 在很多编程语言中:**如果 A 和 B 是两个数组,当前仅当 A 的首个元素字典序大于 B 的首个元素,A > B 返回 true,否则返回 false**
比如:
```
A = [1,2]
B = [2]
A < B # True
A = [1,2]
B = [1,2,3]
A < B # False
```
以合并 [6] 和 [9,5,8,3] 为例,图解过程如下:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfruxjfwlhj31cu0u07c0.jpg)
(图 7)
具体算法:
- 从 nums1 中 取 $min(i, len(nums1))$ 个数形成新的数组 A(取的逻辑同第一题),其中 i 等于 0,1,2, ... k。
- 从 nums2 中 对应取 $min(j, len(nums2))$ 个数形成新的数组 B(取的逻辑同第一题),其中 j 等于 k - i。
- 将 A 和 B 按照上面的 merge 方法合并
- 上面我们暴力了 k 种组合情况,我们只需要将 k 种情况取出最大值即可。
### 代码(Python)
```py
class Solution:
def maxNumber(self, nums1, nums2, k):
def pick_max(nums, k):
stack = []
drop = len(nums) - k
for num in nums:
while drop and stack and stack[-1] < num:
stack.pop()
drop -= 1
stack.append(num)
return stack[:k]
def merge(A, B):
ans = []
while A or B:
bigger = A if A > B else B
ans.append(bigger[0])
bigger.pop(0)
return ans
return max(merge(pick_max(nums1, i), pick_max(nums2, k-i)) for i in range(k+1) if i <= len(nums1) and k-i <= len(nums2))
```
**_复杂度分析_**
- 时间复杂度:pick_max 的时间复杂度为 $O(M + N)$ ,其中 $M$ 为 nums1 的长度,$N$ 为 nums2 的长度。 merge 的时间复杂度为 $O(k)$,再加上外层遍历所有的 k 中可能性。因此总的时间复杂度为 $O(k^2 * (M + N))$。
- 空间复杂度:我们使用了额外的 stack 和 ans 数组,因此空间复杂度为 $O(max(M, N, k))$,其中 $M$ 为 nums1 的长度,$N$ 为 nums2 的长度。
## 总结
这四道题都是删除或者保留若干个字符,使得剩下的数字最小(或最大)或者字典序最小(或最大)。而解决问题的前提是要有一定**数学前提**。而基于这个数学前提,我们贪心地删除栈中相邻的字符。如果你会了这个套路,那么这四个题目应该都可以轻松解决。
`316. 去除重复字母(困难)`,我们使用 hashmap 代替了数组的遍历查找,属于典型的空间换时间方式,可以认识到数据结构的灵活使用是多么的重要。背后的思路是怎么样的?为什么想到空间换时间的方式,我在文中也进行了详细的说明,这都是值得大家思考的问题。然而实际上,这些题目中使用的栈也都是空间换时间的思想。大家下次碰到**需要空间换取时间**的场景,是否能够想到本文给大家介绍的**栈****哈希表**呢?
`321. 拼接最大数(困难)`则需要我们能够对问题进行分解,这绝对不是一件简单的事情。但是对难以解决的问题进行分解是一种很重要的技能,希望大家能够通过这道题加深这种**分治**思想的理解。 大家可以结合我之前写过的几个题解练习一下,它们分别是:
- [【简单易懂】归并排序(Python)](https://leetcode-cn.com/problems/shu-zu-zhong-de-ni-xu-dui-lcof/solution/jian-dan-yi-dong-gui-bing-pai-xu-python-by-azl3979/)
- [一文看懂《最大子序列和问题》](https://lucifer.ren/blog/2019/12/11/LSS/)
更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 30K star 啦。
大家也可以关注我的公众号《力扣加加》获取更多更新鲜的 LeetCode 题解
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg)
此差异已折叠。
此差异已折叠。
# 字节跳动的算法面试题是什么难度?
由于 lucifer 我是一个小前端, 最近也在准备写一个《前端如何搞定算法面试》的专栏,因此最近没少看各大公司的面试题。都说字节跳动算法题比较难,我就先拿 ta 下手,做了几套 。这次我们就拿一套 `2018 年的前端校招(第四批)`来看下字节的算法笔试题的难度几何。地址:https://www.nowcoder.com/test/8536639/summary
> 实际上,这套字节的前端岗位笔试题和后端以及算法岗位的笔试题也只有一道题目(红包的设计题被换成了另外一个设计题)不一样而已,因此也不需要担心你不是前端,题目类型和难度和你的岗位不匹配。
这套题一共四道题, 两道问答题, 两道编程题。
其中一道问答题是 LeetCode 426 的原题,只不过题型变成了找茬(改错)。可惜的是 LeetCode 的 426 题是一个会员题目,没有会员的就看不来了。不过,剑指 Offer 正好也有这个题,并且力扣将剑指 Offer 全部的题目都 OJ 化了。 这道题大家可以去 https://leetcode-cn.com/problems/er-cha-sou-suo-shu-yu-shuang-xiang-lian-biao-lcof 提交答案。简单说一下这个题目的思路,我们只需要中序遍历即可得到一个有序的数列,同时在中序遍历过程中将 pre 和 cur 节点通过指针串起来即可。
另一个问答是红包题目,这里不多说了。我们重点看一下剩下两个算法编程题。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gigxwqs84rj312d0u0the.jpg)
> 两个问答题由于不能在线判题,我没有做,只做了剩下两个编程题。
## 球队比赛
第一个编程题是一个球队比赛的题目。
### 题目描述
有三只球队,每只球队编号分别为球队 1,球队 2,球队 3,这三只球队一共需要进行 n 场比赛。现在已经踢完了 k 场比赛,每场比赛不能打平,踢赢一场比赛得一分,输了不得分不减分。已知球队 1 和球队 2 的比分相差 d1 分,球队 2 和球队 3 的比分相差 d2 分,每场比赛可以任意选择两只队伍进行。求如果打完最后的 (n-k) 场比赛,有没有可能三只球队的分数打平。
### 思路
假设球队 1,球队 2,球队 3 此时的胜利次数分别为 a,b,c,球队 1,球队 2,球队 3 总的胜利次数分别为 n1,n2,n3。
我一开始的想法是只要保证 n1,n2,n3 相等且都小于等于 n / 3 即可。如果题目给了 n1,n2,n3 的值就直接:
```
print(n1 == n2 == n3 == n / 3)
```
可是不仅 n1,n2,n3 没给, a,b,c 也没有给。
实际上此时我们的信息仅仅是:
```
① a + b + c = k
② a - b = d1 or b - a = d1
③ b - c = d2 or c - b = d2
```
其中 k 和 d1,d2 是已知的。a ,b,c 是未知的。 也就是说我们需要枚举所有的 a,b,c 可能性,解方程求出合法的 a,b,c,并且 合法的 a,b,c 都小于等于 n / 3 即可。
> 这个 a,b,c 的求解数学方程就是中学数学难度, 三个等式化简一下即可,具体见下方代码区域。
- a 只需要再次赢得 n / 3 - a 次
- b 只需要再次赢得 n / 3 - b 次
- c 只需要再次赢得 n / 3 - c 次
```
n1 = a + n / 3 - a = n / 3
n2 = b + (n / 3 - b) = n / 3
n3 = c + (n / 3 - c) = n / 3
```
### 代码(Python)
> 牛客有点让人不爽, 需要 print 而不是 return
```py
t = int(input())
for i in range(t):
n, k, d1, d2 = map(int, input().split(" "))
if n % 3 != 0:
print('no')
continue
abcs = []
for r1 in [-1, 1]:
for r2 in [-1, 1]:
a = (k + 2 * r1 * d1 + r2 * d2) / 3
b = (k + -1 * r1 * d1 + r2 * d2) / 3
c = (k + -1 * r1 * d1 + -2 * r2 * d2) / 3
a + r1
if 0 <= a <= k and 0 <= b <= k and 0 <= c <= k and a.is_integer() and b.is_integer() and c.is_integer():
abcs.append([a, b, c])
flag = False
for abc in abcs:
if len(abc) > 0 and max(abc) <= n / 3:
flag = True
break
if flag:
print('yes')
else:
print('no')
```
**复杂度分析**
- 时间复杂度:$O(t)$
- 空间复杂度:$O(t)$
### 小结
感觉这个难度也就是力扣中等水平吧,力扣也有一些数学等式转换的题目, 比如 [494.target-sum](https://github.com/azl397985856/leetcode/blob/master/problems/494.target-sum.md "494.target-sum")
## 转换字符串
### 题目描述
有一个仅包含’a’和’b’两种字符的字符串 s,长度为 n,每次操作可以把一个字符做一次转换(把一个’a’设置为’b’,或者把一个’b’置成’a’);但是操作的次数有上限 m,问在有限的操作数范围内,能够得到最大连续的相同字符的子串的长度是多少。
### 思路
看完题我就有种似曾相识的感觉。
> 每次对妹子说出这句话的时候,她们都会觉得好假 ^\_^
不过这次是真的。 ”哦,不!每次都是真的“。 这道题其实就是我之前写的滑动窗口的一道题[【1004. 最大连续 1 的个数 III】滑动窗口(Python3)](https://leetcode-cn.com/problems/max-consecutive-ones-iii/solution/1004-zui-da-lian-xu-1de-ge-shu-iii-hua-dong-chuang/ "【1004. 最大连续 1 的个数 III】滑动窗口(Python3)")的换皮题。 专题地址:https://github.com/azl397985856/leetcode/blob/master/thinkings/slide-window.md
所以说,如果这道题你完全没有思路的话。说明:
- 抽象能力不够。
- 滑动窗口问题理解不到位。
第二个问题可以看我上面贴的地址,仔细读读,并完成课后练习即可解决。
第一个问题就比较困难了, 不过多看我的题解也可以慢慢提升的。比如:
- [《割绳子》](https://leetcode-cn.com/problems/jian-sheng-zi-ii-lcof/ "《割绳子》") 实际上就是 [343. 整数拆分](https://lucifer.ren/blog/2020/05/16/343.integer-break/ "343. 整数拆分") 的换皮题。
- 力扣 230 和 力扣 645 就是换皮题,详情参考[位运算专题](https://lucifer.ren/blog/2020/03/24/bit/ "位运算专题")
- 以及 [你的衣服我扒了 - 《最长公共子序列》](https://lucifer.ren/blog/2020/07/01/LCS/)
- 以及 [穿上衣服我就不认识你了?来聊聊最长上升子序列](https://lucifer.ren/blog/2020/06/20/LIS/)
- 以及 [一招吃遍力扣四道题,妈妈再也不用担心我被套路啦~](https://lucifer.ren/blog/2020/06/13/%E5%88%A0%E9%99%A4%E9%97%AE%E9%A2%98/)
- 等等
回归这道题。其实我们只需要稍微抽象一下, 就是一个纯算法题。 抽象的另外一个好处则是将很多不同的题目返璞归真,从而可以在茫茫题海中逃脱。这也是我开启[《我是你的妈妈呀》](https://lucifer.ren/blog/2020/08/03/mother-01/) 的原因之一。
如果我们把 a 看成是 0 , b 看成是 1。或者将 b 看成 1, a 看成 0。不就抽象成了:
```
给定一个由若干 0 和 1 组成的数组 A,我们最多可以将 m 个值从 0 变成 1 。
返回仅包含 1 的最长(连续)子数组的长度。
```
这就是 力扣 [1004. 最大连续 1 的个数 III](https://leetcode-cn.com/problems/max-consecutive-ones-iii/solution/1004-zui-da-lian-xu-1de-ge-shu-iii-hua-dong-chuang/) 原题。
因此实际上我们要求的是上面两种情况:
1. a 表示 0, b 表示 1
2. a 表示 1, b 表示 0
的较大值。
> lucifer 小提示: 其实我们也可以仅仅考虑一种情况,比如 a 看成是 0 , b 看成是 1。这个时候, 我们操作变成了两种情况,0 变成 1 或者 1 变成 0,同时求解的也变成了最长连续 0 或者 最长连续 1 。 由于这种抽象操作起来更麻烦, 我们不考虑。
问题得到了抽象就好解决了。我们只需要记录下加入窗口的是 0 还是 1:
- 如果是 1,我们什么都不用做
- 如果是 0,我们将 m 减 1
相应地,我们需要记录移除窗口的是 0 还是 1:
- 如果是 1,我们什么都不做
- 如果是 0,说明加进来的时候就是 1,加进来的时候我们 m 减去了 1,这个时候我们再加 1。
> lucifer 小提示: 实际上题目中是求**连续** a 或者 b 的长度。看到连续,大家也应该有滑动窗口的敏感度, 别管行不行, 想到总该有的。
我们拿 A = [1, 1, 0, 1, 0, 1], m = 1 来说。看下算法的具体过程:
> lucifer 小提示: 左侧的数字表示此时窗口大小,黄色格子表示修补的墙,黑色方框表示的是窗口。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gih11ey3hhj30ks05o0sx.jpg)
这里我形象地将 0 看成是洞,1 看成是墙, 我们的目标就是补洞,使得连续的墙最长。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gih12xgf04j30ik054dfx.jpg)
每次碰到一个洞,我们都去不加选择地修补。由于 m 等于 1, 也就是说我们最多补一个洞。因此需要在修补超过一个洞的时候,我们需要调整窗口范围,使得窗口内最多修补一个墙。由于窗口表示的就是连续的墙(已有的或者修补的),因此最终我们返回窗口的最大值即可。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gih1588r5kj30xe0dm770.jpg)
> 由于下面的图窗口内有两个洞,这和”最多补一个洞“冲突, 我们需要收缩窗口使得满足“最多补一个洞”的先决条件。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gih1ac1v5ij30o60ba76r.jpg)
因此最大的窗口就是 max(2, 3, 4, ...) = 4。
> lucifer 小提示: 可以看出我们不加选择地修补了所有的洞,并调整窗口,使得窗口内最多有 m 个修补的洞,因此窗口的最大值就是答案。然而实际上,我们并不需要真的”修补“(0 变成 1),而是仅仅修改 m 的值即可。
我们先来看下抽象之后的**其中一种情况**的代码:
```py
class Solution:
def longestOnes(self, A: List[int], m: int) -> int:
i = 0
for j in range(len(A)):
m -= 1 - A[j]
if m < 0:
m += 1 - A[i]
i += 1
return j - i + 1
```
因此**完整代码**就是:
```py
class Solution:
def longestOnes(self, A: List[int], m: int) -> int:
i = 0
for j in range(len(A)):
m -= 1 - A[j]
if m < 0:
m += 1 - A[i]
i += 1
return j - i + 1
def longestAorB(self, A:List[int], m: int) -> int:
return max(self.longestOnes(map(lambda x: 0 if x == 'a' else 1, A) ,m), self.longestOnes(map(lambda x: 1 if x == 'a' else 0, A),m))
```
这里的两个 map 会生成两个不同的数组。 我只是为了方便大家理解才新建的两个数组, 实际上根本不需要,具体见后面的代码.
### 代码(Python)
```py
i = 0
n, m = map(int, input().split(" "))
s = input()
ans = 0
k = m # 存一下,后面也要用这个初始值
# 修补 b
for j in range(n):
m -= ord(s[j]) - ord('a')
if m < 0:
m += ord(s[i]) - ord('a')
i += 1
ans = j - i + 1
i = 0
# 修补 a
for j in range(n):
k += ord(s[j]) - ord('b')
if k < 0:
k -= ord(s[i]) - ord('b')
i += 1
print(max(ans, j - i + 1))
```
**复杂度分析**
- 时间复杂度:$O(N)$
- 空间复杂度:$O(1)$
### 小结
这道题就是一道换了皮的力扣题,难度中等。如果你能将问题抽象,同时又懂得滑动窗口,那这道题就很容易。我看了题解区的参考答案, 内容比较混乱,不够清晰。这也是我写下这篇文章的原因之一。
## 总结
这一套字节跳动的题目一共四道,一道设计题,三道算法题。
其中三道算法题从难度上来说,基本都是中等难度。从内容来看,基本都是力扣的换皮题。但是如果我不说他们是换皮题, 你们能发现么? 如果你可以的话,说明你的抽象能力已经略有小成了。如果看不出来也没有关系,关注我。 手把手扒皮给你们看,扒多了慢慢就会了。切记,不要盲目做题!如果你做了很多题, 这几道题还是看不出套路,说明你该缓缓,改变下刷题方式了。
更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 36K+ star 啦。
关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg)
# 构造二叉树系列
构造二叉树是一个常见的二叉树考点,相比于直接考察二叉树的遍历,这种题目的难度会更大。截止到目前(2020-02-08) LeetCode 关于构造二叉树一共有三道题目,分别是:
- [105. 从前序与中序遍历序列构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)
- [106. 从中序与后序遍历序列构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/)
- [889. 根据前序和后序遍历构造二叉树](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-postorder-traversal/)
今天就让我们用一个套路一举攻破他们。
## 105. 从前序与中序遍历序列构造二叉树
### 题目描述
```
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
```
### 思路
我们以题目给出的测试用例来讲解:
![](https://pic.leetcode-cn.com/584db66158d2b497b9fdd69b5dc10c3a76db6e2c0f6cff68789cfb79807b0756.jpg)
前序遍历是`根左右`,因此 preorder 第一个元素一定整个树的根。由于题目说明了没有重复元素,因此我们可以通过 val 去 inorder 找到根在 inorder 中的索引 i。
而由于中序遍历是`左根右`,我们容易找到 i 左边的都是左子树,i 右边都是右子树。
我使用红色表示根,蓝色表示左子树,绿色表示右子树。
![](https://pic.leetcode-cn.com/faea3d9a78c1fa623457b28c8d20e09a47bb0911d78ff53f42fab0e463a7755d.jpg)
根据此时的信息,我们能构造的树是这样的:
![](https://pic.leetcode-cn.com/261696c859c562ca31dface08d3020bcd20362ab2205d614473cca02b1635eb0.jpg)
我们 preorder 继续向后移动一位,这个时候我们得到了第二个根节点”9“,实际上就是左子树的根节点。
![](https://pic.leetcode-cn.com/eb8311e01ed86007b23460d6c933b53ad14bec2d63a0dc01f625754368f22376.jpg)
我们 preorder 继续向后移动一位,这个时候我们得到了第二个根节点”20“,实际上就是右子树的根节点。其中右子树由于个数大于 1,我们无法确定,我们继续执行上述逻辑。
![](https://pic.leetcode-cn.com/d90dc9bae9d819da997eb67d445524c8ef39ce2a4a8defb16b5a3b6b2a0fc783.jpg)
根据此时的信息,我们能构造的树是这样的:
![](https://pic.leetcode-cn.com/f8553f668bed9f897f393a24d78e4469c4b5503c4ba8c59e90dca1b19acf4de5.jpg)
我们不断执行上述逻辑即可。简单起见,递归的时候每次我都开辟了新的数组,这个其实是没有必要的,我们可以通过四个变量来记录 inorder 和 preorder 的起始位置即可。
### 代码
代码支持:Python3
Python3 Code:
```python
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
# 实际上inorder 和 postorder一定是同时为空的,因此你无论判断哪个都行
if not preorder:
return None
root = TreeNode(preorder[0])
i = inorder.index(root.val)
root.left = self.buildTree(preorder[1:i + 1], inorder[:i])
root.right = self.buildTree(preorder[i + 1:], inorder[i+1:])
return root
```
**复杂度分析**
- 时间复杂度:由于每次递归我们的 inorder 和 preorder 的总数都会减 1,因此我们要递归 N 次,故时间复杂度为 $O(N)$,其中 N 为节点个数。
- 空间复杂度:我们使用了递归,也就是借助了额外的栈空间来完成, 由于栈的深度为 N,因此总的空间复杂度为 $O(N)$,其中 N 为节点个数。
> 空间复杂度忽略了开辟数组的内存消耗。
## 106. 从中序与后序遍历序列构造二叉树
如果你会了上面的题目,那么这个题目对你来说也不是难事,我们来看下。
### 题目描述
```
根据一棵树的中序遍历与后序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
中序遍历 inorder = [9,3,15,20,7]
后序遍历 postorder = [9,15,7,20,3]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
```
### 思路
我们以题目给出的测试用例来讲解:
![](https://pic.leetcode-cn.com/fb9d700a67d70b5e68461fa1f0438d9c5c676557a776eda4cd1b196c41ce65a1.jpg)
后序遍历是`左右根`,因此 postorder 最后一个元素一定整个树的根。由于题目说明了没有重复元素,因此我们可以通过 val 去 inorder 找到根在 inorder 中的索引 i。
而由于中序遍历是`左根右`,我们容易找到 i 左边的都是左子树,i 右边都是右子树。
我使用红色表示根,蓝色表示左子树,绿色表示右子树。
![](https://pic.leetcode-cn.com/10176eec270c90d8e0bd4640a628e9320b7d5c30f3c62ffdb1fd2800d87c6f7b.jpg)
根据此时的信息,我们能构造的树是这样的:
![](https://pic.leetcode-cn.com/261696c859c562ca31dface08d3020bcd20362ab2205d614473cca02b1635eb0.jpg)
其中右子树由于个数大于 1,我们无法确定,我们继续执行上述逻辑。我们 postorder 继续向前移动一位,这个时候我们得到了第二个根节点”20“,实际上就是右子树的根节点。
![](https://pic.leetcode-cn.com/e6cac2b6a956c09d977c4cfd7883268644b42bdd0531a509d24b4aafebc147c4.jpg)
根据此时的信息,我们能构造的树是这样的:
![](https://pic.leetcode-cn.com/f8553f668bed9f897f393a24d78e4469c4b5503c4ba8c59e90dca1b19acf4de5.jpg)
我们不断执行上述逻辑即可。简单起见,递归的时候每次我都开辟了新的数组,这个其实是没有必要的,我们可以通过四个变量来记录 inorder 和 postorder 的起始位置即可。
### 代码
代码支持:Python3
Python3 Code:
```python
class Solution:
def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
# 实际上inorder 和 postorder一定是同时为空的,因此你无论判断哪个都行
if not inorder:
return None
root = TreeNode(postorder[-1])
i = inorder.index(root.val)
root.left = self.buildTree(inorder[:i], postorder[:i])
root.right = self.buildTree(inorder[i+1:], postorder[i:-1])
return root
```
**复杂度分析**
- 时间复杂度:由于每次递归我们的 inorder 和 postorder 的总数都会减 1,因此我们要递归 N 次,故时间复杂度为 $O(N)$,其中 N 为节点个数。
- 空间复杂度:我们使用了递归,也就是借助了额外的栈空间来完成, 由于栈的深度为 N,因此总的空间复杂度为 $O(N)$,其中 N 为节点个数。
> 空间复杂度忽略了开辟数组的内存消耗。
## 889. 根据前序和后序遍历构造二叉树
### 题目描述
```
返回与给定的前序和后序遍历匹配的任何二叉树。
 pre 和 post 遍历中的值是不同的正整数。
 
示例:
输入:pre = [1,2,4,5,3,6,7], post = [4,5,2,6,7,3,1]
输出:[1,2,3,4,5,6,7]
 
提示:
1 <= pre.length == post.length <= 30
pre[] 和 post[] 都是 1, 2, ..., pre.length 的排列
每个输入保证至少有一个答案。如果有多个答案,可以返回其中一个。
```
### 思路
我们以题目给出的测试用例来讲解:
![](https://pic.leetcode-cn.com/584db66158d2b497b9fdd69b5dc10c3a76db6e2c0f6cff68789cfb79807b0756.jpg)
前序遍历是`根左右`,因此 preorder 第一个元素一定整个树的根,preorder 第二个元素(如果存在的话)一定是左子树。由于题目说明了没有重复元素,因此我们可以通过 val 去 postorder 找到 pre[1]在 postorder 中的索引 i。
而由于后序遍历是`左右根`,因此我们容易得出。 postorder 中的 0 到 i(包含)是左子树,preorder 的 1 到 i+1(包含)也是左子树。
其他部分可以参考上面两题。
### 代码
代码支持:Python3
Python3 Code:
```python
class Solution:
def constructFromPrePost(self, pre: List[int], post: List[int]) -> TreeNode:
# 实际上pre 和 post一定是同时为空的,因此你无论判断哪个都行
if not pre:
return None
node = TreeNode(pre[0])
if len(pre) == 1:
return node
i = post.index(pre[1])
node.left = self.constructFromPrePost(pre[1:i + 2], post[:i + 1])
node.right = self.constructFromPrePost(pre[i + 2:], post[i + 1:-1])
return node
```
**复杂度分析**
- 时间复杂度:由于每次递归我们的 postorder 和 preorder 的总数都会减 1,因此我们要递归 N 次,故时间复杂度为 $O(N)$,其中 N 为节点个数。
- 空间复杂度:我们使用了递归,也就是借助了额外的栈空间来完成, 由于栈的深度为 N,因此总的空间复杂度为 $O(N)$,其中 N 为节点个数。
> 空间复杂度忽略了开辟数组的内存消耗。
## 总结
如果你仔细对比一下的话,会发现我们的思路和代码几乎一模一样。注意到每次递归我们的两个数组个数都会减去 1,因此我们递归终止条件不难写出,并且递归问题规模如何缩小也很容易,那就是数组总长度减去 1。
我们拿最后一个题目来说:
```python
node.left = self.constructFromPrePost(pre[1:i + 2], post[:i + 1])
node.right = self.constructFromPrePost(pre[i + 2:], post[i + 1:-1])
```
我们发现 pre 被拆分为两份,pre[1:i + 2]和 pre[i + 2:]。很明显总数少了 1,那就是 pre 的第一个元素。 也就是说如果你写出一个,其他一个不用思考也能写出来。
而对于 post 也一样,post[:i + 1] 和 post[i + 1:-1],很明显总数少了 1,那就是 post 最后一个元素。
这个解题模板足够简洁,并且逻辑清晰,大家可以用我的模板试试~
## 关注我
更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 30K star 啦。
大家也可以关注我的公众号《力扣加加》获取更多更新鲜的 LeetCode 题解
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg)
# 《我是你的妈妈呀》 - 第一期
记得我初中的时候,学校发的一个小册子的名字就是母题啥的。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1ghbhlyhaadj308c08c3yv.jpg)
大概意思是市面上的题(尤其是中考题)都是这些母题生的,都是它们的儿子。
熟悉我的朋友应该知道,我有一个风格:”喜欢用通俗易懂的语言以及图片,还原解题过程“。包括我是如何抽象的,如何与其他题目建立联系的等。比如:
- [一招吃遍力扣四道题,妈妈再也不用担心我被套路啦~](https://leetcode-cn.com/problems/smallest-subsequence-of-distinct-characters/solution/yi-zhao-chi-bian-li-kou-si-dao-ti-ma-ma-zai-ye-b-6/)
- [超级详细记忆化递归,图解,带你一次攻克三道 Hard 套路题(44. 通配符匹配)](https://leetcode-cn.com/problems/wildcard-matching/solution/chao-ji-xiang-xi-ji-yi-hua-di-gui-tu-jie-dai-ni-yi/)
- [穿上衣服我就不认识你了?来聊聊最长上升子序列](https://leetcode-cn.com/problems/minimum-number-of-arrows-to-burst-balloons/solution/chuan-shang-yi-fu-wo-jiu-bu-ren-shi-ni-liao-lai-3/)
- [扒一扒这种题的外套(343. 整数拆分)](https://leetcode-cn.com/problems/integer-break/solution/ba-yi-ba-zhe-chong-ti-de-wai-tao-343-zheng-shu-cha/)
如果把这个思考过程称之为自顶向下的话,那么实际上能写出来取决于你:
- 是否有良好的抽象能力
- 是否有足够的基础知识
- 是否能与学过的基础知识建立联系
如果反着呢? 我先把所有抽象之后的纯粹的东西掌握,也就是母题。那么遇到新的题,我就往上套呗?这就是我在《LeetCode 题解仓库》中所说的**只有熟练掌握基础的数据结构与算法,才能对复杂问题迎刃有余。** 这种思路就是**自底向上**。(有点像动态规划?) 市面上的题那么多,但是题目类型就是那几种。甚至出题人出题的时候都是根据以前的题目变个条件,变个说法从而搞出一个“新”的题。
这个专题的目标就是从反的方向来,我们先学习和记忆底层的被抽象过的经典的题目。遇到新的题目,就往这些母题上套即可。
那让我们来自底向上看下第一期的这八道母题吧~
## 母题 1
### 题目描述
给你两个有序的非空数组 nums1 和 nums2,让你从每个数组中分别挑一个,使得二者差的绝对值最小。
### 思路
- 初始化 ans 为无限大
- 使用两个指针,一个指针指向数组 1,一个指针指向数组 2
- 比较两个指针指向的数字的大小,并更新较小的那个的指针,使其向后移动一位。更新的过程顺便计算 ans
- 最后返回 ans
### 代码
```py
def f(nums1, nums2):
i = j = 0
ans = float('inf')
while i < len(nums1) and j < len(nums2):
ans = min(ans, abs(nums1[i] - nums2[j]))
if nums1[i] < nums2[j]:
i += 1
else:
j += 1
return ans
```
**复杂度分析**
- 时间复杂度:$O(N)$
- 空间复杂度:$O(1)$
## 母题 2
### 题目描述
给你两个非空数组 nums1 和 nums2,让你从每个数组中分别挑一个,使得二者差的绝对值最小。
### 思路
数组没有说明是有序的,可以选择暴力。两两计算绝对值,返回最小的即可。
代码:
```py
def f(nums1, nums2):
ans = float('inf')
for num1 in nums1:
for num2 in nums2:
ans = min(ans, abs(num1 - num2))
return ans
```
**复杂度分析**
- 时间复杂度:$O(N ^ 2)$
- 空间复杂度:$O(1)$
由于暴力的时间复杂度是 $O(N^2)$,因此其实也可以先排序将问题转换为母题 1,然后用母题 1 的解法求解。
**复杂度分析**
- 时间复杂度:$O(NlogN)$
- 空间复杂度:$O(1)$
## 母题 3
### 题目描述
给你 k 个有序的非空数组,让你从每个数组中分别挑一个,使得二者差的绝对值最小。
### 思路
继续使用母题 1 的思路,使用 k 个 指针即可。
**复杂度分析**
- 时间复杂度:$O(klogM)$,其中 M 为 k 个非空数组的长度的最小值。
- 空间复杂度:$O(1)$
我们也可以使用堆来处理,代码更简单,逻辑更清晰。这里我们使用小顶堆,作用就是选出最小值。
### 代码
```py
def f(matrix):
ans = float('inf')
max_value = max(nums[0] for nums in matrix)
heap = [(nums[0], i, 0) for i, nums in enumerate(nums)]
heapq.heapify(heap)
while True:
min_value, row, idx = heapq.heappop(heap)
if max_value - min_value < ans:
ans = max_value - min_value
if idx == len(matrix[row]) - 1:
break
max_value = max(max_value, matrix[row][idx + 1])
heapq.heappush(heap, (matrix[row][idx + 1], row, idx + 1))
return ans
```
**复杂度分析**
建堆的时间和空间复杂度为 $O(k)$。
while 循环会执行 M 次 ,其中 M 为 k 个非空数组的长度的最小值。heappop 和 heappush 的时间复杂度都是 logk。因此 while 循环总的时间复杂度为 $O(Mlogk)$。
- 时间复杂度:$O(max(Mlogk, k))$,其中 M 为 k 个非空数组的长度的最小值。
- 空间复杂度:$O(k)$
## 母题 4
### 题目描述
给你 k 个非空数组,让你从每个数组中分别挑一个,使得二者差的绝对值最小。
### 思路
先排序,然后转换为母题 3
## 母题 5
### 题目描述
给你两个有序的非空数组 nums1 和 nums2,让你将两个数组合并,使得新的数组有序。
LeetCode 地址: https://leetcode-cn.com/problems/merge-sorted-array/
### 思路
和母题 1 类似。
### 代码
```py
def f(nums1, nums2):
i = j = 0
ans = []
while i < len(nums1) and j < len(nums2):
if nums1[i] < nums2[j]:
ans.append(nums1[i])
i += 1
else:
ans.append(nums2[j])
j += 1
if nums1:
ans += nums2[j:]
else:
ans += nums1[i:]
return ans
```
**复杂度分析**
- 时间复杂度:$O(N)$
- 空间复杂度:$O(1)$
## 母题 6
### 题目描述
给你 k 个有序的非空数组 nums1 和 nums2,让你将 k 个数组合并,使得新的数组有序。
### 思路
和母题 5 类似。 只不过不是两个,而是多个。我们继续套用堆的思路。
### 代码
```py
import heapq
def f(matrix):
ans = []
heap = []
for row in matrix:
heap += row
heapq.heapify(heap)
while heap:
cur = heapq.heappop(heap)
ans.append(cur)
return ans
```
**复杂度分析**
建堆的时间和空间复杂度为 $O(N)$。
heappop 的时间复杂度为 $O(logN)$。
- 时间复杂度:$O(NlogN)$,其中 N 是矩阵中的数字总数。
- 空间复杂度:$O(N)$,其中 N 是矩阵中的数字总数。
## 母题 7
### 题目描述
给你两个有序的链表 root1 和 root2,让你将两个链表合并,使得新的链表有序。
LeetCode 地址:https://leetcode-cn.com/problems/merge-two-sorted-lists/
### 思路
和母题 5 类似。 不同的地方在于数据结构从数组变成了链表,我们只需要注意链表的操作即可。
这里我使用了迭代和递归两种方式。
> 大家可以把母题 5 使用递归写一下。
### 代码
```py
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
if not l1: return l2
if not l2: return l1
if l1.val < l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
return l1
else:
l2.next = self.mergeTwoLists(l1, l2.next)
return l2
```
**复杂度分析**
- 时间复杂度:$O(N)$,其中 N 为两个链表中较短的那个的长度。
- 空间复杂度:$O(N)$,其中 N 为两个链表中较短的那个的长度。
```py
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
if not l1: return l2
if not l2: return l1
ans = cur = ListNode(0)
while l1 and l2:
if l1.val < l2.val:
cur.next = l1
cur = cur.next
l1 = l1.next
else:
cur.next = l2
cur = cur.next
l2 = l2.next
if l1:
cur.next = l1
else:
cur.next = l2
return ans.next
```
**复杂度分析**
- 时间复杂度:$O(N)$,其中 N 为两个链表中较短的那个的长度。
- 空间复杂度:$O(1)$
## 母题 8
### 题目描述
给你 k 个有序的链表,让你将 k 个链表合并,使得新的链表有序。
LeetCode 地址:https://leetcode-cn.com/problems/merge-k-sorted-lists/
### 思路
和母题 7 类似,我们使用递归可以轻松解决。其实本质上就是
### 代码
```py
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
if not l1: return l2
if not l2: return l1
if l1.val < l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
return l1
else:
l2.next = self.mergeTwoLists(l1, l2.next)
return l2
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
if not lists: return None
if len(lists) == 1: return lists[0]
return self.mergeTwoLists(lists[0], self.mergeKLists(lists[1:]))
```
**复杂度分析**
mergeKLists 执行了 k 次,每次都执行一次 mergeTwoLists,mergeTwoLists 的时间复杂度前面已经分析过了,为 $O(N)$,其中 N 为两个链表中较短的那个的长度。
- 时间复杂度:$O(k * N)$,其中 N 为两个链表中较短的那个的长度
- 空间复杂度:$O(max(k, N))$
```py
# Definition for singly-linked list.
class ListNode:
def __init__(self, x):
self.val = x
self.next = None
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
if not l1: return l2
if not l2: return l1
if l1.val < l2.val:
l1.next = self.mergeTwoLists(l1.next, l2)
return l1
else:
l2.next = self.mergeTwoLists(l1, l2.next)
return l2
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
if not lists: return None
if len(lists) == 1: return lists[0]
return self.mergeTwoLists(self.mergeKLists(lists[:len(lists) // 2]), self.mergeKLists(lists[len(lists) // 2:]))
```
**复杂度分析**
mergeKLists 执行了 logk 次,每次都执行一次 mergeTwoLists,mergeTwoLists 的时间复杂度前面已经分析过了,为 $O(N)$,其中 N 为两个链表中较短的那个的长度。
- 时间复杂度:$O(Nlogk)$,其中 N 为两个链表中较短的那个的长度
- 空间复杂度:$O(max(logk, N))$,其中 N 为两个链表中较短的那个的长度
## 全家福
最后送大家一张全家福:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1ghbq8s05y0j31620u0gs0.jpg)
## 子题
实际子题数量有很多,这里提供几个供大家练习。一定要练习,不能眼高手低。多看我的题解,多练习,多总结,你也可以的。
- [面试题 17.14. 最小 K 个数](https://leetcode-cn.com/problems/smallest-k-lcci/)
- [1200. 最小绝对差](https://leetcode-cn.com/problems/minimum-absolute-difference/)
- [632. 最小区间](https://leetcode-cn.com/problems/smallest-range-covering-elements-from-k-lists/)
- 两数和,三数和,四数和。。。 k 数和
## 总结
母题就是**抽象之后的纯粹的东西**。如果你掌握了母题,即使没有掌握抽象的能力,依然有可能套出来。但是随着题目做的变多,“抽象能力”也会越来越强。因为你知道这些题背后是怎么产生的。
本期给大家介绍了八道母题, 大家可以在之后的刷题过程中尝试使用母题来套模板。之后会给大家带来更多的母题。
更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 35K star 啦。
关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gfcuzagjalj30p00dwabs.jpg)
《我的日程安排表》截止目前(2020-02-03)在 LeetCode 上一共有三道题,其中两个中等难度,一个困难难度,分别是:
- [729. 我的日程安排表 I](https://leetcode-cn.com/problems/my-calendar-i)
- [731. 我的日程安排表 II](https://leetcode-cn.com/problems/my-calendar-ii)
- [732. 我的日程安排表 III](https://leetcode-cn.com/problems/my-calendar-iii)
另外 LeetCode 上有一个类似的系列《会议室》,截止目前(2020-02-03)有两道题目。其中一个简单一个中等,分别是:
- [252. 会议室](https://leetcode-cn.com/problems/meeting-rooms/)
- [253. 会议室 II](https://leetcode-cn.com/problems/meeting-rooms-ii/)
今天我们就来攻克它们。
# 729. 我的日程安排表 I
## 题目地址
https://leetcode-cn.com/problems/my-calendar-i
## 题目描述
实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内没有其他安排,则可以存储这个新的日程安排。
MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数  x 的范围为,  start <= x < end。
当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生重复预订。
每次调用 MyCalendar.book 方法时,如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true。否则,返回 false  并且不要将该日程安排添加到日历中。
请按照以下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)
示例 1:
MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(15, 25); // returns false
MyCalendar.book(20, 30); // returns true
解释:
第一个日程安排可以添加到日历中. 第二个日程安排不能添加到日历中,因为时间 15 已经被第一个日程安排预定了。
第三个日程安排可以添加到日历中,因为第一个日程安排并不包含时间 20 。
说明:
每个测试用例,调用  MyCalendar.book  函数最多不超过  100 次。
调用函数  MyCalendar.book(start, end)时, start 和  end 的取值范围为  [0, 10^9]。
## 暴力法
### 思路
首先我们考虑暴力法。每插入一个元素我们都判断其是否和已有的`所有`课程重叠。
我们定一个函数`intersected(calendar, calendars)`,其中 calendar 是即将要插入的课程,calendars 是已经插入的课程。 只要 calendar 和 calendars 中的任何一个课程有交叉,我们就返回 True,否则返回 False。
对于两个 calendar,我们的判断逻辑都是一样的。假设连个 calendar 分别是`[s1, e1]``[s2, e2]`。那么如果`s1 >= e2 or s2 <= e1`, 则两个课程没有交叉,可以预定,否则不可以。如图,1,2,3 可以预定,剩下的不可以。
![image.png](http://ww1.sinaimg.cn/large/e9f490c8ly1gbj1o8hvivj20w20ra76f.jpg)
代码是这样的:
```python
def intersected(calendar, calendars):
for [start, end] in calendars:
if calendar[0] >= end or calendar[1] <= start:
continue
else:
return True
return False
```
复杂度分析:
- 时间复杂度:$O(N^2)$。N 指的是日常安排的数量,对于每个新的日常安排,我们检查新的日常安排是否发生冲突来决定是否可以预订新的日常安排。
- 空间复杂度: $O(N)$。
这个代码写出来之后整体代码就呼之欲出了,全部代码见下方代码部分。
### 代码
代码支持 Python3:
Python3 Code:
```python
#
# @lc app=leetcode.cn id=729 lang=python3
#
# [729] 我的日程安排表 I
#
# @lc code=start
class MyCalendar:
def __init__(self):
self.calendars = []
def book(self, start: int, end: int) -> bool:
def intersected(calendar, calendars):
for [start, end] in calendars:
if calendar[0] >= end or calendar[1] <= start:
continue
else:
return True
return False
if intersected([start, end], self.calendars):
return False
self.calendars.append([start, end])
return True
# Your MyCalendar object will be instantiated and called as such:
# obj = MyCalendar()
# param_1 = obj.book(start,end)
# @lc code=end
```
实际上我们还可以换个角度,上面的思路判断交叉部分我们考虑的是“如何不交叉”,剩下的就是交叉。我们也可以直接考虑交叉。还是上面的例子,如果两个课程交叉,那么一定满足`s1 < e2 and e1 > s2`。基于此,我们写出下面的代码。
代码支持 Python3:
Python3 Code:
```python
#
# @lc app=leetcode.cn id=729 lang=python3
#
# [729] 我的日程安排表 I
#
# @lc code=start
class MyCalendar:
def __init__(self):
self.calendars = []
def book(self, start: int, end: int) -> bool:
for s, e in self.calendars:
if start < e and end > s:
return False
self.calendars.append([start, end])
return True
# Your MyCalendar object will be instantiated and called as such:
# obj = MyCalendar()
# param_1 = obj.book(start,end)
# @lc code=end
```
## 二叉查找树法
### 思路
和上面思路类似,只不过我们每次都对 calendars 进行排序,那么我们可以通过二分查找日程安排的情况来检查新日常安排是否可以预订。如果每次插入之前都进行一次排序,那么时间复杂度会很高。如图,我们的[s1,e1], [s2,e2], [s3,e3] 是按照时间顺序排好的日程安排。我们现在要插入[s,e],我们使用二分查找,找到要插入的位置,然后和插入位置的课程进行一次比对即可,这部分的时间复杂度是 O(logN)\$
![image.png](http://ww1.sinaimg.cn/large/e9f490c8ly1gbj28k6v4gj21100c2754.jpg)
我们考虑使用平衡二叉树来维护这种动态的变化,在最差的情况时间复杂度会退化到上述的$O(N^2)$,平均情况是$O(NlogN)$,其中 N 是已预订的日常安排数。
![image.png](http://ww1.sinaimg.cn/large/e9f490c8ly1gbj2dirnf0j20xs0fe75j.jpg)
### 代码
代码支持 Python3:
Python3 Code:
```python
class Node:
def __init__(self, start, end):
self.start = start
self.end = end
self.left = self.right = None
def insert(self, node):
if node.start >= self.end:
if not self.right:
self.right = node
return True
return self.right.insert(node)
elif node.end <= self.start:
if not self.left:
self.left = node
return True
return self.left.insert(node)
else:
return False
class MyCalendar(object):
def __init__(self):
self.root = None
def book(self, start, end):
if self.root is None:
self.root = Node(start, end)
return True
return self.root.insert(Node(start, end))
```
# 731. 我的日程安排表 II
## 题目地址
https://leetcode-cn.com/problems/my-calendar-ii
## 题目描述
实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内不会导致三重预订时,则可以存储这个新的日程安排。
MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数  x 的范围为,  start <= x < end。
当三个日程安排有一些时间上的交叉时(例如三个日程安排都在同一时间内),就会产生三重预订。
每次调用 MyCalendar.book 方法时,如果可以将日程安排成功添加到日历中而不会导致三重预订,返回 true。否则,返回 false 并且不要将该日程安排添加到日历中。
请按照以下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)
示例:
MyCalendar();
MyCalendar.book(10, 20); // returns true
MyCalendar.book(50, 60); // returns true
MyCalendar.book(10, 40); // returns true
MyCalendar.book(5, 15); // returns false
MyCalendar.book(5, 10); // returns true
MyCalendar.book(25, 55); // returns true
解释:
前两个日程安排可以添加至日历中。 第三个日程安排会导致双重预订,但可以添加至日历中。
第四个日程安排活动(5,15)不能添加至日历中,因为它会导致三重预订。
第五个日程安排(5,10)可以添加至日历中,因为它未使用已经双重预订的时间 10。
第六个日程安排(25,55)可以添加至日历中,因为时间 [25,40] 将和第三个日程安排双重预订;
时间 [40,50] 将单独预订,时间 [50,55)将和第二个日程安排双重预订。
提示:
每个测试用例,调用  MyCalendar.book  函数最多不超过  1000 次。
调用函数  MyCalendar.book(start, end)时, start 和  end 的取值范围为  [0, 10^9]。
## 暴力法
### 思路
暴力法和上述思路类似。但是我们多维护一个数组 intersectedCalendars 用来存储**二次预定**的日程安排。如果课程第一次冲突,我们将其加入 intersectedCalendars,如果和 intersectedCalendars 也冲突了,说明出现了三次预定,我们直接返回 False。
### 代码
代码支持 Python3:
Python3 Code:
```python
class MyCalendarTwo:
def __init__(self):
self.calendars = []
self.intersectedCalendars = []
def book(self, start: int, end: int) -> bool:
for [s, e] in self.intersectedCalendars:
if start < e and end > s:
return False
for [s, e] in self.calendars:
if start < e and end > s:
self.intersectedCalendars.append([max(start, s), min(end, e)])
self.calendars.append([start, end])
return True
```
## 二叉查找树法
和上面的题目类似,我们仍然可以使用平衡二叉树来简化查找逻辑。具体可以参考[这个 discussion](<https://leetcode.com/problems/my-calendar-ii/discuss/158747/Python-O(logN)>)
每次插入之前我们都需要进行一次判断,判断是否可以插入。如果不可以插入,直接返回 False,否则我们进行一次插入。 插入的时候,如果和已有的相交了,我们判断是否之前已经相交了一次,如果是返回 False,否则返回 True。关于**如何判断是否和已有的相交**,我们可以在 node 节点增加一个字段的方式来标记,在这里我们使用 single_overlap,True 表示产生了二次预定,False 则表示没有产生过两次及以上的预定。
## 代码
代码支持 Python3:
Python3 Code:
```python
class Node:
def __init__(self, start, end):
self.start = start
self.end = end
self.left = None
self.right = None
self.single_overlap = False
class MyCalendarTwo:
def __init__(self):
self.root = None
def book(self, start, end):
if not self.canInsert(start, end, self.root):
return False
self.root = self.insert(start, end, self.root)
return True
def canInsert(self, start, end, root):
if not root:
return True
if start >= end:
return True
if end <= root.start:
return self.canInsert(start, end, root.left)
elif start >= root.end:
return self.canInsert(start, end, root.right)
else:
if root.single_overlap:
return False
elif start >= root.start and end <= root.end:
return True
else:
return self.canInsert(start, root.start, root.left) and self.canInsert(root.end, end, root.right)
def insert(self, start, end, root):
if not root:
root = Node(start, end)
return root
if start >= end:
return root
if start >= root.end:
root.right = self.insert(start, end, root.right)
elif end <= root.start:
root.left = self.insert(start, end, root.left)
else:
root.single_overlap = True
a = min(root.start, start)
b = max(root.start, start)
c = min(root.end, end)
d = max(root.end, end)
root.start, root.end = b, c
root.left, root.right = self.insert(a, b, root.left), self.insert(c, d, root.right)
return root
# Your MyCalendarTwo object will be instantiated and called as such:
# obj = MyCalendarTwo()
# param_1 = obj.book(start,end)
```
# 732. 我的日程安排表 III
## 题目地址
https://leetcode-cn.com/problems/my-calendar-iii/
## 题目描述
实现一个 MyCalendar 类来存放你的日程安排,你可以一直添加新的日程安排。
MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排,注意,这里的时间是半开区间,即 [start, end), 实数  x 的范围为,  start <= x < end。
当 K 个日程安排有一些时间上的交叉时(例如 K 个日程安排都在同一时间内),就会产生 K 次预订。
每次调用 MyCalendar.book 方法时,返回一个整数 K ,表示最大的 K 次预订。
请按照以下步骤调用 MyCalendar 类: MyCalendar cal = new MyCalendar(); MyCalendar.book(start, end)
示例 1:
MyCalendarThree();
MyCalendarThree.book(10, 20); // returns 1
MyCalendarThree.book(50, 60); // returns 1
MyCalendarThree.book(10, 40); // returns 2
MyCalendarThree.book(5, 15); // returns 3
MyCalendarThree.book(5, 10); // returns 3
MyCalendarThree.book(25, 55); // returns 3
解释:
前两个日程安排可以预订并且不相交,所以最大的 K 次预订是 1。
第三个日程安排[10,40]与第一个日程安排相交,最高的 K 次预订为 2。
其余的日程安排的最高 K 次预订仅为 3。
请注意,最后一次日程安排可能会导致局部最高 K 次预订为 2,但答案仍然是 3,原因是从开始到最后,时间[10,20],[10,40]和[5,15]仍然会导致 3 次预订。
说明:
每个测试用例,调用  MyCalendar.book  函数最多不超过  400 次。
调用函数  MyCalendar.book(start, end)时, start 和  end 的取值范围为  [0, 10^9]。
## 二叉查找树法
### 思路
我们仍然可以使用上述的平衡二叉树的做法。只不过我们需要额外维护一个全局的最大值“k”,表示需要多少个预定。最终我们返回 k。 同时每一个 node 我们都增加一个属性 k,用来表示局部的最大值,对于每次插入,我们将 node 的 k 和全部的 k 进行比较,取出最大值即可。
### 代码
代码支持 Python3:
Python3 Code:
```python
class Node(object):
def __init__(self, start, end, ktime=1):
self.k = ktime
self.s = start
self.e = end
self.right = None
self.left = None
class MyCalendarThree(object):
def __init__(self):
self.root = None
self.k = 0
def book(self, start, end):
self.root = self.insert(self.root, start, end, 1)
return self.k
def insert(self, root, start, end, k):
if start >= end:
return root
if not root:
self.k = max(self.k, k)
return Node(start, end, k)
else:
if start >= root.e:
root.right = self.insert(root.right, start, end, k)
return root
elif end <= root.s:
root.left = self.insert(root.left, start, end, k)
return root
else:
a = min(root.s, start)
b = max(root.s, start)
c = min(root.e, end)
d = max(root.e, end)
root.left = self.insert(root.left, a, b, a == root.s and root.k or k)
root.right = self.insert(root.right, c,d, d == root.e and root.k or k)
root.k += k
root.s = b
root.e = c
self.k = max(root.k, self.k)
return root
```
## Count Map 法
### 思路
这个是我在看了 Discussion [[C++] Map Solution, beats 95%+](https://leetcode.com/problems/my-calendar-iii/discuss/176950/C%2B%2B-Map-Solution-beats-95%2B) 之后写的解法,解法非常巧妙。
我们使用一个 count map 来存储所有的预定,对于每次插入,我们执行`count[start] += 1``count[end] -= 1`。 count[t] 表示从 t 开始到下一个 t 我们有几个预定。因此我们需要对 count 进行排序才行。 我们维护一个最大值来 cnt 来表示需要的预定数。
比如预定[1,3]和[5,7],我们产生一个预定即可:
![image.png](http://ww1.sinaimg.cn/large/e9f490c8ly1gbj50c37suj212q0bcq3t.jpg)
再比如预定[1,5]和[3,7],我们需要两个预定:
![image.png](http://ww1.sinaimg.cn/large/e9f490c8ly1gbj45oq6fhj213e0ca0tm.jpg)
我们可以使用红黑树来简化时间复杂度,如果你使用的是 Java,可以直接使用现成的数据结构 TreeMap。我这里偷懒,每次都排序,时间复杂度会很高,但是可以 AC。
读到这里,你可能会发现: 这个解法似乎更具有通用型。对于第一题我们可以判断 cnt 是否小于等于 1,对于第二题我们可以判断 cnt 是否小于等于 2。
> 如果你不借助红黑树等数据结构直接使用 count-map 法,即每次都进行一次排序,第一题和第二题可能会直接超时。
### 代码
代码支持 Python3:
Python3 Code:
```python
class MyCalendarThree:
def __init__(self):
self.count = dict()
def book(self, start: int, end: int) -> int:
self.count[start] = self.count.get(start, 0) + 1
self.count[end] = self.count.get(end, 0) - 1
cnt = 0
cur = 0
for k in sorted(self.count):
cur += self.count[k]
cnt = max(cnt, cur)
return cnt
# Your MyCalendarThree object will be instantiated and called as such:
# obj = MyCalendarThree()
# param_1 = obj.book(start,end)
```
# 相关题目
LeetCode 上有一个类似的系列《会议室》,截止目前(2020-02-03)有两道题目。其中一个简单一个中等,解题思路非常类似,大家用这个解题思路尝试一下,检测一下自己是否已经掌握。两道题分别是:
- [252. 会议室](https://leetcode-cn.com/problems/meeting-rooms/)
- [253. 会议室 II](https://leetcode-cn.com/problems/meeting-rooms-ii/)
# 总结
我们对 LeetCode 上的专题《我的日程安排》的三道题进行了汇总。对于区间判断是否重叠,我们可以反向判断,也可以正向判断。 暴力的方法是每次对所有的课程进行判断是否重叠,这种解法可以 AC。我们也可以进一步优化,使用二叉查找树来简化时间复杂度。最后我们介绍了一种 Count-Map 方法来通用解决所有的问题,不仅可以完美解决这三道题,还可以扩展到《会议室》系列的两道题。
# 文带你看懂二叉树的序列化
我们先来看下什么是序列化,以下定义来自维基百科:
> 序列化(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。
可见,序列化和反序列化在计算机科学中的应用还是非常广泛的。就拿 LeetCode 平台来说,其允许用户输入形如:
```
[1,2,3,null,null,4,5]
```
这样的数据结构来描述一颗树:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh2dqqnyzwj30ba0baglw.jpg)
([1,2,3,null,null,4,5] 对应的二叉树)
其实序列化和反序列化只是一个概念,不是一种具体的算法,而是很多的算法。并且针对不同的数据结构,算法也会不一样。本文主要讲述的是二叉树的序列化和反序列化。看完本文之后,你就可以放心大胆地去 AC 以下两道题:
- [449. 序列化和反序列化二叉搜索树(中等)](https://leetcode-cn.com/problems/serialize-and-deserialize-bst/)
- [297. 二叉树的序列化与反序列化(困难)](https://leetcode-cn.com/problems/serialize-and-deserialize-binary-tree/)
## 前置知识
阅读本文之前,需要你对树的遍历以及 BFS 和 DFS 比较熟悉。如果你还不熟悉,推荐阅读一下相关文章之后再来看。或者我这边也写了一个总结性的文章[二叉树的遍历](https://github.com/azl397985856/leetcode/blob/master/thinkings/binary-tree-traversal.md),你也可以看看。
## 前言
我们知道:二叉树的深度优先遍历,根据访问根节点的顺序不同,可以将其分为`前序遍历``中序遍历`, `后序遍历`。即如果先访问根节点就是前序遍历,最后访问根节点就是后续遍历,其它则是中序遍历。而左右节点的相对顺序是不会变的,一定是先左后右。
> 当然也可以设定为先右后左。
并且知道了三种遍历结果中的任意两种即可还原出原有的树结构。这不就是序列化和反序列化么?如果对这个比较陌生的同学建议看看我之前写的[《构造二叉树系列》](https://lucifer.ren/blog/2020/02/08/%E6%9E%84%E9%80%A0%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%93%E9%A2%98/)
有了这样一个前提之后算法就自然而然了。即先对二叉树进行两次不同的遍历,不妨假设按照前序和中序进行两次遍历。然后将两次遍历结果序列化,比如将两次遍历结果以逗号“,” join 成一个字符串。 之后将字符串反序列即可,比如将其以逗号“,” split 成一个数组。
序列化:
```py
class Solution:
def preorder(self, root: TreeNode):
if not root: return []
return [str(root.val)] +self. preorder(root.left) + self.preorder(root.right)
def inorder(self, root: TreeNode):
if not root: return []
return self.inorder(root.left) + [str(root.val)] + self.inorder(root.right)
def serialize(self, root):
ans = ''
ans += ','.join(self.preorder(root))
ans += '$'
ans += ','.join(self.inorder(root))
return ans
```
反序列化:
这里我直接用了力扣 `105. 从前序与中序遍历序列构造二叉树` 的解法,一行代码都不改。
```py
class Solution:
def deserialize(self, data: str):
preorder, inorder = data.split('$')
if not preorder: return None
return self.buildTree(preorder.split(','), inorder.split(','))
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
# 实际上inorder 和 preorder 一定是同时为空的,因此你无论判断哪个都行
if not preorder:
return None
root = TreeNode(preorder[0])
i = inorder.index(root.val)
root.left = self.buildTree(preorder[1:i + 1], inorder[:i])
root.right = self.buildTree(preorder[i + 1:], inorder[i+1:])
return root
```
实际上这个算法是不一定成立的,原因在于树的节点可能存在重复元素。也就是说我前面说的`知道了三种遍历结果中的任意两种即可还原出原有的树结构`是不对的,严格来说应该是**如果树中不存在重复的元素,那么知道了三种遍历结果中的任意两种即可还原出原有的树结构**
聪明的你应该发现了,上面我的代码用了 `i = inorder.index(root.val)`,如果存在重复元素,那么得到的索引 i 就可能不是准确的。但是,如果题目限定了没有重复元素则可以用这种算法。但是现实中不出现重复元素不太现实,因此需要考虑其他方法。那究竟是什么样的方法呢? 接下来进入正题。
## DFS
### 序列化
我们来模仿一下力扣的记法。 比如:`[1,2,3,null,null,4,5]`(本质上是 BFS 层次遍历),对应的树如下:
> 选择这种记法,而不是 DFS 的记法的原因是看起来比较直观
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh2h5bhjryj30b40am74k.jpg)
序列化的代码非常简单, 我们只需要在普通的遍历基础上,增加对空节点的输出即可(普通的遍历是不处理空节点的)。
比如我们都树进行一次前序遍历的同时增加空节点的处理。选择前序遍历的原因是容易知道根节点的位置,并且代码好写,不信你可以试试。
因此序列化就仅仅是普通的 DFS 而已,直接给大家看看代码。
Python 代码:
```py
class Codec:
def serialize_dfs(self, root, ans):
# 空节点也需要序列化,否则无法唯一确定一棵树,后不赘述。
if not root: return ans + '#,'
# 节点之间通过逗号(,)分割
ans += str(root.val) + ','
ans = self.serialize_dfs(root.left, ans)
ans = self.serialize_dfs(root.right, ans)
return ans
def serialize(self, root):
# 由于最后会添加一个额外的逗号,因此需要去除最后一个字符,后不赘述。
return self.serialize_dfs(root, '')[:-1]
```
Java 代码:
```java
public class Codec {
public String serialize_dfs(TreeNode root, String str) {
if (root == null) {
str += "None,";
} else {
str += str.valueOf(root.val) + ",";
str = serialize_dfs(root.left, str);
str = serialize_dfs(root.right, str);
}
return str;
}
public String serialize(TreeNode root) {
return serialize_dfs(root, "");
}
}
```
`[1,2,3,null,null,4,5]` 会被处理为`1,2,#,#,3,4,#,#,5,#,#`
我们先看一个短视频:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh2z5y87n0g30bo05vx6u.gif)
(动画来自力扣)
### 反序列化
反序列化的第一步就是将其展开。以上面的例子来说,则会变成数组:`[1,2,#,#,3,4,#,#,5,#,#]`,然后我们同样执行一次前序遍历,每次处理一个元素,重建即可。由于我们采用的前序遍历,因此第一个是根元素,下一个是其左子节点,下下一个是其右子节点。
Python 代码:
```py
def deserialize_dfs(self, nodes):
if nodes:
if nodes[0] == '#':
nodes.pop(0)
return None
root = TreeNode(nodes.pop(0))
root.left = self.deserialize_dfs(nodes)
root.right = self.deserialize_dfs(nodes)
return root
return None
def deserialize(self, data: str):
nodes = data.split(',')
return self.deserialize_dfs(nodes)
```
Java 代码:
```java
public TreeNode deserialize_dfs(List<String> l) {
if (l.get(0).equals("None")) {
l.remove(0);
return null;
}
TreeNode root = new TreeNode(Integer.valueOf(l.get(0)));
l.remove(0);
root.left = deserialize_dfs(l);
root.right = deserialize_dfs(l);
return root;
}
public TreeNode deserialize(String data) {
String[] data_array = data.split(",");
List<String> data_list = new LinkedList<String>(Arrays.asList(data_array));
return deserialize_dfs(data_list);
}
```
**复杂度分析**
- 时间复杂度:每个节点都会被处理一次,因此时间复杂度为 $O(N)$,其中 $N$ 为节点的总数。
- 空间复杂度:空间复杂度取决于栈深度,因此空间复杂度为 $O(h)$,其中 $h$ 为树的深度。
## BFS
### 序列化
实际上我们也可以使用 BFS 的方式来表示一棵树。在这一点上其实就和力扣的记法是一致的了。
我们知道层次遍历的时候实际上是有层次的。只不过有的题目需要你记录每一个节点的层次信息,有些则不需要。
这其实就是一个朴实无华的 BFS,唯一不同则是增加了空节点。
Python 代码:
```py
class Codec:
def serialize(self, root):
ans = ''
queue = [root]
while queue:
node = queue.pop(0)
if node:
ans += str(node.val) + ','
queue.append(node.left)
queue.append(node.right)
else:
ans += '#,'
return ans[:-1]
```
### 反序列化
如图有这样一棵树:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh2x3gj9n0j30j00gewfx.jpg)
那么其层次遍历为 [1,2,3,#,#, 4, 5]。我们根据此层次遍历的结果来看下如何还原二叉树,如下是我画的一个示意图:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh2x55lh7qj31780t0gq8.jpg)
容易看出:
- level x 的节点一定指向 level x + 1 的节点,如何找到 level + 1 呢? 这很容易通过层次遍历来做到。
- 对于给的的 level x,从左到右依次对应 level x + 1 的节点,即第 1 个节点的左右子节点对应下一层的第 1 个和第 2 个节点,第 2 个节点的左右子节点对应下一层的第 3 个和第 4 个节点。。。
- 接上,其实如果你仔细观察的话,实际上 level x 和 level x + 1 的判断是无需特别判断的。我们可以把思路逆转过来:`即第 1 个节点的左右子节点对应第 1 个和第 2 个节点,第 2 个节点的左右子节点对应第 3 个和第 4 个节点。。。`(注意,没了下一层三个字)
因此我们的思路也是同样的 BFS,并依次连接左右节点。
Python 代码:
```py
def deserialize(self, data: str):
if data == '#': return None
# 数据准备
nodes = data.split(',')
if not nodes: return None
# BFS
root = TreeNode(nodes[0])
queue = [root]
# 已经有 root 了,因此从 1 开始
i = 1
while i < len(nodes) - 1:
node = queue.pop(0)
#
lv = nodes[i]
rv = nodes[i + 1]
i += 2
# 对于给的的 level x,从左到右依次对应 level x + 1 的节点
# node 是 level x 的节点,l 和 r 则是 level x + 1 的节点
if lv != '#':
l = TreeNode(lv)
node.left = l
queue.append(l)
if rv != '#':
r = TreeNode(rv)
node.right = r
queue.append(r)
return root
```
**复杂度分析**
- 时间复杂度:每个节点都会被处理一次,因此时间复杂度为 $O(N)$,其中 $N$ 为节点的总数。
- 空间复杂度:$O(N)$,其中 $N$ 为节点的总数。
## 总结
除了这种方法还有很多方案, 比如括号表示法。 关于这个可以参考力扣[606. 根据二叉树创建字符串](https://leetcode-cn.com/problems/construct-string-from-binary-tree/),这里就不再赘述了。
本文从 BFS 和 DFS 角度来思考如何序列化和反序列化一棵树。 如果用 BFS 来序列化,那么相应地也需要 BFS 来反序列化。如果用 DFS 来序列化,那么就需要用 DFS 来反序列化。
我们从马后炮的角度来说,实际上对于序列化来说,BFS 和 DFS 都比较常规。对于反序列化,大家可以像我这样举个例子,画一个图。可以先在纸上,电脑上,如果你熟悉了之后,也可以画在脑子里。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1gh30bapydej30rq0tcad5.jpg)
(Like This)
更多题解可以访问我的 LeetCode 题解仓库:https://github.com/azl397985856/leetcode 。 目前已经 30K star 啦。
关注公众号力扣加加,努力用清晰直白的语言还原解题思路,并且有大量图解,手把手教你识别套路,高效刷题。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册