Unverified Commit 505dc412 authored by chilimyan's avatar chilimyan Committed by GitHub
Browse files

Merge pull request #2 from MisterBooo/master

更新项目
parents b67eec32 109bddb3
This image diff could not be displayed because it is too large. You can view the blob instead.
# LeetCode 第 5 号问题:最长回文串
> 本文首发于公众号「图解面试算法」,是 [图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>) 系列文章之一。
>
> 同步博客:https://www.algomooc.com
>
题目来源于 LeetCode 上第 5 号问题:最长回文串。题目难度为 Medium,目前通过率为 29% 。
## 题目描述
给定一个字符串,要求这个字符串当中最长的回文串。
## 示例
```
Input: "babad"
Output: "bab"
Note: "aba" is also a valid answer.
```
```
Input: "cbbd"
Output: "bb"
```
## 题目分析
这道题目是典型的看着简单,但是实际上并不简单的问题。
我们先从简单的算法开始,最简单的方法当然是暴力。由于我们需要求出最长的回文串,一种方法是求出s串所有的子串,然后一一对比它们是否构成回文。这样当然是可行的,但是我们简单分析一下复杂度就会发现,这并不能接受。对于一个长度为n的字符串来说,我们任意选择其中两个位置,就可以找到它的一个子串,那么我们选择两个位置的数量就是$C_n^2 = \frac{n(n-1)}{2}$。对于每一个子串,我们需要遍历一遍才能判断是否回文,所以整体的复杂度是$O(n^3)$。
但是如果你对回文串非常熟悉的话,会发现其实这是可以优化的。因为我们要求的是最长的回文串,如果我们确定了对称中心的位置,它能够构成的最长回文串就是确定的。所以我们只需要遍历所有的回文串中心,和每个中心能找到的最长回文串。这样我们的复杂度就降低了一维,变成了$O(n^2)$。
回文串有两种形式,一种是奇回文,也就是回文中心是一个字符,比如aba。还有一种是偶回文,回文中心是两个字符之间,比如abba。这两种情况我们需要分开讨论。
我们写出代码:
```python
class Solution:
def longestPalindrome(self, s: str) -> str:
n = len(s)
ret = ''
for i in range(n):
# 奇回文的情况
l, r = i, i
while s[l] == s[r]:
l -= 1
r += 1
if l < 0 or r >= n:
break
if r - l - 1 > len(ret):
ret = s[l+1: r]
# 偶回文的情况
l, r = i-1, i
while l >= 0 and s[l] == s[r]:
l -= 1
r += 1
if l < 0 or r >= n:
break
if r - l - 1 > len(ret):
ret = s[l+1: r]
return ret
```
到这里还没有结束,接下来我们介绍一个经典的回文串求解算法——Manacher,也叫做马拉车算法。
首先,我们需要统一奇回文和偶回文这两种情况,这也很方便,我们把原串进行处理,在两个相邻字符当中插入一个分隔字符#,比如abcd转化成#a#b#c#d#。一般我们还会在首尾加入防止超界的字符,比如$&等。之后我们维护两个值,分别是id和mr。mr表示当前能够构成的回文串向右延伸最远的位置,id表示这个位置对应的对称中心。根据这个位置id以及mr我们可以快速地求解出当前位置i能够构成的合法回文串的长度。
我们假设每一个位置构成的合法回文串半径是p[i], 那么对于i这个位置,我们可以得到p[i] >= min(mr - i, p[id * 2 - i])。其中id * 2 - i是i这个位置关于id的对称位置,并且以i为中心对称的回文串小于mr位置的部分也关于id对称。所以如果p[id * 2 - i] < mr - i的话,说明i关于id的对称位置没能突破id对称的限制,既然i的对称点没有能突破限制,那么i显然也不行。同理,如果p[id * 2 - i] > mr - i的话,说明i的对称位置没有被id限制住,但是这恰恰说明i被限制住了。因为如果i也能突破mr这个限制的话,那么说明id的对称范围还能扩大,这和我们的前提假设矛盾了。所以只有p[id * 2 - i] == mr - i的情况,i才有可能继续延伸。
如果能理解上面的关系,整个算法已经很清楚了,如果没看懂也没有关系,可以看下下面的动图,会展示得更加清楚。
理解了上述的算法过程之后剩下的工作就简单了,我们只需要在求解p[i]的同时维护id和mr即可。
最后我们来看下算法的复杂度,为什么这是一个O(n)的算法呢?原因很简单,我们只需要关注mr这个变量即可。mr这个变量是递增的,mr每次递增的大小,其实就是p[i] - (mr - i)的长度。所以虽然看似我们用了两重循环,但是由于mr最多只能递增n次,所以它依然是O(n)的算法。
#### 动画描述
![](../Animation/LeetCode5.gif)
#### 代码实现
```python
class Solution:
def longestPalindrome(self, s: str) -> str:
# 在所有字符中间插入#
def transform(s):
return '$#' + '#'.join(list(s)) + '#&'
if s == '':
return s
# 初始化
s = transform(s)
p = [0 for _ in range(len(s)+1)]
mr, id_ = 0, 0
# 首尾是特殊字符,所以下标从1到len(s)-2
for i in range(1, len(s)-1):
# 计算p[i]
p[i] = 1 if mr <= i else min(p[2*id_-i], mr - i)
# 只有当前i已经摆脱id限制,或者是第三种情况时,才有可能继续延伸
# 这个只是优化,不加这个判断一样可以运行
if mr <= i or p[2*id_-i] == mr - i:
while s[i - p[i]] == s[i + p[i]]:
p[i] += 1
if i + p[i] > mr:
mr, id_ = i + p[i], i
# 找到长度最长的下标
id_ = p.index(max(p))
# 获得整个回文的字符串
palindromic = s[id_ - p[id_]+1: id_ + p[id_]]
# 过滤掉#,还原为原字符
return ''.join(filter(lambda x: x != '#', list(palindromic)))
```
![](../../Pictures/qrcode.jpg)
\ No newline at end of file
> 本文首发于公众号「图解面试算法」,是 [图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>) 系列文章之一。
>
> 同步个人博客:https://www.zhangxiaoshuai.fun
**本题选自leetcode的第11题,medium级别,目前通过率:61.3%**
**题目描述:**
```txt
给你n个非负整数a1,a2,...,an,每个数代表坐标中的一个点(i,ai)。在坐标内画n条垂直线,
垂直线i的两个端点分别为(i,ai)和(i,0)。找出其中的两条线,使得它们与x轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器,且n的值至少为2。
示例:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
```
我们都应该听说过**木桶原理**,一个木桶可以装入多少水取决于最短的那块板;而这道题也可以与木桶装水的问题对应上。
很容易的可以得到---->**容器可以容纳水的容量=两条垂直线中最短的那条*两条线之间的距离**
现在的情况是,有很多条线,让你计算两两之间能装的最多的水,其实暴力法之间就能解决这个问题,但是它的时间复杂度也达到了**O(n^2)**
ok,那我们先试试用**暴力法**来解 决问题:
### 1.暴力法
直接上代码:
```java
public int maxArea(int[] height) {
int res = 0;
for(int i = 0;i < height.length;i++){
for(int j = i+1;j < height.length;j++){
int temp = Math.min(height[i],height[j]) * (j-i);
res = Math.max(res,temp);
}
}
return res;
}
```
暴力法是可以通过测试的,但是可以看到**程序执行用时**并不理想
```
执行用时 :440 ms, 在所有 Java 提交中击败了17.44% 的用户
内存消耗 :39.9 MB, 在所有 Java 提交中击败了37.86%的用户
```
### 2.双指针
思路:使用两个指针(**resource****last**)分别指向数组的第一个元素和最后一个元素,然后我们计算这两条“线”之间能容纳的水的容量,并更新最大容量(初始值为0);接着我们需要将指向元素值小的那个指针前移一步,然后重复上面的步骤,直到**resource = last**循环截止。
**GIF动画演示:**
![](../Animation/maxArea.gif)
**来看看代码:**
```java
public int maxArea(int[] height) {
int resource = 0;
int last = height.length - 1;
int res = 0;
while (resource < last) {
if (height[resource] >= height[last]) {
res = Math.max(res, (last - resource) * height[last]);
last--;
} else {
res = Math.max(res, (last - resource) * height[resource]);
resource++;
}
}
return res;
}
```
**可以很明显的看到,虽然内存消耗两者是差不多的,但是双指针的速度比暴力解法的速度可是高出好多倍。**
时间复杂度:**O(n)** 空间复杂度:**O(1)**
```
执行用时 :3 ms, 在所有 Java 提交中击败了92.69% 的用户
内存消耗 :40.3 MB, 在所有 Java 提交中击败了7.86%的用户
```
[视频演示](../Animation/maxArea.mp4)
\ No newline at end of file
# 1394. 找出数组中的幸运数
> 本文首发于公众号「图解面试算法」,是 [图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>) 系列文章之一。
>
> 同步博客:https://www.algomooc.com
题目来源于 LeetCode 上 1394题: 找出数组中的幸运数。,主要涉及哈希表。
## 题目
在整数数组中,如果一个整数的出现频次和它的数值大小相等,我们就称这个整数为「幸运数」。
给你一个整数数组 arr,请你从中找出并返回一个幸运数。
如果数组中存在多个幸运数,只需返回 最大 的那个。
如果数组中不含幸运数,则返回 -1 。
 
示例 1:
```
输入:arr = [2,2,3,4]
输出:2
解释:数组中唯一的幸运数是 2 ,因为数值 2 的出现频次也是 2 。
```
示例 2:
```
输入:arr = [1,2,2,3,3,3]
输出:3
解释:1、2 以及 3 都是幸运数,只需要返回其中最大的 3 。
```
示例 3:
```
输入:arr = [2,2,2,3,3]
输出:-1
解释:数组中不存在幸运数。
```
示例 4:
```
输入:arr = [5]
输出:-1
```
示例 5:
```
输入:arr = [7,7,7,7,7,7,7]
输出:7
```
提示:
1 <= arr.length <= 500
1 <= arr[i] <= 500
## 题目解析
1. 遍历arr,用哈希表记录每个数组元素出现的次数
2. 遍历哈希表,每次找到一个幸运数就和当前的幸运数对比,最后找到最大的幸运数,如果没有找到的话输出-1
## 动画理解
<video id="video" controls="" preload="none" >
<source id="mp4" src="../Animation/01394.mp4" type="video/mp4">
</video>
## 参考代码
```javaScript
/**
* @param {number[]} arr
* @return {number}
*/
var findLucky = function(arr) {
let map = new Map()
let maxLucky = -1
arr.map(i => {
map.set(i, map.get(i)+1 || 1)
})
map.forEach((key, value)=>{
if(key == value){
maxLucky = Math.max(maxLucky, key)
}
})
return maxLucky
};
```
## 复杂度分析
假设元素的个数是n个,那么哈希表中最多有 n 个键值对。
时间复杂度:这个算法里有两次遍历,遍历数组的时间复杂度是O(n),遍历哈希表的时间复杂度是O(n),所以最后的时间复杂度就是O(n)。
空间复杂度:额外只用了哈希表,哈希表中最多有 n 个键值对,所以空间复杂度是 O(n)。
![](../../Pictures/qrcode.jpg)
\ No newline at end of file
# 202. 快乐数
> 本文首发于公众号「图解面试算法」,是 [图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>) 系列文章之一。
>
> 同步博客:https://www.algomooc.com
题目来源于 LeetCode 上 202 题,主要涉及**集合**。
## 题目
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为  1,那么这个数就是快乐数。
如果 n 是快乐数就返回 True ;不是,则返回 False 。
 
示例:
```
输入:19
输出:true
解释:
1^2 + 9^2 = 82
8^2 + 2^2 = 68
6^2 + 8^2 = 100
1^2 + 0^2 + 0^2 = 1
```
## 题目解析
我们先举一个例子
```
输入: 59
1. 5^2 + 9^2 = 106
2. 1^2 + 0^2 + 6^2 = 37
3. 3^2 + 7^2 = 58
4. 5^2 + 8^2 = 89
5. 8^2 + 9^2 = 145
6. 1^2 + 4^2 + 5^2 = 42
7. 4^2 + 2^2 = 20
8. 2^2 + 0^2 = 4
9. 4^2 = 16
10. 1^2 + 6^2 = 37
```
这个例子,我们可以看到输入59,第10步的结果和第2步一样,一直进行计算的话,会死循环,所以这个数肯定不会是快乐树。
那么会不会有结果一直无穷大的时候,理论上是不会的。
所以我们就会有两种情况,一种是计算后能得到1的,还有就是进入循环的。
1. 创建Set
2. 循环条件为当前值不为1,循环计算结果,并对每次结果进行判断
3. 在Set里出现的,那么说明循环了,所以不会是快乐数,返回false
4. 没在Set里出现的,那么将当前值存入Set中
5. 循环结束,说明计算结果为1,所以return true
## 动画理解
<video id="video" controls="" preload="none" >
<source id="mp4" src="../Animation/202.mp4" type="video/mp4">
</video>
## 参考代码
```javaScript
/**
* @param {number} n
* @return {boolean}
*/
var isHappy = function(n) {
let set= new Set()
let sum = 0
n += ''
while (sum !== 1) {
sum = 0
for (let i = 0; i < n.length; i++) {
sum += n[i]*n[i]
}
n = sum + ''
if (set.has(sum)) return false
set.add(sum)
}
return true
};
```
## 复杂度分析
时间复杂度为O(logn),空间复杂度为O(logn)
![](../../Pictures/qrcode.jpg)
\ No newline at end of file
This image diff could not be displayed because it is too large. You can view the blob instead.
# LeetCode 第 407 号问题:接雨水 II
> 本文首发于公众号「图解面试算法」,是 [图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>) 系列文章之一。
>
> 同步博客:https://www.algomooc.com
题目来源于 LeetCode 上第 407 号问题:接雨水 II。题目难度为 Hard,目前通过率为 38% 。
### 题目描述
给你一个 m x n 的矩阵,其中的值均为正整数,代表二维高度图每个单元的高度,请计算图中形状最多能接多少体积的雨水。
**示例:**
```
给出如下 3x6 的高度图:
[
[1,4,3,1,3,2],
[3,2,1,3,2,4],
[2,3,3,2,3,1]
]
返回 4 。
```
![](../Animation/example.png)
### 题目解析
在 1 个 2 维的矩阵中,每个格子都有其高度,问这个 2 维矩阵能够盛多少的水。首先我们分析,格子能够盛水的必要条件是其周围存在格子比当前格子高,这样水才能够被框得住,但是仔细一想,最外围的格子怎么办?它们是存不了水的,可以把最外围的格子想象成围栏,它们的作用就是保证里面格子的水不会流出来,所以我们就得先考虑这些格子,它们的高度直接决定了内部格子的蓄水量,但是这些格子也有局部性,一个格子的长短并不会影响矩阵当中所有的格子,但是它会影响与其相邻的格子,那么我们就需要有一个考虑的顺序,那就是优先考虑最外层最短的格子,由于每个格子都会影响到其周围的格子,内部格子也需要列入考虑范围,每次我们都考虑最短的格子,然后看其周围有没有没考虑过的比它还短的格子,于是就有了考虑的先后顺序:
1. 考虑最外层格子
2. 选出最外层最短的格子
3. 考虑该格子与其相邻的内部格子是否能盛水,并把这个内部格子也纳入考虑范围
4. 在考虑范围内的所有格子中选出最短的格子,重复步骤 3
这里需要注意的是,每次纳入考虑范围的格子是加了水之后的高度,而不是之前的高度,原因想一下应该不难理解。另外就是可以使用了 “堆” 这个数据结构来帮助实现寻找 “当前考虑范围内最短的格子” 这个操作步骤。
### 动画描述
![](../Animation/407.gif)
### 代码实现
```java
private class Pair {
int x, y, h;
Pair(int x, int y, int h) {
this.x = x;
this.y = y;
this.h = h;
}
}
private int[] dirX = {0, 0, -1, 1};
private int[] dirY = {-1, 1, 0, 0};
public int trapRainWater(int[][] heightMap) {
if (heightMap.length == 0 || heightMap[0].length == 0) {
return 0;
}
int m = heightMap.length;
int n = heightMap[0].length;
PriorityQueue<Pair> pq = new PriorityQueue<>(new Comparator<Pair>() {
@Override
public int compare(Pair a, Pair b) {
return a.h - b.h;
}
});
boolean[][] visited = new boolean[m][n];
// 优先将外围的元素加入队列中
for (int i = 0; i < n; ++i) {
pq.offer(new Pair(0, i, heightMap[0][i]));
pq.offer(new Pair(m - 1, i, heightMap[m - 1][i]));
visited[0][i] = true;
visited[m - 1][i] = true;
}
for (int i = 1; i < m - 1; ++i) {
pq.offer(new Pair(i, 0, heightMap[i][0]));
pq.offer(new Pair(i, n - 1, heightMap[i][n - 1]));
visited[i][0] = true;
visited[i][n - 1] = true;
}
int result = 0;
while (!pq.isEmpty()) {
Pair cur = pq.poll();
// 遍历当前位置上下左右四个方向
for (int k = 0; k < 4; ++k) {
int curX = cur.x + dirX[k];
int curY = cur.y + dirY[k];
if (curX < 0 || curY < 0 || curX >= m || curY >= n || visited[curX][curY]) {
continue;
}
if (heightMap[curX][curY] < cur.h) {
result += cur.h - heightMap[curX][curY];
}
pq.offer(new Pair(curX, curY,
Math.max(heightMap[curX][curY], cur.h)));
visited[curX][curY] = true;
}
}
return result;
}
```
<br>
### 复杂度分析
因为使用了优先队列这个数据结构,每次元素出入队列的时间复杂度是 O(logn),于是我们可以得出整体时间复杂度是 `O(m*n*logm*n)`,当然,需要说明的是,这是最差时间复杂度,由于并不是所有的元素都一次性加入队列,平均时间复杂度要比这个来的低,具体是什么就得看输入数据了。空间复杂度是 `O(m*n)`,这里也不难理解。通过这道题,堆的用法又被很好地展现了出来。
![](../../Pictures/qrcode.jpg)
\ No newline at end of file
> 本文首发于公众号「图解面试算法」,是 [图解 LeetCode ](<https://github.com/MisterBooo/LeetCodeAnimation>) 系列文章之一。
>
> 同步个人博客:https://www.zhangxiaoshuai.fun
本题在leetcode中题目序号994,属于medium级别,目前通过率为50.7%
**题目描述:**
```
在给定的网格中,每个单元格可以有以下三个值之一:
值0代表空单元格;
值1代表新鲜橘子;
值2代表腐烂的橘子。
每分钟,任何与腐烂的橘子(在4个正方向上)相邻的新鲜橘子都会腐烂。返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回-1。
示例1:
输入:[[2,1,1],[1,1,0],[0,1,1]]
输出:4
示例2:
输入:[[2,1,1],[0,1,1],[1,0,1]]
输出:-1
解释:左下角的橘子(第2行,第0列)永远不会腐烂,因为腐烂只会发生在4个正向上。
示例3:
输入:[[0,2]]
输出:0
解释:因为0分钟时已经没有新鲜橘子了,所以答案就是0。
提示:
1<=grid.length<=10
1<=grid[0].length<=10
grid[i][j]仅为0、1或2
```
**由题意:只有腐烂的橘子才可以去污染它周围四个方向上存在的新鲜橘子,且它每一分钟只能污染一次,下一次,被它腐蚀的橘子再去腐蚀自己周边的新鲜橘子,每次只有被新腐蚀的橘子才能继续向外腐蚀(因为旧的腐烂的橘子已经被“包围”了)**
这就很像一个人得了传染病,只要他遇见人就会将病传染给那个人,而被传染的又会去感染别的人(不同的是,这里的橘子的位置是固定的,无法移动)
思路是非常简单的,我们通过动态图直观理解下:
![腐烂的橘子gif演示](../Animation/腐烂的橘子01.gif)
既然理清了思路,那么我们来试试代码:
首先,我们需要知道初始状态下的单元格中有多少腐烂的橘子,并且要将它们的位置信息保存下来,我们可以用一个队列(**先入先出**)将(x,y)保存下来;然后我们开始遍历整个队列,每次弹出一个保存的位置信息,将这个位置周围的新鲜橘子全部腐蚀,并且将被腐蚀的橘子的位置信息存入队列中,在下次循环中从它们的位置上再“**向外延伸**”(注意:为了模拟同步,我们需要将每次存入队列中的所有位置都要在下一次全部取出来);直到队列为空,循环结束,这个时候并不能说明整个单元格中已经不存在新鲜的橘子,因为可能存在下面这种情况:
![](../Animation/example01.png)
很明显,标红的区域(新鲜橘子)永远不能被腐蚀,因为它周围唯一的两个单元格是空的。
那么针对这种情况,我们在前面遍历统计腐烂橘子的时候可以顺便统计一下新鲜橘子的数量count,后面我们每腐蚀一个橘子就从count中减去1。最终循环结束的时候,我们只需要判断count是否大于0,若是,返回-1,否则返回轮数res。
------
**代码:**
```java
public static int orangesRotting02(int[][] grid){
int row = grid.length,col = grid[0].length;
Queue<int[]> queue = new ArrayDeque();
int count = 0;//统计新鲜橘子的数量
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
if (grid[i][j] == 2) {
queue.add(new int[]{i,j});
}
if (grid[i][j] == 1) {
count++;
}
}
}
int res = 0;
while (count > 0 && !queue.isEmpty()) {
res++;
int size = queue.size();
for (int i = 0; i < size; i++) {
int[] temp = queue.poll();
int r = temp[0],c = temp[1];//(x,y)
//上
if (r > 0 && grid[r-1][c] == 1) {
grid[r-1][c] = 2;
count--;
queue.add(new int[]{r-1,c});
}
//下
if (r < grid.length-1 && grid[r+1][c] == 1) {
grid[r+1][c] = 2;
count--;
queue.add(new int[]{r+1,c});
}
//左
if (c > 0 && grid[r][c-1] == 1) {
grid[r][c-1] = 2;
count--;
queue.add(new int[]{r,c-1});
}
//右
if (c < grid[0].length-1 && grid[r][c+1] == 1) {
grid[r][c+1] = 2;
count--;
queue.add(new int[]{r,c+1});
}
}
}
if (count > 0) {
return -1;
}
return res;
}
```
\ No newline at end of file
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