Commit b6bdf8cc authored by JingmingZhang's avatar JingmingZhang
Browse files

Merge branch 'master' of github.com:zjming/LeetCodeAnimation

these are solved leetcode problem!
parents 5bf978cf 6db598b8
# LeetCode 第 110 号问题:平衡二叉树
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 110 号问题:平衡二叉树。
### 题目描述
给定一个二叉树,判断它是否是高度平衡的二叉树。
### 题目解析
采取**后序遍历**的方式遍历二叉树的每一个结点。
在遍历到一个结点之前已经遍历了它的左右子树,那么只要在遍历每个结点的时候记录它的深度(某一结点的深度等于它到叶结点的路径的长度),就可以一边遍历一边判断每个结点是不是平衡的。
### 动画描述
待补充
### 代码实现
```java
class Solution {
private boolean isBalanced = true;
public boolean isBalanced(TreeNode root) {
getDepth(root);
return isBalanced;
}
public int getDepth(TreeNode root) {
if (root == null)
return 0;
int left = getDepth(root.left);
int right = getDepth(root.right);
if (Math.abs(left - right) > 1) {
isBalanced = false;
}
return right > left ? right + 1 : left + 1;
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/z7w57.png)
\ No newline at end of file
# 杨辉三角
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/yf0dp.png)
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode](https://github.com/MisterBooo/LeetCodeAnimation) 系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
>
杨辉三角应该是大家很早就接触到的一个数学知识,它有很多有趣的性质:
- 每个数字等于上一行的左右两个数字之和,即 *C(n+1,i) = C(n,i) + C(n,i-1)*
- 每行数字左右对称,由 1 开始逐渐变大
- 第 n 行的数字有 n 项
- 第 n 行的第 m 个数和第 n - m + 1 个数相等 ,为[组合数](https://baike.baidu.com/item/%E7%BB%84%E5%90%88%E6%95%B0)性质之一
- ( a + b )<sup>n</sup>的展开式中的各项[系数](https://baike.baidu.com/item/%E7%B3%BB%E6%95%B0)依次对应杨辉三角的第 ( n + 1 ) 行中的每一项
- 。。。
## 杨辉三角
题目来源于 LeetCode 上第 118 号问题:杨辉三角。题目难度为 Easy,目前通过率为 61.8% 。
### 题目描述
给定一个非负整数 *numRows,*生成杨辉三角的前 *numRows* 行。
![img](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/ehk16.gif)
在杨辉三角中,每个数是它左上方和右上方的数的和。
**示例:**
```
输入: 5
输出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]
```
### 题目解析
> 这道题目在各大高校的习题中经常出现。
对于本题而言,利用性质 1 :每一行的首个和结尾一个数字都是 1,从第三行开始,中间的每个数字都是上一行的左右两个数字之和。
### 代码实现
```java
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> result = new ArrayList<>();
if (numRows < 1) return result;
for (int i = 0; i < numRows; ++i) {
//扩容
List<Integer> list = Arrays.asList(new Integer[i+1]);
list.set(0, 1); list.set(i, 1);
for (int j = 1; j < i; ++j) {
//等于上一行的左右两个数字之和
list.set(j, result.get(i-1).get(j-1) + result.get(i-1).get(j));
}
result.add(list);
}
return result;
}
}
```
## 杨辉三角II
题目来源于 LeetCode 上第 119 号问题:杨辉三角II。题目难度为 Easy,目前通过率为 55.5% 。
### 题目描述
给定一个非负索引 *k*,其中 *k* ≤ 33,返回杨辉三角的第 *k* 行。
![img](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/ehk16.gif)
在杨辉三角中,每个数是它左上方和右上方的数的和。
**示例:**
```
输入: 3
输出: [1,3,3,1]
```
**进阶:**
你可以优化你的算法到 *O*(*k*) 空间复杂度吗?
### 题目解析
这道题目的难点与思考点在于题目有额外限制条件,程序只能使用 O(k) 的额外空间,因此无法通过累加的方式将每一行都输出打印。
这里依旧使用杨辉三角的规律,很隐藏的规律:对于杨辉三角的同一行,第 ( i + 1) 项是第 i 项的` ( k - i ) /( i + 1 )` 倍。
比如:
- 第 k 索引行的第 0 项:1
- 第 k 索引行的第 1 项:1 * k
- 第 k 索引行的第 2 项:1 * k * ( k - 1) / 2
- 第 k 索引行的第 3 项:[1 * k * ( k - 1) / 2 ] * ( k - 2 ) / 3
### 代码实现
```java
class Solution {
public List<Integer> getRow(int rowIndex) {
List<Integer> res = new ArrayList<>(rowIndex + 1);
long index = 1;
for (int i = 0; i <= rowIndex; i++) {
res.add((int) index);
index = index * ( rowIndex - i ) / ( i + 1 );
}
return res;
}
}
```
## 一个有趣的结论
感兴趣小伙伴的可以搜索一下李永乐讲得抽奖概率相关的视频,里面提及到了很多杨辉三角的神奇特点。
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/ggto5.gif)
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/bhn6z.png)
\ No newline at end of file
# 杨辉三角
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/inihp.png)
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode](https://github.com/MisterBooo/LeetCodeAnimation) 系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
>
杨辉三角应该是大家很早就接触到的一个数学知识,它有很多有趣的性质:
- 每个数字等于上一行的左右两个数字之和,即 *C(n+1,i) = C(n,i) + C(n,i-1)*
- 每行数字左右对称,由 1 开始逐渐变大
- 第 n 行的数字有 n 项
- 第 n 行的第 m 个数和第 n - m + 1 个数相等 ,为[组合数](https://baike.baidu.com/item/%E7%BB%84%E5%90%88%E6%95%B0)性质之一
- ( a + b )<sup>n</sup>的展开式中的各项[系数](https://baike.baidu.com/item/%E7%B3%BB%E6%95%B0)依次对应杨辉三角的第 ( n + 1 ) 行中的每一项
- 。。。
## 杨辉三角
题目来源于 LeetCode 上第 118 号问题:杨辉三角。题目难度为 Easy,目前通过率为 61.8% 。
### 题目描述
给定一个非负整数 *numRows,*生成杨辉三角的前 *numRows* 行。
![img](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/ks594.gif)
在杨辉三角中,每个数是它左上方和右上方的数的和。
**示例:**
```
输入: 5
输出:
[
[1],
[1,1],
[1,2,1],
[1,3,3,1],
[1,4,6,4,1]
]
```
### 题目解析
> 这道题目在各大高校的习题中经常出现。
对于本题而言,利用性质 1 :每一行的首个和结尾一个数字都是 1,从第三行开始,中间的每个数字都是上一行的左右两个数字之和。
### 代码实现
```java
class Solution {
public List<List<Integer>> generate(int numRows) {
List<List<Integer>> result = new ArrayList<>();
if (numRows < 1) return result;
for (int i = 0; i < numRows; ++i) {
//扩容
List<Integer> list = Arrays.asList(new Integer[i+1]);
list.set(0, 1); list.set(i, 1);
for (int j = 1; j < i; ++j) {
//等于上一行的左右两个数字之和
list.set(j, result.get(i-1).get(j-1) + result.get(i-1).get(j));
}
result.add(list);
}
return result;
}
}
```
## 杨辉三角II
题目来源于 LeetCode 上第 119 号问题:杨辉三角II。题目难度为 Easy,目前通过率为 55.5% 。
### 题目描述
给定一个非负索引 *k*,其中 *k* ≤ 33,返回杨辉三角的第 *k* 行。
![img](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/ks594.gif)
在杨辉三角中,每个数是它左上方和右上方的数的和。
**示例:**
```
输入: 3
输出: [1,3,3,1]
```
**进阶:**
你可以优化你的算法到 *O*(*k*) 空间复杂度吗?
### 题目解析
这道题目的难点与思考点在于题目有额外限制条件,程序只能使用 O(k) 的额外空间,因此无法通过累加的方式将每一行都输出打印。
这里依旧使用杨辉三角的规律,很隐藏的规律:对于杨辉三角的同一行,第 ( i + 1) 项是第 i 项的` ( k - i ) /( i + 1 )` 倍。
比如:
- 第 k 索引行的第 0 项:1
- 第 k 索引行的第 1 项:1 * k
- 第 k 索引行的第 2 项:1 * k * ( k - 1) / 2
- 第 k 索引行的第 3 项:[1 * k * ( k - 1) / 2 ] * ( k - 2 ) / 3
### 代码实现
```java
class Solution {
public List<Integer> getRow(int rowIndex) {
List<Integer> res = new ArrayList<>(rowIndex + 1);
long index = 1;
for (int i = 0; i <= rowIndex; i++) {
res.add((int) index);
index = index * ( rowIndex - i ) / ( i + 1 );
}
return res;
}
}
```
## 一个有趣的结论
感兴趣小伙伴的可以搜索一下李永乐讲得抽奖概率相关的视频,里面提及到了很多杨辉三角的神奇特点。
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/0b495.gif)
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/besbk.png)
\ No newline at end of file
# 浅谈什么是动态规划以及相关的「股票」算法题
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
## 动态规划
### 1 概念
  **动态规划**算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。在学习动态规划之前需要明确掌握几个重要概念。
  **阶段**:对于一个完整的问题过程,适当的切分为若干个相互联系的子问题,每次在求解一个子问题,则对应一个阶段,整个问题的求解转化为按照阶段次序去求解。
  **状态**:状态表示每个阶段开始时所处的客观条件,即在求解子问题时的已知条件。状态描述了研究的问题过程中的状况。
  **决策**:决策表示当求解过程处于某一阶段的某一状态时,可以根据当前条件作出不同的选择,从而确定下一个阶段的状态,这种选择称为决策。
  **策略**:由所有阶段的决策组成的决策序列称为全过程策略,简称策略。
  **最优策略**:在所有的策略中,找到代价最小,性能最优的策略,此策略称为最优策略。
  **状态转移方程**:状态转移方程是确定两个相邻阶段状态的演变过程,描述了状态之间是如何演变的。
### 2 使用场景
能采用动态规划求解的问题的一般要具有 3 个性质:
  (1)**最优化**:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。子问题的局部最优将导致整个问题的全局最优。换句话说,就是问题的一个最优解中一定包含子问题的一个最优解。
  (2)**无后效性**:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关,与其他阶段的状态无关,特别是与未发生的阶段的状态无关。
   (3)**重叠子问题**:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
### 3 算法流程
  (1)划分阶段:按照问题的时间或者空间特征将问题划分为若干个阶段。
  (2)确定状态以及状态变量:将问题的不同阶段时期的不同状态描述出来。
  (3)确定决策并写出状态转移方程:根据相邻两个阶段的各个状态之间的关系确定决策。
  (4)寻找边界条件:一般而言,状态转移方程是递推式,必须有一个递推的边界条件。
  (5)设计程序,解决问题
## 实战练习
下面的三道算法题都是来源于 LeetCode 上与股票买卖相关的问题 ,我们按照 **动态规划** 的算法流程来处理该类问题。
**股票买卖**这一类的问题,都是给一个输入数组,里面的每个元素表示的是每天的股价,并且你只能持有一支股票(也就是你必须在再次购买前出售掉之前的股票),一般来说有下面几种问法:
- 只能买卖一次
- 可以买卖无数次
- 可以买卖 k 次
需要你设计一个算法去获取最大的利润。
## 买卖股票的最佳时机
题目来源于 LeetCode 上第 121 号问题:买卖股票的最佳时机。题目难度为 Easy,目前通过率为 49.4% 。
### 题目描述
给定一个数组,它的第 *i* 个元素是一支给定股票第 *i* 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
**示例 1:**
```
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
```
**示例 2:**
```
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
```
### 题目解析
我们按照动态规划的思想来思考这道问题。
#### 状态
**买入(buy)****卖出(sell)** 这两种状态。
#### 转移方程
对于买来说,买之后可以卖出(进入卖状态),也可以不再进行股票交易(保持买状态)。
对于卖来说,卖出股票后不在进行股票交易(还在卖状态)。
只有在手上的钱才算钱,手上的钱购买当天的股票后相当于亏损。也就是说当天买的话意味着损失`-prices[i]`,当天卖的话意味着增加`prices[i]`,当天卖出总的收益就是 `buy+prices[i]`
所以我们只要考虑当天买和之前买哪个收益更高,当天卖和之前卖哪个收益更高。
* buy = max(buy, -price[i]) (注意:根据定义 buy 是负数)
* sell = max(sell, prices[i] + buy)
#### 边界
第一天 `buy = -prices[0]`, `sell = 0`,最后返回 sell 即可。
###代码实现
```java
class Solution {
public int maxProfit(int[] prices) {
if(prices.length <= 1)
return 0;
int buy = -prices[0], sell = 0;
for(int i = 1; i < prices.length; i++) {
buy = Math.max(buy, -prices[i]);
sell = Math.max(sell, prices[i] + buy);
}
return sell;
}
}
```
## 买卖股票的最佳时机 II
题目来源于 LeetCode 上第 122 号问题:买卖股票的最佳时机 II。题目难度为 Easy,目前通过率为 53.0% 。
### 题目描述
给定一个数组,它的第 *i* 个元素是一支给定股票第 *i* 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
**注意**:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
**示例 1:**
```
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
```
**示例 2:**
```
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
```
**示例 3:**
```
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
```
### 题目解析
#### 状态
**买入(buy)****卖出(sell)** 这两种状态。
#### 转移方程
对比上题,这里可以有无限次的买入和卖出,也就是说 **买入** 状态之前可拥有 **卖出** 状态,所以买入的转移方程需要变化。
- buy = max(buy, sell - price[i])
- sell = max(sell, buy + prices[i] )
#### 边界
第一天 `buy = -prices[0]`, `sell = 0`,最后返回 sell 即可。
### 代码实现
```java
class Solution {
public int maxProfit(int[] prices) {
if(prices.length <= 1)
return 0;
int buy = -prices[0], sell = 0;
for(int i = 1; i < prices.length; i++) {
sell = Math.max(sell, prices[i] + buy);
buy = Math.max( buy,sell - prices[i]);
}
return sell;
}
}
```
## 买卖股票的最佳时机 III
题目来源于 LeetCode 上第 123 号问题:买卖股票的最佳时机 III。题目难度为 Hard,目前通过率为 36.1% 。
### 题目描述
给定一个数组,它的第 *i* 个元素是一支给定的股票在第 *i* 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 *两笔* 交易。
**注意:** 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
**示例 1:**
```
输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
```
**示例 2:**
```
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
```
**示例 3:**
```
输入: [7,6,4,3,1]
输出: 0
解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。
```
### 题目解析
这里限制了最多两笔交易。
#### 状态
**第一次买入(fstBuy)****第一次卖出(fstSell)****第二次买入(secBuy)****第二次卖出(secSell)** 这四种状态。
#### 转移方程
这里最多两次买入和两次卖出,也就是说 **买入** 状态之前可拥有 **卖出** 状态,**卖出** 状态之前可拥有 **买入** 状态,所以买入和卖出的转移方程都需要变化。
- fstBuy = max(fstBuy , -price[i])
- fstSell = max(fstSell,fstBuy + prices[i] )
- secBuy = max(secBuy ,fstSell -price[i]) (受第一次卖出状态的影响)
- secSell = max(secSell ,secBuy + prices[i] )
#### 边界
* 一开始 `fstBuy = -prices[0]`
* 买入后直接卖出,`fstSell = 0`
* 买入后再卖出再买入,`secBuy - prices[0]`
* 买入后再卖出再买入再卖出,`secSell = 0`
最后返回 secSell 。
### 代码实现
```java
class Solution {
public int maxProfit(int[] prices) {
int fstBuy = Integer.MIN_VALUE, fstSell = 0;
int secBuy = Integer.MIN_VALUE, secSell = 0;
for(int i = 0; i < prices.length; i++) {
fstBuy = Math.max(fstBuy, -prices[i]);
fstSell = Math.max(fstSell, fstBuy + prices[i]);
secBuy = Math.max(secBuy, fstSell - prices[i]);
secSell = Math.max(secSell, secBuy + prices[i]);
}
return secSell;
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/w3oig.png)
\ No newline at end of file
# LeetCode第122号问题:买卖股票的最佳时机II
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
在之前有关 [**动态规划与股票问题一文**](https://github.com/MisterBooo/LeetCodeAnimation/tree/master/notes/LeetCode第122号问题:买卖股票的最佳时机II.md) 中,小吴使用了动态规划的思想进行了分析和写套路代码,但还是有一些小伙伴不是很明白,今天重新拿出一题从另外一个角度进行分析,希望能帮助大家更容易理解。
### 题目描述
题目来源于 LeetCode 上第 122 号问题:买卖股票的最佳时机 II。题目难度为 Easy,目前通过率为 53.0% 。
### 题目描述
给定一个数组,它的第 *i* 个元素是一支给定股票第 *i* 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
**注意**:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
**示例 1:**
```
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
```
**示例 2:**
```
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
```
**示例 3:**
```
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
```
### 题目解析
我们从实际场景去思考,假设你处于股票市场,你想获得最大收益的话理想操作是什么?
当然是 **低点买入,高点卖出**
举个简单的数组为例 [100,1,20,81],肉眼扫过去,第二天买第四天卖的话收益最高( 81 - 1) = 80 。
那为什么可以知道在第四天卖而不在第三天卖呢?
实际上,注意题目是没有限制买卖交易次数的,完全可以在第三天卖出去,只不过发现第四天有涨了,那么就在第三天再买回来。
` (81 - 1) = [( 20 - 1 ) + ( 81 - 20 )]`
也就是说,第二天买、第三天卖,第三天买、第四天卖这四个动作与第二天买、第四天卖结果是一致的。
**所以只需要今天的价格比昨天更高,就卖出**!(反正可以再买入)
总结一下就是:从第二天开始观察,如果当前价格(今天)比之前价格(昨天)高,则把差值加入到利润中(因为我们可以昨天买入,今天卖出,如果明天价位更高的话,还可以今天买入,明天再抛出)。以此类推,遍历完整个数组后即可求得最大利润。
### 图片描述
![买卖股票的最佳时机 II](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/nkvdj.png)
### 代码实现
```java
//程序员小吴
class Solution {
public int maxProfit(int[] prices) {
int profit = 0;
for (int i = 1 ; i < prices.length; i++){
if (prices[i] > prices[i-1]){
profit = profit + prices[i] - prices[i - 1];
}
}
return profit;
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/4s1oh.png)
# 浅谈什么是动态规划以及相关的「股票」算法题
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
## 动态规划
### 1 概念
  **动态规划**算法是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。在学习动态规划之前需要明确掌握几个重要概念。
  **阶段**:对于一个完整的问题过程,适当的切分为若干个相互联系的子问题,每次在求解一个子问题,则对应一个阶段,整个问题的求解转化为按照阶段次序去求解。
  **状态**:状态表示每个阶段开始时所处的客观条件,即在求解子问题时的已知条件。状态描述了研究的问题过程中的状况。
  **决策**:决策表示当求解过程处于某一阶段的某一状态时,可以根据当前条件作出不同的选择,从而确定下一个阶段的状态,这种选择称为决策。
  **策略**:由所有阶段的决策组成的决策序列称为全过程策略,简称策略。
  **最优策略**:在所有的策略中,找到代价最小,性能最优的策略,此策略称为最优策略。
  **状态转移方程**:状态转移方程是确定两个相邻阶段状态的演变过程,描述了状态之间是如何演变的。
### 2 使用场景
能采用动态规划求解的问题的一般要具有 3 个性质:
  (1)**最优化**:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。子问题的局部最优将导致整个问题的全局最优。换句话说,就是问题的一个最优解中一定包含子问题的一个最优解。
  (2)**无后效性**:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关,与其他阶段的状态无关,特别是与未发生的阶段的状态无关。
   (3)**重叠子问题**:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
### 3 算法流程
  (1)划分阶段:按照问题的时间或者空间特征将问题划分为若干个阶段。
  (2)确定状态以及状态变量:将问题的不同阶段时期的不同状态描述出来。
  (3)确定决策并写出状态转移方程:根据相邻两个阶段的各个状态之间的关系确定决策。
  (4)寻找边界条件:一般而言,状态转移方程是递推式,必须有一个递推的边界条件。
  (5)设计程序,解决问题
## 实战练习
下面的三道算法题都是来源于 LeetCode 上与股票买卖相关的问题 ,我们按照 **动态规划** 的算法流程来处理该类问题。
**股票买卖**这一类的问题,都是给一个输入数组,里面的每个元素表示的是每天的股价,并且你只能持有一支股票(也就是你必须在再次购买前出售掉之前的股票),一般来说有下面几种问法:
- 只能买卖一次
- 可以买卖无数次
- 可以买卖 k 次
需要你设计一个算法去获取最大的利润。
## 买卖股票的最佳时机
题目来源于 LeetCode 上第 121 号问题:买卖股票的最佳时机。题目难度为 Easy,目前通过率为 49.4% 。
### 题目描述
给定一个数组,它的第 *i* 个元素是一支给定股票第 *i* 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
注意你不能在买入股票前卖出股票。
**示例 1:**
```
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
```
**示例 2:**
```
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
```
### 题目解析
我们按照动态规划的思想来思考这道问题。
#### 状态
**买入(buy)****卖出(sell)** 这两种状态。
#### 转移方程
对于买来说,买之后可以卖出(进入卖状态),也可以不再进行股票交易(保持买状态)。
对于卖来说,卖出股票后不在进行股票交易(还在卖状态)。
只有在手上的钱才算钱,手上的钱购买当天的股票后相当于亏损。也就是说当天买的话意味着损失`-prices[i]`,当天卖的话意味着增加`prices[i]`,当天卖出总的收益就是 `buy+prices[i]`
所以我们只要考虑当天买和之前买哪个收益更高,当天卖和之前卖哪个收益更高。
- buy = max(buy, -price[i]) (注意:根据定义 buy 是负数)
- sell = max(sell, prices[i] + buy)
#### 边界
第一天 `buy = -prices[0]`, `sell = 0`,最后返回 sell 即可。
### 代码实现
```java
class Solution {
public int maxProfit(int[] prices) {
if(prices.length <= 1)
return 0;
int buy = -prices[0], sell = 0;
for(int i = 1; i < prices.length; i++) {
buy = Math.max(buy, -prices[i]);
sell = Math.max(sell, prices[i] + buy);
}
return sell;
}
}
```
## 买卖股票的最佳时机 II
题目来源于 LeetCode 上第 122 号问题:买卖股票的最佳时机 II。题目难度为 Easy,目前通过率为 53.0% 。
### 题目描述
给定一个数组,它的第 *i* 个元素是一支给定股票第 *i* 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
**注意**:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
**示例 1:**
```
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
```
**示例 2:**
```
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
```
**示例 3:**
```
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
```
### 题目解析
#### 状态
**买入(buy)****卖出(sell)** 这两种状态。
#### 转移方程
对比上题,这里可以有无限次的买入和卖出,也就是说 **买入** 状态之前可拥有 **卖出** 状态,所以买入的转移方程需要变化。
- buy = max(buy, sell - price[i])
- sell = max(sell, buy + prices[i] )
#### 边界
第一天 `buy = -prices[0]`, `sell = 0`,最后返回 sell 即可。
### 代码实现
```java
class Solution {
public int maxProfit(int[] prices) {
if(prices.length <= 1)
return 0;
int buy = -prices[0], sell = 0;
for(int i = 1; i < prices.length; i++) {
sell = Math.max(sell, prices[i] + buy);
buy = Math.max( buy,sell - prices[i]);
}
return sell;
}
}
```
## 买卖股票的最佳时机 III
题目来源于 LeetCode 上第 123 号问题:买卖股票的最佳时机 III。题目难度为 Hard,目前通过率为 36.1% 。
### 题目描述
给定一个数组,它的第 *i* 个元素是一支给定的股票在第 *i* 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 *两笔* 交易。
**注意:** 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
**示例 1:**
```
输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
```
**示例 2:**
```
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
```
**示例 3:**
```
输入: [7,6,4,3,1]
输出: 0
解释: 在这个情况下, 没有交易完成, 所以最大利润为 0。
```
### 题目解析
这里限制了最多两笔交易。
#### 状态
**第一次买入(fstBuy)****第一次卖出(fstSell)****第二次买入(secBuy)****第二次卖出(secSell)** 这四种状态。
#### 转移方程
这里最多两次买入和两次卖出,也就是说 **买入** 状态之前可拥有 **卖出** 状态,**卖出** 状态之前可拥有 **买入** 状态,所以买入和卖出的转移方程都需要变化。
- fstBuy = max(fstBuy , -price[i])
- fstSell = max(fstSell,fstBuy + prices[i] )
- secBuy = max(secBuy ,fstSell -price[i]) (受第一次卖出状态的影响)
- secSell = max(secSell ,secBuy + prices[i] )
#### 边界
- 一开始 `fstBuy = -prices[0]`
- 买入后直接卖出,`fstSell = 0`
- 买入后再卖出再买入,`secBuy - prices[0]`
- 买入后再卖出再买入再卖出,`secSell = 0`
最后返回 secSell 。
### 代码实现
```java
class Solution {
public int maxProfit(int[] prices) {
int fstBuy = Integer.MIN_VALUE, fstSell = 0;
int secBuy = Integer.MIN_VALUE, secSell = 0;
for(int i = 0; i < prices.length; i++) {
fstBuy = Math.max(fstBuy, -prices[i]);
fstSell = Math.max(fstSell, fstBuy + prices[i]);
secBuy = Math.max(secBuy, fstSell - prices[i]);
secSell = Math.max(secSell, secBuy + prices[i]);
}
return secSell;
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/pr1o6.png)
# LeetCode 第 125 号问题:验证回文串
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 第 125 号问题:验证回文串。这道题目是 **初级程序员** 在面试的时候经常遇到的一道算法题,而且面试官喜欢面试者手写!
### 题目描述
给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。
**说明:**本题中,我们将空字符串定义为有效的回文串。
**示例 1:**
```
输入: "A man, a plan, a canal: Panama"
输出: true
```
**示例 2:**
```
输入: "race a car"
输出: false
```
### 题目解析
先理解一个概念:所谓回文,就是一个正读和反读都一样的字符串。
先假设是验证单词 `level` 是否是回文字符串,通过概念涉及到 正 与 反 ,那么很容易想到使用双指针,从字符的开头和结尾处开始遍历整个字符串,相同则继续向前寻找,不同则直接返回 false。
而这里与单独验证一个单词是否是回文字符串有所区别的是加入了 空格 与 非字母数字的字符,但实际上的做法一样的:
一开始先建立两个指针,left 和 right , 让它们分别从字符的开头和结尾处开始遍历整个字符串。
如果遇到非字母数字的字符就跳过,继续往下找,直到找到下一个字母数字或者结束遍历,如果遇到大写字母,就将其转为小写。
当左右指针都找到字母数字时,可以进行比较的时候,比较这两个字符,如果相等,则两个指针向它们的前进方向挪动,然后继续比较下面两个分别找到的字母数字,若不相等,直接返回 false。
### 动画描述
![](<https://bucket-1257126549.cos.ap-guangzhou.myqcloud.com/blog/pvbiv.gif>)
### 代码实现
注:`isLetterOrDigit ` 方法确定指定的字符是否为字母或数字。
```java
class Solution {
public boolean isPalindrome(String s) {
if(s.length() == 0)
return true;
int l = 0, r = s.length() - 1;
while(l < r){
//确定指定的字符是否为字母或数字
if(!Character.isLetterOrDigit(s.charAt(l))){
l++;
}else if(!Character.isLetterOrDigit(s.charAt(r))){
r--;
}else{
if(Character.toLowerCase(s.charAt(l)) != Character.toLowerCase(s.charAt(r)))
return false;
l++;
r--;
}
}
return true;
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/k5tcp.png)
\ No newline at end of file
# LeetCode 第 131 号问题:分割回文串
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 131 号问题:分割回文串。题目难度为 Medium,目前通过率为 45.8% 。
### 题目描述
给定一个字符串 *s*,将 *s* 分割成一些子串,使每个子串都是回文串。
返回 *s* 所有可能的分割方案。
**示例:**
```yaml
输入: "aab"
输出:
[
["aa","b"],
["a","a","b"]
]
```
###
### 题目解析
首先,对于一个字符串的分割,肯定需要将所有分割情况都遍历完毕才能判断是不是回文数。不能因为 **abba** 是回文串,就认为它的所有子串都是回文的。
既然需要将所有的分割方法都找出来,那么肯定需要用到DFS(深度优先搜索)或者BFS(广度优先搜索)。
在分割的过程中对于每一个字符串而言都可以分为两部分:左边一个回文串加右边一个子串,比如 "abc" 可分为 "a" + "bc" 。 然后对"bc"分割仍然是同样的方法,分为"b"+"c"。
在处理的时候去优先寻找更短的回文串,然后回溯找稍微长一些的回文串分割方法,不断回溯,分割,直到找到所有的分割方法。
举个🌰:分割"aac"。
1. 分割为 a + ac
2. 分割为 a + a + c,分割后,得到一组结果,再回溯到 a + ac
3. a + ac 中 ac 不是回文串,继续回溯,回溯到 aac
4. 分割为稍长的回文串,分割为 aa + c 分割完成得到一组结果,再回溯到 aac
5. aac 不是回文串,搜索结束
### 动画描述
![](<https://bucket-1257126549.cos.ap-guangzhou.myqcloud.com/blog/4r4mw.gif>)
### 代码实现
```java
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> partition(String s) {
if(s==null||s.length()==0)
return res;
dfs(s,new ArrayList<String>(),0);
return res;
}
public void dfs(String s,List<String> remain,int left){
if(left==s.length()){ //判断终止条件
res.add(new ArrayList<String>(remain)); //添加到结果中
return;
}
for(int right=left;right<s.length();right++){ //从left开始,依次判断left->right是不是回文串
if(isPalindroom(s,left,right)){ //判断是否是回文串
remain.add(s.substring(left,right+1)); //添加到当前回文串到list中
dfs(s,remain,right+1); //从right+1开始继续递归,寻找回文串
remain.remove(remain.size()-1); //回溯,从而寻找更长的回文串
}
}
}
/**
* 判断是否是回文串
*/
public boolean isPalindroom(String s,int left,int right){
while(left<right&&s.charAt(left)==s.charAt(right)){
left++;
right--;
}
return left>=right;
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/buypf.png)
\ No newline at end of file
# LeetCode 第 136 号问题:只出现一次的数字
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 136 号问题:只出现一次的数字。题目难度为 Easy,目前通过率为 66.8% 。
### 题目描述
给定一个**非空**整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
**说明:**
你的算法应该具有线性时间复杂度。 你可以不使用额外空间来实现吗?
**示例 1:**
```
输入: [2,2,1]
输出: 1
```
**示例 2:**
```
输入: [4,1,2,1,2]
输出: 4
```
### 题目解析
根据题目描述,由于加上了时间复杂度必须是 O(n) ,并且空间复杂度为 O(1) 的条件,因此不能用排序方法,也不能使用 map 数据结构。
程序员小吴想了一下午没想出来,答案是使用 **位操作Bit Operation** 来解此题。
将所有元素做异或运算,即a[1] ⊕ a[2] ⊕ a[3] ⊕ …⊕ a[n],所得的结果就是那个只出现一次的数字,时间复杂度为O(n)。
### 异或
异或运算A ⊕ B的真值表如下:
| A | B | ⊕ |
| :--- | :--: | ---: |
| F | F | F |
| F | T | T |
| T | F | T |
| T | T | F |
### 动画演示
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/8720h.gif)
### 进阶版
有一个 n 个元素的数组,除了两个数只出现一次外,其余元素都出现两次,让你找出这两个只出现一次的数分别是几,要求时间复杂度为 O(n) 且再开辟的内存空间固定(与 n 无关)。
#### 示例 :
输入: [1,2,2,1,3,4]
输出: [3,4]
### 题目再解析
根据前面找一个不同数的思路算法,在这里把所有元素都异或,那么得到的结果就是那两个只出现一次的元素异或的结果。
然后,因为这两个只出现一次的元素一定是不相同的,所以这两个元素的二进制形式肯定至少有某一位是不同的,即一个为 0 ,另一个为 1 ,现在需要找到这一位。
根据异或的性质 `任何一个数字异或它自己都等于 0 `,得到这个数字二进制形式中任意一个为 1 的位都是我们要找的那一位。
再然后,以这一位是 1 还是 0 为标准,将数组的 n 个元素分成两部分。
- 将这一位为 0 的所有元素做异或,得出的数就是只出现一次的数中的一个
- 将这一位为 1 的所有元素做异或,得出的数就是只出现一次的数中的另一个。
这样就解出题目。忽略寻找不同位的过程,总共遍历数组两次,时间复杂度为O(n)。
### 动画再演示
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/5uz1n.gif)
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/2hfta.png)
\ No newline at end of file
# LeetCode 第 138 号问题:复制带随机指针的链表
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 138 号问题:复制带随机指针的链表。题目难度为 Medium,目前通过率为 40.5% 。
### 题目描述
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的**深拷贝**
**示例:**
```
输入:
{"$id":"1","next":{"$id":"2","next":null,"random":{"$ref":"2"},"val":2},"random":{"$ref":"2"},"val":1}
解释:
节点 1 的值是 1,它的下一个指针和随机指针都指向节点 2 。
节点 2 的值是 2,它的下一个指针指向 null,随机指针指向它自己。
```
### 题目解析
1. 在原链表的每个节点后面拷贝出一个新的节点
2. 依次给新的节点的随机指针赋值,而且这个赋值非常容易 cur->next->random = cur->random->next
3. 断开链表可得到深度拷贝后的新链表
之所以说这个方法比较巧妙是因为相较于一般的解法(如使用 Hash map )来处理,上面这个解法 **不需要占用额外的空间**
### 动画描述
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/whvl5.gif)
### 代码实现
我发现带指针的题目使用 C++ 版本更容易描述,所以下面的代码实现是 C++ 版本。
```c++
class Solution {
public:
RandomListNode *copyRandomList(RandomListNode *head) {
if (!head) return NULL;
RandomListNode *cur = head;
while (cur) {
RandomListNode *node = new RandomListNode(cur->label);
node->next = cur->next;
cur->next = node;
cur = node->next;
}
cur = head;
while (cur) {
if (cur->random) {
cur->next->random = cur->random->next;
}
cur = cur->next->next;
}
cur = head;
RandomListNode *res = head->next;
while (cur) {
RandomListNode *tmp = cur->next;
cur->next = tmp->next;
if(tmp->next) tmp->next = tmp->next->next;
cur = cur->next;
}
return res;
}
};
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/ijlxu.png)
# LeetCode 第 139 号问题:单词拆分
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 139 号问题:单词拆分。
### 题目描述
给定一个**非空**字符串 *s* 和一个包含**非空**单词列表的字典 *wordDict*,判定 *s* 是否可以被空格拆分为一个或多个在字典中出现的单词。
**说明:**
- 拆分时可以重复使用字典中的单词。
- 你可以假设字典中没有重复的单词。
### 题目解析
**分割回文串** 有些类似,都是拆分,但是如果此题采取 深度优先搜索 的方法来解决的话,答案是超时的,不信的同学可以试一下~
为什么会超时呢?
因为使用 深度优先搜索 会重复的计算了有些位的可拆分情况,这种情况的优化肯定是需要 动态规划 来处理的。
如果不知道动态规划的,可以看一下小吴之前的万字长文,比较详细的介绍了动态规划的概念。
在这里,只需要去定义一个数组 boolean[] memo,其中第 i 位 memo[i] 表示待拆分字符串从第 0 位到第 i-1 位是否可以被成功地拆分。
然后分别计算每一位是否可以被成功地拆分。
### 动画描述
暂无~
### 代码实现
```java
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
int max_length=0;
for(String temp:wordDict){
max_length = temp.length() > max_length ? temp.length() : max_length;
}
// memo[i] 表示 s 中以 i - 1 结尾的字符串是否可被 wordDict 拆分
boolean[] memo = new boolean[n + 1];
memo[0] = true;
for (int i = 1; i <= n; i++) {
for (int j = i-1; j >= 0 && max_length >= i - j; j--) {
if (memo[j] && wordDict.contains(s.substring(j, i))) {
memo[i] = true;
break;
}
}
}
return memo[n];
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/8s46l.png)
\ No newline at end of file
# 使用快慢指针求解「环形链表」so easy!
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
今天分享的题目来源于 LeetCode 上第 141 号问题:环形链表。题目难度为 Easy,目前通过率为 40.4% 。
使用快慢指针的方式去求解 **so easy**
### 题目描述
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 `pos` 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 `pos``-1`,则在该链表中没有环。
**示例 1:**
```
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/vweoq.png)
**示例 2:**
```
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/kxbrz.png)
**示例 3:**
```
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/w3vsg.png)
**进阶:**
你能用 O(1)(即,常量)内存解决此问题吗?
### 题目解析
这道题是快慢指针的**经典应用**
设置两个指针,一个每次走一步的**慢指针**和一个每次走两步的**快指针**
* 如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环
* 如果含有环,快指针会超慢指针一圈,和慢指针相遇,说明链表含有环。
### 动画描述
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/mj4qo.gif)
### 代码实现
```java
//author:程序员小吴
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head, fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
if (slow == fast) return true;
}
return false;
}
}
```
# LeetCode 第 144 号问题:二叉树的前序遍历
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 144 号问题:二叉树的前序遍历。题目难度为 Medium,目前通过率为 59.8% 。
### 题目描述
给定一个二叉树,返回它的 *前序* 遍历。
**示例:**
```
输入: [1,null,2,3]
1
\
2
/
3
输出: [1,2,3]
```
**进阶:** 递归算法很简单,你可以通过迭代算法完成吗?
### 题目解析
**栈(Stack)**的思路来处理问题。
前序遍历的顺序为**根-左-右**,具体算法为:
- 把根节点 push 到栈中
- 循环检测栈是否为空,若不空,则取出栈顶元素,保存其值
- 看其右子节点是否存在,若存在则 push 到栈中
- 看其左子节点,若存在,则 push 到栈中。
### 动画描述
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/uu82j.gif)
### 代码实现
```
class Solution {
public List<Integer> preorderTraversal(TreeNode root) {
//非递归前序遍历,需要借助栈
Stack<TreeNode> stack = new Stack<>();
List<Integer> list = new LinkedList<>();
//当树为空树时,直接返回一个空list
if(root == null){
return list;
}
//第一步是将根节点压入栈中
stack.push(root);
//当栈不为空时,出栈的元素插入list尾部。
//当它的孩子不为空时,将孩子压入栈,一定是先压右孩子再压左孩子
while(!stack.isEmpty()){
//此处的root只是一个变量的复用
root = stack.pop();
list.add(root.val);
if(root.right != null) stack.push(root.right);
if(root.left != null) stack.push(root.left);
}
return list;
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/obddd.png)
\ No newline at end of file
# LeetCode 第 145 号问题:二叉树的后序遍历
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 145 号问题:二叉树的后序遍历。题目难度为 Hard,目前通过率为 25.8% 。
### 题目描述
给定一个二叉树,返回它的 *后序* 遍历。
**示例:**
```
输入: [1,null,2,3]
1
\
2
/
3
输出: [3,2,1]
```
**进阶:** 递归算法很简单,你可以通过迭代算法完成吗?
### 题目解析
**栈(Stack)**的思路来处理问题。
后序遍历的顺序为**左-右-根**,具体算法为:
- 先将根结点压入栈,然后定义一个辅助结点 head
- while 循环的条件是栈不为空
- 在循环中,首先将栈顶结点t取出来
- 如果栈顶结点没有左右子结点,或者其左子结点是 head,或者其右子结点是 head 的情况下。我们将栈顶结点值加入结果 res 中,并将栈顶元素移出栈,然后将 head 指向栈顶元素
- 否则的话就看如果右子结点不为空,将其加入栈
- 再看左子结点不为空的话,就加入栈
### 动画描述
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/y7nxo.gif)
### 代码实现
```
public class Solution {
public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>();
if(root == null)
return res;
Stack<TreeNode> stack = new Stack<TreeNode>();
stack.push(root);
while(!stack.isEmpty()){
TreeNode node = stack.pop();
if(node.left != null) stack.push(node.left);//和传统先序遍历不一样,先将左结点入栈
if(node.right != null) stack.push(node.right);//后将右结点入栈
res.add(0,node.val); //逆序添加结点值
}
return res;
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/8yuu3.png)
\ No newline at end of file
# LeetCode 第 146 号问题:LRU缓存机制
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 146 号问题:LRU缓存机制。题目难度为 Hard,目前通过率为 15.8% 。
### 题目描述
运用你所掌握的数据结构,设计和实现一个 [LRU (最近最少使用) 缓存机制](https://baike.baidu.com/item/LRU)。它应该支持以下操作: 获取数据 `get` 和 写入数据 `put`
获取数据 `get(key)` - 如果密钥 (key) 存在于缓存中,则获取密钥的值(总是正数),否则返回 -1。
写入数据 `put(key, value)` - 如果密钥不存在,则写入其数据值。当缓存容量达到上限时,它应该在写入新数据之前删除最近最少使用的数据值,从而为新的数据值留出空间。
**进阶:**
你是否可以在 **O(1)** 时间复杂度内完成这两种操作?
**示例:**
```
LRUCache cache = new LRUCache( 2 /* 缓存容量 */ );
cache.put(1, 1);
cache.put(2, 2);
cache.get(1); // 返回 1
cache.put(3, 3); // 该操作会使得密钥 2 作废
cache.get(2); // 返回 -1 (未找到)
cache.put(4, 4); // 该操作会使得密钥 1 作废
cache.get(1); // 返回 -1 (未找到)
cache.get(3); // 返回 3
cache.get(4); // 返回 4
```
### 题目解析
这道题是让我们实现一个 LRU 缓存器,LRU是Least Recently Used的简写,就是最近最少使用的意思。
这个缓存器主要有两个成员函数,get和put。
其中 get 函数是通过输入 key 来获得 value,如果成功获得后,这对 (key, value) 升至缓存器中最常用的位置(顶部),如果 key 不存在,则返回 -1 。
而 put 函数是插入一对新的 (key, value),如果原缓存器中有该 key,则需要先删除掉原有的,将新的插入到缓存器的顶部。如果不存在,则直接插入到顶部。
若加入新的值后缓存器超过了容量,则需要删掉一个最不常用的值,也就是底部的值。
具体实现时我们需要三个私有变量,cap , l 和 m,其中 cap 是缓存器的容量大小,l 是保存缓存器内容的列表,m 是 HashMap,保存关键值 key 和缓存器各项的迭代器之间映射,方便我们以 O(1) 的时间内找到目标项。
然后我们再来看 get 和 put 如何实现。
其中,get 相对简单些,我们在 m 中查找给定的key,若不存在直接返回 -1;如果存在则将此项移到顶部。
对于 put ,我们也是现在 m 中查找给定的 key,如果存在就删掉原有项,并在顶部插入新来项,然后判断是否溢出,若溢出则删掉底部项(最不常用项)。
### 动画描述
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/90896.gif)
### 代码实现
```c++
class LRUCache{
public:
LRUCache(int capacity) {
cap = capacity;
}
int get(int key) {
auto it = m.find(key);
if (it == m.end()) return -1;
l.splice(l.begin(), l, it->second);
return it->second->second;
}
void put(int key, int value) {
auto it = m.find(key);
if (it != m.end()) l.erase(it->second);
l.push_front(make_pair(key, value));
m[key] = l.begin();
if (m.size() > cap) {
int k = l.rbegin()->first;
l.pop_back();
m.erase(k);
}
}
private:
int cap;
list<pair<int, int>> l;
unordered_map<int, list<pair<int, int>>::iterator> m;
};
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/inind.png)
\ No newline at end of file
# LeetCode 第 150 号问题:逆波兰表达式求值
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 150 号问题:逆波兰表达式求值。题目难度为 Medium,目前通过率为 43.7% 。
### 题目描述
根据[逆波兰表示法](https://baike.baidu.com/item/%E9%80%86%E6%B3%A2%E5%85%B0%E5%BC%8F/128437),求表达式的值。
有效的运算符包括 `+`, `-`, `*`, `/` 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
**说明:**
- 整数除法只保留整数部分。
- 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。
**示例 1:**
```
输入: ["2", "1", "+", "3", "*"]
输出: 9
解释: ((2 + 1) * 3) = 9
```
**示例 2:**
```
输入: ["4", "13", "5", "/", "+"]
输出: 6
解释: (4 + (13 / 5)) = 6
```
**示例 3:**
```
输入: ["10", "6", "9", "3", "+", "-11", "*", "/", "*", "17", "+", "5", "+"]
输出: 22
解释:
((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22
```
### 题目解析
用数据结构`栈`来解决这个问题。
- 从前往后遍历数组
- 遇到数字则压入栈中
- 遇到符号,则把栈顶的两个数字拿出来运算,把结果再压入栈中
- 遍历完整个数组,栈顶数字即为最终答案
### 动画描述
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/4ieg7.gif)
### 代码实现
```
class Solution {
public:
int evalRPN(vector<string>& tokens) {
stack<int> nums;
stack<char> ops;
for(const string& s: tokens){
if(s == "+" || s == "-" || s == "*" || s == "/"){
int a = nums.top();
nums.pop();
int b = nums.top();
nums.pop();
if(s == "+"){
nums.push(b + a);
}else if(s == "-"){
nums.push(b - a);
} else if(s == "*"){
nums.push(b * a);
}else if(s == "/"){
nums.push(b / a);
}
}
else{
nums.push(atoi(s.c_str()));
}
}
return nums.top();
}
};
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/fhe4g.png)
\ No newline at end of file
# LeetCode 第 15 号问题:三数之和
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 15 号问题:三数之和。
### 题目描述
给定一个包含 *n* 个整数的数组 `nums`,判断 `nums` 中是否存在三个元素 *a,b,c ,*使得 *a + b + c =* 0 ?找出所有满足条件且不重复的三元组。
### 题目解析
题目需要我们找出三个数且和为 0 ,那么除了三个数全是 0 的情况之外,肯定会有负数和正数,所以一开始可以先选择一个数,然后再去找另外两个数,这样只要找到两个数且和为第一个选择的数的相反数就行了。也就是说需要枚举 a 和 b ,将 c 的存入 map 即可。
需要注意的是返回的结果中,不能有有重复的结果。这样的代码时间复杂度是 O(n^2)。在这里可以先将原数组进行排序,然后再遍历排序后的数组,这样就可以使用双指针以线性时间复杂度来遍历所有满足题意的两个数组合。
### 动画描述
待补充
### 代码实现
###
```c++
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
sort(nums.begin(), nums.end());
if (nums.empty() || nums.back() < 0 || nums.front() > 0) return {};
for (int k = 0; k < nums.size(); ++k) {
if (nums[k] > 0) break;
if (k > 0 && nums[k] == nums[k - 1]) continue;
int target = 0 - nums[k];
int i = k + 1, j = nums.size() - 1;
while (i < j) {
if (nums[i] + nums[j] == target) {
res.push_back({nums[k], nums[i], nums[j]});
while (i < j && nums[i] == nums[i + 1]) ++i;
while (i < j && nums[j] == nums[j - 1]) --j;
++i; --j;
} else if (nums[i] + nums[j] < target) ++i;
else --j;
}
}
return res;
}
};
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/m1phx.gif)
\ No newline at end of file
# LeetCode 第 167 号问题:两数之和 II - 输入有序数组
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 167 号问题:两数之和 II - 输入有序数组。题目难度为 Easy,目前通过率为 48.2% 。
### 题目描述
给定一个已按照**升序排列** 的有序数组,找到两个数使得它们相加之和等于目标数。
函数应该返回这两个下标值 index1 和 index2,其中 index1 必须小于 index2*。*
**说明:**
- 返回的下标值(index1 和 index2)不是从零开始的。
- 你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。
**示例:**
```
输入: numbers = [2, 7, 11, 15], target = 9
输出: [1,2]
解释: 2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。
```
### 题目解析
初始化左指针 left 指向数组起始,初始化右指针 right 指向数组结尾。
根据**已排序**这个特性,
- (1)如果 numbers[left] 与 numbers[right] 的和 tmp 小于 target ,说明应该增加 tmp ,因此 left 右移指向一个较大的值。
- (2)如果 tmp大于 target ,说明应该减小 tmp ,因此 right 左移指向一个较小的值。
- (3)tmp 等于 target ,则找到,返回 left + 1 和 right + 1。(注意以 1 为起始下标)
### 动画描述
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/59rnm.gif)
### 代码实现
```
// 对撞指针
// 时间复杂度: O(n)
// 空间复杂度: O(1)
class Solution {
public:
vector<int> twoSum(vector<int>& numbers, int target) {
int l = 0, r = numbers.size() - 1;
while(l < r){
if(numbers[l] + numbers[r] == target){
int res[2] = {l+1, r+1};
return vector<int>(res, res+2);
}
else if(numbers[l] + numbers[r] < target)
l ++;
else // numbers[l] + numbers[r] > target
r --;
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/9lro6.png)
\ No newline at end of file
# 【数组中超过一半的数字】三种解法,最后一个解法太牛逼了!
今天分享的题目来源于 LeetCode 上第 169 号问题:求众数(求数组中超过一半的数字)。题目难度为 Easy,目前通过率为 45.8% 。
最后一种解法 **Cool** !!!
# 题目描述
给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素。
你可以假设数组是非空的,并且给定的数组总是存在众数。
**示例 1:**
```
输入: [3,2,3]
输出: 3
```
**示例 2:**
```
输入: [2,2,1,1,1,2,2]
输出: 2
```
# 题目解析
题目意思很好理解:给你一个数组,里面有一个数字出现的次数超过了一半,你要找到这个数字并返回。
## 解法一:暴力解法
遍历整个数组,同时统计每个数字出现的次数。
最后将出现次数大于一半的元素返回即可。
### 动画描述
![](https://raw.githubusercontent.com/MisterBooo/myBlogPic/master/20190626114223.gif)
### **代码实现**
```java
class Solution {
public int majorityElement(int[] nums) {
int majorityCount = nums.length/2;
for (int num : nums) {
int count = 0;
for (int elem : nums) {
if (elem == num) {
count += 1;
}
}
if (count > majorityCount) {
return num;
}
}
}
}
```
### 复杂度分析
**时间复杂度**:O(n<sup>2</sup>)
暴力解法包含两重嵌套的 for 循环,每一层 n 次迭代,因此时间复杂度为 O(n<sup>2</sup>) 。
**空间复杂度**:O(1)
暴力解法没有分配任何与输入规模成比例的额外的空间,因此空间复杂度为 O(1)。
## 解法二:哈希表法
这个问题可以视为查找问题,对于查找问题往往可以使用时间复杂度为 O(1) 的 **哈希表**,通过以空间换时间的方式进行优化。
直接遍历整个 **数组** ,将每一个数字(num)与它出现的次数(count)存放在 **哈希表** 中,同时判断该数字出现次数是否是最大的,动态更新 maxCount,最后输出 maxNum。
### 动画描述
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/bbjtv.gif)
### 代码实现
```java
class Solution {
public int majorityElement(int[] nums) {
Map<Integer, Integer> map = new HashMap<>();
// maxNum 表示元素,maxCount 表示元素出现的次数
int maxNum = 0, maxCount = 0;
for (int num: nums) {
int count = map.getOrDefault(num, 0) + 1;
map.put(num, count);
if (count > maxCount) {
maxCount = count;
maxNum = num;
}
}
return maxNum;
}
}
```
### 复杂度分析
**时间复杂度**:O(n)
总共有一个循环,里面哈希表的插入是常数时间的,因此时间复杂度为 O(n)。
**空间复杂度**:O(n)
哈希表占用了额外的空间 O(n),因此空间复杂度为 O(n)。
## 解法三:摩尔投票法
再来回顾一下题目:寻找数组中超过一半的数字,这意味着数组中**其他数字出现次数的总和都是比不上这个数字出现的次数**
即如果把 该众数记为 `+1` ,把其他数记为 `−1` ,将它们全部加起来,和是大于 0 的。
所以可以这样操作:
* 设置两个变量 candidate 和 count,**candidate** 用来保存数组中遍历到的某个数字,**count** 表示当前数字的出现次数,一开始 **candidate** 保存为数组中的第一个数字,**count** 为 1
* 遍历整个数组
* 如果数字与之前 **candidate** 保存的数字相同,则 **count** 加 1
* 如果数字与之前 **candidate** 保存的数字不同,则 **count** 减 1
* 如果出现次数 **count** 变为 0 ,**candidate** 进行变化,保存为当前遍历的那个数字,并且同时把 **count** 重置为 1
* 遍历完数组中的所有数字即可得到结果
### 动画描述
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/8wyb2.gif)
### 代码实现
```java
class Solution {
public int majorityElement(int[] nums) {
int candidate = nums[0], count = 1;
for (int i = 1; i < nums.length; ++i) {
if (count == 0) {
candidate = nums[i];
count = 1;
} else if (nums[i] == candidate) {
count++;
} else{
count--;
}
}
return candidate;
}
}
```
### 复杂度分析
**时间复杂度**:O(n)
总共只有一个循环,因此时间复杂度为 O(n)。
**空间复杂度**:O(1)
只需要常数级别的额外空间,因此空间复杂度为 O(1)。
\ No newline at end of file
# LeetCode第 172 号问题:阶乘后的零
> 本文首发于公众号「五分钟学算法」,是[图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>)系列文章之一。
>
> 个人网站:[https://www.cxyxiaowu.com](https://www.cxyxiaowu.com)
题目来源于 LeetCode 上第 172 号问题:阶乘后的零。题目难度为 Easy,目前通过率为 38.0% 。
### 题目描述
给定一个整数 *n*,返回 *n*! 结果尾数中零的数量。
**示例 1:**
```
输入: 3
输出: 0
解释: 3! = 6, 尾数中没有零。
```
**示例 2:**
```
输入: 5
输出: 1
解释: 5! = 120, 尾数中有 1 个零.
```
**说明:** 你算法的时间复杂度应为 *O*(log *n*) 。
### 题目解析
题目很好理解,数阶乘后的数字末尾有多少个零。
最简单粗暴的方法就是先乘完再说,然后一个一个数。
事实上,你在使用暴力破解法的过程中就能发现规律: **这 9 个数字中只有 2(它的倍数) 与 5 (它的倍数)相乘才有 0 出现**
所以,现在问题就变成了这个阶乘数中能配 **多少对 2 与 5**
举个复杂点的例子:
` 10! = 【 2 *( 2 * 2 )* 5 *( 2 * 3 )*( 2 * 2 * 2 )*( 2 * 5)】`
在 10!这个阶乘数中可以匹配两对 2 * 5 ,所以10!末尾有 2 个 0。
可以发现,一个数字进行拆分后 2 的个数肯定是大于 5 的个数的,所以能匹配多少对取决于 5 的个数。(好比现在男女比例悬殊,最多能有多少对异性情侣取决于女生的多少)。
那么问题又变成了 **统计阶乘数里有多少个 5 这个因子**
需要注意的是,像 25,125 这样的不只含有一个 5 的数字的情况需要考虑进去。
比如 `n = 15`。那么在 `15!` 中 有 `3``5` (来自其中的`5`, `10`, `15`), 所以计算 `n/5` 就可以 。
但是比如 `n=25`,依旧计算 `n/5` ,可以得到 `5``5`,分别来自其中的`5, 10, 15, 20, 25`,但是在 `25` 中其实是包含 `2 ``5` 的,这一点需要注意。
所以除了计算 `n/5` , 还要计算 `n/5/5 , n/5/5/5 , n/5/5/5/5 , ..., n/5/5/5,,,/5`直到商为0,然后求和即可。
### 代码实现
```java
public class Solution {
public int trailingZeroes(int n) {
return n == 0 ? 0 : n / 5 + trailingZeroes(n / 5);
}
}
```
![](https://blog-1257126549.cos.ap-guangzhou.myqcloud.com/blog/tvt94.png)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment