LeetCode 51-75 题解合集
文章目录
- [LeetCode 51-75 题解合集](#LeetCode 51-75 题解合集)
-
- [51. N-Queens(N 皇后)](#51. N-Queens(N 皇后))
- [52. N-Queens II(N 皇后 II)](#52. N-Queens II(N 皇后 II))
- [53. Maximum Subarray(最大子数组和)](#53. Maximum Subarray(最大子数组和))
- [54. Spiral Matrix(螺旋矩阵)](#54. Spiral Matrix(螺旋矩阵))
- [55. Jump Game(跳跃游戏)](#55. Jump Game(跳跃游戏))
- [56. Merge Intervals(合并区间)](#56. Merge Intervals(合并区间))
- [57. Insert Interval(插入区间)](#57. Insert Interval(插入区间))
- [58. Length of Last Word(最后一个单词的长度)](#58. Length of Last Word(最后一个单词的长度))
- [59. Spiral Matrix II(螺旋矩阵 II)](#59. Spiral Matrix II(螺旋矩阵 II))
- [60. Permutation Sequence(排列序列)](#60. Permutation Sequence(排列序列))
- [61. Rotate List(旋转链表)](#61. Rotate List(旋转链表))
- [62. Unique Paths(不同路径)](#62. Unique Paths(不同路径))
- [63. Unique Paths II(不同路径 II)](#63. Unique Paths II(不同路径 II))
- [64. Minimum Path Sum(最小路径和)](#64. Minimum Path Sum(最小路径和))
- [65. Valid Number(有效数字)](#65. Valid Number(有效数字))
- [66. Plus One(加一)](#66. Plus One(加一))
- [67. Add Binary(二进制求和)](#67. Add Binary(二进制求和))
- [68. Text Justification(文本左右对齐)](#68. Text Justification(文本左右对齐))
- [69. Sqrt(x)(x 的平方根)](#69. Sqrt(x)(x 的平方根))
- [70. Climbing Stairs(爬楼梯)](#70. Climbing Stairs(爬楼梯))
- [71. Simplify Path(简化路径)](#71. Simplify Path(简化路径))
- [72. Edit Distance(编辑距离)](#72. Edit Distance(编辑距离))
- [73. Set Matrix Zeroes(矩阵置零)](#73. Set Matrix Zeroes(矩阵置零))
- [74. Search a 2D Matrix(搜索二维矩阵)](#74. Search a 2D Matrix(搜索二维矩阵))
- [75. Sort Colors(颜色分类)](#75. Sort Colors(颜色分类))
51. N-Queens(N 皇后)
题目描述
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n,返回所有不同的 n 皇后问题的解决方案 。每一种解法包含一个不同的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例:
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
约束: 1 <= n <= 9
解题思路
核心思想:回溯逐行放置皇后,用列、主对角线、副对角线三组标记剪枝。
解法:回溯 + 对角线标记(推荐) 。逐行 row 遍历每一列 col:用 cols[col]、diag[row+col](主对角线)、antiDiag[row-col+n-1](副对角线,加偏移避免负数下标)判断是否可放。可放则放置并递归下一行。行号天然避免同行冲突。n≤9,组合数有限。时间 O(n!),空间 O(n)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
private List<List<String>> res = new ArrayList<List<String>>();
private boolean[] cols, diag, antiDiag;
private char[][] board;
// 回溯 + 对角线标记(推荐)
public List<List<String>> solveNQueens(int n) {
cols = new boolean[n];
diag = new boolean[2 * n];
antiDiag = new boolean[2 * n];
board = new char[n][n];
for (char[] row : board) java.util.Arrays.fill(row, '.');
dfs(0, n);
return res;
}
private void dfs(int row, int n) {
if (row == n) { // 全部行都放好
List<String> list = new ArrayList<String>();
for (char[] r : board) list.add(new String(r));
res.add(list);
return;
}
for (int col = 0; col < n; col++) {
int d = row + col, ad = row - col + n; // 主/副对角线索引
if (cols[col] || diag[d] || antiDiag[ad]) continue;
board[row][col] = 'Q';
cols[col] = diag[d] = antiDiag[ad] = true;
dfs(row + 1, n);
board[row][col] = '.';
cols[col] = diag[d] = antiDiag[ad] = false; // 回溯
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.solveNQueens(4)); // [[.Q..,...Q,Q...,..Q.],[..Q.,Q...,...Q,.Q..]]
System.out.println(s.solveNQueens(1)); // [[Q]]
}
}
复杂度分析
- 时间复杂度:O(n!) --- 每行尝试 n 个位置,剪枝大幅削减
- 空间复杂度:O(n) --- 标记数组 + 递归栈
52. N-Queens II(N 皇后 II)
题目描述
n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n,返回 n 皇后问题不同的解决方案的数量。
示例:
输入:n = 4
输出:2
约束: 1 <= n <= 9
解题思路
核心思想:与 51 题完全相同的回溯,但只用计数不用记录棋盘。
解法:回溯计数(推荐) 。同上题的三组布尔标记,dfs(row) 每放一个合法位置就累加计数,不再构造棋盘。时间 O(n!),空间 O(n)。
代码实现
java
class Solution {
private int count = 0;
private boolean[] cols, diag, antiDiag;
// 回溯计数(推荐)
public int totalNQueens(int n) {
cols = new boolean[n];
diag = new boolean[2 * n];
antiDiag = new boolean[2 * n];
dfs(0, n);
return count;
}
private void dfs(int row, int n) {
if (row == n) { count++; return; }
for (int col = 0; col < n; col++) {
int d = row + col, ad = row - col + n;
if (cols[col] || diag[d] || antiDiag[ad]) continue;
cols[col] = diag[d] = antiDiag[ad] = true;
dfs(row + 1, n);
cols[col] = diag[d] = antiDiag[ad] = false;
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.totalNQueens(4)); // 2
System.out.println(s.totalNQueens(1)); // 1
}
}
复杂度分析
- 时间复杂度:O(n!) --- 同 51 题
- 空间复杂度:O(n) --- 标记数组
53. Maximum Subarray(最大子数组和)
题目描述
给你一个整数数组 nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。
示例:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6。
约束: 1 <= nums.length <= 10^5,-10^4 <= nums[i] <= 10^4
解题思路
核心思想:贪心/Kadane 算法------遍历时维持"以当前位置结尾的最大子数组和",若前面积累为负则果断丢弃重新开始。
解法一:Kadane 动态规划(推荐) 。cur = max(nums[i], cur + nums[i])(要么从 numsi 重新开始,要么接上前面的最佳累加),ans = max(ans, cur)。时间 O(n),空间 O(1)。
解法二:分治。把数组一分为二,最大子数组要么完全在左/右半区,要么跨越中点(从中点向两侧扩展求最大和再拼接)。时间 O(n log n),空间 O(log n)。作为对比写法。
代码实现
java
class Solution {
// 解法一:Kadane(推荐)
public int maxSubArray(int[] nums) {
int cur = 0, ans = Integer.MIN_VALUE;
for (int x : nums) {
cur = Math.max(x, cur + x); // 重新开始 or 延续累加
ans = Math.max(ans, cur);
}
return ans;
}
// 解法二:分治
public int maxSubArrayDC(int[] nums) {
return divide(nums, 0, nums.length - 1);
}
private int divide(int[] a, int l, int r) {
if (l == r) return a[l];
int mid = (l + r) / 2;
int leftBest = divide(a, l, mid);
int rightBest = divide(a, mid + 1, r);
// 跨中点:向左扩展 + 向右扩展
int leftSum = 0, sum = 0;
for (int i = mid; i >= l; i--) { sum += a[i]; leftSum = Math.max(leftSum, sum); }
int rightSum = 0; sum = 0;
for (int i = mid + 1; i <= r; i++) { sum += a[i]; rightSum = Math.max(rightSum, sum); }
return Math.max(Math.max(leftBest, rightBest), leftSum + rightSum);
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.maxSubArray(new int[]{-2,1,-3,4,-1,2,1,-5,4})); // 6
System.out.println(s.maxSubArray(new int[]{1})); // 1
System.out.println(s.maxSubArray(new int[]{-1})); // -1
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| Kadane(推荐) | O(n) | O(1) | 一维滚动 |
| 分治 | O(n log n) | O(log n) | 递归栈 |
54. Spiral Matrix(螺旋矩阵)
题目描述
给你一个 m 行 n 列的矩阵 matrix,请按照顺时针螺旋顺序,返回矩阵中的所有元素。
示例:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]
约束: m == matrix.length,n == matrix[i].length,1 <= m, n <= 10,-100 <= matrix[i][j] <= 100
解题思路
核心思想:用上下左右四个边界不断收缩,按 右→下→左→上 循环输出。
解法:边界收缩(推荐) 。维护 top, bottom, left, right 四条边界与方向。每次沿当前方向收集元素,走完一条边就收缩对应边界;边界交叉即结束。时间 O(m×n),空间 O(1)(不含输出)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
// 边界收缩(推荐)
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<Integer>();
int top = 0, bottom = matrix.length - 1;
int left = 0, right = matrix[0].length - 1;
while (top <= bottom && left <= right) {
for (int j = left; j <= right; j++) res.add(matrix[top][j]); // 上边→
top++;
for (int i = top; i <= bottom; i++) res.add(matrix[i][right]); // 右边↓
right--;
if (top > bottom || left > right) break; // 防止单行/单列重复
for (int j = right; j >= left; j--) res.add(matrix[bottom][j]); // 下边←
bottom--;
for (int i = bottom; i >= top; i--) res.add(matrix[i][left]); // 左边↑
left++;
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.spiralOrder(new int[][]{{1,2,3},{4,5,6},{7,8,9}})); // [1,2,3,6,9,8,7,4,5]
System.out.println(s.spiralOrder(new int[][]{{1,2,3,4},{5,6,7,8},{9,10,11,12}})); // 螺旋输出
}
}
复杂度分析
- 时间复杂度:O(m×n) --- 每个元素访问一次
- 空间复杂度:O(1) --- 不含输出列表
55. Jump Game(跳跃游戏)
题目描述
给你一个非负整数数组 nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标,如果可以返回 true,否则返回 false。
示例:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1,然后再从下标 1 跳 3 步到达最后一个下标。
输入:nums = [3,2,1,0,4]
输出:false
约束: 1 <= nums.length <= 10^4,0 <= nums[i] <= 10^5
解题思路
核心思想:贪心维护"最远可达位置",只要可覆盖到终点即可。
解法:贪心(推荐) 。遍历每个位置 i:若 i > farthest 说明中途断档,返回 false;否则更新 farthest = max(farthest, i + nums[i])。遍历完若能覆盖 n-1 则 true。时间 O(n),空间 O(1)。
代码实现
java
class Solution {
// 贪心(推荐)
public boolean canJump(int[] nums) {
int farthest = 0;
for (int i = 0; i < nums.length; i++) {
if (i > farthest) return false; // 当前位置已不可达
farthest = Math.max(farthest, i + nums[i]); // 扩展最远可达
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.canJump(new int[]{2,3,1,1,4})); // true
System.out.println(s.canJump(new int[]{3,2,1,0,4})); // false
}
}
复杂度分析
- 时间复杂度:O(n) --- 单次遍历
- 空间复杂度:O(1)
56. Merge Intervals(合并区间)
题目描述
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi]。请你合并所有重叠的区间,并返回一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间。
示例:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠,合并为 [1,6]
约束: 1 <= intervals.length <= 10^4,intervals[i].length == 2,0 <= starti <= endi <= 10^4
解题思路
核心思想:先按区间起点排序,再线性合并重叠区间。
解法:排序 + 线性合并(推荐) 。按 starti 升序排序。遍历时维护当前合并区间 [curStart, curEnd]:若新区间起点 <= curEnd 则重叠,curEnd = max(curEnd, 新end);否则把当前区间加入结果并重置。时间 O(n log n)(排序主导),空间 O(log n)(排序栈,不含输出)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
// 排序 + 线性合并(推荐)
public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> a[0] - b[0]); // 按起点排序
List<int[]> res = new ArrayList<int[]>();
int curStart = intervals[0][0], curEnd = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] <= curEnd) { // 重叠(含相接)
curEnd = Math.max(curEnd, intervals[i][1]);
} else {
res.add(new int[]{curStart, curEnd}); // 结束当前区间
curStart = intervals[i][0];
curEnd = intervals[i][1];
}
}
res.add(new int[]{curStart, curEnd});
return res.toArray(new int[res.size()][]);
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(Arrays.deepToString(s.merge(new int[][]{{1,3},{2,6},{8,10},{15,18}}))); // [[1,6],[8,10],[15,18]]
System.out.println(Arrays.deepToString(s.merge(new int[][]{{1,4},{4,5}}))); // [[1,5]]
}
}
复杂度分析
- 时间复杂度:O(n log n) --- 排序主导
- 空间复杂度:O(log n) --- 排序栈空间,不含输出
57. Insert Interval(插入区间)
题目描述
给你一个无重叠 的、按照区间起始端点排序的区间列表 intervals,其中 intervals[i] = [starti, endi] 表示第 i 个区间的开始和结束,并且 intervals 按照 starti 升序排列。同样给定一个区间 newInterval = [start, end] 表示另一个区间的开始和结束。在 intervals 中插入 newInterval,使 intervals 仍然按照 starti 升序排列,且不重叠(如有必要则合并区间)。返回插入之后的区间列表。
示例:
输入:intervals = [[1,3],[6,9]], newInterval = [2,5]
输出:[[1,5],[6,9]]
输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
输出:[[1,2],[3,10],[12,16]]
约束: 0 <= intervals.length <= 10^4,intervals[i].length == 2,0 <= starti <= endi <= 10^5,intervals 互不重叠且有序,newInterval.length == 2
解题思路
核心思想 :一次遍历找出与 newInterval 重叠的所有区间合并,其余原样放入。
解法:三阶段线性扫描(推荐) 。分三段处理:① intervals[i].end < newInterval.start 直接加入结果(nei 在右侧之前);② intervals[i].start > newInterval.end 时把 newInterval 加入并开始收尾(后续原样加入);③ 中间重叠的区间不断取并集 [minStart, maxEnd]。时间 O(n),空间 O(n)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
// 三阶段线性扫描(推荐)
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> res = new ArrayList<int[]>();
int i = 0, n = intervals.length;
// 1. 新区间右侧之前、无重叠的区间
while (i < n && intervals[i][1] < newInterval[0]) {
res.add(intervals[i++]);
}
// 2. 与 newInterval 重叠的区间合并
int start = newInterval[0], end = newInterval[1];
while (i < n && intervals[i][0] <= end) {
start = Math.min(start, intervals[i][0]);
end = Math.max(end, intervals[i][1]);
i++;
}
res.add(new int[]{start, end});
// 3. 剩余区间原样加入
while (i < n) res.add(intervals[i++]);
return res.toArray(new int[res.size()][]);
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(Arrays.deepToString(s.insert(new int[][]{{1,3},{6,9}}, new int[]{2,5}))); // [[1,5],[6,9]]
System.out.println(Arrays.deepToString(s.insert(new int[][]{{1,2},{3,5},{6,7},{8,10},{12,16}}, new int[]{4,8}))); // [[1,2],[3,10],[12,16]]
}
}
复杂度分析
- 时间复杂度:O(n) --- 一次遍历
- 空间复杂度:O(n) --- 结果列表
58. Length of Last Word(最后一个单词的长度)
题目描述
给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中最后一个单词的长度。单词是指仅由字母组成、不包含任何空格字符的最大子字符串。
示例:
输入:s = "Hello World"
输出:5
输入:s = " fly me to the moon "
输出:4
约束: 1 <= s.length <= 10^4,s 仅有英文字母和空格 ' ' 组成,s 中至少存在一个单词
解题思路
核心思想:从末尾向前跳过空格,再数单词字母数。
解法:反向扫描(推荐) 。先 i = s.length()-1 跳过结尾空格;然后从 i 向前数连续非空格字符个数。时间 O(n),空间 O(1)。
代码实现
java
class Solution {
// 反向扫描(推荐)
public int lengthOfLastWord(String s) {
int i = s.length() - 1;
while (i >= 0 && s.charAt(i) == ' ') i--; // 跳过末尾空格
int len = 0;
while (i >= 0 && s.charAt(i) != ' ') { // 数单词
len++; i--;
}
return len;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.lengthOfLastWord("Hello World")); // 5
System.out.println(s.lengthOfLastWord(" fly me to the moon ")); // 4
}
}
复杂度分析
- 时间复杂度:O(n) --- 单次反向扫描
- 空间复杂度:O(1)
59. Spiral Matrix II(螺旋矩阵 II)
题目描述
给你一个正整数 n,生成一个包含 1 到 n² 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix。
示例:
输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]
约束: 1 <= n <= 20
解题思路
核心思想:与 54 题相反------用边界收缩 + 递增计数器填充螺旋矩阵。
解法:边界收缩填充(推荐) 。维护 top/bottom/left/right 四边界,按 右→下→左→上 循环填充 num 1...n²,每条边填完收缩对应边界。时间 O(n²),空间 O(1)(不含输出矩阵)。
代码实现
java
import java.util.Arrays;
class Solution {
// 边界收缩填充(推荐)
public int[][] generateMatrix(int n) {
int[][] m = new int[n][n];
int top = 0, bottom = n - 1, left = 0, right = n - 1;
int num = 1;
while (top <= bottom && left <= right) {
for (int j = left; j <= right; j++) m[top][j] = num++; // →
top++;
for (int i = top; i <= bottom; i++) m[i][right] = num++; // ↓
right--;
for (int j = right; j >= left; j--) m[bottom][j] = num++; // ←
bottom--;
for (int i = bottom; i >= top; i--) m[i][left] = num++; // ↑
left++;
}
return m;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(Arrays.deepToString(s.generateMatrix(3))); // [[1,2,3],[8,9,4],[7,6,5]]
System.out.println(Arrays.deepToString(s.generateMatrix(1))); // [[1]]
}
}
复杂度分析
- 时间复杂度:O(n²) --- 填充 n² 个元素
- 空间复杂度:O(1) --- 不含输出
60. Permutation Sequence(排列序列)
题目描述
给出集合 [1,2,3,...,n],其所有元素共有 n! 种排列。按大小顺序列出所有排列情况,并一一标记。给定 n 和 k,返回第 k 个排列。题目保证 1 <= k <= n!。
示例:
输入:n = 3, k = 3
输出:"213"
输入:n = 4, k = 9
输出:"2314"
约束: 1 <= n <= 9,1 <= k <= n!
解题思路
核心思想:数学构造------康托展开的逆过程,按阶乘定位每一位数字,无需生成全部排列。
解法:阶乘定位(推荐) 。维护候选数字列表 1..n 与阶乘表 fact[0..n]。对第 pos 位(从 0 起):block = fact[n-1-pos],idx = (k-1) / block,该位取候选列表中第 idx 个数字并移除,k -= idx * block。每次移动一位后在候选里继续定位。时间 O(n²)(List 移除 O(n)),空间 O(n)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
// 阶乘定位(康托展开逆过程,推荐)
public String getPermutation(int n, int k) {
int[] fact = new int[n + 1];
fact[0] = 1;
for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i;
List<Integer> nums = new ArrayList<Integer>();
for (int i = 1; i <= n; i++) nums.add(i);
StringBuilder sb = new StringBuilder();
k--; // 转 0 基
for (int pos = 0; pos < n; pos++) {
int block = fact[n - 1 - pos]; // 剩余数字的全排列数
int idx = k / block; // 当前位取第几个候选
k %= block;
sb.append(nums.get(idx));
nums.remove(idx); // 使用后移除
}
return sb.toString();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.getPermutation(3, 3)); // 213
System.out.println(s.getPermutation(4, 9)); // 2314
}
}
复杂度分析
- 时间复杂度:O(n²) --- n 次定位,每次 List 移除 O(n)
- 空间复杂度:O(n) --- 候选列表与阶乘表
61. Rotate List(旋转链表)
题目描述
给你一个链表的头节点 head,旋转链表,将链表每个节点向右移动 k 个位置。
示例:
输入:head = [1,2,3,4,5], k = 2
输出:[4,5,1,2,3]
输入:head = [0,1,2], k = 4
输出:[2,0,1]
约束: 链表中节点的数目在范围 [0, 500] 内,-100 <= Node.val <= 100,0 <= k <= 2 * 10^9
解题思路
核心思想:先成环再拆开------k 对链长取模后,从"新头"位置断开。
解法:成环 + 定位断点(推荐) 。先求链长 len,把尾节点接到头节点形成环。k %= len,新头在"从 head 出发走 len-k 步"处(新尾是第 len-k 个节点,它的 next 即新头)。断开 newTail.next = null 返回新头。时间 O(n),空间 O(1)。
代码实现
java
class ListNode {
int val;
ListNode next;
ListNode() {}
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
// 成环 + 定位断点(推荐)
public ListNode rotateRight(ListNode head, int k) {
if (head == null || head.next == null || k == 0) return head;
int len = 1;
ListNode tail = head;
while (tail.next != null) { tail = tail.next; len++; } // 求长并到尾
tail.next = head; // 成环
k %= len;
int steps = len - k; // 从 head 走到新尾的步数
ListNode newTail = head;
for (int i = 1; i < steps; i++) newTail = newTail.next;
ListNode newHead = newTail.next; // 新头
newTail.next = null; // 断开环
return newHead;
}
private static String str(ListNode h) {
StringBuilder sb = new StringBuilder("[");
while (h != null) { sb.append(h.val).append(h.next == null ? "" : ","); h = h.next; }
return sb.append("]").toString();
}
public static void main(String[] args) {
Solution s = new Solution();
ListNode h1 = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
System.out.println(str(s.rotateRight(h1, 2))); // [4,5,1,2,3]
ListNode h2 = new ListNode(0, new ListNode(1, new ListNode(2)));
System.out.println(str(s.rotateRight(h2, 4))); // [2,0,1]
}
}
复杂度分析
- 时间复杂度:O(n) --- 求长 + 定位
- 空间复杂度:O(1)
62. Unique Paths(不同路径)
题目描述
一个机器人位于一个 m x n 网格的左上角,机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。问总共有多少条不同的路径?
示例:
输入:m = 3, n = 7
输出:28
输入:m = 3, n = 2
输出:3
约束: 1 <= m, n <= 100,答案保证在 32 位整数范围内
解题思路
核心思想 :动态规划------dp[i][j] = dp[i-1][j] + dp[i][j-1](只能从上方或左方来);或用组合数学 C(m+n-2, m-1)。
解法一:二维 DP(推荐) 。dp[i][j] 表示到 (i,j) 的路径数,第一行与第一列全为 1(只能直线走),其余按状态转移累加。时间 O(m×n),空间 O(m×n),可滚动数组优化到 O(n)。
解法二:组合数学 。总共走 m+n-2 步,其中向下 m-1 步,答案是 C(m+n-2, m-1),用 long 计算。时间 O(min(m,n)),空间 O(1)。
代码实现
java
class Solution {
// 解法一:二维 DP(推荐)
public int uniquePaths(int m, int n) {
int[] dp = new int[n]; // 滚动数组,空间优化 O(n)
for (int j = 0; j < n; j++) dp[j] = 1; // 第一行全 1
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] += dp[j - 1]; // dp[j](上) + dp[j-1](左)
}
}
return dp[n - 1];
}
// 解法二:组合数学 C(m+n-2, m-1)
public int uniquePathsComb(int m, int n) {
long ans = 1;
int steps = m + n - 2;
int k = Math.min(m - 1, n - 1);
for (int i = 1; i <= k; i++) {
ans = ans * (steps - k + i) / i; // 组合数公式逐步乘除防溢出
}
return (int) ans;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.uniquePaths(3, 7)); // 28
System.out.println(s.uniquePaths(3, 2)); // 3
System.out.println(s.uniquePathsComb(3, 7)); // 28
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 滚动 DP(推荐) | O(m×n) | O(n) | 每行只依赖上一行 |
| 组合数学 | O(min(m,n)) | O(1) | 公式一步到位 |
63. Unique Paths II(不同路径 II)
题目描述
一个机器人位于一个 m x n 网格的左上角。网格中的障碍物和空位置分别用 1 和 0 来表示。机器人每次只能向下或者向右移动一步,试图达到网格的右下角。问总共有多少条不同的路径从左上角到达右下角?(障碍物处不可通过)
示例:
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
约束: 1 <= m, n <= 100,obstacleGridij 为 0 或 1
解题思路
核心思想 :与 62 题几乎相同,但遇到障碍物 dp[j] = 0。
解法:滚动 DP + 障碍处理(推荐) 。首行首列初始化时遇到障碍即后续全 0(被阻断);状态转移时若 obstacleGrid[i][j] == 1 则 dp[j] = 0,否则 dp[j] += dp[j-1]。时间 O(m×n),空间 O(n)。
代码实现
java
class Solution {
// 滚动 DP + 障碍处理(推荐)
public int uniquePathsWithObstacles(int[][] grid) {
int m = grid.length, n = grid[0].length;
if (grid[0][0] == 1) return 0; // 起点即障碍
int[] dp = new int[n];
dp[0] = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
dp[j] = 0; // 障碍处路径数为 0
} else if (j > 0) {
dp[j] += dp[j - 1]; // 上方 + 左侧
}
}
}
return dp[n - 1];
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.uniquePathsWithObstacles(new int[][]{{0,0,0},{0,1,0},{0,0,0}})); // 2
System.out.println(s.uniquePathsWithObstacles(new int[][]{{1}})); // 0
}
}
复杂度分析
- 时间复杂度:O(m×n)
- 空间复杂度:O(n) --- 滚动数组
64. Minimum Path Sum(最小路径和)
题目描述
给定一个包含非负整数的 m x n 网格 grid,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。每次只能向下或者向右移动一步。
示例:
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:路径 1→3→1→1→1 的总和最小,为 7。
约束: 1 <= m, n <= 200,0 <= grid[i][j] <= 200
解题思路
核心思想:动态规划------每个格子的最小路径和 = 该格值 + min(上方, 左侧)。
解法:原地 DP(推荐) 。直接复用 grid 数组:第一行/第一列先前缀累加(只能直走);其余 grid[i][j] += min(grid[i-1][j], grid[i][j-1])。时间 O(m×n),空间 O(1)(原地)。
代码实现
java
class Solution {
// 原地 DP(推荐)
public int minPathSum(int[][] grid) {
int m = grid.length, n = grid[0].length;
for (int i = 1; i < m; i++) grid[i][0] += grid[i - 1][0]; // 第一列
for (int j = 1; j < n; j++) grid[0][j] += grid[0][j - 1]; // 第一行
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]); // 取上方/左侧较小
}
}
return grid[m - 1][n - 1];
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.minPathSum(new int[][]{{1,3,1},{1,5,1},{4,2,1}})); // 7
System.out.println(s.minPathSum(new int[][]{{1,2,3},{4,5,6}})); // 12
}
}
复杂度分析
- 时间复杂度:O(m×n)
- 空间复杂度:O(1) --- 原地修改
65. Valid Number(有效数字)
题目描述
给定一个字符串 s,返回 s 是否是一个有效数字。有效数字按顺序必须满足以下格式(按顺序):可选符号 (+/-) → 有效整数/有效小数 → 可选指数部分(e/E + 可选符号 + 有效整数)。空格只允许出现在最前和最后。
示例:
有效:"2"、"0089"、"-0.1"、"+3.14"、"4."、"-.9"、"2e10"、"-90E3"、"3e+7"、"+6e-1"、"53.5e93"、"-123.456e789"
无效:"abc"、"1a"、"1e"、"e3"、"99e2.5"、"--6"、"-+3"、"95a54e53"
约束: 1 <= s.length <= 20,s 仅含英文字母(大写和小写)、数字、+、-、空格、. 之一
解题思路
核心思想:有限状态机或分段解析------先把最简"无空串"判断做掉,再用布尔标记逐步扫描。
解法:状态标记扫描(推荐) 。去掉首尾空格后遍历,维护 numSeen / dotSeen / eSeen 标记:数字则 numSeen=true;. 要求未见过点且未在指数中;e/E 要求已见数字且未见过 e;+/- 只能出现在开头或 e 之后紧邻;其余字符 false。结尾必须 numSeen。时间 O(n),空间 O(1)。
代码实现
java
class Solution {
// 状态标记扫描(推荐)
public boolean isNumber(String s) {
s = s.trim();
if (s.length() == 0) return false;
boolean numSeen = false, dotSeen = false, eSeen = false;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (Character.isDigit(c)) {
numSeen = true;
} else if (c == '.') {
if (dotSeen || eSeen) return false; // 小数点唯一且不能在指数部分
dotSeen = true;
} else if (c == 'e' || c == 'E') {
if (eSeen || !numSeen) return false; // e 唯一且前面必须有数字
eSeen = true;
numSeen = false; // 指数部分必须重新出现数字
} else if (c == '+' || c == '-') {
if (i > 0 && s.charAt(i - 1) != 'e' && s.charAt(i - 1) != 'E') return false;
} else {
return false; // 非法字符
}
}
return numSeen;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isNumber("0")); // true
System.out.println(s.isNumber("2e10")); // true
System.out.println(s.isNumber("-90E3")); // true
System.out.println(s.isNumber(".1")); // true
System.out.println(s.isNumber("1e")); // false
System.out.println(s.isNumber("99e2.5")); // false
System.out.println(s.isNumber("--6")); // false
}
}
复杂度分析
- 时间复杂度:O(n) --- 单次扫描
- 空间复杂度:O(1)
66. Plus One(加一)
题目描述
给定一个由整数组成的非空数组所表示的非负整数,在该数的基础上加一。最高位数字存放在数组的首位,数组中每个元素只存储单个数字。你可以假设除了整数 0 之外,这个整数不会以零开头。
示例:
输入:digits = [1,2,3]
输出:[1,2,4]
输入:digits = [9,9,9]
输出:[1,0,0,0]
约束: 1 <= digits.length <= 100,0 <= digits[i] <= 9
解题思路
核心思想:从末位模拟进位,若全为 9 则数组整体进位加长。
解法:倒序模拟进位(推荐) 。从最后一位开始 digits[i]++,若小于 10 则直接返回;否则置 0 继续前一位。若全部进位(如 999),new 一个 n+1 长度数组,首位 1 即可。时间 O(n),空间 O(1)(除 999 场景 O(n))。
代码实现
java
import java.util.Arrays;
class Solution {
// 倒序模拟进位(推荐)
public int[] plusOne(int[] digits) {
for (int i = digits.length - 1; i >= 0; i--) {
digits[i]++;
if (digits[i] < 10) return digits; // 无进位直接返回
digits[i] = 0; // 进位继续
}
int[] res = new int[digits.length + 1]; // 999 -> 1000
res[0] = 1;
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(Arrays.toString(s.plusOne(new int[]{1,2,3}))); // [1,2,4]
System.out.println(Arrays.toString(s.plusOne(new int[]{9,9,9}))); // [1,0,0,0]
}
}
复杂度分析
- 时间复杂度:O(n) --- 最坏遍历全部
- 空间复杂度:O(1)(一般) / O(n)(全 9 进位)
67. Add Binary(二进制求和)
题目描述
给你两个二进制字符串 a 和 b,以二进制字符串的形式返回它们的和。
示例:
输入:a = "11", b = "1"
输出:"100"
输入:a = "1010", b = "1011"
输出:"10101"
约束: 1 <= a.length, b.length <= 10^4,a 和 b 仅由字符 '0' 和 '1' 组成,不含前导零
解题思路
核心思想:模拟竖式二进制加法,从低位到高位逐位相加并维护进位。
解法:双指针模拟(推荐) 。i, j 分别从两串末尾前移,carry 进位。每轮 sum = carry + (i>=0 ? a[i]-'0' : 0) + (j>=0 ? b[j]-'0' : 0),结果位 sum % 2,进位 sum / 2。最后若 carry=1 追加。时间 O(m+n),空间 O(max(m,n))(结果串)。
代码实现
java
class Solution {
// 双指针模拟(推荐)
public String addBinary(String a, String b) {
StringBuilder sb = new StringBuilder();
int i = a.length() - 1, j = b.length() - 1, carry = 0;
while (i >= 0 || j >= 0 || carry > 0) {
int sum = carry;
if (i >= 0) sum += a.charAt(i--) - '0';
if (j >= 0) sum += b.charAt(j--) - '0';
sb.append(sum % 2); // 当前位
carry = sum / 2; // 进位
}
return sb.reverse().toString(); // 逆序构建需反转
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.addBinary("11", "1")); // 100
System.out.println(s.addBinary("1010", "1011")); // 10101
}
}
复杂度分析
- 时间复杂度:O(m+n)
- 空间复杂度:O(max(m,n)) --- 结果串
68. Text Justification(文本左右对齐)
题目描述
给定一个单词数组 words 和一个长度 maxWidth,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。要求:尽可能多地往每行中放置单词;必要时用额外空格 ' ' 填充,使每行恰好有 maxWidth 字符;要求均匀分配单词间的空格数量,如果某行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧。文本的最后一行应为左对齐,且单词之间不插入额外的空格。
示例:
输入:words = ["This", "is", "an", "example", "of", "text", "justification."], maxWidth = 16
输出:
[
"This is an",
"example of text",
"justification. "
]
约束: 1 <= words.length <= 300,1 <= words[i].length <= 20,wordsi 由小写英文字母和符号组成,1 <= maxWidth <= 100,wordsi.length <= maxWidth
解题思路
核心思想:贪心凑行 + 均匀分配空格(多解用左右余数分配)。
解法:按行贪心构建(推荐) 。对每行:先贪心收集能放下的单词(至少 1 个空格间隔)。然后分三种情况填充:① 最后一行或只有一个单词 → 左对齐,右侧补空格;② 普通行 → 总空格数 = maxWidth - 所有单词长度和,gap = 空格数 / (k-1) 均分,extra = 空格数 % (k-1) 前 extra 个间隔多放 1 个空格(左侧多放)。时间 O(n×maxWidth),空间 O(maxWidth)(行缓冲)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
// 按行贪心构建(推荐)
public List<String> fullJustify(String[] words, int maxWidth) {
List<String> res = new ArrayList<String>();
int i = 0, n = words.length;
while (i < n) {
// 1. 贪心收集本行单词
int j = i, len = 0; // len = 已计入的字符(不含间隔)
while (j < n && len + words[j].length() + (j - i) <= maxWidth) {
len += words[j].length();
j++;
}
int cnt = j - i; // 本行单词数
int totalSpaces = maxWidth - len; // 需分配的空格总数
// 2. 构建本行
StringBuilder line = new StringBuilder();
if (j == n || cnt == 1) { // 最后一行或单单词:左对齐
for (int k = i; k < j; k++) {
line.append(words[k]);
if (k < j - 1) line.append(' ');
}
while (line.length() < maxWidth) line.append(' ');
} else { // 普通行:均匀分配空格
int gap = totalSpaces / (cnt - 1);
int extra = totalSpaces % (cnt - 1); // 前 extra 个间隔多 1 空格
for (int k = i; k < j; k++) {
line.append(words[k]);
if (k < j - 1) {
for (int s = 0; s < gap; s++) line.append(' ');
if (k - i < extra) line.append(' ');
}
}
}
res.add(line.toString());
i = j;
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
String[] words = {"This", "is", "an", "example", "of", "text", "justification."};
System.out.println(s.fullJustify(words, 16));
}
}
复杂度分析
- 时间复杂度:O(n×maxWidth) --- 每个单词被处理一次,填充空格 O(maxWidth)
- 空间复杂度:O(maxWidth) --- 行缓冲(不含结果)
69. Sqrt(x)(x 的平方根)
题目描述
给你一个非负整数 x,计算并返回 x 的算术平方根 。由于返回类型是整数,结果只保留整数部分,小数部分将被舍去。不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或 x ** 0.5。
示例:
输入:x = 4 输出:2
输入:x = 8 输出:2
约束: 0 <= x <= 2^31 - 1
解题思路
核心思想 :在 0, x 上二分找最大的 mid 使 mid*mid <= x,用除法防溢出。
解法一:二分(推荐) 。lo=0, hi=x,mid = lo + (hi-lo)/2,用 mid <= x/mid 判断(等价 mid² ≤ x 且不溢出)。满足则 ans=mid, lo=mid+1,否则 hi=mid-1。时间 O(log x),空间 O(1)。
解法二:牛顿迭代 。x_{n+1} = (x_n + a/x_n)/2,收敛很快。时间 O(log x) 级,常数更小。
代码实现
java
class Solution {
// 解法一:二分(推荐)
public int mySqrt(int x) {
int lo = 0, hi = x, ans = 0;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
if (mid <= x / mid) { // mid*mid <= x 防溢出写法
ans = mid;
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return ans;
}
// 解法二:牛顿迭代
public int mySqrtNewton(int x) {
long r = x;
while (r * r > x) {
r = (r + x / r) / 2; // 迭代逼近
}
return (int) r;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.mySqrt(4)); // 2
System.out.println(s.mySqrt(8)); // 2
System.out.println(s.mySqrt(2147483647)); // 46340
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 二分(推荐) | O(log x) | O(1) | 标准二分 |
| 牛顿迭代 | O(log x) | O(1) | 收敛更快,常数小 |
70. Climbing Stairs(爬楼梯)
题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
示例:
输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。1. 1 阶 + 1 阶 2. 2 阶
输入:n = 3
输出:3
约束: 1 <= n <= 45
解题思路
核心思想 :斐波那契数列------dp[n] = dp[n-1] + dp[n-2](最后一步爬 1 或 2 阶)。
解法一:滚动 DP(推荐) 。a=1, b=1(dp0=1, dp1=1),循环 n-1 次:next = a + b; a = b; b = next。时间 O(n),空间 O(1)。
解法二:快速幂矩阵。斐波那契可用矩阵 \[1,1,1,0] 的 n 次幂 O(log n) 求解,n ≤ 45 时收益不大。
代码实现
java
class Solution {
// 解法一:滚动 DP(推荐)
public int climbStairs(int n) {
int a = 1, b = 1; // dp[0]=1, dp[1]=1
for (int i = 2; i <= n; i++) {
int next = a + b;
a = b;
b = next;
}
return b;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.climbStairs(2)); // 2
System.out.println(s.climbStairs(3)); // 3
System.out.println(s.climbStairs(45)); // 1836311903
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 滚动 DP(推荐) | O(n) | O(1) | 斐波那契 |
| 矩阵快速幂 | O(log n) | O(1) | n 大时的进阶写法 |
71. Simplify Path(简化路径)
题目描述
给你一个字符串 path,表示指向某一文件或目录的 Unix 风格绝对路径(以 / 开头),请你将其转化为更加简洁的规范路径 。在 Unix 风格的文件系统中,一个点 . 表示当前目录本身;两个点 .. 表示将目录切换到上一级(指向父目录);两者都可以是复杂相对路径的组成部分。任意多个连续的斜杠(即 //)都被视为单个斜杠 /。对于此问题,任何其他格式的点(例如 ...)均被视为文件/目录名称。
返回的规范路径必须遵循下述格式:始终以斜杠 / 开头;两个目录名之间必须只有一个斜杠 /;最后一个目录名(如果存在)不能以 / 结尾;根目录 / 除外。
示例:
输入:path = "/home/" 输出:"/home"
输入:path = "/../" 输出:"/"
输入:path = "/home//foo/" 输出:"/home/foo"
输入:path = "/a/./b/../../c/" 输出:"/c"
输入:path = "/a//b////c/d//././/.." 输出:"/a/b/c"
约束: 1 <= path.length <= 3000,path 由英文字母、数字、.、/ 或 _ 组成
解题思路
核心思想 :用 / 切分路径段,用栈处理 .(跳过)与 ..(弹栈)。
解法:切分 + 栈(推荐) 。按 / split 后遍历各段:空串或 . 忽略;.. 若非空则弹栈;其余入栈。最后用 / 拼接并前缀 /,空栈输出 /。时间 O(n),空间 O(n)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 切分 + 栈(推荐)
public String simplifyPath(String path) {
Deque<String> stack = new ArrayDeque<String>();
for (String part : path.split("/")) {
if (part.isEmpty() || part.equals(".")) continue; // 空或当前目录
if (part.equals("..")) {
if (!stack.isEmpty()) stack.pop(); // 回退父目录
} else {
stack.push(part);
}
}
if (stack.isEmpty()) return "/";
StringBuilder sb = new StringBuilder();
while (!stack.isEmpty()) {
sb.insert(0, "/" + stack.pop()); // 栈顶是最后段,前插拼接
}
return sb.toString();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.simplifyPath("/home/")); // /home
System.out.println(s.simplifyPath("/../")); // /
System.out.println(s.simplifyPath("/home//foo/")); // /home/foo
System.out.println(s.simplifyPath("/a/./b/../../c/")); // /c
}
}
复杂度分析
- 时间复杂度:O(n) --- 扫描与入栈
- 空间复杂度:O(n) --- 栈
72. Edit Distance(编辑距离)
题目描述
给你两个单词 word1 和 word2,请返回将 word1 转换成 word2 所使用的最少操作数。你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符。
示例:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:horse -> rorse(将 'h' 替换为 'r')-> rose(删除 'r')-> ros(删除 'e')
约束: 0 <= word1.length, word2.length <= 500,word1 和 word2 由小写英文字母组成
解题思路
核心思想 :经典双串 DP------dp[i][j] 表示 word1 前 i 个字符转成 word2 前 j 个字符的最少操作数。
解法:二维 DP(推荐)。初始化第一行/第一列为自然增长(插入/删除)。转移:
word1[i-1] == word2[j-1]:dp[i][j] = dp[i-1][j-1](不需操作);- 否则
dp[i][j] = 1 + min(dp[i-1][j] 删除, dp[i][j-1] 插入, dp[i-1][j-1] 替换)。
时间 O(m×n),空间 O(m×n)(可滚动到 O(n))。
代码实现
java
class Solution {
// 二维 DP(推荐)
public int minDistance(String word1, String word2) {
int m = word1.length(), n = word2.length();
int[] dp = new int[n + 1];
for (int j = 0; j <= n; j++) dp[j] = j; // 第一行:word1 空 -> 插入
for (int i = 1; i <= m; i++) {
int prev = dp[0]; // 上一行的 dp[i-1][0]
dp[0] = i; // 当前行第一列:删除
for (int j = 1; j <= n; j++) {
int temp = dp[j];
if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
dp[j] = prev; // 字符相同无需操作
} else {
dp[j] = 1 + Math.min(prev, Math.min(dp[j], dp[j - 1])); // 替换/删除/插入
}
prev = temp;
}
}
return dp[n];
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.minDistance("horse", "ros")); // 3
System.out.println(s.minDistance("intention", "execution")); // 5
}
}
复杂度分析
- 时间复杂度:O(m×n)
- 空间复杂度:O(n) --- 滚动数组
73. Set Matrix Zeroes(矩阵置零)
题目描述
给定一个 m x n 的矩阵,如果一个元素为 0,则将其所在行和列的所有元素都设为 0。请使用原地算法。
示例:
输入:matrix = [[1,1,1],[1,0,1],[1,1,1]]
输出:[[1,0,1],[0,0,0],[1,0,1]]
约束: 1 <= m, n <= 200,-2^31 <= matrix[i][j] <= 2^31 - 1。进阶:常数空间方案?
解题思路
核心思想:用矩阵第一行/第一列作为"是否有零"的标记,再用两个标志位处理首行首列本身。
解法:首行首列作为标记(推荐,O(1) 空间) 。先记录第一行与第一列是否本身含 0(row0Zero / col0Zero)。然后遍历 [1..m)×[1..n):若 matrix[i][j] == 0,标记 matrix[i][0] = 0 与 matrix[0][j] = 0。第二次遍历按标记清零(除首行首列),最后根据标志位处理首行首列。时间 O(m×n),空间 O(1)。
代码实现
java
import java.util.Arrays;
class Solution {
// 首行首列作为标记(推荐)
public void setZeroes(int[][] matrix) {
int m = matrix.length, n = matrix[0].length;
boolean row0Zero = false, col0Zero = false;
for (int j = 0; j < n; j++) if (matrix[0][j] == 0) row0Zero = true; // 首行含 0?
for (int i = 0; i < m; i++) if (matrix[i][0] == 0) col0Zero = true; // 首列含 0?
// 1. 用首行首列记录零位置
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) { matrix[i][0] = 0; matrix[0][j] = 0; }
}
}
// 2. 按标记清零
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) matrix[i][j] = 0;
}
}
// 3. 处理首行首列本身
if (row0Zero) for (int j = 0; j < n; j++) matrix[0][j] = 0;
if (col0Zero) for (int i = 0; i < m; i++) matrix[i][0] = 0;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] m = {{1,1,1},{1,0,1},{1,1,1}};
s.setZeroes(m);
System.out.println(Arrays.deepToString(m)); // [[1,0,1],[0,0,0],[1,0,1]]
}
}
复杂度分析
- 时间复杂度:O(m×n) --- 三趟遍历
- 空间复杂度:O(1) --- 原地
74. Search a 2D Matrix(搜索二维矩阵)
题目描述
给你一个满足下述两条属性的 m x n 整数矩阵:每行中的整数从左到右按非递减顺序排列;每行的第一个整数大于前一行的最后一个整数。给你一个整数 target,如果 target 在矩阵中,返回 true,否则返回 false。
示例:
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 3
输出:true
输入:matrix = [[1,3,5,7],[10,11,16,20],[23,30,34,60]], target = 13
输出:false
约束: 1 <= m, n <= 100,-10^4 <= matrix[i][j], target <= 10^4
解题思路
核心思想:矩阵整体按行展开是一个升序数组,直接用一维二分(下标换算)。
解法一:整体二分(推荐) 。把 m×n 矩阵视作长度 m×n 的递增序列:lo=0, hi=m*n-1,mid 换算行列 matrix[mid/n][mid%n]。时间 O(log(m×n)),空间 O(1)。
解法二:Z 字形搜索。从右上角开始:比 target 大则左移列,比 target 小则下移行。时间 O(m+n),空间 O(1)。
代码实现
java
class Solution {
// 解法一:整体二分(推荐)
public boolean searchMatrix(int[][] matrix, int target) {
int m = matrix.length, n = matrix[0].length;
int lo = 0, hi = m * n - 1;
while (lo <= hi) {
int mid = (lo + hi) / 2;
int val = matrix[mid / n][mid % n]; // 一维下标换算二维
if (val == target) return true;
else if (val < target) lo = mid + 1;
else hi = mid - 1;
}
return false;
}
// 解法二:右上角 Z 字形
public boolean searchMatrixZ(int[][] matrix, int target) {
int i = 0, j = matrix[0].length - 1;
while (i < matrix.length && j >= 0) {
if (matrix[i][j] == target) return true;
else if (matrix[i][j] > target) j--; // 列左移变小
else i++; // 行下移变大
}
return false;
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] m = {{1,3,5,7},{10,11,16,20},{23,30,34,60}};
System.out.println(s.searchMatrix(m, 3)); // true
System.out.println(s.searchMatrix(m, 13)); // false
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 整体二分(推荐) | O(log(m×n)) | O(1) | 最优 |
| Z 字形搜索 | O(m+n) | O(1) | 适合二分不可行的变体 |
75. Sort Colors(颜色分类)
题目描述
给定一个包含红色、白色和蓝色、一共 n 个元素的数组 nums,原地 对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。我们使用整数 0、1 和 2 分别表示红色、白色和蓝色。不使用代码库中的排序函数,且仅使用常数级额外空间的条件下解决。
示例:
输入:nums = [2,0,2,1,1,0]
输出:[0,0,1,1,2,2]
约束: n == nums.length,1 <= n <= 300,numsi 为 0、1 或 2
解题思路
核心思想:三指针(荷兰国旗问题)------p0 放 0,p2 放 2,i 遍历中间。
解法:三指针分区(推荐) 。p0=0, i=0, p2=n-1。遍历 i ≤ p2:nums[i]==0 则与 p0 交换,p0++、i++;nums[i]==2 则与 p2 交换,p2--(i 不动,因为换来的可能是 0);nums[i]==1 则 i++。时间 O(n),空间 O(1)。
代码实现
java
import java.util.Arrays;
class Solution {
// 三指针分区(荷兰国旗,推荐)
public void sortColors(int[] nums) {
int p0 = 0, i = 0, p2 = nums.length - 1;
while (i <= p2) {
if (nums[i] == 0) { // 0 放左边
swap(nums, i++, p0++);
} else if (nums[i] == 2) { // 2 放右边,i 不动待检查换回的值
swap(nums, i, p2--);
} else {
i++; // 1 留在中间
}
}
}
private void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }
public static void main(String[] args) {
Solution s = new Solution();
int[] a = {2,0,2,1,1,0};
s.sortColors(a);
System.out.println(Arrays.toString(a)); // [0,0,1,1,2,2]
}
}
复杂度分析
- 时间复杂度:O(n) --- 单趟
- 空间复杂度:O(1)