🔥 LeetCode 热题 HOT 100(21-30)
46. 全排列
思路:典型回溯法
class Solution {
public List<List<Integer>> permute(int[] nums) {
LinkedList<Integer> track = new LinkedList<>();
boolean[] visited = new boolean[nums.length];
dfs(nums, track, visited);
return res;
}
private List<List<Integer>> res = new LinkedList<>();
private void dfs(int[] nums, LinkedList<Integer> track, boolean[] visited) {
//base case
if (track.size() == nums.length) {
res.add(new LinkedList<>(track));
return;
}
for (int i = 0; i < nums.length; i++) {
//剪枝
if (visited[i]) continue;
// 选择
track.offerLast(nums[i]);
visited[i] = true;
dfs(nums, track, visited);
//撤销选择
track.pollLast();
visited[i] = false;
}
}
}
48. 旋转图像
思路:(技巧题,记住就好)
- 先沿对角线翻转
- 然后每一行按中间元素翻转
class Solution {
public void rotate(int[][] matrix) {
int row = matrix.length;
int col = matrix[0].length;
for (int i = 0; i < row; i++) {
for (int j = i + 1; j < row; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
for (int i = 0; i < row; i++) {
int left = 0;
int right = col - 1;
while (left < right) {
int temp = matrix[i][left];
matrix[i][left] = matrix[i][right];
matrix[i][right] = temp;
left++;
right--;
}
}
}
}
49. 字母异位词分组
思路:根据字母异位词定义可知:
- 互为异位词的字符串根据字符排序后得到的字符串相同;
- 互为异位词的字符串中各个字符的出现次数相同。
因此有两种方法:排序法、计数法。
排序法:
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
char[] strArr = str.toCharArray();
Arrays.sort(strArr);
String orderStr = new String(strArr);
List<String> list = map.getOrDefault(orderStr, new LinkedList<>());
list.add(str);
map.put(orderStr, list);
}
return new ArrayList<List<String>>(map.values());
}
}
计数法:
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
//统计str中字符出现次数
int[] count = new int[26];
for (int i = 0; i < str.length(); i++) {
count[str.charAt(i) - 'a']++;
}
//拼接key
StringBuilder sb = new StringBuilder();
for (int i = 0; i < count.length; i++) {
if (count[i] > 0) {
sb.append('a' + i);
sb.append(count[i]);
}
}
String key = sb.toString();
List<String> list = map.getOrDefault(key, new LinkedList<>());
list.add(str);
map.put(key, list);
}
return new ArrayList<List<String>>(map.values());
}
}
53. 最大子序和
思路:动态规划
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
//以当前位置结尾的子数组的最大和
int[] dp = new int[len];
//base case
dp[0] = nums[0];
int max = dp[0];
//转移方程:若dp[i - 1] 大于 0, dp[i] = dp[i - 1] + nums[i];否则 dp[i] = nums[i];
for (int i = 1; i < len; i++) {
if (dp[i - 1] > 0) {
dp[i] = dp[i - 1] + nums[i];
} else {
dp[i] = nums[i];
}
max = Math.max(max, dp[i]);
}
return max;
}
}
以上代码可以看出当前状态只与前一个状态相关,故可以将 空间复杂度由O(n)
降到O(1)
:
class Solution {
public int maxSubArray(int[] nums) {
int len = nums.length;
int sum = nums[0];
int max = sum;
for (int i = 1; i < len; i++) {
if (sum > 0) {
sum = sum + nums[i];
} else {
sum = nums[i];
}
max = Math.max(max, sum);
}
return max;
}
}
55. 跳跃游戏
思路一:动态规划,推荐
class Solution {
public boolean canJump(int[] nums) {
int len = nums.length;
//起始位置能否跳至当前位置
boolean[] dp = new boolean[len];
//base case
dp[0] = true;
//转移方程:i前面所有的点只要有一个能跳到当前结点就说明当前结点可达
for (int i = 1; i < len; i++) {
for (int j = 0; j < i; j++) {
if (dp[j] && (j + nums[j] >= i)) {
dp[i] = true;
break;
}
}
}
return dp[len - 1];
}
}
思路二:贪心,时间复杂度更低,多数靠死记硬背。如果能用动态规划就尽量用动规,超时了再说
class Solution {
public boolean canJump(int[] nums) {
int len = nums.length;
//目前从起点能调到的最远位置
int maxFar = 0;
for (int i = 0; i <= maxFar; i++) {
if (nums[i] + i > maxFar) {
maxFar = nums[i] + i;
}
if (maxFar >= (len - 1)) {
return true;
}
}
return false;
}
}
56. 合并区间
class Solution {
public int[][] merge(int[][] intervals) {
int len = intervals.length;
//按照 左 边界排序
Arrays.sort(intervals, (arr1, arr2) -> arr1[0] - arr2[0]);
int[][] res = new int[len][2];
//i指向新区间的第一个小区间,k为合并后的新区间个数
int i = 0, k = 0;
while (i < len) {
int left = intervals[i][0];
//合并区间的过程就是修改right的过程
int right = intervals[i][1];
//寻找可以合并的区间并完成合并
int j = i + 1;
while (j < len && right >= intervals[j][0]) {
right = Math.max(right, intervals[j][1]);
j++;
}
//存储新区间的范围
res[k][0] = left;
res[k][1] = right;
k++;
i = j;
}
return Arrays.copyOf(res, k);
}
}
62. 不同路径
思路:动态规划
-
状态:从起点到当前结点的不同路径数。
-
转移方程:起点到当前点的不同路径数
dp[i][j]
等于:起点到当前结点相邻左结点dp[i][j - 1]
和相邻上结点dp[i - 1][j]
不同路径数之和。 -
base case:第0行
dp[0][x]
和0列dp[x][0]
都为1,前者只能通过其相邻左节点到达,后者只能通过相邻上结点到达。
class Solution {
public int uniquePaths(int m, int n) {
//状态
int dp[][] = new int[m][n];
//base case
for (int i = 0; i < n; i++) {
dp[0][i] = 1;
}
for (int i = 1; i < m; i++) {
dp[i][0] = 1;
}
//转移方程
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
}
}
return dp[m - 1][n - 1];
}
}
64. 最小路径和
思路:动态规划,和上一题相似。
-
状态:起点到当前结点的最小路径和
-
转移方程:起点到当前结点最小路径和
dp[i][j]
等于:min
(起点到其相邻左结点最小路径和dp[i][j - 1]
,起点到其相邻上结点最小路径和dp[i - 1][j]
) + 当前结点值grid[i][j]
-
base case:
dp[0][0] = grid[0][0]
; 第一行dp[0][x]
都为其相邻左结点dp[0][x -1]
+ 自身结点值grid[0][x]
,x >= 1
;第一列dp[x][0]
都为其相邻上结点dp[x - 1][0]
+ 自身结点值grid[x][0]
,x >= 1
。
class Solution {
public int minPathSum(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
//从左上角到i, j的最短路径和
int[][] dp = new int[m][n];
//base case
dp[0][0] = grid[0][0];
for (int i = 1; i < m; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
for (int i = 1; i < n; i++) {
dp[0][i] = dp[0][i - 1] + grid[0][i];
}
//转移方程
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp [i][j - 1]) + grid[i][j];
}
}
return dp[m - 1][n - 1];
}
}
70. 爬楼梯
思路:动态规划
-
状态:从第0个台阶跳到当前台阶的不同方式
-
转移方程:第0个台阶到当前台阶的不同方式
dp[i]
等于:第0个台阶到当前台阶下面两个台阶的不同方式之和(dp[i - 1] + dp[i - 2]
) -
base case:
dp[0] = dp[1] = 1
class Solution {
public int climbStairs(int n) {
//状态:从第0个台阶跳到当前台阶的不同方式
int[] dp = new int[n + 1];
//base case
dp[0] = 1;
dp[1] = 1;
//转移方程
for (int i = 2; i < n + 1; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
72. 编辑距离
思路:动态规划
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
//word1 前m个字符和 word2 前n个字符之间的编辑距离,z
int[][] dp = new int[m + 1][n + 1];
//base case
dp[0][0] = 0;
for (int i = 1; i < m + 1; i++) {
dp[i][0] = i;
}
for (int i = 1; i < n + 1; i++) {
dp[0][i] = i;
}
//状态转移
for (int i = 1; i < m + 1; i++) {
for (int j = 1; j < n + 1; j++) {
//最后一个字符相等
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
//不等则分别进行 替换、删除word1中最后一个字符,或在word1最后插入一个字符,取代价最小的一个
} else {
dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i - 1][j]), dp[i][j - 1]) + 1;
}
}
}
return dp[m][n];
}
}