Skip to content
体验新版
项目
组织
正在加载...
登录
切换导航
打开侧边栏
coolalex776
Fucking Algorithm
提交
3d91fdd9
F
Fucking Algorithm
项目概览
coolalex776
/
Fucking Algorithm
与 Fork 源项目一致
从无法访问的项目Fork
通知
1
Star
1
Fork
0
代码
文件
提交
分支
Tags
贡献者
分支图
Diff
Issue
0
列表
看板
标记
里程碑
合并请求
0
DevOps
流水线
流水线任务
计划
Wiki
0
Wiki
分析
仓库
DevOps
项目成员
Pages
F
Fucking Algorithm
项目概览
项目概览
详情
发布
仓库
仓库
文件
提交
分支
标签
贡献者
分支图
比较
Issue
0
Issue
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
Pages
DevOps
DevOps
流水线
流水线任务
计划
分析
分析
仓库分析
DevOps
Wiki
0
Wiki
成员
成员
收起侧边栏
关闭侧边栏
动态
分支图
创建新Issue
流水线任务
提交
Issue看板
体验新版 GitCode,发现更多精彩内容 >>
提交
3d91fdd9
编写于
3月 16, 2023
作者:
L
labuladong
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
update content
上级
9acc1b15
变更
22
隐藏空白更改
内联
并排
Showing
22 changed file
with
554 addition
and
403 deletion
+554
-403
动态规划系列/LCS.md
动态规划系列/LCS.md
+1
-0
动态规划系列/动态规划之KMP字符匹配算法.md
动态规划系列/动态规划之KMP字符匹配算法.md
+5
-0
动态规划系列/背包问题.md
动态规划系列/背包问题.md
+2
-1
数据结构系列/二叉树总结.md
数据结构系列/二叉树总结.md
+7
-0
数据结构系列/单调栈.md
数据结构系列/单调栈.md
+8
-0
数据结构系列/递归反转链表的一部分.md
数据结构系列/递归反转链表的一部分.md
+3
-0
算法思维系列/BFS解决滑动拼图.md
算法思维系列/BFS解决滑动拼图.md
+2
-0
算法思维系列/字符串乘法.md
算法思维系列/字符串乘法.md
+2
-1
算法思维系列/常用的位操作.md
算法思维系列/常用的位操作.md
+8
-0
算法思维系列/洗牌算法.md
算法思维系列/洗牌算法.md
+6
-0
算法思维系列/烧饼排序.md
算法思维系列/烧饼排序.md
+39
-35
算法思维系列/集合划分.md
算法思维系列/集合划分.md
+66
-52
高频面试系列/一行代码解决的智力题.md
高频面试系列/一行代码解决的智力题.md
+3
-0
高频面试系列/二分查找判定子序列.md
高频面试系列/二分查找判定子序列.md
+9
-0
高频面试系列/判断回文链表.md
高频面试系列/判断回文链表.md
+9
-0
高频面试系列/安排会议室.md
高频面试系列/安排会议室.md
+33
-28
高频面试系列/岛屿题目.md
高频面试系列/岛屿题目.md
+176
-151
高频面试系列/座位调度.md
高频面试系列/座位调度.md
+88
-73
高频面试系列/打印素数.md
高频面试系列/打印素数.md
+20
-13
高频面试系列/接雨水.md
高频面试系列/接雨水.md
+62
-49
高频面试系列/水塘抽样.md
高频面试系列/水塘抽样.md
+2
-0
高频面试系列/缺失和重复的元素.md
高频面试系列/缺失和重复的元素.md
+3
-0
未找到文件。
动态规划系列/LCS.md
浏览文件 @
3d91fdd9
...
...
@@ -38,6 +38,7 @@ title: '详解最长公共子序列问题,秒杀三道动态规划题目'
给你输入两个字符串
`s1`
和
`s2`
,请你找出他们俩的最长公共子序列,返回这个子序列的长度。函数签名如下:
<!-- muliti_language -->
```
java
int
longestCommonSubsequence
(
String
s1
,
String
s2
);
```
...
...
动态规划系列/动态规划之KMP字符匹配算法.md
浏览文件 @
3d91fdd9
...
...
@@ -43,6 +43,7 @@ KMP 算法(Knuth-Morris-Pratt 算法)是一个著名的字符串匹配算法
力扣第 28 题「实现 strStr」就是字符串匹配问题,暴力的字符串匹配算法很容易写,看一下它的运行逻辑:
<!-- muliti_language -->
```
java
// 暴力匹配(伪码)
int
search
(
String
pat
,
String
txt
)
{
...
...
@@ -117,6 +118,7 @@ pat = "aaab"
明白了
`dp`
数组只和
`pat`
有关,那么我们这样设计 KMP 算法就会比较漂亮:
<!-- muliti_language -->
```
java
public
class
KMP
{
private
int
[][]
dp
;
...
...
@@ -208,6 +210,7 @@ pat 应该转移到状态 2
根据我们这个 dp 数组的定义和刚才状态转移的过程,我们可以先写出 KMP 算法的 search 函数代码:
<!-- muliti_language -->
```
java
public
int
search
(
String
txt
)
{
int
M
=
pat
.
length
();
...
...
@@ -285,6 +288,7 @@ for 0 <= j < M:
如果之前的内容你都能理解,恭喜你,现在就剩下一个问题:影子状态
`X`
是如何得到的呢?下面先直接看完整代码吧。
<!-- muliti_language -->
```
java
public
class
KMP
{
private
int
[][]
dp
;
...
...
@@ -360,6 +364,7 @@ for (int i = 0; i < N; i++) {
至此,KMP 算法的核心终于写完啦啦啦啦!看下 KMP 算法的完整代码吧:
<!-- muliti_language -->
```
java
public
class
KMP
{
private
int
[][]
dp
;
...
...
动态规划系列/背包问题.md
浏览文件 @
3d91fdd9
...
...
@@ -29,7 +29,7 @@ title: '动态规划之背包问题'
举个简单的例子,输入如下:
```
```
py
N
=
3
,
W
=
4
wt
=
[
2
,
1
,
3
]
val
=
[
4
,
2
,
3
]
...
...
@@ -124,6 +124,7 @@ return dp[N][W]
我用 Java 写的代码,把上面的思路完全翻译了一遍,并且处理了
`w - wt[i-1]`
可能小于 0 导致数组索引越界的问题:
<!-- muliti_language -->
```
java
int
knapsack
(
int
W
,
int
N
,
int
[]
wt
,
int
[]
val
)
{
assert
N
==
wt
.
length
;
...
...
数据结构系列/二叉树总结.md
浏览文件 @
3d91fdd9
...
...
@@ -234,6 +234,12 @@ void traverse(ListNode head) {
**二叉树题目的递归解法可以分两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,这两类思路分别对应着 [回溯算法核心框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 和 [动态规划核心框架](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶)**
。
> tip:这里说一下我的函数命名习惯:二叉树中用遍历思路解题时函数签名一般是 `void traverse(...)`,没有返回值,靠更新外部变量来计算结果,而用分解问题思路解题时函数名根据该函数具体功能而定,而且一般会有返回值,返回值是子问题的计算结果。
>
> 与此对应的,你会发现我在 [回溯算法核心框架](https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版) 中给出的函数签名一般也是没有返回值的 `void backtrack(...)`,而在 [动态规划核心框架](https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶) 中给出的函数签名是带有返回值的 `dp` 函数。这也说明它俩和二叉树之间千丝万缕的联系。
>
> 虽然函数命名没有什么硬性的要求,但我还是建议你也遵循我的这种风格,这样更能突出函数的作用和解题的思维模式,便于你自己理解和运用。
当时我是用二叉树的最大深度这个问题来举例,重点在于把这两种思路和动态规划和回溯算法进行对比,而本文的重点在于分析这两种思路如何解决二叉树的题目。
力扣第 104 题「二叉树的最大深度」就是最大深度的题目,所谓最大深度就是根节点到「最远」叶子节点的最长路径上的节点数,比如输入这棵二叉树,算法应该返回 3:
...
...
@@ -710,6 +716,7 @@ class Solution {
-
[
两种思路解决单词拼接问题
](
https://labuladong.github.io/article/fname.html?fname=单词拼接
)
-
[
二叉树(递归)专题课
](
https://labuladong.github.io/article/fname.html?fname=tree课程简介
)
-
[
前缀树算法模板秒杀五道算法题
](
https://labuladong.github.io/article/fname.html?fname=trie
)
-
[
后序遍历的妙用
](
https://labuladong.github.io/article/fname.html?fname=后序遍历
)
-
[
回溯算法秒杀所有排列/组合/子集问题
](
https://labuladong.github.io/article/fname.html?fname=子集排列组合
)
-
[
回溯算法解题套路框架
](
https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版
)
-
[
在插件中解锁二叉树专属题解
](
https://labuladong.github.io/article/fname.html?fname=解锁tree插件
)
...
...
数据结构系列/单调栈.md
浏览文件 @
3d91fdd9
...
...
@@ -34,6 +34,7 @@ title: '特殊数据结构:单调栈'
现在给你出这么一道题:输入一个数组
`nums`
,请你返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。函数签名如下:
<!-- muliti_language -->
```
java
int
[]
nextGreaterElement
(
int
[]
nums
);
```
...
...
@@ -48,6 +49,7 @@ int[] nextGreaterElement(int[] nums);
这个情景很好理解吧?带着这个抽象的情景,先来看下代码。
<!-- muliti_language -->
```
java
int
[]
nextGreaterElement
(
int
[]
nums
)
{
int
n
=
nums
.
length
;
...
...
@@ -83,12 +85,14 @@ int[] nextGreaterElement(int[] nums) {
这道题给你输入两个数组
`nums1`
和
`nums2`
,让你求
`nums1`
中的元素在
`nums2`
中的下一个更大元素,函数签名如下:
<!-- muliti_language -->
```
java
int
[]
nextGreaterElement
(
int
[]
nums1
,
int
[]
nums2
)
```
其实和把我们刚才的代码改一改就可以解决这道题了,因为题目说
`nums1`
是
`nums2`
的子集,那么我们先把
`nums2`
中每个元素的下一个更大元素算出来存到一个映射里,然后再让
`nums1`
中的元素去查表即可:
<!-- muliti_language -->
```
java
int
[]
nextGreaterElement
(
int
[]
nums1
,
int
[]
nums2
)
{
// 记录 nums2 中每个元素的下一个更大元素
...
...
@@ -116,6 +120,7 @@ int[] nextGreaterElement(int[] nums) {
给你一个数组
`temperatures`
,这个数组存放的是近几天的天气气温,你返回一个等长的数组,计算:对于每一天,你还要至少等多少天才能等到一个更暖和的气温;如果等不到那一天,填 0。函数签名如下:
<!-- muliti_language -->
```
java
int
[]
dailyTemperatures
(
int
[]
temperatures
);
```
...
...
@@ -126,6 +131,7 @@ int[] dailyTemperatures(int[] temperatures);
相同的思路,直接调用单调栈的算法模板,稍作改动就可以,直接上代码吧:
<!-- muliti_language -->
```
java
int
[]
dailyTemperatures
(
int
[]
temperatures
)
{
int
n
=
temperatures
.
length
;
...
...
@@ -156,6 +162,7 @@ int[] dailyTemperatures(int[] temperatures) {
我们一般是通过 % 运算符求模(余数),来模拟环形特效:
<!-- muliti_language -->
```
java
int
[]
arr
=
{
1
,
2
,
3
,
4
,
5
};
int
n
=
arr
.
length
,
index
=
0
;
...
...
@@ -176,6 +183,7 @@ while (true) {
有了思路,最简单的实现方式当然可以把这个双倍长度的数组构造出来,然后套用算法模板。但是,
**我们可以不用构造新数组,而是利用循环数组的技巧来模拟数组长度翻倍的效果**
。直接看代码吧:
<!-- muliti_language -->
```
java
int
[]
nextGreaterElements
(
int
[]
nums
)
{
int
n
=
nums
.
length
;
...
...
数据结构系列/递归反转链表的一部分.md
浏览文件 @
3d91fdd9
...
...
@@ -106,6 +106,7 @@ head.next.next = head;
接下来:
<!-- muliti_language -->
```
java
head
.
next
=
null
;
return
last
;
...
...
@@ -117,6 +118,7 @@ return last;
1、递归函数要有 base case,也就是这句:
<!-- muliti_language -->
```
java
if
(
head
==
null
||
head
.
next
==
null
)
{
return
head
;
...
...
@@ -127,6 +129,7 @@ if (head == null || head.next == null) {
2、当链表递归反转之后,新的头结点是
`last`
,而之前的
`head`
变成了最后一个节点,别忘了链表的末尾要指向 null:
<!-- muliti_language -->
```
java
head
.
next
=
null
;
```
...
...
算法思维系列/BFS解决滑动拼图.md
浏览文件 @
3d91fdd9
...
...
@@ -75,6 +75,7 @@ title: 'BFS 算法秒杀各种益智游戏'
对于这道题,题目说输入的数组大小都是 2 x 3,所以我们可以直接手动写出来这个映射:
<!-- muliti_language -->
```
java
// 记录一维字符串的相邻索引
int
[][]
neighbor
=
new
int
[][]{
...
...
@@ -99,6 +100,7 @@ int[][] neighbor = new int[][]{
至此,我们就把这个问题完全转化成标准的 BFS 问题了,借助前文
[
BFS 算法框架
](
https://labuladong.github.io/article/fname.html?fname=BFS框架
)
的代码框架,直接就可以套出解法代码了:
<!-- muliti_language -->
```
java
public
int
slidingPuzzle
(
int
[][]
board
)
{
int
m
=
2
,
n
=
3
;
...
...
算法思维系列/字符串乘法.md
浏览文件 @
3d91fdd9
...
...
@@ -59,7 +59,8 @@ title: '字符串乘法'
明白了这一点,就可以用代码模仿出这个计算过程了:
```
java
<!-- muliti_language -->
```
cpp
string
multiply
(
string
num1
,
string
num2
)
{
int
m
=
num1
.
size
(),
n
=
num2
.
size
();
// 结果最多为 m + n 位数
...
...
算法思维系列/常用的位操作.md
浏览文件 @
3d91fdd9
...
...
@@ -104,6 +104,7 @@ boolean f = ((x ^ y) < 0); // false
我在
[
单调栈解题套路
](
https://labuladong.github.io/article/fname.html?fname=单调栈
)
中介绍过环形数组,其实就是利用求模(余数)的方式让数组看起来头尾相接形成一个环形,永远都走不完:
<!-- muliti_language -->
```
java
int
[]
arr
=
{
1
,
2
,
3
,
4
};
int
index
=
0
;
...
...
@@ -117,6 +118,7 @@ while (true) {
但模运算
`%`
对计算机来说其实是一个比较昂贵的操作,所以我们可以用
`&`
运算来求余数:
<!-- muliti_language -->
```
java
int
[]
arr
=
{
1
,
2
,
3
,
4
};
int
index
=
0
;
...
...
@@ -136,6 +138,7 @@ while (true) {
答案是,如果你使用
`%`
求模的方式,那么当
`index`
小于 0 之后求模的结果也会出现负数,你需要特殊处理。但通过
`&`
与运算的方式,
`index`
不会出现负数,依然可以正常工作:
<!-- muliti_language -->
```
java
int
[]
arr
=
{
1
,
2
,
3
,
4
};
int
index
=
0
;
...
...
@@ -167,6 +170,7 @@ while (true) {
就是让你返回
`n`
的二进制表示中有几个 1。因为
`n & (n - 1)`
可以消除最后一个 1,所以可以用一个循环不停地消除 1 同时计数,直到
`n`
变成 0 为止。
<!-- muliti_language -->
```
java
int
hammingWeight
(
int
n
)
{
int
res
=
0
;
...
...
@@ -192,6 +196,7 @@ int hammingWeight(int n) {
如果使用
`n & (n-1)`
的技巧就很简单了(注意运算符优先级,括号不可以省略):
<!-- muliti_language -->
```
java
boolean
isPowerOfTwo
(
int
n
)
{
if
(
n
<=
0
)
return
false
;
...
...
@@ -213,6 +218,7 @@ boolean isPowerOfTwo(int n) {
对于这道题目,我们只要把所有数字进行异或,成对儿的数字就会变成 0,落单的数字和 0 做异或还是它本身,所以最后异或的结果就是只出现一次的元素:
<!-- muliti_language -->
```
java
int
singleNumber
(
int
[]
nums
)
{
int
res
=
0
;
...
...
@@ -241,6 +247,7 @@ int singleNumber(int[] nums) {
题目的意思可以这样理解:现在有个等差数列
`0, 1, 2,..., n`
,其中少了某一个数字,请你把它找出来。那这个数字不就是
`sum(0,1,..n) - sum(nums)`
嘛?
<!-- muliti_language -->
```
java
int
missingNumber
(
int
[]
nums
)
{
int
n
=
nums
.
length
;
...
...
@@ -277,6 +284,7 @@ int missingNumber(int[] nums) {
如何找这个落单的数字呢,
**只要把所有的元素和索引做异或运算,成对儿的数字都会消为 0,只有这个落单的元素会剩下**
,也就达到了我们的目的:
<!-- muliti_language -->
```
java
int
missingNumber
(
int
[]
nums
)
{
int
n
=
nums
.
length
;
...
...
算法思维系列/洗牌算法.md
浏览文件 @
3d91fdd9
...
...
@@ -26,7 +26,9 @@ title: '洗牌算法'
我知道大家会各种花式排序算法,但是如果叫你打乱一个数组,你是否能做到胸有成竹?即便你拍脑袋想出一个算法,怎么证明你的算法就是正确的呢?乱序算法不像排序算法,结果唯一可以很容易检验,因为「乱」可以有很多种,你怎么能证明你的算法是「真的乱」呢?
所以我们面临两个问题:
1.
什么叫做「真的乱」?
2.
设计怎样的算法来打乱数组才能做到「真的乱」?
这种算法称为「随机乱置算法」或者「洗牌算法」。
...
...
@@ -37,6 +39,7 @@ title: '洗牌算法'
此类算法都是靠随机选取元素交换来获取随机性,直接看代码(伪码),该算法有 4 种形式,都是正确的:
<!-- muliti_language -->
```
java
// 得到一个在闭区间 [min, max] 内的随机整数
int
randInt
(
int
min
,
int
max
);
...
...
@@ -71,6 +74,7 @@ void shuffle(int[] arr) {
我们先用这个准则分析一下
**第一种写法**
的正确性:
<!-- muliti_language -->
```
java
// 假设传入这样一个 arr
int
[]
arr
=
{
1
,
3
,
5
,
7
,
9
};
...
...
@@ -113,6 +117,7 @@ for 循环第二轮迭代时,`i = 1`,`rand` 的取值范围是 `[1, 4]`,
如果读者思考过洗牌算法,可能会想出如下的算法,但是
**这种写法是错误的**
:
<!-- muliti_language -->
```
java
void
shuffle
(
int
[]
arr
)
{
int
n
=
arr
.
length
();
...
...
@@ -151,6 +156,7 @@ void shuffle(int[] arr) {
每次进行洗牌算法后,就把得到的打乱结果对应的频数加一,重复进行 100 万次,如果每种结果出现的总次数差不多,那就说明每种结果出现的概率应该是相等的。写一下这个思路的伪代码:
<!-- muliti_language -->
```
java
void
shuffle
(
int
[]
arr
);
...
...
算法思维系列/烧饼排序.md
浏览文件 @
3d91fdd9
...
...
@@ -43,6 +43,7 @@ title: '烧饼排序'
为什么说这个问题有递归性质呢?比如说我们需要实现这样一个函数:
<!-- muliti_language -->
```
java
// cakes 是一堆烧饼,函数会将前 n 个烧饼排序
void
sort
(
int
[]
cakes
,
int
n
);
...
...
@@ -82,45 +83,48 @@ base case:`n == 1` 时,排序 1 个饼时不需要翻转。
只要把上述的思路用代码实现即可,唯一需要注意的是,数组索引从 0 开始,而我们要返回的结果是从 1 开始算的。
<!-- muliti_language -->
```
java
// 记录反转操作序列
LinkedList
<
Integer
>
res
=
new
LinkedList
<>();
class
Solution
{
// 记录反转操作序列
LinkedList
<
Integer
>
res
=
new
LinkedList
<>();
List
<
Integer
>
pancakeSort
(
int
[]
cakes
)
{
sort
(
cakes
,
cakes
.
length
);
return
res
;
}
List
<
Integer
>
pancakeSort
(
int
[]
cakes
)
{
sort
(
cakes
,
cakes
.
length
);
return
res
;
}
void
sort
(
int
[]
cakes
,
int
n
)
{
// base case
if
(
n
==
1
)
return
;
// 寻找最大饼的索引
int
maxCake
=
0
;
int
maxCakeIndex
=
0
;
for
(
int
i
=
0
;
i
<
n
;
i
++)
if
(
cakes
[
i
]
>
maxCake
)
{
maxCakeIndex
=
i
;
maxCake
=
cakes
[
i
];
}
// 第一次翻转,将最大饼翻到最上面
reverse
(
cakes
,
0
,
maxCakeIndex
);
res
.
add
(
maxCakeIndex
+
1
);
// 第二次翻转,将最大饼翻到最下面
reverse
(
cakes
,
0
,
n
-
1
);
res
.
add
(
n
);
// 递归调用
sort
(
cakes
,
n
-
1
);
}
void
sort
(
int
[]
cakes
,
int
n
)
{
// base case
if
(
n
==
1
)
return
;
// 寻找最大饼的索引
int
maxCake
=
0
;
int
maxCakeIndex
=
0
;
for
(
int
i
=
0
;
i
<
n
;
i
++)
if
(
cakes
[
i
]
>
maxCake
)
{
maxCakeIndex
=
i
;
maxCake
=
cakes
[
i
];
}
// 第一次翻转,将最大饼翻到最上面
reverse
(
cakes
,
0
,
maxCakeIndex
);
res
.
add
(
maxCakeIndex
+
1
);
// 第二次翻转,将最大饼翻到最下面
reverse
(
cakes
,
0
,
n
-
1
);
res
.
add
(
n
);
void
reverse
(
int
[]
arr
,
int
i
,
int
j
)
{
while
(
i
<
j
)
{
int
temp
=
arr
[
i
];
arr
[
i
]
=
arr
[
j
];
arr
[
j
]
=
temp
;
i
++;
j
--;
// 递归调用
sort
(
cakes
,
n
-
1
);
}
void
reverse
(
int
[]
arr
,
int
i
,
int
j
)
{
while
(
i
<
j
)
{
int
temp
=
arr
[
i
];
arr
[
i
]
=
arr
[
j
];
arr
[
j
]
=
temp
;
i
++;
j
--;
}
}
}
```
...
...
算法思维系列/集合划分.md
浏览文件 @
3d91fdd9
...
...
@@ -37,6 +37,7 @@ title: '经典回溯算法:集合划分问题'
函数签名如下:
<!-- muliti_language -->
```
java
boolean
canPartitionKSubsets
(
int
[]
nums
,
int
k
);
```
...
...
@@ -139,6 +140,7 @@ void traverse(int[] nums, int index) {
那么回到这道题,以数字的视角,选择
`k`
个桶,用 for 循环写出来是下面这样:
<!-- muliti_language -->
```
java
// k 个桶(集合),记录每个桶装的数字之和
int
[]
bucket
=
new
int
[
k
];
...
...
@@ -155,6 +157,7 @@ for (int index = 0; index < nums.length; index++) {
如果改成递归的形式,就是下面这段代码逻辑:
<!-- muliti_language -->
```
java
// k 个桶(集合),记录每个桶装的数字之和
int
[]
bucket
=
new
int
[
k
];
...
...
@@ -179,6 +182,7 @@ void backtrack(int[] nums, int index) {
虽然上述代码仅仅是穷举逻辑,还不能解决我们的问题,但是只要略加完善即可:
<!-- muliti_language -->
```
java
// 主函数
boolean
canPartitionKSubsets
(
int
[]
nums
,
int
k
)
{
...
...
@@ -236,6 +240,7 @@ boolean backtrack(
主要看
`backtrack`
函数的递归部分:
<!-- muliti_language -->
```
java
for
(
int
i
=
0
;
i
<
bucket
.
length
;
i
++)
{
// 剪枝
...
...
@@ -257,6 +262,7 @@ for (int i = 0; i < bucket.length; i++) {
所以可以在之前的代码中再添加一些代码:
<!-- muliti_language -->
```
java
boolean
canPartitionKSubsets
(
int
[]
nums
,
int
k
)
{
// 其他代码不变
...
...
@@ -284,6 +290,7 @@ boolean canPartitionKSubsets(int[] nums, int k) {
这个思路可以用下面这段代码表示出来:
<!-- muliti_language -->
```
java
// 装满所有桶为止
while
(
k
>
0
)
{
...
...
@@ -305,6 +312,7 @@ while (k > 0) {
那么我们也可以把这个 while 循环改写成递归函数,不过比刚才略微复杂一些,首先写一个
`backtrack`
递归函数出来:
<!-- muliti_language -->
```
java
boolean
backtrack
(
int
k
,
int
bucket
,
int
[]
nums
,
int
start
,
boolean
[]
used
,
int
target
);
...
...
@@ -318,6 +326,7 @@ boolean backtrack(int k, int bucket,
根据这个函数定义,可以这样调用
`backtrack`
函数:
<!-- muliti_language -->
```
java
boolean
canPartitionKSubsets
(
int
[]
nums
,
int
k
)
{
// 排除一些基本情况
...
...
@@ -341,6 +350,7 @@ boolean canPartitionKSubsets(int[] nums, int k) {
下面的代码就实现了这个逻辑:
<!-- muliti_language -->
```
java
boolean
backtrack
(
int
k
,
int
bucket
,
int
[]
nums
,
int
start
,
boolean
[]
used
,
int
target
)
{
...
...
@@ -423,6 +433,7 @@ boolean backtrack(int k, int bucket,
看下代码实现,只要稍微改一下
`backtrack`
函数即可:
<!-- muliti_language -->
```
java
// 备忘录,存储 used 数组的状态
HashMap
<
String
,
Boolean
>
memo
=
new
HashMap
<>();
...
...
@@ -465,65 +476,68 @@ boolean backtrack(int k, int bucket, int[] nums, int start, boolean[] used, int
看下最终的解法代码:
<!-- muliti_language -->
```
java
public
boolean
canPartitionKSubsets
(
int
[]
nums
,
int
k
)
{
// 排除一些基本情况
if
(
k
>
nums
.
length
)
return
false
;
int
sum
=
0
;
for
(
int
v
:
nums
)
sum
+=
v
;
if
(
sum
%
k
!=
0
)
return
false
;
int
used
=
0
;
// 使用位图技巧
int
target
=
sum
/
k
;
// k 号桶初始什么都没装,从 nums[0] 开始做选择
return
backtrack
(
k
,
0
,
nums
,
0
,
used
,
target
);
}
HashMap
<
Integer
,
Boolean
>
memo
=
new
HashMap
<>();
boolean
backtrack
(
int
k
,
int
bucket
,
int
[]
nums
,
int
start
,
int
used
,
int
target
)
{
// base case
if
(
k
==
0
)
{
// 所有桶都被装满了,而且 nums 一定全部用完了
return
true
;
}
if
(
bucket
==
target
)
{
// 装满了当前桶,递归穷举下一个桶的选择
// 让下一个桶从 nums[0] 开始选数字
boolean
res
=
backtrack
(
k
-
1
,
0
,
nums
,
0
,
used
,
target
);
// 缓存结果
memo
.
put
(
used
,
res
);
return
res
;
}
if
(
memo
.
containsKey
(
used
))
{
// 避免冗余计算
return
memo
.
get
(
used
);
class
Solution
{
public
boolean
canPartitionKSubsets
(
int
[]
nums
,
int
k
)
{
// 排除一些基本情况
if
(
k
>
nums
.
length
)
return
false
;
int
sum
=
0
;
for
(
int
v
:
nums
)
sum
+=
v
;
if
(
sum
%
k
!=
0
)
return
false
;
int
used
=
0
;
// 使用位图技巧
int
target
=
sum
/
k
;
// k 号桶初始什么都没装,从 nums[0] 开始做选择
return
backtrack
(
k
,
0
,
nums
,
0
,
used
,
target
);
}
for
(
int
i
=
start
;
i
<
nums
.
length
;
i
++)
{
// 剪枝
if
(((
used
>>
i
)
&
1
)
==
1
)
{
// 判断第 i 位是否是 1
// nums[i] 已经被装入别的桶中
continue
;
HashMap
<
Integer
,
Boolean
>
memo
=
new
HashMap
<>();
boolean
backtrack
(
int
k
,
int
bucket
,
int
[]
nums
,
int
start
,
int
used
,
int
target
)
{
// base case
if
(
k
==
0
)
{
// 所有桶都被装满了,而且 nums 一定全部用完了
return
true
;
}
if
(
nums
[
i
]
+
bucket
>
target
)
{
continue
;
if
(
bucket
==
target
)
{
// 装满了当前桶,递归穷举下一个桶的选择
// 让下一个桶从 nums[0] 开始选数字
boolean
res
=
backtrack
(
k
-
1
,
0
,
nums
,
0
,
used
,
target
);
// 缓存结果
memo
.
put
(
used
,
res
);
return
res
;
}
// 做选择
used
|=
1
<<
i
;
// 将第 i 位置为 1
bucket
+=
nums
[
i
];
// 递归穷举下一个数字是否装入当前桶
if
(
backtrack
(
k
,
bucket
,
nums
,
i
+
1
,
used
,
target
))
{
return
true
;
if
(
memo
.
containsKey
(
used
))
{
// 避免冗余计算
return
memo
.
get
(
used
);
}
// 撤销选择
used
^=
1
<<
i
;
// 使用异或运算将第 i 位恢复 0
bucket
-=
nums
[
i
];
}
return
false
;
for
(
int
i
=
start
;
i
<
nums
.
length
;
i
++)
{
// 剪枝
if
(((
used
>>
i
)
&
1
)
==
1
)
{
// 判断第 i 位是否是 1
// nums[i] 已经被装入别的桶中
continue
;
}
if
(
nums
[
i
]
+
bucket
>
target
)
{
continue
;
}
// 做选择
used
|=
1
<<
i
;
// 将第 i 位置为 1
bucket
+=
nums
[
i
];
// 递归穷举下一个数字是否装入当前桶
if
(
backtrack
(
k
,
bucket
,
nums
,
i
+
1
,
used
,
target
))
{
return
true
;
}
// 撤销选择
used
^=
1
<<
i
;
// 使用异或运算将第 i 位恢复 0
bucket
-=
nums
[
i
];
}
return
false
;
}
}
```
...
...
高频面试系列/一行代码解决的智力题.md
浏览文件 @
3d91fdd9
...
...
@@ -51,6 +51,7 @@ title: '一行代码就能解决的算法题'
这样一直循环下去,我们发现只要踩到 4 的倍数,就落入了圈套,永远逃不出 4 的倍数,而且一定会输。所以这道题的解法非常简单:
<!-- muliti_language -->
```
java
boolean
canWinNim
(
int
n
)
{
// 如果上来就踩到 4 的倍数,那就认输吧
...
...
@@ -85,6 +86,7 @@ boolean canWinNim(int n) {
这道题又涉及到两人的博弈,也可以用动态规划算法暴力试,比较麻烦。但我们只要对规则深入思考,就会大惊失色:只要你足够聪明,你是必胜无疑的,因为你是先手。
<!-- muliti_language -->
```
java
boolean
stoneGame
(
int
[]
piles
)
{
return
true
;
...
...
@@ -119,6 +121,7 @@ boolean stoneGame(int[] piles) {
我们当然可以用一个布尔数组表示这些灯的开关情况,然后模拟这些操作过程,最后去数一下就能出结果。但是这样显得没有灵性,最好的解法是这样的:
<!-- muliti_language -->
```
java
int
bulbSwitch
(
int
n
)
{
return
(
int
)
Math
.
sqrt
(
n
);
...
...
高频面试系列/二分查找判定子序列.md
浏览文件 @
3d91fdd9
...
...
@@ -34,9 +34,11 @@ title: '二分查找高效判定子序列'
举两个例子:
```
s = "abc", t = "**a**h**b**gd**c**", return true.
s = "axc", t = "ahbgdc", return false.
```
题目很容易理解,而且看起来很简单,但很难想到这个问题跟二分查找有关吧?
...
...
@@ -44,6 +46,7 @@ s = "axc", t = "ahbgdc", return false.
首先,一个很简单的解法是这样的:
<!-- muliti_language -->
```
java
boolean
isSubsequence
(
String
s
,
String
t
)
{
int
i
=
0
,
j
=
0
;
...
...
@@ -67,6 +70,7 @@ boolean isSubsequence(String s, String t) {
如果给你一系列字符串
`s1,s2,...`
和字符串
`t`
,你需要判定每个串
`s`
是否是
`t`
的子序列(可以假定
`s`
较短,
`t`
很长)。
<!-- muliti_language -->
```
java
boolean
[]
isSubsequence
(
String
[]
sn
,
String
t
);
```
...
...
@@ -79,6 +83,7 @@ boolean[] isSubsequence(String[] sn, String t);
二分思路主要是对
`t`
进行预处理,用一个字典
`index`
将每个字符出现的索引位置按顺序存储下来:
<!-- muliti_language -->
```
java
int
m
=
s
.
length
(),
n
=
t
.
length
();
ArrayList
<
Integer
>[]
index
=
new
ArrayList
[
256
];
...
...
@@ -111,6 +116,7 @@ for (int i = 0; i < n; i++) {
什么意思呢,就是说如果在数组
`[0,1,3,4]`
中搜索元素 2,算法会返回索引 2,也就是元素 3 的位置,元素 3 是数组中大于 2 的最小元素。所以我们可以利用二分搜索避免线性扫描。
<!-- muliti_language -->
```
java
// 查找左侧边界的二分查找
int
left_bound
(
ArrayList
<
Integer
>
arr
,
int
target
)
{
...
...
@@ -134,6 +140,7 @@ int left_bound(ArrayList<Integer> arr, int target) {
这里以单个字符串
`s`
为例,对于多个字符串
`s`
,可以把预处理部分抽出来。
<!-- muliti_language -->
```
java
boolean
isSubsequence
(
String
s
,
String
t
)
{
int
m
=
s
.
length
(),
n
=
t
.
length
();
...
...
@@ -173,12 +180,14 @@ boolean isSubsequence(String s, String t) {
函数签名如下:
<!-- muliti_language -->
```
java
int
numMatchingSubseq
(
String
s
,
String
[]
words
)
```
我们直接把上一道题的代码稍微改改即可完成这道题:
<!-- muliti_language -->
```
java
int
numMatchingSubseq
(
String
s
,
String
[]
words
)
{
// 对 s 进行预处理
...
...
高频面试系列/判断回文链表.md
浏览文件 @
3d91fdd9
...
...
@@ -28,6 +28,7 @@ title: '如何高效判断回文链表'
**寻找**
回文串的核心思想是从中心向两端扩展:
<!-- muliti_language -->
```
java
// 在 s 中寻找以 s[left] 和 s[right] 为中心的最长回文串
String
palindrome
(
String
s
,
int
left
,
int
right
)
{
...
...
@@ -47,6 +48,7 @@ String palindrome(String s, int left, int right) {
而
**判断**
一个字符串是不是回文串就简单很多,不需要考虑奇偶情况,只需要
[
双指针技巧
](
https://labuladong.github.io/article/fname.html?fname=双指针技巧
)
,从两端向中间逼近即可:
<!-- muliti_language -->
```
java
boolean
isPalindrome
(
String
s
)
{
// 一左一右两个指针相向而行
...
...
@@ -94,6 +96,7 @@ boolean isPalindrome(ListNode head);
对于二叉树的几种遍历方式,我们再熟悉不过了:
<!-- muliti_language -->
```
java
void
traverse
(
TreeNode
root
)
{
// 前序遍历代码
...
...
@@ -106,6 +109,7 @@ void traverse(TreeNode root) {
在
[
学习数据结构的框架思维
](
https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法
)
中说过,链表兼具递归结构,树结构不过是链表的衍生。那么,
**链表其实也可以有前序遍历和后序遍历**
:
<!-- muliti_language -->
```
java
void
traverse
(
ListNode
head
)
{
// 前序遍历代码
...
...
@@ -116,6 +120,7 @@ void traverse(ListNode head) {
这个框架有什么指导意义呢?如果我想正序打印链表中的
`val`
值,可以在前序遍历位置写代码;反之,如果想倒序遍历链表,就可以在后序遍历位置操作:
<!-- muliti_language -->
```
java
/* 倒序打印单链表中的元素值 */
void
traverse
(
ListNode
head
)
{
...
...
@@ -128,6 +133,7 @@ void traverse(ListNode head) {
说到这了,其实可以稍作修改,模仿双指针实现回文判断的功能:
<!-- muliti_language -->
```
java
// 左侧指针
ListNode
left
;
...
...
@@ -159,6 +165,7 @@ boolean traverse(ListNode right) {
**1、先通过 [双指针技巧](https://labuladong.github.io/article/fname.html?fname=链表技巧) 中的快慢指针来找到链表的中点**
:
<!-- muliti_language -->
```
java
ListNode
slow
,
fast
;
slow
=
fast
=
head
;
...
...
@@ -182,6 +189,7 @@ if (fast != null)
**3、从`slow`开始反转后面的链表,现在就可以开始比较回文串了**
:
<!-- muliti_language -->
```
java
ListNode
left
=
head
;
ListNode
right
=
reverse
(
slow
);
...
...
@@ -199,6 +207,7 @@ return true;
至此,把上面 3 段代码合在一起就高效地解决这个问题了,其中
`reverse`
函数很容易实现:
<!-- muliti_language -->
```
java
boolean
isPalindrome
(
ListNode
head
)
{
ListNode
slow
,
fast
;
...
...
高频面试系列/安排会议室.md
浏览文件 @
3d91fdd9
...
...
@@ -33,6 +33,7 @@ title: '扫描线技巧解决会议室安排问题'
函数签名如下:
<!-- muliti_language -->
```
java
// 返回需要申请的会议室数量
int
minMeetingRooms
(
int
[][]
meetings
);
...
...
@@ -152,38 +153,42 @@ int minMeetingRooms(int[][] meetings) {
然后就简单了,扫描线从左向右前进,遇到红点就对计数器加一,遇到绿点就对计数器减一,计数器
`count`
的最大值就是答案:
<!-- muliti_language -->
```
java
int
minMeetingRooms
(
int
[][]
meetings
)
{
int
n
=
meetings
.
length
;
int
[]
begin
=
new
int
[
n
];
int
[]
end
=
new
int
[
n
];
for
(
int
i
=
0
;
i
<
n
;
i
++)
{
begin
[
i
]
=
meetings
[
i
][
0
];
end
[
i
]
=
meetings
[
i
][
1
];
}
Arrays
.
sort
(
begin
);
Arrays
.
sort
(
end
);
// 扫描过程中的计数器
int
count
=
0
;
// 双指针技巧
int
res
=
0
,
i
=
0
,
j
=
0
;
while
(
i
<
n
&&
j
<
n
)
{
if
(
begin
[
i
]
<
end
[
j
])
{
// 扫描到一个红点
count
++;
i
++;
}
else
{
// 扫描到一个绿点
count
--;
j
++;
class
Solution
{
int
minMeetingRooms
(
int
[][]
meetings
)
{
int
n
=
meetings
.
length
;
int
[]
begin
=
new
int
[
n
];
int
[]
end
=
new
int
[
n
];
for
(
int
i
=
0
;
i
<
n
;
i
++)
{
begin
[
i
]
=
meetings
[
i
][
0
];
end
[
i
]
=
meetings
[
i
][
1
];
}
Arrays
.
sort
(
begin
);
Arrays
.
sort
(
end
);
// 扫描过程中的计数器
int
count
=
0
;
// 双指针技巧
int
res
=
0
,
i
=
0
,
j
=
0
;
while
(
i
<
n
&&
j
<
n
)
{
if
(
begin
[
i
]
<
end
[
j
])
{
// 扫描到一个红点
count
++;
i
++;
}
else
{
// 扫描到一个绿点
count
--;
j
++;
}
// 记录扫描过程中的最大值
res
=
Math
.
max
(
res
,
count
);
}
// 记录扫描过程中的最大值
re
s
=
Math
.
max
(
res
,
count
)
;
re
turn
res
;
}
return
res
;
}
```
这里使用的是
[
双指针技巧
](
https://labuladong.github.io/article/fname.html?fname=双指针技巧
)
,根据
`i, j`
的相对位置模拟扫描线前进的过程。
...
...
高频面试系列/岛屿题目.md
浏览文件 @
3d91fdd9
...
...
@@ -39,6 +39,7 @@ title: 'DFS 算法秒杀岛屿系列题目'
根据
[
学习数据结构和算法的框架思维
](
https://labuladong.github.io/article/fname.html?fname=学习数据结构和算法的高效方法
)
,完全可以根据二叉树的遍历框架改写出二维矩阵的 DFS 代码框架:
<!-- muliti_language -->
```
java
// 二叉树遍历框架
void
traverse
(
TreeNode
root
)
{
...
...
@@ -70,6 +71,7 @@ void dfs(int[][] grid, int i, int j, boolean[][] visited) {
这里额外说一个处理二维数组的常用小技巧,你有时会看到使用「方向数组」来处理上下左右的遍历,和前文
[
图遍历框架
](
https://labuladong.github.io/article/fname.html?fname=图
)
的代码很类似:
<!-- muliti_language -->
```
java
// 方向数组,分别代表上、下、左、右
int
[][]
dirs
=
new
int
[][]{{-
1
,
0
},
{
1
,
0
},
{
0
,-
1
},
{
0
,
1
}};
...
...
@@ -105,6 +107,7 @@ void dfs(int[][] grid, int i, int j, boolean[][] visited) {
我们说连成片的陆地形成岛屿,那么请你写一个算法,计算这个矩阵
`grid`
中岛屿的个数,函数签名如下:
<!-- muliti_language -->
```
java
int
numIslands
(
char
[][]
grid
);
```
...
...
@@ -115,43 +118,46 @@ int numIslands(char[][] grid);
思路很简单,关键在于如何寻找并标记「岛屿」,这就要 DFS 算法发挥作用了,我们直接看解法代码:
<!-- muliti_language -->
```
java
// 主函数,计算岛屿数量
int
numIslands
(
char
[][]
grid
)
{
int
res
=
0
;
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
// 遍历 grid
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid
[
i
][
j
]
==
'1'
)
{
// 每发现一个岛屿,岛屿数量加一
res
++;
// 然后使用 DFS 将岛屿淹了
dfs
(
grid
,
i
,
j
);
class
Solution
{
// 主函数,计算岛屿数量
int
numIslands
(
char
[][]
grid
)
{
int
res
=
0
;
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
// 遍历 grid
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid
[
i
][
j
]
==
'1'
)
{
// 每发现一个岛屿,岛屿数量加一
res
++;
// 然后使用 DFS 将岛屿淹了
dfs
(
grid
,
i
,
j
);
}
}
}
return
res
;
}
return
res
;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void
dfs
(
char
[][]
grid
,
int
i
,
int
j
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
if
(
i
<
0
||
j
<
0
||
i
>=
m
||
j
>=
n
)
{
// 超出索引边界
return
;
}
if
(
grid
[
i
][
j
]
==
'0'
)
{
// 已经是海水了
return
;
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void
dfs
(
char
[][]
grid
,
int
i
,
int
j
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
if
(
i
<
0
||
j
<
0
||
i
>=
m
||
j
>=
n
)
{
// 超出索引边界
return
;
}
if
(
grid
[
i
][
j
]
==
'0'
)
{
// 已经是海水了
return
;
}
// 将 (i, j) 变成海水
grid
[
i
][
j
]
=
'0'
;
// 淹没上下左右的陆地
dfs
(
grid
,
i
+
1
,
j
);
dfs
(
grid
,
i
,
j
+
1
);
dfs
(
grid
,
i
-
1
,
j
);
dfs
(
grid
,
i
,
j
-
1
);
}
// 将 (i, j) 变成海水
grid
[
i
][
j
]
=
'0'
;
// 淹没上下左右的陆地
dfs
(
grid
,
i
+
1
,
j
);
dfs
(
grid
,
i
,
j
+
1
);
dfs
(
grid
,
i
-
1
,
j
);
dfs
(
grid
,
i
,
j
-
1
);
}
```
...
...
@@ -175,6 +181,7 @@ void dfs(char[][] grid, int i, int j) {
函数签名如下:
<!-- muliti_language -->
```
java
int
closedIsland
(
int
[][]
grid
)
```
...
...
@@ -189,52 +196,55 @@ int closedIsland(int[][] grid)
有了这个思路,就可以直接看代码了,注意这题规定
`0`
表示陆地,用
`1`
表示海水:
<!-- muliti_language -->
```
java
// 主函数:计算封闭岛屿的数量
int
closedIsland
(
int
[][]
grid
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
// 把靠上边的岛屿淹掉
dfs
(
grid
,
0
,
j
);
// 把靠下边的岛屿淹掉
dfs
(
grid
,
m
-
1
,
j
);
}
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
// 把靠左边的岛屿淹掉
dfs
(
grid
,
i
,
0
);
// 把靠右边的岛屿淹掉
dfs
(
grid
,
i
,
n
-
1
);
}
// 遍历 grid,剩下的岛屿都是封闭岛屿
int
res
=
0
;
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
class
Solution
{
// 主函数:计算封闭岛屿的数量
int
closedIsland
(
int
[][]
grid
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid
[
i
][
j
]
==
0
)
{
res
++;
dfs
(
grid
,
i
,
j
);
// 把靠上边的岛屿淹掉
dfs
(
grid
,
0
,
j
);
// 把靠下边的岛屿淹掉
dfs
(
grid
,
m
-
1
,
j
);
}
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
// 把靠左边的岛屿淹掉
dfs
(
grid
,
i
,
0
);
// 把靠右边的岛屿淹掉
dfs
(
grid
,
i
,
n
-
1
);
}
// 遍历 grid,剩下的岛屿都是封闭岛屿
int
res
=
0
;
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid
[
i
][
j
]
==
0
)
{
res
++;
dfs
(
grid
,
i
,
j
);
}
}
}
return
res
;
}
return
res
;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void
dfs
(
int
[][]
grid
,
int
i
,
int
j
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
if
(
i
<
0
||
j
<
0
||
i
>=
m
||
j
>=
n
)
{
return
;
}
if
(
grid
[
i
][
j
]
==
1
)
{
// 已经是海水了
return
;
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void
dfs
(
int
[][]
grid
,
int
i
,
int
j
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
if
(
i
<
0
||
j
<
0
||
i
>=
m
||
j
>=
n
)
{
return
;
}
if
(
grid
[
i
][
j
]
==
1
)
{
// 已经是海水了
return
;
}
// 将 (i, j) 变成海水
grid
[
i
][
j
]
=
1
;
// 淹没上下左右的陆地
dfs
(
grid
,
i
+
1
,
j
);
dfs
(
grid
,
i
,
j
+
1
);
dfs
(
grid
,
i
-
1
,
j
);
dfs
(
grid
,
i
,
j
-
1
);
}
// 将 (i, j) 变成海水
grid
[
i
][
j
]
=
1
;
// 淹没上下左右的陆地
dfs
(
grid
,
i
+
1
,
j
);
dfs
(
grid
,
i
,
j
+
1
);
dfs
(
grid
,
i
-
1
,
j
);
dfs
(
grid
,
i
,
j
-
1
);
}
```
...
...
@@ -246,36 +256,40 @@ void dfs(int[][] grid, int i, int j) {
其实思路都是一样的,先把靠边的陆地淹掉,然后去数剩下的陆地数量就行了,注意第 1020 题中
`1`
代表陆地,
`0`
代表海水:
<!-- muliti_language -->
```
java
int
numEnclaves
(
int
[][]
grid
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
// 淹掉靠边的陆地
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
dfs
(
grid
,
i
,
0
);
dfs
(
grid
,
i
,
n
-
1
);
}
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
dfs
(
grid
,
0
,
j
);
dfs
(
grid
,
m
-
1
,
j
);
}
// 数一数剩下的陆地
int
res
=
0
;
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
class
Solution
{
int
numEnclaves
(
int
[][]
grid
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
// 淹掉靠边的陆地
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
dfs
(
grid
,
i
,
0
);
dfs
(
grid
,
i
,
n
-
1
);
}
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid
[
i
][
j
]
==
1
)
{
res
+=
1
;
dfs
(
grid
,
0
,
j
);
dfs
(
grid
,
m
-
1
,
j
);
}
// 数一数剩下的陆地
int
res
=
0
;
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid
[
i
][
j
]
==
1
)
{
res
+=
1
;
}
}
}
return
res
;
}
return
res
;
// 和之前的实现类似
void
dfs
(
int
[][]
grid
,
int
i
,
int
j
)
{
// ...
}
}
// 和之前的实现类似
void
dfs
(
int
[][]
grid
,
int
i
,
int
j
)
{
// ...
}
```
篇幅所限,具体代码我就不写了,我们继续看其他的岛屿题目。
...
...
@@ -284,6 +298,7 @@ void dfs(int[][] grid, int i, int j) {
这是力扣第 695 题「岛屿的最大面积」,
`0`
表示海水,
`1`
表示陆地,现在不让你计算岛屿的个数了,而是让你计算最大的那个岛屿的面积,函数签名如下:
<!-- muliti_language -->
```
java
int
maxAreaOfIsland
(
int
[][]
grid
)
```
...
...
@@ -298,40 +313,43 @@ int maxAreaOfIsland(int[][] grid)
我们可以给
`dfs`
函数设置返回值,记录每次淹没的陆地的个数,直接看解法吧:
<!-- muliti_language -->
```
java
int
maxAreaOfIsland
(
int
[][]
grid
)
{
// 记录岛屿的最大面积
int
res
=
0
;
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid
[
i
][
j
]
==
1
)
{
// 淹没岛屿,并更新最大岛屿面积
res
=
Math
.
max
(
res
,
dfs
(
grid
,
i
,
j
));
class
Solution
{
int
maxAreaOfIsland
(
int
[][]
grid
)
{
// 记录岛屿的最大面积
int
res
=
0
;
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid
[
i
][
j
]
==
1
)
{
// 淹没岛屿,并更新最大岛屿面积
res
=
Math
.
max
(
res
,
dfs
(
grid
,
i
,
j
));
}
}
}
return
res
;
}
return
res
;
}
// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
int
dfs
(
int
[][]
grid
,
int
i
,
int
j
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
if
(
i
<
0
||
j
<
0
||
i
>=
m
||
j
>=
n
)
{
// 超出索引边界
return
0
;
}
if
(
grid
[
i
][
j
]
==
0
)
{
// 已经是海水了
return
0
;
}
// 将 (i, j) 变成海水
grid
[
i
][
j
]
=
0
;
// 淹没与 (i, j) 相邻的陆地,并返回淹没的陆地面积
int
dfs
(
int
[][]
grid
,
int
i
,
int
j
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
if
(
i
<
0
||
j
<
0
||
i
>=
m
||
j
>=
n
)
{
// 超出索引边界
return
0
;
}
if
(
grid
[
i
][
j
]
==
0
)
{
// 已经是海水了
return
0
;
}
// 将 (i, j) 变成海水
grid
[
i
][
j
]
=
0
;
return
dfs
(
grid
,
i
+
1
,
j
)
+
dfs
(
grid
,
i
,
j
+
1
)
+
dfs
(
grid
,
i
-
1
,
j
)
+
dfs
(
grid
,
i
,
j
-
1
)
+
1
;
return
dfs
(
grid
,
i
+
1
,
j
)
+
dfs
(
grid
,
i
,
j
+
1
)
+
dfs
(
grid
,
i
-
1
,
j
)
+
dfs
(
grid
,
i
,
j
-
1
)
+
1
;
}
}
```
...
...
@@ -355,45 +373,48 @@ int dfs(int[][] grid, int i, int j) {
依据这个思路,可以直接写出下面的代码:
<!-- muliti_language -->
```
java
int
countSubIslands
(
int
[][]
grid1
,
int
[][]
grid2
)
{
int
m
=
grid1
.
length
,
n
=
grid1
[
0
].
length
;
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid1
[
i
][
j
]
==
0
&&
grid2
[
i
][
j
]
==
1
)
{
// 这个岛屿肯定不是子岛,淹掉
dfs
(
grid2
,
i
,
j
);
class
Solution
{
int
countSubIslands
(
int
[][]
grid1
,
int
[][]
grid2
)
{
int
m
=
grid1
.
length
,
n
=
grid1
[
0
].
length
;
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid1
[
i
][
j
]
==
0
&&
grid2
[
i
][
j
]
==
1
)
{
// 这个岛屿肯定不是子岛,淹掉
dfs
(
grid2
,
i
,
j
);
}
}
}
}
// 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
int
res
=
0
;
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
for
(
int
j
=
0
;
j
<
n
;
j
++
)
{
if
(
grid2
[
i
][
j
]
==
1
)
{
res
++
;
dfs
(
grid2
,
i
,
j
);
// 现在 grid2 中剩下的岛屿都是子岛,计算岛屿数量
int
res
=
0
;
for
(
int
i
=
0
;
i
<
m
;
i
++)
{
for
(
int
j
=
0
;
j
<
n
;
j
++)
{
if
(
grid2
[
i
][
j
]
==
1
)
{
res
++;
dfs
(
grid2
,
i
,
j
)
;
}
}
}
return
res
;
}
return
res
;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void
dfs
(
int
[][]
grid
,
int
i
,
int
j
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
if
(
i
<
0
||
j
<
0
||
i
>=
m
||
j
>=
n
)
{
return
;
}
if
(
grid
[
i
][
j
]
==
0
)
{
return
;
}
// 从 (i, j) 开始,将与之相邻的陆地都变成海水
void
dfs
(
int
[][]
grid
,
int
i
,
int
j
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
if
(
i
<
0
||
j
<
0
||
i
>=
m
||
j
>=
n
)
{
return
;
}
if
(
grid
[
i
][
j
]
==
0
)
{
return
;
}
grid
[
i
][
j
]
=
0
;
dfs
(
grid
,
i
+
1
,
j
);
dfs
(
grid
,
i
,
j
+
1
);
dfs
(
grid
,
i
-
1
,
j
);
dfs
(
grid
,
i
,
j
-
1
);
grid
[
i
][
j
]
=
0
;
dfs
(
grid
,
i
+
1
,
j
);
dfs
(
grid
,
i
,
j
+
1
);
dfs
(
grid
,
i
-
1
,
j
);
dfs
(
grid
,
i
,
j
-
1
);
}
}
```
...
...
@@ -405,6 +426,7 @@ void dfs(int[][] grid, int i, int j) {
力扣第 694 题「不同的岛屿数量」,题目还是输入一个二维矩阵,
`0`
表示海水,
`1`
表示陆地,这次让你计算
**不同的 (distinct)**
岛屿数量,函数签名如下:
<!-- muliti_language -->
```
java
int
numDistinctIslands
(
int
[][]
grid
)
```
...
...
@@ -423,6 +445,7 @@ int numDistinctIslands(int[][] grid)
因为遍历顺序是写死在你的递归函数里面的,不会动态改变:
<!-- muliti_language -->
```
java
void
dfs
(
int
[][]
grid
,
int
i
,
int
j
)
{
// 递归顺序:
...
...
@@ -453,6 +476,7 @@ void dfs(int[][] grid, int i, int j) {
所以我们需要稍微改造
`dfs`
函数,添加一些函数参数以便记录遍历顺序:
<!-- muliti_language -->
```
java
void
dfs
(
int
[][]
grid
,
int
i
,
int
j
,
StringBuilder
sb
,
int
dir
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
...
...
@@ -476,6 +500,7 @@ void dfs(int[][] grid, int i, int j, StringBuilder sb, int dir) {
`dir`
记录方向,
`dfs`
函数递归结束后,
`sb`
记录着整个遍历顺序。有了这个
`dfs`
函数就好办了,我们可以直接写出最后的解法代码:
<!-- muliti_language -->
```
java
int
numDistinctIslands
(
int
[][]
grid
)
{
int
m
=
grid
.
length
,
n
=
grid
[
0
].
length
;
...
...
高频面试系列/座位调度.md
浏览文件 @
3d91fdd9
...
...
@@ -33,6 +33,7 @@ title: '如何调度考生的座位'
也就是请你实现下面这样一个类:
<!-- muliti_language -->
```
java
class
ExamRoom
{
// 构造函数,传入座位总数 N
...
...
@@ -87,47 +88,52 @@ class ExamRoom {
这个问题还用到一个常用的编程技巧,就是使用一个「虚拟线段」让算法正确启动,这就和链表相关的算法需要「虚拟头结点」一个道理。
<!-- muliti_language -->
```
java
// 将端点 p 映射到以 p 为左端点的线段
private
Map
<
Integer
,
int
[]>
startMap
;
// 将端点 p 映射到以 p 为右端点的线段
private
Map
<
Integer
,
int
[]>
endMap
;
// 根据线段长度从小到大存放所有线段
private
TreeSet
<
int
[]>
pq
;
private
int
N
;
public
ExamRoom
(
int
N
)
{
this
.
N
=
N
;
startMap
=
new
HashMap
<>();
endMap
=
new
HashMap
<>();
pq
=
new
TreeSet
<>((
a
,
b
)
->
{
// 算出两个线段的长度
int
distA
=
distance
(
a
);
int
distB
=
distance
(
b
);
// 长度更长的更大,排后面
return
distA
-
distB
;
});
// 在有序集合中先放一个虚拟线段
addInterval
(
new
int
[]
{-
1
,
N
});
}
class
ExamRoom
{
// 将端点 p 映射到以 p 为左端点的线段
private
Map
<
Integer
,
int
[]>
startMap
;
// 将端点 p 映射到以 p 为右端点的线段
private
Map
<
Integer
,
int
[]>
endMap
;
// 根据线段长度从小到大存放所有线段
private
TreeSet
<
int
[]>
pq
;
private
int
N
;
public
ExamRoom
(
int
N
)
{
this
.
N
=
N
;
startMap
=
new
HashMap
<>();
endMap
=
new
HashMap
<>();
pq
=
new
TreeSet
<>((
a
,
b
)
->
{
// 算出两个线段的长度
int
distA
=
distance
(
a
);
int
distB
=
distance
(
b
);
// 长度更长的更大,排后面
return
distA
-
distB
;
});
// 在有序集合中先放一个虚拟线段
addInterval
(
new
int
[]
{-
1
,
N
});
}
/* 去除一个线段 */
private
void
removeInterval
(
int
[]
intv
)
{
pq
.
remove
(
intv
);
startMap
.
remove
(
intv
[
0
]);
endMap
.
remove
(
intv
[
1
]);
}
/* 去除一个线段 */
private
void
removeInterval
(
int
[]
intv
)
{
pq
.
remove
(
intv
);
startMap
.
remove
(
intv
[
0
]);
endMap
.
remove
(
intv
[
1
]);
}
/* 增加一个线段 */
private
void
addInterval
(
int
[]
intv
)
{
pq
.
add
(
intv
);
startMap
.
put
(
intv
[
0
],
intv
);
endMap
.
put
(
intv
[
1
],
intv
);
}
/* 增加一个线段 */
private
void
addInterval
(
int
[]
intv
)
{
pq
.
add
(
intv
);
startMap
.
put
(
intv
[
0
],
intv
);
endMap
.
put
(
intv
[
1
],
intv
);
}
/* 计算一个线段的长度 */
private
int
distance
(
int
[]
intv
)
{
return
intv
[
1
]
-
intv
[
0
]
-
1
;
/* 计算一个线段的长度 */
private
int
distance
(
int
[]
intv
)
{
return
intv
[
1
]
-
intv
[
0
]
-
1
;
}
// ...
}
```
...
...
@@ -138,37 +144,41 @@ private int distance(int[] intv) {
有了上述铺垫,主要 API
`seat`
和
`leave`
就可以写了:
```
java
public
int
seat
()
{
// 从有序集合拿出最长的线段
int
[]
longest
=
pq
.
last
();
int
x
=
longest
[
0
];
int
y
=
longest
[
1
];
int
seat
;
if
(
x
==
-
1
)
{
// 情况一
seat
=
0
;
}
else
if
(
y
==
N
)
{
// 情况二
seat
=
N
-
1
;
}
else
{
// 情况三
seat
=
(
y
-
x
)
/
2
+
x
;
class
ExamRoom
{
// ...
public
int
seat
()
{
// 从有序集合拿出最长的线段
int
[]
longest
=
pq
.
last
();
int
x
=
longest
[
0
];
int
y
=
longest
[
1
];
int
seat
;
if
(
x
==
-
1
)
{
// 情况一
seat
=
0
;
}
else
if
(
y
==
N
)
{
// 情况二
seat
=
N
-
1
;
}
else
{
// 情况三
seat
=
(
y
-
x
)
/
2
+
x
;
}
// 将最长的线段分成两段
int
[]
left
=
new
int
[]
{
x
,
seat
};
int
[]
right
=
new
int
[]
{
seat
,
y
};
removeInterval
(
longest
);
addInterval
(
left
);
addInterval
(
right
);
return
seat
;
}
// 将最长的线段分成两段
int
[]
left
=
new
int
[]
{
x
,
seat
};
int
[]
right
=
new
int
[]
{
seat
,
y
};
removeInterval
(
longest
);
addInterval
(
left
);
addInterval
(
right
);
return
seat
;
}
public
void
leave
(
int
p
)
{
// 将 p 左右的线段找出来
int
[]
right
=
startMap
.
get
(
p
);
int
[]
left
=
endMap
.
get
(
p
);
// 合并两个线段成为一个线段
int
[]
merged
=
new
int
[]
{
left
[
0
],
right
[
1
]};
removeInterval
(
left
);
removeInterval
(
right
);
addInterval
(
merged
);
public
void
leave
(
int
p
)
{
// 将 p 左右的线段找出来
int
[]
right
=
startMap
.
get
(
p
);
int
[]
left
=
endMap
.
get
(
p
);
// 合并两个线段成为一个线段
int
[]
merged
=
new
int
[]
{
left
[
0
],
right
[
1
]};
removeInterval
(
left
);
removeInterval
(
right
);
addInterval
(
merged
);
}
}
```
...
...
@@ -201,14 +211,19 @@ pq = new TreeSet<>((a, b) -> {
除此之外,还要改变
`distance`
函数,
**不能简单地让它计算一个线段两个端点间的长度,而是让它计算该线段中点和端点之间的长度**
。
<!-- muliti_language -->
```
java
private
int
distance
(
int
[]
intv
)
{
int
x
=
intv
[
0
];
int
y
=
intv
[
1
];
if
(
x
==
-
1
)
return
y
;
if
(
y
==
N
)
return
N
-
1
-
x
;
// 中点和端点之间的长度
return
(
y
-
x
)
/
2
;
class
ExamRoom
{
// ...
private
int
distance
(
int
[]
intv
)
{
int
x
=
intv
[
0
];
int
y
=
intv
[
1
];
if
(
x
==
-
1
)
return
y
;
if
(
y
==
N
)
return
N
-
1
-
x
;
// 中点和端点之间的长度
return
(
y
-
x
)
/
2
;
}
}
```
...
...
高频面试系列/打印素数.md
浏览文件 @
3d91fdd9
...
...
@@ -29,6 +29,7 @@ title: '如何高效寻找素数'
比如力扣第 204 题「计数质数」,让你写这样一个函数:
<!-- muliti_language -->
```
java
// 返回区间 [2, n) 中有几个素数
int
countPrimes
(
int
n
)
...
...
@@ -39,6 +40,7 @@ int countPrimes(int n)
你会如何写这个函数?我想大家应该会这样写:
<!-- muliti_language -->
```
java
int
countPrimes
(
int
n
)
{
int
count
=
0
;
...
...
@@ -100,6 +102,7 @@ Wikipedia 的这个 GIF 很形象:
看到这里,你是否有点明白这个排除法的逻辑了呢?先看我们的第一版代码:
<!-- muliti_language -->
```
java
int
countPrimes
(
int
n
)
{
boolean
[]
isPrime
=
new
boolean
[
n
];
...
...
@@ -150,21 +153,25 @@ for (int j = i * i; j < n; j += i)
这样,素数计数的算法就高效实现了,其实这个算法有一个名字,叫做 Sieve of Eratosthenes。看下完整的最终代码:
<!-- muliti_language -->
```
java
int
countPrimes
(
int
n
)
{
boolean
[]
isPrime
=
new
boolean
[
n
];
Arrays
.
fill
(
isPrime
,
true
);
for
(
int
i
=
2
;
i
*
i
<
n
;
i
++)
if
(
isPrime
[
i
])
for
(
int
j
=
i
*
i
;
j
<
n
;
j
+=
i
)
isPrime
[
j
]
=
false
;
int
count
=
0
;
for
(
int
i
=
2
;
i
<
n
;
i
++)
if
(
isPrime
[
i
])
count
++;
return
count
;
class
Solution
{
public
int
countPrimes
(
int
n
)
{
boolean
[]
isPrime
=
new
boolean
[
n
];
Arrays
.
fill
(
isPrime
,
true
);
for
(
int
i
=
2
;
i
*
i
<
n
;
i
++)
if
(
isPrime
[
i
])
for
(
int
j
=
i
*
i
;
j
<
n
;
j
+=
i
)
isPrime
[
j
]
=
false
;
int
count
=
0
;
for
(
int
i
=
2
;
i
<
n
;
i
++)
if
(
isPrime
[
i
])
count
++;
return
count
;
}
}
```
**该算法的时间复杂度比较难算**
,显然时间跟这两个嵌套的 for 循环有关,其操作数应该是:
...
...
高频面试系列/接雨水.md
浏览文件 @
3d91fdd9
...
...
@@ -34,6 +34,7 @@ title: '接雨水问题详解'
就是用一个数组表示一个条形图,问你这个条形图最多能接多少水。
<!-- muliti_language -->
```
java
int
trap
(
int
[]
height
);
```
...
...
@@ -70,6 +71,7 @@ water[i] = min(
这就是本问题的核心思路,我们可以简单写一个暴力算法:
<!-- muliti_language -->
```
java
int
trap
(
int
[]
height
)
{
int
n
=
height
.
length
;
...
...
@@ -98,29 +100,32 @@ int trap(int[] height) {
**我们开两个数组 `r_max` 和 `l_max` 充当备忘录,`l_max[i]` 表示位置 `i` 左边最高的柱子高度,`r_max[i]` 表示位置 `i` 右边最高的柱子高度**
。预先把这两个数组计算好,避免重复计算:
<!-- muliti_language -->
```
java
int
trap
(
int
[]
height
)
{
if
(
height
.
length
==
0
)
{
return
0
;
class
Solution
{
int
trap
(
int
[]
height
)
{
if
(
height
.
length
==
0
)
{
return
0
;
}
int
n
=
height
.
length
;
int
res
=
0
;
// 数组充当备忘录
int
[]
l_max
=
new
int
[
n
];
int
[]
r_max
=
new
int
[
n
];
// 初始化 base case
l_max
[
0
]
=
height
[
0
];
r_max
[
n
-
1
]
=
height
[
n
-
1
];
// 从左向右计算 l_max
for
(
int
i
=
1
;
i
<
n
;
i
++)
l_max
[
i
]
=
Math
.
max
(
height
[
i
],
l_max
[
i
-
1
]);
// 从右向左计算 r_max
for
(
int
i
=
n
-
2
;
i
>=
0
;
i
--)
r_max
[
i
]
=
Math
.
max
(
height
[
i
],
r_max
[
i
+
1
]);
// 计算答案
for
(
int
i
=
1
;
i
<
n
-
1
;
i
++)
res
+=
Math
.
min
(
l_max
[
i
],
r_max
[
i
])
-
height
[
i
];
return
res
;
}
int
n
=
height
.
length
;
int
res
=
0
;
// 数组充当备忘录
int
[]
l_max
=
new
int
[
n
];
int
[]
r_max
=
new
int
[
n
];
// 初始化 base case
l_max
[
0
]
=
height
[
0
];
r_max
[
n
-
1
]
=
height
[
n
-
1
];
// 从左向右计算 l_max
for
(
int
i
=
1
;
i
<
n
;
i
++)
l_max
[
i
]
=
Math
.
max
(
height
[
i
],
l_max
[
i
-
1
]);
// 从右向左计算 r_max
for
(
int
i
=
n
-
2
;
i
>=
0
;
i
--)
r_max
[
i
]
=
Math
.
max
(
height
[
i
],
r_max
[
i
+
1
]);
// 计算答案
for
(
int
i
=
1
;
i
<
n
-
1
;
i
++)
res
+=
Math
.
min
(
l_max
[
i
],
r_max
[
i
])
-
height
[
i
];
return
res
;
}
```
...
...
@@ -132,6 +137,7 @@ int trap(int[] height) {
首先,看一部分代码:
<!-- muliti_language -->
```
java
int
trap
(
int
[]
height
)
{
int
left
=
0
,
right
=
height
.
length
-
1
;
...
...
@@ -152,26 +158,29 @@ int trap(int[] height) {
明白了这一点,直接看解法:
<!-- muliti_language -->
```
java
int
trap
(
int
[]
height
)
{
int
left
=
0
,
right
=
height
.
length
-
1
;
int
l_max
=
0
,
r_max
=
0
;
class
Solution
{
int
trap
(
int
[]
height
)
{
int
left
=
0
,
right
=
height
.
length
-
1
;
int
l_max
=
0
,
r_max
=
0
;
int
res
=
0
;
while
(
left
<
right
)
{
l_max
=
Math
.
max
(
l_max
,
height
[
left
]);
r_max
=
Math
.
max
(
r_max
,
height
[
right
]);
int
res
=
0
;
while
(
left
<
right
)
{
l_max
=
Math
.
max
(
l_max
,
height
[
left
]);
r_max
=
Math
.
max
(
r_max
,
height
[
right
]);
// res += min(l_max, r_max) - height[i]
if
(
l_max
<
r_max
)
{
res
+=
l_max
-
height
[
left
];
left
++;
}
else
{
res
+=
r_max
-
height
[
right
];
right
--;
// res += min(l_max, r_max) - height[i]
if
(
l_max
<
r_max
)
{
res
+=
l_max
-
height
[
left
];
left
++;
}
else
{
res
+=
r_max
-
height
[
right
];
right
--;
}
}
return
res
;
}
return
res
;
}
```
...
...
@@ -212,6 +221,7 @@ if (l_max < r_max) {
函数签名如下:
<!-- muliti_language -->
```
java
int
maxArea
(
int
[]
height
);
```
...
...
@@ -242,22 +252,25 @@ min(height[left], height[right]) * (right - left)
先直接看解法代码吧:
<!-- muliti_language -->
```
java
int
maxArea
(
int
[]
height
)
{
int
left
=
0
,
right
=
height
.
length
-
1
;
int
res
=
0
;
while
(
left
<
right
)
{
// [left, right] 之间的矩形面积
int
cur_area
=
Math
.
min
(
height
[
left
],
height
[
right
])
*
(
right
-
left
);
res
=
Math
.
max
(
res
,
cur_area
);
// 双指针技巧,移动较低的一边
if
(
height
[
left
]
<
height
[
right
])
{
left
++;
}
else
{
right
--;
class
Solution
{
public
int
maxArea
(
int
[]
height
)
{
int
left
=
0
,
right
=
height
.
length
-
1
;
int
res
=
0
;
while
(
left
<
right
)
{
// [left, right] 之间的矩形面积
int
cur_area
=
Math
.
min
(
height
[
left
],
height
[
right
])
*
(
right
-
left
);
res
=
Math
.
max
(
res
,
cur_area
);
// 双指针技巧,移动较低的一边
if
(
height
[
left
]
<
height
[
right
])
{
left
++;
}
else
{
right
--;
}
}
return
res
;
}
return
res
;
}
```
...
...
高频面试系列/水塘抽样.md
浏览文件 @
3d91fdd9
...
...
@@ -40,6 +40,7 @@ title: '随机算法之水塘抽样算法'
**先说结论,当你遇到第 `i` 个元素时,应该有 `1/i` 的概率选择该元素,`1 - 1/i` 的概率保持原有的选择**
。看代码容易理解这个思路:
<!-- muliti_language -->
```
java
/* 返回链表中一个随机节点的值 */
int
getRandom
(
ListNode
head
)
{
...
...
@@ -72,6 +73,7 @@ int getRandom(ListNode head) {
**同理,如果要随机选择 `k` 个数,只要在第 `i` 个元素处以 `k/i` 的概率选择该元素,以 `1 - k/i` 的概率保持原有选择即可**
。代码如下:
<!-- muliti_language -->
```
java
/* 返回链表中 k 个随机节点的值 */
int
[]
getRandom
(
ListNode
head
,
int
k
)
{
...
...
高频面试系列/缺失和重复的元素.md
浏览文件 @
3d91fdd9
...
...
@@ -29,6 +29,7 @@ title: '如何寻找缺失和重复的元素'
给一个长度为
`N`
的数组
`nums`
,其中本来装着
`[1..N]`
这
`N`
个元素,无序。但是现在出现了一些错误,
`nums`
中的一个元素出现了重复,也就同时导致了另一个元素的缺失。请你写一个算法,找到
`nums`
中的重复元素和缺失元素的值。
<!-- muliti_language -->
```
java
// 返回两个数字,分别是 {dup, missing}
int
[]
findErrorNums
(
int
[]
nums
);
...
...
@@ -70,6 +71,7 @@ O(N) 的时间复杂度遍历数组是无法避免的,所以我们可以想想
对于这个现象,我们就可以翻译成代码了:
<!-- muliti_language -->
```
java
int
[]
findErrorNums
(
int
[]
nums
)
{
int
n
=
nums
.
length
;
...
...
@@ -95,6 +97,7 @@ int[] findErrorNums(int[] nums) {
这个问题就基本解决了,别忘了我们刚才为了方便分析,假设元素是
`[0..N-1]`
,但题目要求是
`[1..N]`
,所以只要简单修改两处地方即可得到原题的答案:
<!-- muliti_language -->
```
java
int
[]
findErrorNums
(
int
[]
nums
)
{
int
n
=
nums
.
length
;
...
...
编辑
预览
Markdown
is supported
0%
请重试
或
添加新附件
.
添加附件
取消
You are about to add
0
people
to the discussion. Proceed with caution.
先完成此消息的编辑!
取消
想要评论请
注册
或
登录