- 所有题目来源为力扣
- 题解为原创,仅为个人当前阶段能想到/能接受的解法,不一定是优解
- 🟢简单 🟠中等 🔴困难 ⚪未完成 ⚫未更新
链表
🟠 92. 反转链表 II
给你单链表的头指针 head
和两个整数 left
和 right
,其中 left <= right
。请你反转从位置 left
到位置 right
的链表节点,返回 反转后的链表 。
示例 1:
1 | 输入:head = [1,2,3,4,5], left = 2, right = 4 |
示例 2:
1 | 输入:head = [5], left = 1, right = 1 |
题解
分三步走即可。
- 将用于遍历的指针
p
定位到要反转的section
头节点处,同时跟踪到section
头节点的前一个节点,记作pBeforeMiddle
; - 使用头插法将
section
内的right-left+1
个节点插入一个新的链表中,完成反转需求。注意第一个进行头插的节点firstHeadInserted
会成为反转后的最后一个节点,为了将反转section
接上后续不用反转的部分,第一个进行头插的节点要用指针存一下; - 将
pBeforeMiddle
节点接上section
的第一个节点,同时将firstHeadInserted
接上section
后面的那个节点即可。
完整代码:
1 | public class Solution { |
🟠 86. 分隔链表
给你一个链表的头节点 head
和一个特定值 x
,请你对链表进行分隔,使得所有 小于 x
的节点都出现在 大于或等于 x
的节点之前。
你应当 保留 两个分区中每个节点的初始相对位置。
示例 1:
1 | 输入:head = [1,4,3,2,5,2], x = 3 |
提示:
- 链表中节点的数目在范围
[0, 200]
内 -100 <= Node.val <= 100
-200 <= x <= 200
题解
应是简单题。遍历链表,按顺序将小于 x
和大于等于 x
的节点分别收集到两个链表中,最后将两个链表相接即可实现相对位置不变的链表分隔。
完整代码:
1 | public class Solution { |
动态规划 / 记忆化搜索
🔴 403. 青蛙过河
一只青蛙想要过河。 假定河流被等分为若干个单元格,并且在每一个单元格内都有可能放有一块石子(也有可能没有)。 青蛙可以跳上石子,但是不可以跳入水中。
给你石子的位置列表 stones
(用单元格序号 升序 表示), 请判定青蛙能否成功过河(即能否在最后一步跳至最后一块石子上)。
开始时, 青蛙默认已站在第一块石子上,并可以假定它第一步只能跳跃一个单位(即只能从单元格 1 跳至单元格 2 )。
如果青蛙上一步跳跃了 k
个单位,那么它接下来的跳跃距离只能选择为 k - 1
、k
或 k + 1
个单位。 另请注意,青蛙只能向前方(终点的方向)跳跃。
示例 1:
1 | 输入:stones = [0,1,3,5,6,8,12,17] |
示例 2:
1 | 输入:stones = [0,1,2,3,4,8,9,11] |
提示:
2 <= stones.length <= 2000
0 <= stones[i] <= 231 - 1
stones[0] == 0
题解
通过直觉,不难想到回溯遍历的方式找到可能的到达终点的路径。对于青蛙的任意状态,都能够由当前石头下标 currentI
和由上一块石头经过几跳 lastJump
到达当前石头这么两个参数确定,由于青蛙下一跳只能跳 lastJump-1
~ lastJump+1
跳,故从 stone[]
中向后找能恰用这些步数到达的石头进行转移,若没有石头可以转移,说明该路径不能到达终点。
完整代码:
1 | class Solution { |
虽然在用例计算量较小时可以通过,但对于递归较深重复很多的情况下会超时,故用记忆化搜索优化之,易得:
1 | class Solution { |
🟢 70. 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
1 | 输入: 2 |
示例 2:
1 | 输入: 3 |
题解
首先很容易想到回溯遍历,每种状态都 DFS 往下找走一步和走两步的情况,直到 n
耗尽,就不重不漏地找到了所有路径。
1 | class Solution { |
不过稍微测试一个大一点的用例,比如输入 42
,执行结果就超时了,因此我们需要思考一下进行优化。
虽然挺久没有做到记忆化搜索的题了,但是很幸运,当提交用例超时的那一瞬间,瞬间就触电般地想到了借助记忆化搜索对该 DFS 剪枝便能完美解决此题。遂改之:
1 | class Solution { |
用记忆化搜索改进后,从超时到 0ms,顺利 AC 了。
虽然 AC 了,但是想到既然能用记忆化搜索,那不如顺带再用动态规划解一遍吧,就当复习一下动态规划了。
不难定义状态 dp[i]
表示走 i
阶楼梯的情况数,那么同样不难想到, dp[i]
可由 dp[i-1]
和 dp[i-2]
转移得来,因为第 i
阶楼梯可由 i-1
阶楼梯走一步到达,也可由 i-2
阶楼梯走两步到达。故有转移方程 dp[i] = dp[i-1] + dp[i-2]
。同时初始状态显然为 dp[0] = dp[1] = 1
。
同样 AC 的动态规划解法完整代码:
1 | class Solution { |
🟠 64. 最小路径和
给定一个包含非负整数的 *m* x *n*
网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例 1:
1 | 输入:grid = [[1,3,1],[1,5,1],[4,2,1]] |
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 200
0 <= grid[i][j] <= 100
题解
- 状态定义:
dp[i,j]
表示移动到(i,j)
处的最小代价。 - 状态转移:
- 若
i == 0
且j > 0
,则dp[i,j] = dp[i,j-1] + grid[i][j]
,因为只能从左侧转移来; - 若
j == 0
且i > 0
,则dp[i,j] = dp[i-1,j] + grid[i][j]
,因为只能从上方转移来; - 若
i > 0
且j > 0
,则dp[i,j] = Max(dp[i,j-1],dp[i-1,j]) + grid[i][j]
以满足最小代价。
- 若
完整代码:
1 | public class Solution { |
🟠 63. 不同路径 II
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用 1
和 0
来表示。
示例 1:
1 | 输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] |
提示:
m == obstacleGrid.length
n == obstacleGrid[i].length
1 <= m, n <= 100
obstacleGrid[i][j]
为0
或1
题解
状态定义:
dp[i,j]
代表移动到(i,j)
的路径数,且obstacleGrid[i,j] = 1
的地方dp[i,j]
必为 0。状态转移:
- 当
i == 0
且j > 0
时,dp[i,j] = dp[i,j-1]
,即与左边来的路径数一致; 当
j == 0
且i > 0
时,dp[i,j] = dp[i-1,j]
,即与上边来的路径数一致;当
i > 0
且j > 0
时,dp[i,j] = dp[i-1,j] + dp[i,j-1]
,即从左来的路径数与从上来的路径数之和。
- 当
完整代码:
1 | public class Solution { |
🟠 62. 不同路径
一个机器人位于一个 m x n
网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
示例 1:
1 | 输入:m = 3, n = 7 |
提示:
1 <= m, n <= 100
- 题目数据保证答案小于等于
2 * 109
题解
- 状态定义:
dp[i,j]
代表移动到(i,j)
的路径数,且显然当i == 0
或j == 0
时,路径数量为一条。 - 状态转移:当
i > 0
且j > 0
时,dp[i,j] = dp[i-1,j] + dp[i,j-1]
,即从左来的路径数与从上来的路径数之和。
完整代码:
1 | public class Solution { |
🟠 120. 三角形最小路径和
给定一个三角形 triangle
,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i
,那么下一步可以移动到下一行的下标 i
或 i + 1
。
示例 1:
1 | 输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]] |
示例 2:
1 | 输入:triangle = [[-10]] |
提示:
1 <= triangle.length <= 200
triangle[0].length == 1
triangle[i].length == triangle[i - 1].length + 1
-104 <= triangle[i][j] <= 104
题解
- 状态定义:
dp[Two2OneDemensional(i,j)]
表示移动到位置(i,j)
的最小路径和,且显然初始条件为dp[0] = triangle[0][0]
,表示顶点到自己的最短路径和即为自己本身。 - 状态转移:由题意“每一步只能移动到下一行中相邻的结点上”可知,
dp[k]
可由上方节点转移来,也可能由左上方节点转移来。不过特别地,最右侧节点只可能由左上方转移来,因为其没有正上方节点;同理最左侧节点只可能由上方转移来,因为其不存在左上方的节点。
题目要返回的是到达三角形底部的最小路径和,因此取底部节点的 dp
中最小的那个即可。
完整代码:
1 | public class Solution { |
🟠 931. 下降路径最小和
给你一个 n x n
的 方形 整数数组 matrix
,请你找出并返回通过 matrix
的下降路径 的 最小和 。
下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col)
的下一个元素应当是 (row + 1, col - 1)
、(row + 1, col)
或者 (row + 1, col + 1)
。
示例 1:
1 | 输入:matrix = [[2,1,3],[6,5,4],[7,8,9]] |
示例 2:
1 | 输入:matrix = [[-19,57],[-40,-5]] |
示例 3:
1 | 输入:matrix = [[-48]] |
提示:
n == matrix.length
n == matrix[i].length
1 <= n <= 100
-100 <= matrix[i][j] <= 100
题解
- 状态定义:
dp[i][j]
表示移动到位置(i,j)
的最小路径和,且显然初始条件为dp[0][:] = matrix[0][:]
,表示第一行各节点到自己的最短路径和即为自己本身。 - 状态转移:由题意可知,
dp[k]
可由上方、左上方或右上方节点转移来。不过特别地,最右侧节点只可能由上方或左上方转移来,因为其没有右上方节点;同理最左侧节点只可能由上方或右上方转移来,因为其不存在左上方的节点。
题目要返回的是到达方形底部的最小路径和,因此取底部节点的 dp
中最小的那个即可。
备注:这题和 120. 三角形最小路径和 几乎一样,但在编码本题的时候使用使用了一个空间优化技巧。
考虑到填充到达每层的最短路径时,仅与到达上一层的最短路径相关,与更上方的层无关,因此只需要存储当前层的 dp 值,给下层提供依赖即可,使用
层数&1
的方式来实现只存储到达上一层和当前层的最短路径值,当遍历到下一层时,就自动覆盖之前使用完毕的上层 dp 值,实现滚动数组,复用空间,将dp[length][length]
$O(n^2)$ 的空间复杂度降到了dp[2][length]
的 $O(n)$。
完整代码:
1 | class Solution { |
🔴 1289. 下降路径最小和 II
给你一个整数方阵 arr
,定义「非零偏移下降路径」为:从 arr
数组中的每一行选择一个数字,且按顺序选出来的数字中,相邻数字不在原数组的同一列。
请你返回非零偏移下降路径数字和的最小值。
示例 1:
1 | 输入:arr = [[1,2,3],[4,5,6],[7,8,9]] |
提示:
1 <= arr.length == arr[i].length <= 200
-99 <= arr[i][j] <= 99
题解
这题非常适合 DP 练习。首先可以依照正常 DP 思路解之:
- 状态定义:
dp[i][j]
表示移动到位置(i,j)
的最小路径和,且显然初始条件为dp[0][:] = matrix[0][:]
,表示第一行各节点到自己的最短路径和即为自己本身。 - 状态转移:由题意可知,
dp[k]
可由上行除了正上方之的任意节点转移来,因此选择上层除正上方外的最小的那个 DP 节点进行转移即可。
完整代码:
1 | class Solution { |
不过由于转移来源范围太广了,导致算法的时间复杂度直接飙到了 $O(n^3)$,虽然能 AC,但实在不够优雅。为了降低时间复杂度,还是少偷点懒吧。
其实很容易发现,下一层将使用到的 dp 在遍历当前层的时候就可以被基本确定下来,因为下层 dp 期待的是由上层最小的 dp 转移来,那么只需要记录当前层最小的两个 dp 值供下层用就行了。
为什么是两个呢?Obviously,最小的那一个可能恰好处于下层遍历节点的正上方,这样的话最小的上层 dp 就不能用了,而如果最小的在正上方,那么第二小的就一定不在正上方,因此转移到下层的 dp 必然是当前层最小的那两个 dp 值当中的一个。
由于下层找转移来源的时候不需要再遍历整个上一层了,时间复杂度也就降到了 $O(n^2)$,执行用时从 45ms 优化到了 4ms,效果还算不错。
1 | class Solution { |
且慢!
仅仅这样就可以了吗?
可以看到,经过上面的优化后,我们实际只用到了每层最小的那两个 dp 而已,其它的数据都并没有用到,那么为什么还要白画 $O(n)$ 的空间复杂度去存储这些用不到的 dp 值呢?因此我们可以将优化后的“残垣断壁”清理一下了,至只存储当前层最小的两个 dp,其它数据一概不要,这样直接将空间复杂度压到了 $O(1)$。
同时,尽管此举的目的是优化空间,但由于之前离散地访问二维数组,空间局部性很差,经过空间优化取消了数组直接用整型变量存储需要的最小和次小 dp 后,执行用时受益,进一步大大降低到了 2ms。
1 | class Solution { |
回溯遍历
🟠 22. 括号生成
数字 n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
有效括号组合需满足:左括号必须以正确的顺序闭合。
示例 1:
1 | 输入:n = 3 |
示例 2:
1 | 输入:n = 1 |
提示:
1 <= n <= 8
题解
使用回溯遍历的方式,不断对当前状态追加可能的括号。
- 初始时左右括号余量均为
n
; - 当左括号有余量时,可以加入左括号;
- 当右括号有余量且余量大于左括号的余量时,可以加入右括号;
- 当左右括号都无余量时,一个合法的括号组合构造完毕,递归结束。
完整代码:
1 | class Solution { |
DFS + 剪枝
🔴 301. 删除无效的括号
给你一个由若干括号和字母组成的字符串 s
,删除最小数量的无效括号,使得输入的字符串有效。
返回所有可能的结果。答案可以按 任意顺序 返回。
示例 1:
1 | 输入:s = "()())()" |
示例 2:
1 | 输入:s = "(a)())()" |
示例 3:
1 | 输入:s = ")(" |
提示:
1 <= s.length <= 25
s
由小写英文字母以及括号'('
和')'
组成s
中至多含20
个括号
题解
因为每一个半括号都有可能被删除,因此首先考虑用回溯遍历解决本题。在每一个下标处,如果对应字符是括号,那么就可以选择保留,也可以选择不保留,直到字符串的最后一个字符扫描完毕,检查其是否构成一个有效的串,
如何检查其括号是否有效呢?非常简单,只需设置一个偏移变量 bias
记录左括号的盈余状态
- 若扫描并加入了一个左括号,则
bias++
- 若扫描并加入了一个右括号,则
bias--
那么显然在扫描的途中,一旦出现 bias < 0
则说明串已经出现非法状态,必然无效,不需要再往后扫描了,此时可以剪枝。而在整个串扫描完毕后,检查 bias
的值即可,若为 0
则说明没有左括号盈余,也就是有效,否则无效。
不过这样的遍历耗时还算非常严重的,经过分析思考不难发现仍有可剪枝的地方。由于题目要求删除最小数量的括号,这就给优化提供了一条思路,即在 DFS 的过程中记录前面已经删除的括号数量,若在递归过程中删除的括号数量已经大于了之前产生过的有效串所删除的括号数,那么就不必再往深处遍历了,就算再找到有效串,也必然不是删除最小数量括号的串,不能作为结果之一。
完整代码:
1 | class Solution { |
双指针
🟠 11. 盛最多水的容器
给你 n
个非负整数 a1,a2,...,a``n
,每个数代表坐标中的一个点 (i, ai)
。在坐标内画 n
条垂直线,垂直线 i
的两个端点分别为 (i, ai)
和 (i, 0)
。找出其中的两条线,使得它们与 x
轴共同构成的容器可以容纳最多的水。
说明:你不能倾斜容器。
示例 1:
1 | 输入:[1,8,6,2,5,4,8,3,7] |
示例 2:
1 | 输入:height = [1,1] |
提示:
n == height.length
2 <= n <= 10^5
0 <= height[i] <= 10^4
题解
最容易想到的当然是遍历所有可能垂线组合的暴力法了,不过注意到 n <= 10^5
的数据范围,显然 $O(n^2)$ 的搜索办法是会超时的。本题可以使用双指针的思想来解决。首先将两个指针分别置于两侧的垂线处,考虑到若将较长的垂线向内侧移动的话,面积必然变小,因此较短一侧能够产生的最大面积必然就是当前时刻的面积,其不可能再提供更大的面积了,因此可以直接舍弃较短侧的垂线,将短侧向内移动,按此规则不断移动,则能够记录下所有的垂线能产生的最大面积,那么答案就必然在这个“所有垂线能产生的最大面积”的集合中,因此,只需利用双指针移动遍历依次,记录过程中产生的最大面积值即是本题的答案。
完整代码:
1 | class Solution { |