46. 全排列 (Permutations)
标签: 回溯
解法:
- 首先注意到题目已经约定数组中不会出现重复的数字, 并且可以按照任意顺序返回所有全排列, 这两点大大降低了解题难度.
- 由于需要返回所有全排列, 因此只能老老实实使用回溯来获取所有全排列.
代码:
|
|
48. 旋转图像 (Rotate Image)
标签: 模拟
解法:
- 手动模拟即可. 注意到这是个方阵, 直接使用第一层循环来遍历矩阵中的各个同心方框; 对于每个同心方框, 使用第二层循环以及 $O(1)$ 的额外空间来原地旋转该方框的四条边, 例如第一次循环用于旋转左上角, 右上角, 右下角, 以及左下角这四个元素, 然后第二次循环用于旋转左上角的右边, 右上角的下面, 右下角的左边, 以及左下角的上面这四个元素, 以此类推.
代码:
|
|
49. 字母异位词分组 (Group Anagrams)
标签: 哈希表
解法:
- 由于所有字母异位词所含有的字母的数量均相同, 对所有字母异位词进行排序之后得到的字符串也应该相同, 因此可以使用排序后的字符串的哈希值作为该字母异位词等价类的代表值, 并使用哈希表来维护从排序后的字符串的哈希值到字母异位词等价类的映射.
- 注意到上述从哈希值到字母异位词等价类的映射并不是一个双射 (即有可能存在两个不同的等价类, 其拥有相同的哈希值), 但出现这种情况的概率极低, 完全可以忽略不计.
代码:
|
|
53. 最大子数组和 (Maximum Subarray)
标签: 动态规划
解法:
考虑从数组最左端开始到当前位置为止这一范围中的所有子数组和, 根据这些子数组和的右端点是否位于当前位置可以将其分为不相交的两部分:
- 对于右端点位于当前位置的所有子数组和, 其最大值等于当前元素加上 "以位于当前元素左边的元素为右端点的最大子数组和" 与零之间的较大值 (因为所要求的是最大值, 因此如果是负数就得忽略, 只有当其为非负的时候才可以加上).
- 对于右端点不位于当前位置的所有子数组和, 其最大值就等于 "从数组最左端开始到当前位置的左边位置为止这一范围中的所有子数组和" 的最大值.
综上所属即可得到状态转移方程.
代码:
|
|
55. 跳跃游戏 (Jump Game)
标签: 贪心
解法:
- 本题解法比较简单, 直接进行模拟即可. 我们可以维护一个表示 "当前最远可以跳跃到的位置" 的变量, 在跳跃的过程中不断更新该变量, 并在该变量到达数组尾部的时候返回
true
, 否则说明数组尾部不可到达, 返回false
.
代码:
|
|
56. 合并区间 (Merge Intervals)
标签: 模拟
解法:
本题直接根据在草稿纸上手动合并区间的过程进行模拟即可. 首先将所有区间按照左端点从小到大进行排序, 左端点相同的区间按照右端点从大到小进行排序, 然后对所有左端点相同的区间进行去重 (留下右端点最大的那个区间作为代表) (去重步骤为可选). 之后开始遍历每个区间, 在遍历过程中维护 "目前为止所合并的最大区间", 对于当前区间:
- 如果 "目前为止所合并的最大区间" 的右端点严格小于当前区间的左端点, 说明这两个区间不重叠, 因此可以将 "目前为止所合并的最大区间" 收集至答案集合中, 并将 "目前为止所合并的最大区间" 更新为当前区间;
- 如果 "目前为止所合并的最大区间" 的右端点大于等于当前区间的左端点, 说明两个区间发生重叠, "目前为止所合并的最大区间" 的右端点更新为其原值与当前区间的右端点之间的较大值.
遍历结束之后还需要记得将 "目前为止所合并的最大区间" 中留有的最后一个合并区间收集至答案集合中.
代码:
|
|
62. 不同路径 (Unique Paths)
标签: 动态规划, 数学
解法:
- 第一种解法是使用动态规划, 状态转移方程也十分简单, 即 "当前位置的路径总数" = "上方位置的路径总数" + "左边位置的路径总数", 使用一个滚动数组进行动态规划即可.
- 第二种解法是使用数学中的组合数学. 观察到动态规划解法中的状态转移方程实际上就是杨辉三角的计算公式, 而第 m 行, 第 n 列位置对应于杨辉三角中的第 (m + n - 1) 行的第 n 个元素, 其计算公式为 $C_{m + n - 2}^{n - 1}$ (或等价的 $C_{m + n - 2}^{m - 1}$).
代码 (动态规划的解法):
|
|
代码 (数学的解法):
(略)
64. 最小路径和 (Minimum Path Sum)
标签: 动态规划
解法:
- 本题虽然属于动态规划, 其解法却非常简单, 类似于 "62. 不同路径 (Unique Paths)". 状态转移方程为 "当前位置的最小路径和" = "上面位置的最小路径和" 与 "左边位置的最小路径和" 之间的较小值 + 当前位置的权重.
代码:
|
|
70. 爬楼梯 (Climbing Stairs)
标签: 动态规划
解法:
- 本题属于一维动态规划, 只需使用 $O(1)$ 的额外空间即可, 其解法也非常简单, 状态转移方程为 "当前位置的方法总数" = "前一个位置的方法总数" + "前两个位置的方法总数".
代码:
|
|
72. 编辑距离 (Edit Distance)
标签: 动态规划
解法:
本题为最经典的编辑距离问题, 其解法也是最经典的二维动态规划. 我们维护当前在二维矩阵中的位置
(i, j)
, 其中i
为字符串word1
的下标, 表示word1
的[0, i)
这一范围的子串 (注意是左闭右开);j
为字符串word2
的下标, 其意义类似i
; 而二维矩阵中当前位置(i, j)
的值则表示word1
的[0, i)
这一范围的子串与word2
的[0, j)
这一范围的子串之间的编辑距离. 位置(i, j)
处的值与三种情况有关:- 如果删除
word1
的第i - 1
个字符 (等价于在word2
的第j
个位置处增加一个相同的字符), 则状态转移至(i - 1, j)
. - 如果删除
word2
的第j - 1
个字符 (等价于在word1
的第i
个位置处增加一个相同的字符), 则状态转移至(i, j - 1)
. - 如果替换
word1
的第i - 1
个字符 (等价于替换word2
的第j - 1
个字符), 则状态转移至(i - 1, j - 1)
.
可以看到增加字符的操作实际上等价于删除字符的操作, 同时上述状态转移需要用到 dp 数组中当前位置上方, 左边, 以及左上方这三个位置的数据, 因此在使用滚动数组的时候必须使用两个滚动数组.
- 如果删除
代码:
|
|
75. 颜色分类 (Sort Colors)
标签: 快速排序, 扫描, 主元
解法:
- 直接使用快速排序中的扫描算法对数组执行一趟扫描即可.
代码:
|
|
76. 最小覆盖子串 (Minimum Window Substring)
标签: 哈希表, 滑动窗口
解法:
- 本题的类型为滑动窗口. 初始时两个指针
i
和j
处在原始字符串的最左端, 然后j
开始向右走并不断吸纳字符, 直至所吸纳的字符构成的子串形成了目标字符串的一个覆盖, 由于在吸纳的过程中有可能吸纳进一些无用的字符, 为了确定最小覆盖, 还需要将i
指针向右走并排出无用的字符, 直至排出这样一个字符, 该字符的排出使得子串失去覆盖性质, 那么排出该字符前的子串大小即为最小覆盖子串大小的一个候选, 可以用于更新最终的最小覆盖子串大小. 不断重复上述过程直至循环结束为止即可.
代码:
|
|
78. 子集 (Subsets)
标签: 回溯, 迭代, 位运算
解法:
- 第一种解法是使用回溯, 直接使用一般的 DFS 样板代码套进去即可.
- 第二种解法是使用迭代模拟回溯中的调用栈, 避免函数调用开销.
- 第三种解法是使用位运算, 直接使用整数的二进制模式来表示要选取原始集合中的哪些元素. 例如对于一个大小为 $2^5 = 32$ 的原始集合, 整数
3
的二进制模式是0x00011
, 表示选取第 4 和第 5 个数 (因为第 4 位和第 5 位被置一).
代码 (回溯的解法):
|
|
代码 (迭代的解法):
|
|
代码 (位运算的解法):
(略)
79. 单词搜索 (Word Search)
标签: 回溯
解法:
- 直接使用标准的回溯解法即可. 维护一个
visited
二维布尔数组用于存储当前已经遍历到的所有元素的位置信息, 即若visited[i][j]
等于true
表示元素(i, j)
已经遍历到 (或者说已被使用过). 对于二维字符数组board
的当前位置的元素, 如果该元素等于字符串word
的当前位置的元素, 那么可以将visited
中的对应元素置为true
, 然后使用回溯法对board
中的该位置的上下左右四个位置进行进一步探索; 否则直接返回false
.
代码:
|
|
84. 柱状图中最大的矩形 (Largest Rectangle in Histogram)
标签: 单调栈
解法:
- 遍历数组中的所有墙. 对于当前遍历到的墙, 将其暂时作为右墙, 并枚举目前单调栈中所有比该右墙高 (同时由于单调栈的性质也必然比左墙高, 于是能够形成合法的矩形) 的中间墙, 易知所形成的矩形的高度等于中间墙的高度, 而矩形宽度等于右墙位置与左墙位置之差. 于是我们便可通过遍历所有右墙来更新最终答案.
- 此外为了处理遍历结束时留存在单调栈中的未处理的左墙, 我们可以在原始数组中额外 push 进去一个高度为零的墙作为哨兵.
代码:
|
|
85. 最大矩形 (Maximal Rectangle)
标签: 单调栈
解法:
- 本题和 "84. 柱状图中最大的矩形 (Largest Rectangle in Histogram)" 有着千丝万缕的联系, 可以合理怀疑第 84 题其实就是为了求解本题而提出的一个子问题.
- 考虑一个矩形, 我们实际上可以将其视作 "从该矩形的底边开始向上生长的柱状图" 中的一个矩形, 于是求解 "矩阵中的最大矩形" 的问题就转化为了求解 "所有柱状图中的最大矩形的最大值" 的问题, 而后者已经在第 84 题中被解决, 直接套用即可.
代码:
|
|
94. 二叉树的中序遍历 (Binary Tree Inorder Traversal)
标签: 递归, 迭代
解法:
- 第一种解法是使用经典的递归.
- 第二种解法是使用迭代来模拟递归.
代码 (递归的解法):
(略)
代码 (迭代的解法):
|
|
96. 不同的二叉搜索树 (Unique Binary Search Trees)
标签: 动态规划
解法:
- 由于二叉搜索树的左子树和右子树也是二叉搜索树, 原问题被转化为等价的子问题, 状态转移方程为 "当前大小的二叉搜索树的不同形状的总数" = "从零开始到当前大小减一的所有可能的左子树以及对应的右子树的不同形状的总数的乘积之和". 由于二叉搜索树区分左右, 因此左子树的大小应该从从零开始一直遍历到 "当前大小减一".
代码:
|
|
98. 验证二叉搜索树 (Validate Binary Search Tree)
标签: 递归, 中序遍历
解法:
- 第一种解法是使用递归. 因为二叉搜索树的左子树和右子树也必然是二叉搜索树, 因此我们可以通过递归验证左右子树来确定当前树是否为合法的二叉搜索树. 此外要想当前树为合法的二叉搜索树, 我们还需要确保左子树的最大值不超过当前树的树根的值, 同时右子树的最小值也不低于当前树的树根的值, 这可以通过在递归进入下一层时传入恰当的信息来做到.
- 第二种解法是使用中序遍历. 因为一棵树是二叉搜索树当且仅当对其进行中序遍历所得到的序列有序, 因此我们可以对树进行中序遍历, 在遍历过程中维护 "当前已遍历到的序列中的最大值" (因为我们并不需要整个序列, 而仅仅需要判断加入当前树的树根的值之后该序列是否仍然有序), 并使用该值检查当前树是否为合法的二叉搜索树.
代码 (递归的解法):
(略)
代码 (中序遍历的解法):
|
|
101. 对称二叉树 (Symmetric Tree)
标签: 递归, 迭代
解法:
- 第一种解法是使用递归. 因为一棵二叉树是对称的当且仅当其左右子树为镜像对称的, 而两棵二叉树为镜像对称的当且仅当两棵二叉树的树根的值相同并且第一棵树的左子树与第二棵树的右子树镜像对称, 第一棵树的右子树也与第二棵树的左子树镜像对称. 于是原问题便被分解为等价的两个子问题, 可以使用递归进行求解.
- 第二种解法是使用迭代. 观察到在递归解法中, 所检查的实际上是每一对根结点的值是否相等, 因此我们完全可以通过迭代以任意顺序遍历所有根结点对, 只要确保不重复且不遗漏即可. 具体迭代方法包括但不限于广度优先搜索, 深度优先搜索等等.
代码 (递归的解法):
(略)
代码 (迭代的解法):
|
|