文章目录
-
- [1. Two Sum(两数之和)](#1. Two Sum(两数之和))
- [2. Add Two Numbers(两数相加)](#2. Add Two Numbers(两数相加))
- [3. Longest Substring Without Repeating Characters(无重复字符的最长子串)](#3. Longest Substring Without Repeating Characters(无重复字符的最长子串))
- [4. Median of Two Sorted Arrays(寻找两个正序数组的中位数)](#4. Median of Two Sorted Arrays(寻找两个正序数组的中位数))
- [5. Longest Palindromic Substring(最长回文子串)](#5. Longest Palindromic Substring(最长回文子串))
- [6. Zigzag Conversion(Z 字形变换)](#6. Zigzag Conversion(Z 字形变换))
- [7. Reverse Integer(整数反转)](#7. Reverse Integer(整数反转))
- [8. String to Integer (atoi)(字符串转换整数)](#8. String to Integer (atoi)(字符串转换整数))
- [9. Palindrome Number(回文数)](#9. Palindrome Number(回文数))
- [10. Regular Expression Matching(正则表达式匹配)](#10. Regular Expression Matching(正则表达式匹配))
- [11. Container With Most Water(盛最多水的容器)](#11. Container With Most Water(盛最多水的容器))
- [12. Integer to Roman(整数转罗马数字)](#12. Integer to Roman(整数转罗马数字))
- [13. Roman to Integer(罗马数字转整数)](#13. Roman to Integer(罗马数字转整数))
- [14. Longest Common Prefix(最长公共前缀)](#14. Longest Common Prefix(最长公共前缀))
- [15. 3Sum(三数之和)](#15. 3Sum(三数之和))
- [16. 3Sum Closest(最接近的三数之和)](#16. 3Sum Closest(最接近的三数之和))
- [17. Letter Combinations of a Phone Number(电话号码的字母组合)](#17. Letter Combinations of a Phone Number(电话号码的字母组合))
- [18. 4Sum(四数之和)](#18. 4Sum(四数之和))
- [19. Remove Nth Node From End of List(删除链表的倒数第 N 个结点)](#19. Remove Nth Node From End of List(删除链表的倒数第 N 个结点))
- [20. Valid Parentheses(有效的括号)](#20. Valid Parentheses(有效的括号))
- [21. Merge Two Sorted Lists(合并两个有序链表)](#21. Merge Two Sorted Lists(合并两个有序链表))
- [22. Generate Parentheses(括号生成)](#22. Generate Parentheses(括号生成))
- [23. Merge k Sorted Lists(合并 K 个升序链表)](#23. Merge k Sorted Lists(合并 K 个升序链表))
- [24. Swap Nodes in Pairs(两两交换链表中的节点)](#24. Swap Nodes in Pairs(两两交换链表中的节点))
- [25. Reverse Nodes in k-Group(K 个一组翻转链表)](#25. Reverse Nodes in k-Group(K 个一组翻转链表))
- [26. Remove Duplicates from Sorted Array(删除有序数组中的重复项)](#26. Remove Duplicates from Sorted Array(删除有序数组中的重复项))
- [27. Remove Element(移除元素)](#27. Remove Element(移除元素))
- [28. Find the Index of the First Occurrence in a String(找出字符串中第一个匹配项的下标)](#28. Find the Index of the First Occurrence in a String(找出字符串中第一个匹配项的下标))
- [29. Divide Two Integers(两数相除)](#29. Divide Two Integers(两数相除))
- [30. Substring with Concatenation of All Words(串联所有单词的子串)](#30. Substring with Concatenation of All Words(串联所有单词的子串))
- [31. Next Permutation(下一个排列)](#31. Next Permutation(下一个排列))
- [32. Longest Valid Parentheses(最长有效括号)](#32. Longest Valid Parentheses(最长有效括号))
- [33. Search in Rotated Sorted Array(搜索旋转排序数组)](#33. Search in Rotated Sorted Array(搜索旋转排序数组))
- [34. Find First and Last Position of Element in Sorted Array(在排序数组中查找元素的第一个和最后一个位置)](#34. Find First and Last Position of Element in Sorted Array(在排序数组中查找元素的第一个和最后一个位置))
- [35. Search Insert Position(搜索插入位置)](#35. Search Insert Position(搜索插入位置))
- [36. Valid Sudoku(有效的数独)](#36. Valid Sudoku(有效的数独))
- [37. Sudoku Solver(解数独)](#37. Sudoku Solver(解数独))
- [38. Count and Say(外观数列)](#38. Count and Say(外观数列))
- [39. Combination Sum(组合总和)](#39. Combination Sum(组合总和))
- [40. Combination Sum II(组合总和 II)](#40. Combination Sum II(组合总和 II))
- [41. First Missing Positive(缺失的第一个正数)](#41. First Missing Positive(缺失的第一个正数))
- [42. Trapping Rain Water(接雨水)](#42. Trapping Rain Water(接雨水))
- [43. Multiply Strings(字符串相乘)](#43. Multiply Strings(字符串相乘))
- [44. Wildcard Matching(通配符匹配)](#44. Wildcard Matching(通配符匹配))
- [45. Jump Game II(跳跃游戏 II)](#45. Jump Game II(跳跃游戏 II))
- [46. Permutations(全排列)](#46. Permutations(全排列))
- [47. Permutations II(全排列 II)](#47. Permutations II(全排列 II))
- [48. Rotate Image(旋转图像)](#48. Rotate Image(旋转图像))
- [49. Group Anagrams(字母异位词分组)](#49. Group Anagrams(字母异位词分组))
- [50. Pow(x, n)(Pow(x, n))](#50. Pow(x, n)(Pow(x, n)))
1. Two Sum(两数之和)
题目描述
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出和为目标值 target 的那两个整数,并返回它们的数组下标。
- 每种输入只会对应一个答案,不能重复使用同一个元素,可任意顺序返回。
示例 1:
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
示例 2:
输入:nums = [3,2,4], target = 6
输出:[1,2]
示例 3:
输入:nums = [3,3], target = 6
输出:[0,1]
约束:
2 <= nums.length <= 10^4,-10^9 <= nums[i], target <= 10^9- 只会存在一个有效答案
解题思路
核心思想:哈希表把"查找另一半"从 O(n) 降为 O(1)。
解法一:暴力枚举 。双重循环枚举所有下标对 (i, j),检查 nums[i] + nums[j] == target。时间 O(n²),空间 O(1)。
解法二:哈希表一遍遍历(推荐) 。遍历数组时边查边存:对每个 nums[i],若表中已有 target - nums[i] 则配对返回;否则将 (nums[i], i) 存入表中。由于表里只保存了当前元素之前的下标,"先查后存"天然避免元素与自身配对。时间 O(n),空间 O(n)。
解法三:哈希表两遍遍历 。第一遍全部入表,第二遍查询,需额外判断 map.get(diff) != i 防自我配对。
代码实现
提交 LeetCode 仅保留
Solution类核心,main无需粘贴。
java
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
class Solution {
// 解法一:暴力枚举
public int[] twoSumBrute(int[] nums, int target) {
int n = nums.length;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[i] + nums[j] == target) {
return new int[] { i, j };
}
}
}
return new int[0];
}
// 解法二:哈希表一遍遍历(推荐)
public int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; i++) {
int diff = target - nums[i];
if (map.containsKey(diff)) { // 另一半已在表中
return new int[] { map.get(diff), i };
}
map.put(nums[i], i);
}
return new int[0];
}
// 解法三:哈希表两遍遍历
public int[] twoSumTwoPass(int[] nums, int target) {
Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for (int i = 0; i < nums.length; i++) map.put(nums[i], i);
for (int i = 0; i < nums.length; i++) {
int diff = target - nums[i];
if (map.containsKey(diff) && map.get(diff) != i) {
return new int[] { i, map.get(diff) };
}
}
return new int[0];
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(Arrays.toString(s.twoSum(new int[]{2,7,11,15}, 9))); // [0, 1]
System.out.println(Arrays.toString(s.twoSum(new int[]{3,2,4}, 6))); // [1, 2]
System.out.println(Arrays.toString(s.twoSum(new int[]{3,3}, 6))); // [0, 1]
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 暴力枚举 | O(n²) | O(1) | n=10^4 时约 5×10^7 次比较,不满足进阶 |
| 哈希表一遍(推荐) | O(n) | O(n) | 边查边存,代码最简洁,标准最优解 |
| 哈希表两遍 | O(n) | O(n) | 需判断 != i 防自我配对 |
2. Add Two Numbers(两数相加)
题目描述
给你两个非空 的链表,表示两个非负的整数。它们每位数字都是按照逆序 的方式存储的,并且每个节点只能存储一位数字。请你将两个数相加,并以相同形式返回一个表示和的链表。
- 除数字 0 之外,这两个数都不会以 0 开头。
示例 1:
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807
示例 2:
输入:l1 = [0], l2 = [0]
输出:[0]
示例 3:
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]
约束:
- 每个链表中的节点数在范围
[1, 100]内;节点值0 <= Node.val <= 9 - 题目数据保证列表表示的数字不含前导零
解题思路
核心思想:模拟竖式加法,同步遍历两条链表,逐位相加并处理进位。
解法一:常规迭代(推荐) 。维护进位 carry,每次取 l1.val + l2.val + carry,节点值取 sum % 10,进位取 sum / 10。两条链表可能不等长(短链表补 0 处理),最后若进位为 1 需追加一个值为 1 的节点。
解法二:递归。把"当前位相加"与"剩余链表相加"递归拆解,同样需要传递进位;递归深度等于链表长度(最多 100),安全。
代码实现
链表节点
ListNode为 LeetCode 预置类,本地保留定义,提交时勿粘贴。
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 addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0); // 哨兵节点,简化头结点处理
ListNode cur = dummy;
int carry = 0;
while (l1 != null || l2 != null || carry != 0) {
int sum = carry;
if (l1 != null) { sum += l1.val; l1 = l1.next; }
if (l2 != null) { sum += l2.val; l2 = l2.next; }
cur.next = new ListNode(sum % 10); // 当前位
carry = sum / 10; // 进位
cur = cur.next;
}
return dummy.next;
}
// 解法二:递归
public ListNode addTwoNumbersRec(ListNode l1, ListNode l2) {
return addRec(l1, l2, 0);
}
private ListNode addRec(ListNode l1, ListNode l2, int carry) {
if (l1 == null && l2 == null && carry == 0) return null;
int sum = carry;
if (l1 != null) { sum += l1.val; l1 = l1.next; }
if (l2 != null) { sum += l2.val; l2 = l2.next; }
ListNode node = new ListNode(sum % 10);
node.next = addRec(l1, l2, sum / 10);
return node;
}
// 本地测试辅助:数组 -> 链表
private static ListNode build(int[] arr) {
ListNode dummy = new ListNode(0), cur = dummy;
for (int v : arr) { cur.next = new ListNode(v); cur = cur.next; }
return dummy.next;
}
// 本地测试辅助:链表 -> 字符串
private static String str(ListNode n) {
StringBuilder sb = new StringBuilder("[");
while (n != null) { sb.append(n.val).append(n.next == null ? "" : ","); n = n.next; }
return sb.append("]").toString();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(str(s.addTwoNumbers(build(new int[]{2,4,3}), build(new int[]{5,6,4})))); // [7,0,8]
System.out.println(str(s.addTwoNumbers(build(new int[]{0}), build(new int[]{0})))); // [0]
System.out.println(str(s.addTwoNumbers(build(new int[]{9,9,9,9,9,9,9}), build(new int[]{9,9,9,9})))); // [8,9,9,9,0,0,0,1]
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 迭代 | O(max(m,n)) | O(max(m,n)) | 结果链表长度最多 max+1;除结果外仅常数辅助空间 |
| 递归 | O(max(m,n)) | O(max(m,n)) | 递归栈深度 = 链表长度 |
3. Longest Substring Without Repeating Characters(无重复字符的最长子串)
题目描述
给定一个字符串 s,请你找出其中不含有重复字符的最长子串的长度。
示例 1:
输入:s = "abcabcbb"
输出:3
解释:因为无重复字符的最长子串是 "abc",所以其长度为 3。
示例 2:
输入:s = "bbbbb"
输出:1
示例 3:
输入:s = "pwwkew"
输出:3
解释:因为无重复字符的最长子串是 "wke",所以其长度为 3。
约束:
0 <= s.length <= 5 * 10^4,s 由英文字母、数字、符号和空格组成
解题思路
核心思想:滑动窗口 + 哈希表(记录字符最近一次出现的位置)。
解法一:滑动窗口 + HashMap(推荐) 。用 left 指向窗口左边界,遍历 right:若字符 s[right] 在窗口中重复,就把 left 跳到 max(left, 上次出现位置+1),保证窗口内无重复;每次更新 ans = max(ans, right-left+1) 并记录/更新字符最新位置。时间 O(n),空间 O(min(n, 字符集))。
解法二:滑动窗口 + HashSet。窗口中无重复时右指针扩张,有重复时左指针收缩,用法类似但需要 while 循环收缩。
解法三:暴力枚举。枚举所有子串并检查是否含重复,O(n³)/O(n²),仅用于理解。
代码实现
java
import java.util.HashMap;
import java.util.Map;
class Solution {
// 解法一:滑动窗口 + HashMap(推荐)
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> last = new HashMap<Character, Integer>();
int left = 0, ans = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
if (last.containsKey(c)) {
left = Math.max(left, last.get(c) + 1); // 左边界跳过重复字符
}
last.put(c, right); // 更新最新位置
ans = Math.max(ans, right - left + 1);
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.lengthOfLongestSubstring("abcabcbb")); // 3
System.out.println(s.lengthOfLongestSubstring("bbbbb")); // 1
System.out.println(s.lengthOfLongestSubstring("pwwkew")); // 3
System.out.println(s.lengthOfLongestSubstring("")); // 0
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 滑动窗口 + HashMap | O(n) | O(字符集大小) | left/right 各移动一次,均摊 O(1) |
| 滑动窗口 + HashSet | O(n) | O(字符集大小) | 收缩步骤需 while,总体仍 O(n) |
| 暴力枚举 | O(n²)~O(n³) | O(1) | 只作理解 |
4. Median of Two Sorted Arrays(寻找两个正序数组的中位数)
题目描述
给定两个大小分别为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。请你找出并返回这两个正序数组的中位数。
- 算法的时间复杂度应该为
O(log (m+n))。
示例 1:
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
示例 2:
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
约束:
nums1.length == m,nums2.length == n,0 <= m, n <= 1000,1 <= m + n <= 2000-10^6 <= nums1[i], nums2[i] <= 10^6
解题思路
核心思想:二分划分两个数组,利用"左侧最大值 ≤ 右侧最小值"的条件找分割点。
解法一:二分分割(推荐) 。总是对较短的数组二分。设 i 为 nums1 的分割点(左侧元素数),j = (m+n+1)/2 - i 为 nums2 的分割点。条件:nums1[i-1] <= nums2[j] 且 nums2[j-1] <= nums1[i] 时分割合法。据此调整 i。总长度为奇数取 max(left1, left2),偶数取 (max(left1,left2)+min(right1,right2))/2.0。时间 O(log(min(m,n))),空间 O(1)。
解法二:合并取中位数。归并两个数组 O(m+n) 后直接取中位数,空间 O(m+n)。不满足 O(log) 要求,仅作对照。
代码实现
java
class Solution {
// 解法一:二分分割(推荐)
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
// 保证 nums1 是较短数组,二分复杂度取 log(min)
if (nums1.length > nums2.length) return findMedianSortedArrays(nums2, nums1);
int m = nums1.length, n = nums2.length;
int totalLeft = (m + n + 1) / 2; // 左侧元素总数
int lo = 0, hi = m;
while (lo <= hi) {
int i = (lo + hi) / 2; // nums1 左侧元素数
int j = totalLeft - i; // nums2 左侧元素数
int left1 = (i == 0) ? Integer.MIN_VALUE : nums1[i - 1];
int right1 = (i == m) ? Integer.MAX_VALUE : nums1[i];
int left2 = (j == 0) ? Integer.MIN_VALUE : nums2[j - 1];
int right2 = (j == n) ? Integer.MAX_VALUE : nums2[j];
if (left1 <= right2 && left2 <= right1) {
// 分割合法,计算中位数
if ((m + n) % 2 == 1) return Math.max(left1, left2);
return (Math.max(left1, left2) + Math.min(right1, right2)) / 2.0;
} else if (left1 > right2) {
hi = i - 1; // nums1 划分太靠右,左移
} else {
lo = i + 1; // nums2 左侧过大,nums1 划分右移
}
}
return 0.0; // 理论不可达
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.findMedianSortedArrays(new int[]{1,3}, new int[]{2})); // 2.0
System.out.println(s.findMedianSortedArrays(new int[]{1,2}, new int[]{3,4})); // 2.5
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 二分分割(推荐) | O(log(min(m,n))) | O(1) | 对较短数组二分,标准最优解 |
| 归并后取中位数 | O(m+n) | O(m+n) | 简单直观,但不满足进阶复杂度 |
5. Longest Palindromic Substring(最长回文子串)
题目描述
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。
示例 2:
输入:s = "cbbd"
输出:"bb"
约束:
1 <= s.length <= 1000,s 仅由数字和英文字母组成
解题思路
核心思想:以每个位置(或两个位置间的空隙)为中心向两边扩展,找到最长回文。
解法一:中心扩展(推荐)。回文有两种中心:单个字符(奇数长度)或两个相邻字符(偶数长度)。对每个中心向两侧扩展,直到两侧字符不等,记录最长区间。总中心数 2n-1,每个中心扩展 O(n),总时间 O(n²),空间 O(1)。
解法二:动态规划 。dp[i][j] 表示 s[i..j] 是否回文,转移 dp[i][j] = (s[i]==s[j]) && (j-i<3 || dp[i+1][j-1]),同时记录最长区间。时间 O(n²),空间 O(n²)。
解法三:Manacher 算法。线性 O(n) 时间求最长回文,实现较复杂,竞赛常用。
代码实现
java
class Solution {
private int start = 0, maxLen = 0;
// 解法一:中心扩展(推荐)
public String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
for (int i = 0; i < s.length(); i++) {
expand(s, i, i); // 奇数长度中心
expand(s, i, i + 1); // 偶数长度中心
}
return s.substring(start, start + maxLen);
}
private void expand(String s, int l, int r) {
while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
l--; r++;
}
if (r - l - 1 > maxLen) { // 当前回文长度更大则更新
start = l + 1;
maxLen = r - l - 1;
}
}
// 解法二:动态规划
public String longestPalindromeDP(String s) {
int n = s.length();
if (n < 2) return s;
boolean[][] dp = new boolean[n][n];
int begin = 0, max = 1;
for (int i = 0; i < n; i++) dp[i][i] = true;
for (int len = 2; len <= n; len++) {
for (int i = 0; i + len - 1 < n; i++) {
int j = i + len - 1;
if (s.charAt(i) != s.charAt(j)) continue;
dp[i][j] = (j - i < 3) || dp[i + 1][j - 1];
if (dp[i][j] && len > max) { max = len; begin = i; }
}
}
return s.substring(begin, begin + max);
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.longestPalindrome("babad")); // bab 或 aba
System.out.println(s.longestPalindrome("cbbd")); // bb
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 中心扩展(推荐) | O(n²) | O(1) | 实现最简洁,n=1000 足够 |
| 动态规划 | O(n²) | O(n²) | 表格记录,便于扩展为计数类问题 |
| Manacher | O(n) | O(n) | 线性最优,实现复杂 |
6. Zigzag Conversion(Z 字形变换)
题目描述
将一个给定字符串 s 根据给定的行数 numRows,以从上往下、从左到右进行 Z 字形排列。
示例 1:
输入:s = "PAYPALISHIRING", numRows = 3
输出:"PAHNAPLSIIGYIR"
示例 2:
输入:s = "PAYPALISHIRING", numRows = 4
输出:"PINALSIGYAHRPI"
约束:
1 <= s.length <= 1000,1 <= numRows <= 1000
解题思路
核心思想:模拟 Z 字形行走,按行收集字符。
解法一:按行模拟(推荐) 。用 numRows 个 StringBuilder。用一个方向变量 dir(1 向下 / -1 向上)控制移动:从 0 行出发,走到第 0 行或第 numRows-1 行时掉头。最后把所有行拼接。时间 O(n),空间 O(n)。
解法二:数学规律 。每周期含 2*numRows-2 个字符,直接按周期定位第 r 行该放哪些下标,跳过模拟。时间 O(n),空间 O(n)。
代码实现
java
class Solution {
// 解法一:按行模拟(推荐)
public String convert(String s, int numRows) {
if (numRows == 1) return s;
StringBuilder[] rows = new StringBuilder[numRows];
for (int i = 0; i < numRows; i++) rows[i] = new StringBuilder();
int row = 0, dir = -1;
for (char c : s.toCharArray()) {
rows[row].append(c);
if (row == 0 || row == numRows - 1) dir = -dir; // 到边界掉头
row += dir;
}
StringBuilder res = new StringBuilder();
for (StringBuilder r : rows) res.append(r);
return res.toString();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.convert("PAYPALISHIRING", 3)); // PAHNAPLSIIGYIR
System.out.println(s.convert("PAYPALISHIRING", 4)); // PINALSIGYAHRPI
System.out.println(s.convert("A", 1)); // A
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 按行模拟(推荐) | O(n) | O(n) | 直观,单次遍历 |
| 数学规律 | O(n) | O(n) | 直接算下标,常数更小 |
7. Reverse Integer(整数反转)
题目描述
给你一个 32 位的有符号整数 x,返回将 x 中的数字部分反转后的结果。
- 如果反转后整数超过 32 位的有符号整数的范围
[−2^31, 2^31 − 1],就返回 0。
示例:
输入:x = 123 输出:321
输入:x = -123 输出:-321
输入:x = 120 输出:21
约束:
-2^31 <= x <= 2^31 - 1
解题思路
核心思想:逐位弹出再累加,在累加前预判是否会溢出。
解法一:溢出预判(推荐) 。pop = x % 10; x /= 10; rev = rev * 10 + pop。溢出条件:rev > Integer.MAX_VALUE/10(或等于且 pop > 7)时正溢出;rev < Integer.MIN_VALUE/10(或等于且 pop < -8)时负溢出。在乘法和加法之前判断,避免中间结果溢出。
解法二:用 long 接收再强转 。Java 中可用 long 保存结果,最后检查是否超出 int 范围。代码更短,但解法一更体现严谨性。
代码实现
java
class Solution {
// 解法一:溢出预判(推荐)
public int reverse(int x) {
int rev = 0;
while (x != 0) {
int pop = x % 10;
x /= 10;
// 正溢出预判:rev*10+pop > 2147483647
if (rev > Integer.MAX_VALUE / 10 || (rev == Integer.MAX_VALUE / 10 && pop > 7)) return 0;
// 负溢出预判:rev*10+pop < -2147483648
if (rev < Integer.MIN_VALUE / 10 || (rev == Integer.MIN_VALUE / 10 && pop < -8)) return 0;
rev = rev * 10 + pop;
}
return rev;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.reverse(123)); // 321
System.out.println(s.reverse(-123)); // -321
System.out.println(s.reverse(120)); // 21
System.out.println(s.reverse(1534236469)); // 0(溢出)
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 溢出预判 | O(log₁₀ n) | O(1) | 位数即数字长度 |
| long 中转 | O(log₁₀ n) | O(1) | 写法更短,原理相同 |
8. String to Integer (atoi)(字符串转换整数)
题目描述
请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 的 atoi 函数)。
规则:跳过前导空格;可选的正负号;读入连续数字;若后跟非数字则停止;越界则返回 Integer.MAX_VALUE / Integer.MIN_VALUE;无有效数字返回 0。
示例:
输入:s = "42" 输出:42
输入:s = " -42" 输出:-42
输入:s = "4193 with words" 输出:4193
输入:s = "words and 987" 输出:0
输入:s = "-91283472332" 输出:-2147483648
解题思路
核心思想:状态机或顺序解析,关键在边界判断。
解法一:顺序解析(推荐) 。三个阶段:跳过空格 → 解析符号 → 解析数字。数值累加时用 long 保存,只要超出 int 范围立即截断返回,避免复杂的分界判断。时间 O(n),空间 O(1)。
解法二:有限状态机。定义 start/signed/in_number/end 四状态,严格按状态转移,适合"非常规输入"很多的场景,代码量稍大。
代码实现
java
class Solution {
// 解法一:顺序解析(推荐)
public int myAtoi(String s) {
int i = 0, n = s.length(), sign = 1;
while (i < n && s.charAt(i) == ' ') i++; // 跳过前导空格
if (i < n && (s.charAt(i) == '+' || s.charAt(i) == '-')) {
sign = (s.charAt(i) == '-') ? -1 : 1;
i++;
}
long num = 0;
while (i < n && Character.isDigit(s.charAt(i))) {
num = num * 10 + (s.charAt(i) - '0');
if (sign * num > Integer.MAX_VALUE) return Integer.MAX_VALUE; // 正溢出截断
if (sign * num < Integer.MIN_VALUE) return Integer.MIN_VALUE; // 负溢出截断
i++;
}
return (int) (sign * num);
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.myAtoi("42")); // 42
System.out.println(s.myAtoi(" -42")); // -42
System.out.println(s.myAtoi("4193 with words")); // 4193
System.out.println(s.myAtoi("words and 987")); // 0
System.out.println(s.myAtoi("-91283472332")); // -2147483648
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 顺序解析(推荐) | O(n) | O(1) | 单次扫描,long 中转防溢出 |
| 有限状态机 | O(n) | O(1) | 结构严谨,适合复杂输入 |
9. Palindrome Number(回文数)
题目描述
给你一个整数 x,如果 x 是一个回文整数 ,返回 true;否则返回 false。
回文数是指正序(从左向右)和倒序(从右向左)读都是一样的整数。例如,121 是回文,而 123 不是。
示例:
输入:x = 121 输出:true
输入:x = -121 输出:false(从右往左读为 121-,不是回文)
输入:x = 10 输出:false
约束: -2^31 <= x <= 2^31 - 1
解题思路
核心思想:负数直接 false;否则反转后半段数字与前半段比较。
解法一:反转后半段(推荐) 。负数和个位为 0 的非零数直接排除。将 x 的末尾数字依次反转累积到 rev,当 rev >= x(即已经反转一半)时停止:若 x 为偶数位则 rev == x,奇数位则 rev / 10 == x。时间 O(log₁₀ x),空间 O(1)。
解法二:转字符串双指针 。String.valueOf(x) 后左右指针向中间比较。时间 O(n),空间 O(n),最简单。
代码实现
java
class Solution {
// 解法一:反转后半段(推荐)
public boolean isPalindrome(int x) {
if (x < 0 || (x % 10 == 0 && x != 0)) return false; // 负数和尾零非零数
int rev = 0;
while (x > rev) {
rev = rev * 10 + x % 10;
x /= 10;
}
return x == rev || x == rev / 10; // 偶数位或奇数位
}
// 解法二:字符串双指针
public boolean isPalindromeStr(int x) {
String s = String.valueOf(x);
int l = 0, r = s.length() - 1;
while (l < r) {
if (s.charAt(l) != s.charAt(r)) return false;
l++; r--;
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isPalindrome(121)); // true
System.out.println(s.isPalindrome(-121)); // false
System.out.println(s.isPalindrome(10)); // false
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 反转后半段(推荐) | O(log₁₀ x) | O(1) | 不需额外空间 |
| 字符串双指针 | O(n) | O(n) | 直观,需字符串空间 |
10. Regular Expression Matching(正则表达式匹配)
题目描述
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 . 和 * 的正则表达式匹配。
.匹配任意单个字符*匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖整个 字符串 s 的,而不是部分字符串。
示例:
输入:s = "aa", p = "a" 输出:false
输入:s = "aa", p = "a*" 输出:true
输入:s = "ab", p = ".*" 输出:true
输入:s = "aab", p = "c*a*b" 输出:true
约束:
1 <= s.length <= 20,1 <= p.length <= 30- s 只含小写字母;p 含小写字母、
.和*,且*不会出现在首位
解题思路
核心思想 :动态规划,dp[i][j] 表示 s 前 i 个字符与 p 前 j 个字符是否匹配。
解法:动态规划(推荐)。状态转移:
p[j-1]是普通字符或.:dp[i][j] = dp[i-1][j-1] && match(s[i-1], p[j-1]);p[j-1]是*:分两种情况------- 匹配零个:
dp[i][j] = dp[i][j-2](*连同前面字符整体作废); - 匹配多个:
dp[i][j] = dp[i-1][j] && match(s[i-1], p[j-2])(s减少一个,p保持c*继续匹配)。
- 匹配零个:
match(a, b) 即 a==b || b=='.'。边界:dp[0][0]=true,并初始化 p 中 a* 等可为空的组合。时间 O(mn),空间 O(mn)(可滚动为 O(n))。
代码实现
java
class Solution {
// 动态规划(推荐)
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
// 初始化:空 s 匹配 p 的前缀(如 a*、a*b*)
for (int j = 2; j <= n; j++) {
if (p.charAt(j - 1) == '*') dp[0][j] = dp[0][j - 2];
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
char pc = p.charAt(j - 1);
if (pc == '*') {
// * 匹配零次:丢弃 p[j-2] 和 *;或匹配一次以上:s 退一格
dp[i][j] = dp[i][j - 2]
|| (dp[i - 1][j] && (s.charAt(i - 1) == p.charAt(j - 2) || p.charAt(j - 2) == '.'));
} else {
dp[i][j] = dp[i - 1][j - 1]
&& (s.charAt(i - 1) == pc || pc == '.');
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isMatch("aa", "a")); // false
System.out.println(s.isMatch("aa", "a*")); // true
System.out.println(s.isMatch("ab", ".*")); // true
System.out.println(s.isMatch("aab", "c*a*b")); // true
}
}
复杂度分析
- 时间复杂度:O(m×n) --- 双层遍历两个串
- 空间复杂度:O(m×n) --- dp 表;可滚动数组优化为 O(n)
11. Container With Most Water(盛最多水的容器)
题目描述
给定一个长度为 n 的整数数组 height。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i])。找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。返回容器可以储存的最大水量。
示例:
输入:[1,8,6,2,5,4,8,3,7]
输出:49
约束: n == height.length,2 <= n <= 10^5,0 <= height[i] <= 10^4
解题思路
核心思想:双指针从两端向中间收拢,每次移动较矮的一边。
解法:双指针(推荐) 。面积 = min(左高, 右高) × 宽度。直觉:若固定较矮边移动较高边,宽度变小而高度被矮边钳制,面积不可能增大;因此只移动较矮的一边才有机会增大面积。双指针不断更新最大面积,直到相遇。时间 O(n),空间 O(1)。
对照:暴力枚举。枚举所有下标对,O(n²),n=10^5 时超时。
代码实现
java
class Solution {
// 双指针(推荐)
public int maxArea(int[] height) {
int left = 0, right = height.length - 1, max = 0;
while (left < right) {
int h = Math.min(height[left], height[right]); // 盛水高度 = 较矮边
max = Math.max(max, h * (right - left));
if (height[left] < height[right]) left++; // 移动较矮的一边
else right--;
}
return max;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.maxArea(new int[]{1,8,6,2,5,4,8,3,7})); // 49
System.out.println(s.maxArea(new int[]{1,1})); // 1
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 双指针(推荐) | O(n) | O(1) | 每次移动一边,最多 n-1 步 |
| 暴力枚举 | O(n²) | O(1) | n=10^5 超时,仅作理解 |
12. Integer to Roman(整数转罗马数字)
题目描述
罗马数字包含以下七种字符:I, V, X, L, C, D, M(分别对应 1, 5, 10, 50, 100, 500, 1000)。例如 2 写做 II,12 写做 XII,27 写做 XXVII。
特殊情况:4 不写做 IIII,而是 IV;9 写做 IX。同理 XL/X/XC/C/CD/D/CM 处理 40/90/400/900。
给你一个整数,将其转为罗马数字。
示例:
输入:num = 3 输出:"III"
输入:num = 4 输出:"IV"
输入:num = 9 输出:"IX"
输入:num = 58 输出:"LVIII"
输入:num = 1994 输出:"MCMXCIV"
约束: 1 <= num <= 3999
解题思路
核心思想:贪心地从大到小用罗马符号表示数值。
解法一:贪心查表(推荐)。建立 13 个"值 → 符号"对(含 4/9/40/90/400/900 的特殊组合)。从大到小遍历,只要 num 不小于当前值就追加对应符号并减去该值。时间 O(1)(最多循环固定次数),空间 O(1)。
解法二:硬编码位表。把个/十/百/千位分别查表拼接,O(n) 但写法机械化。
代码实现
java
class Solution {
// 贪心查表(推荐)
private static final int[] VALS = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
private static final String[] SYMS = {"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"};
public String intToRoman(int num) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < VALS.length; i++) {
while (num >= VALS[i]) { // 能减就减,贪心拼接
sb.append(SYMS[i]);
num -= VALS[i];
}
}
return sb.toString();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.intToRoman(3)); // III
System.out.println(s.intToRoman(4)); // IV
System.out.println(s.intToRoman(9)); // IX
System.out.println(s.intToRoman(58)); // LVIII
System.out.println(s.intToRoman(1994)); // MCMXCIV
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 贪心查表(推荐) | O(1) | O(1) | 表格固定 13 项 |
| 位表拼接 | O(1) | O(1) | 写法机械,同样 O(1) |
13. Roman to Integer(罗马数字转整数)
题目描述
罗马数字包含七种字符:I(1) V(5) X(10) L(50) C(100) D(500) M(1000)。特殊的减法规则:IV=4, IX=9, XL=40, XC=90, CD=400, CM=900。给定一个罗马数字,将其转换成整数。
示例:
输入:s = "III" 输出:3
输入:s = "IV" 输出:4
输入:s = "IX" 输出:9
输入:s = "LVIII" 输出:58
输入:s = "MCMXCIV" 输出:1994
约束: 1 <= s.length <= 15,s 仅含 (I, V, X, L, C, D, M),值范围 [1, 3999]
解题思路
核心思想:从左往右扫描,若当前字符代表的值小于下一个字符的值则做减法,否则做加法。
解法一:逐位扫描(推荐) 。比如 IV:I(1) < V(5),所以 I 贡献是负数 -1,加上的 V(5) 共 4。统一规则:当前值 < 右侧值 时减,否则加。时间 O(n),空间 O(1)。
解法二:直接替换 。把所有 4/9 开头的双字符组合(IV, IX, ...)先替换成单占位符再求和。写法取巧,不通用。
代码实现
java
import java.util.HashMap;
import java.util.Map;
class Solution {
private static final Map<Character, Integer> M = new HashMap<Character, Integer>() {{
put('I', 1); put('V', 5); put('X', 10); put('L', 50);
put('C', 100); put('D', 500); put('M', 1000);
}};
// 逐位扫描(推荐)
public int romanToInt(String s) {
int sum = 0;
for (int i = 0; i < s.length(); i++) {
int cur = M.get(s.charAt(i));
if (i + 1 < s.length() && cur < M.get(s.charAt(i + 1))) {
sum -= cur; // 左小右大:做减法(如 IV 中的 I)
} else {
sum += cur;
}
}
return sum;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.romanToInt("III")); // 3
System.out.println(s.romanToInt("IV")); // 4
System.out.println(s.romanToInt("IX")); // 9
System.out.println(s.romanToInt("LVIII")); // 58
System.out.println(s.romanToInt("MCMXCIV")); // 1994
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 逐位扫描(推荐) | O(n) | O(1) | 单次遍历 |
| 字符串替换 | O(n) | O(n) | 简单但依赖字符集固定 |
14. Longest Common Prefix(最长公共前缀)
题目描述
编写一个函数来查找字符串数组中的最长公共前缀。如果不存在公共前缀,返回空字符串 ""。
示例:
输入:strs = ["flower","flow","flight"] 输出:"fl"
输入:strs = ["dog","racecar","car"] 输出:""
约束: 1 <= strs.length <= 200,0 <= strs[i].length <= 200,strsi 仅由小写字母组成
解题思路
核心思想:以第一个字符串为基准,逐字符与其余字符串比较。
解法一:纵向扫描(推荐) 。取第一个串的每个字符 c,检查其余所有串对应位置是否同为 c,一旦发现越界或不相等即返回已积累的前缀。时间 O(m×n)(m=串长,n=串数),空间 O(1)。
解法二:横向扫描。初始前缀 = 第 0 个串,然后依次与每个串求公共前缀(两两比较裁剪)。思路对称,同样 O(m×n)。
解法三:分治。递归地把数组一分为二,左右各求公共前缀再合并。O(m×n),体现"分治"思想。
代码实现
java
class Solution {
// 纵向扫描(推荐)
public String longestCommonPrefix(String[] strs) {
if (strs == null || strs.length == 0) return "";
for (int i = 0; i < strs[0].length(); i++) {
char c = strs[0].charAt(i);
for (int j = 1; j < strs.length; j++) {
// 其他串越界或字符不同:前缀到此为止
if (i >= strs[j].length() || strs[j].charAt(i) != c) {
return strs[0].substring(0, i);
}
}
}
return strs[0];
}
// 横向扫描(两两裁剪)
public String longestCommonPrefix2(String[] strs) {
if (strs == null || strs.length == 0) return "";
String prefix = strs[0];
for (int i = 1; i < strs.length; i++) {
while (strs[i].indexOf(prefix) != 0) { // 不是前缀则缩短
prefix = prefix.substring(0, prefix.length() - 1);
if (prefix.isEmpty()) return "";
}
}
return prefix;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.longestCommonPrefix(new String[]{"flower","flow","flight"})); // fl
System.out.println(s.longestCommonPrefix(new String[]{"dog","racecar","car"})); // (空)
System.out.println(s.longestCommonPrefix(new String[]{"a"})); // a
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 纵向扫描(推荐) | O(m×n) | O(1) | 最多比较所有字符一次 |
| 横向扫描 | O(m×n) | O(1) | 用 indexOf 加速判断 |
| 分治 | O(m×n) | O(log n) | 递归栈空间 |
15. 3Sum(三数之和)
题目描述
给你一个整数数组 nums,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k,同时还满足 nums[i] + nums[j] + nums[k] == 0。请你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。
示例:
输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
约束: 3 <= nums.length <= 3000,-10^5 <= nums[i] <= 10^5
解题思路
核心思想:排序 + 双指针,先固定一个数,再对剩余区间用双指针找两数之和。
解法:排序 + 双指针(推荐) 。排序后,外层循环固定 nums[i](去重:与上一个数相同则跳过),内层用双指针在 [i+1, n-1] 找两数之和 = -nums[i]。找到后左右指针同时收缩,且跳过重复值。剪枝:nums[i] > 0 直接结束(后续都为正,和不可能为 0)。时间 O(n²),空间 O(1)(排序原地)。
对照:三重循环暴力 O(n³),n=3000 必然超时。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
// 排序 + 双指针(推荐)
public List<List<Integer>> threeSum(int[] nums) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<List<Integer>>();
int n = nums.length;
for (int i = 0; i < n - 2; i++) {
if (nums[i] > 0) break; // 最小值 >0 不可能凑 0
if (i > 0 && nums[i] == nums[i - 1]) continue; // 固定数去重
int target = -nums[i];
int left = i + 1, right = n - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
res.add(Arrays.asList(nums[i], nums[left], nums[right]));
// 左右去重后继续找下一对
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++; right--;
} else if (sum < target) {
left++;
} else {
right--;
}
}
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.threeSum(new int[]{-1,0,1,2,-1,-4})); // [[-1,-1,2],[-1,0,1]]
System.out.println(s.threeSum(new int[]{0,1,1})); // []
System.out.println(s.threeSum(new int[]{0,0,0})); // [[0,0,0]]
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 排序 + 双指针(推荐) | O(n²) | O(1)(不含输出) | 排序 O(n log n) + 双指针 O(n²) |
| 暴力三重循环 | O(n³) | O(1) | n=3000 超时 |
16. 3Sum Closest(最接近的三数之和)
题目描述
给你一个长度为 n 的整数数组 nums 和 一个目标值 target。请你从 nums 中选出三个整数,使它们的和与 target 最接近。返回这三个数的和。
假定每组输入只存在恰好一个解。
示例:
输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2)。
约束: 3 <= nums.length <= 1000,-1000 <= nums[i] <= 1000,-10^4 <= target <= 10^4
解题思路
核心思想:与 15 题同构------排序 + 双指针,维护"最接近"的距离。
解法:排序 + 双指针(推荐) 。外层固定 nums[i],双指针扫 [i+1, n-1]。每次算 sum = nums[i]+nums[left]+nums[right],更新最小绝对差 abs(sum-target);比 target 小则左移,大则右移。可与 target 完全相等时直接返回。时间 O(n²),空间 O(1)。
代码实现
java
import java.util.Arrays;
class Solution {
// 排序 + 双指针(推荐)
public int threeSumClosest(int[] nums, int target) {
Arrays.sort(nums);
int n = nums.length, best = nums[0] + nums[1] + nums[2];
for (int i = 0; i < n - 2; i++) {
int left = i + 1, right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (Math.abs(sum - target) < Math.abs(best - target)) best = sum;
if (sum < target) {
left++; // 需要更大和
} else if (sum > target) {
right--; // 需要更小和
} else {
return sum; // 恰好相等,最优
}
}
}
return best;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.threeSumClosest(new int[]{-1,2,1,-4}, 1)); // 2
System.out.println(s.threeSumClosest(new int[]{0,0,0}, 1)); // 0
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 排序 + 双指针(推荐) | O(n²) | O(1) | 排序 O(n log n) + 扫描 O(n²) |
| 暴力三重循环 | O(n³) | O(1) | n=1000 勉强,仍不优 |
17. Letter Combinations of a Phone Number(电话号码的字母组合)
题目描述
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。数字到字母的映射与电话按键相同(2→abc, 3→def, 4→ghi, 5→jkl, 6→mno, 7→pqrs, 8→tuv, 9→wxyz)。
示例:
输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
约束: 0 <= digits.length <= 4,digitsi 是范围 ['2', '9'] 的数字
解题思路
核心思想:回溯(DFS)逐位选择字母,穷举所有组合。
解法:回溯(推荐) 。先建 数字→字母串 映射表。递归函数 dfs(idx, path):若 path 长度达到 digits.length() 则加入结果;否则遍历 digits[idx] 对应的每个字母并递归。时间 O(3^m × 4^n)(m=对应 3 字母的数字个数,n=4 字母的个数),空间 O(m+n)(递归栈,不含结果)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
private static final String[] M = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
private List<String> res = new ArrayList<String>();
// 回溯(推荐)
public List<String> letterCombinations(String digits) {
if (digits == null || digits.length() == 0) return res;
dfs(digits, 0, new StringBuilder());
return res;
}
private void dfs(String digits, int idx, StringBuilder path) {
if (idx == digits.length()) { // 已选满所有位
res.add(path.toString());
return;
}
String letters = M[digits.charAt(idx) - '0'];
for (char c : letters.toCharArray()) {
path.append(c);
dfs(digits, idx + 1, path);
path.deleteCharAt(path.length() - 1); // 回溯撤销
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.letterCombinations("23")); // [ad, ae, af, bd, be, bf, cd, ce, cf]
System.out.println(s.letterCombinations("")); // []
}
}
复杂度分析
- 时间复杂度:O(3^m × 4^n) --- 组合总数即解个数(m 为 3 字母数字数,n 为 4 字母数字数)
- 空间复杂度:O(m+n) --- 递归栈深度;不算输出结果
18. 4Sum(四数之和)
题目描述
给你一个由 n 个整数组成的数组 nums 和一个目标值 target。请你找出并返回满足下述全部条件且不重复 的四元组 [nums[a], nums[b], nums[c], nums[d]](若两个四元组元素一一对应,则视为重复):
0 <= a, b, c, d < n,a、b、c 和 d 互不相同nums[a] + nums[b] + nums[c] + nums[d] == target
示例:
输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
约束: 1 <= nums.length <= 200,-10^9 <= nums[i] <= 10^9,-10^9 <= target <= 10^9
解题思路
核心思想:在 3Sum 基础上再套一层循环------固定前两个数,双指针扫后两个数。
解法:排序 + 双层固定 + 双指针(推荐) 。排序后,两层 for 固定 i、j(都去重 + 越界剪枝),再对 [j+1, n-1] 双指针找两数和 = target - nums[i] - nums[j]。注意用 long 累加避免 -10^9 级数值相加溢出 int。时间 O(n³),空间 O(1)(不含输出)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
// 排序 + 双层固定 + 双指针(推荐)
public List<List<Integer>> fourSum(int[] nums, int target) {
Arrays.sort(nums);
List<List<Integer>> res = new ArrayList<List<Integer>>();
int n = nums.length;
for (int i = 0; i < n - 3; i++) {
if (i > 0 && nums[i] == nums[i - 1]) continue; // 第一层去重
for (int j = i + 1; j < n - 2; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) continue; // 第二层去重
long need = (long) target - nums[i] - nums[j]; // long 防溢出
int left = j + 1, right = n - 1;
while (left < right) {
long sum = (long) nums[left] + nums[right];
if (sum == need) {
res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
while (left < right && nums[left] == nums[left + 1]) left++;
while (left < right && nums[right] == nums[right - 1]) right--;
left++; right--;
} else if (sum < need) {
left++;
} else {
right--;
}
}
}
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.fourSum(new int[]{1,0,-1,0,-2,2}, 0)); // [[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]
System.out.println(s.fourSum(new int[]{2,2,2,2,2}, 8)); // [[2,2,2,2]]
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 排序 + 双指针(推荐) | O(n³) | O(1)(不含输出) | 三层扫描,n=200 足够 |
| 暴力四重循环 | O(n⁴) | O(1) | 超时 |
19. Remove Nth Node From End of List(删除链表的倒数第 N 个结点)
题目描述
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例:
输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]
输入:head = [1], n = 1
输出:[]
约束: 链表中结点的数目为 sz,1 <= sz <= 30,0 <= Node.val <= 100,1 <= n <= sz
解题思路
核心思想:快慢指针,快指针先走 n 步,慢指针与快指针同步走,快指针到尾时慢指针恰好指向待删节点的前驱。
解法一:快慢指针(推荐) 。dummy 哨兵避免头结点特判。快指针先走 n 步,然后快慢同步前进,快指针到末尾 null 时,慢指针的 next 即待删节点,执行 slow.next = slow.next.next。时间 O(sz),空间 O(1)。
解法二:两次遍历。第一次遍历求长度 L,第二次走到第 L-n 个节点删除。时间 O(sz),空间 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 removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head); // 哨兵避免头结点特判
ListNode fast = dummy, slow = dummy;
for (int i = 0; i < n; i++) fast = fast.next; // 快指针先走 n 步
while (fast.next != null) { // 快慢同步前进
fast = fast.next;
slow = slow.next;
}
slow.next = slow.next.next; // 删除倒数第 n 个
return dummy.next;
}
private static ListNode build(int[] arr) {
ListNode dummy = new ListNode(0), cur = dummy;
for (int v : arr) { cur.next = new ListNode(v); cur = cur.next; }
return dummy.next;
}
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();
System.out.println(str(s.removeNthFromEnd(build(new int[]{1,2,3,4,5}), 2))); // [1,2,3,5]
System.out.println(str(s.removeNthFromEnd(build(new int[]{1}), 1))); // []
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 快慢指针(推荐) | O(sz) | O(1) | 一次遍历完成 |
| 两次遍历 | O(sz) | O(1) | 先求长度再删除 |
20. Valid Parentheses(有效的括号)
题目描述
给定一个只包括 ( ) { } [ ] 的字符串 s,判断字符串是否有效。
有效字符串需满足:左括号必须用相同类型的右括号闭合;左括号必须以正确的顺序闭合;每个右括号都有一个对应的相同类型的左括号。
示例:
输入:s = "()" 输出:true
输入:s = "()[]{}" 输出:true
输入:s = "(]" 输出:false
约束: 1 <= s.length <= 10^4,s 仅由括号组成
解题思路
核心思想:栈的经典应用------左括号入栈,右括号与栈顶匹配。
解法:栈(推荐)。遍历字符:遇到左括号入栈对应的右括号(技巧:直接压入"期待匹配的右括号",出栈比较更简洁);遇到右括号:若栈空或栈顶不匹配则 false,否则弹栈。最后栈必须为空。时间 O(n),空间 O(n)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 栈(推荐)
public boolean isValid(String s) {
Deque<Character> stack = new ArrayDeque<Character>();
for (char c : s.toCharArray()) {
if (c == '(') stack.push(')'); // 压入"期待配对"的右括号
else if (c == '[') stack.push(']');
else if (c == '{') stack.push('}');
else {
if (stack.isEmpty() || stack.pop() != c) return false; // 栈空或多/错配
}
}
return stack.isEmpty();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isValid("()")); // true
System.out.println(s.isValid("()[]{}")); // true
System.out.println(s.isValid("(]")); // false
System.out.println(s.isValid("([)]")); // false
}
}
复杂度分析
- 时间复杂度:O(n) --- 单次遍历
- 空间复杂度:O(n) --- 栈最深时存 n/2 个元素
21. Merge Two Sorted Lists(合并两个有序链表)
题目描述
将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]
输入:l1 = [], l2 = []
输出:[]
约束: 两个链表的节点数目范围是 [0, 50],-100 <= Node.val <= 100,l1 和 l2 均按非递减顺序排列
解题思路
核心思想:双指针依次取较小者拼接,或递归合并。
解法一:迭代(推荐) 。dummy 哨兵 + 游标 cur。每次比较 l1.val 与 l2.val,取较小者接到 cur.next。循环结束把剩余链表整体接上。时间 O(m+n),空间 O(1)。
解法二:递归 。merge(l1,l2) = 较小节点作为头 + 递归合并其 next 与另一链表。时间 O(m+n),空间 O(m+n)(递归栈)。
代码实现
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 mergeTwoLists(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0), cur = dummy;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) { cur.next = l1; l1 = l1.next; }
else { cur.next = l2; l2 = l2.next; }
cur = cur.next;
}
cur.next = (l1 != null) ? l1 : l2; // 接上剩余
return dummy.next;
}
// 递归
public ListNode mergeTwoListsRec(ListNode l1, ListNode l2) {
if (l1 == null) return l2;
if (l2 == null) return l1;
if (l1.val <= l2.val) {
l1.next = mergeTwoListsRec(l1.next, l2);
return l1;
} else {
l2.next = mergeTwoListsRec(l1, l2.next);
return l2;
}
}
private static ListNode build(int[] arr) {
ListNode dummy = new ListNode(0), cur = dummy;
for (int v : arr) { cur.next = new ListNode(v); cur = cur.next; }
return dummy.next;
}
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();
System.out.println(str(s.mergeTwoLists(build(new int[]{1,2,4}), build(new int[]{1,3,4})))); // [1,1,2,3,4,4]
System.out.println(str(s.mergeTwoLists(build(new int[]{}), build(new int[]{})))); // []
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 迭代(推荐) | O(m+n) | O(1) | 只改指针 |
| 递归 | O(m+n) | O(m+n) | 递归栈深度为合并长度 |
22. Generate Parentheses(括号生成)
题目描述
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。
示例:
输入:n = 3
输出:["((()))","(()())","(())()","()(())","()()()"]
约束: 1 <= n <= 8
解题思路
核心思想 :回溯 + 剪枝------只允许当前未闭合左括号数大于 0 时放 )。
解法:回溯(推荐) 。维护 open(已放左括号数)与 close(已放右括号数)。规则:open < n 可放 (;close < open 可放 ),保证任意前缀右括号不超过左括号,生成的必然有效。到长度 2n 时记录结果。时间 O(4ⁿ/√n)(卡特兰数级),空间 O(n)。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
private List<String> res = new ArrayList<String>();
// 回溯(推荐)
public List<String> generateParenthesis(int n) {
dfs(n, 0, 0, new StringBuilder());
return res;
}
private void dfs(int n, int open, int close, StringBuilder sb) {
if (sb.length() == 2 * n) { // 已生成完整组合
res.add(sb.toString());
return;
}
if (open < n) { // 还能放左括号
sb.append('(');
dfs(n, open + 1, close, sb);
sb.deleteCharAt(sb.length() - 1);
}
if (close < open) { // 右括号不能多于左括号
sb.append(')');
dfs(n, open, close + 1, sb);
sb.deleteCharAt(sb.length() - 1);
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.generateParenthesis(3)); // [((())), (()()), (())(), ()(()), ()()()]
System.out.println(s.generateParenthesis(1)); // [()]
}
}
复杂度分析
- 时间复杂度:O(4ⁿ/√n) --- 有效组合数为第 n 个卡特兰数
- 空间复杂度:O(n) --- 递归深度 + 字符串长度;不含结果集
23. Merge k Sorted Lists(合并 K 个升序链表)
题目描述
给你一个链表数组,每个链表都已经按升序排列。请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
输入:lists = []
输出:[]
约束: k == lists.length,0 <= k <= 10^4,0 <= lists[i].length <= 500,-10^4 <= Node.val <= 10^4,所有链表节点总数为 N,N <= 10^4
解题思路
核心思想:把"多路归并"用最小堆维护当前最小头,或分治两两合并。
解法一:优先队列(推荐)。把所有非空链表头放入小顶堆(按 val 排序)。循环弹出最小节点接到结果链,若其 next 非空则压回堆。堆操作 log k,共 N 次 → 时间 O(N log k),空间 O(k)。
解法二:分治两两合并。把 k 个链表反复两两归并(复用 21 题),每轮合并后数量减半,共 log k 轮,总时间 O(N log k),空间 O(log k) 递归栈。
代码实现
java
import java.util.PriorityQueue;
class ListNode {
int val;
ListNode next;
ListNode(int val) { this.val = val; }
ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}
class Solution {
// 优先队列(推荐)
public ListNode mergeKLists(ListNode[] lists) {
PriorityQueue<ListNode> pq = new PriorityQueue<ListNode>((a, b) -> a.val - b.val);
for (ListNode node : lists) {
if (node != null) pq.offer(node); // 各链表头入堆
}
ListNode dummy = new ListNode(0), cur = dummy;
while (!pq.isEmpty()) {
ListNode min = pq.poll(); // 当前最小节点
cur.next = min;
cur = cur.next;
if (min.next != null) pq.offer(min.next); // 补位
}
return dummy.next;
}
// 分治两两合并
public ListNode mergeKListsDC(ListNode[] lists) {
return merge(lists, 0, lists.length - 1);
}
private ListNode merge(ListNode[] lists, int l, int r) {
if (l > r) return null;
if (l == r) return lists[l];
int mid = (l + r) / 2;
ListNode a = merge(lists, l, mid);
ListNode b = merge(lists, mid + 1, r);
return mergeTwo(a, b);
}
private ListNode mergeTwo(ListNode a, ListNode b) {
ListNode dummy = new ListNode(0), cur = dummy;
while (a != null && b != null) {
if (a.val <= b.val) { cur.next = a; a = a.next; }
else { cur.next = b; b = b.next; }
cur = cur.next;
}
cur.next = (a != null) ? a : b;
return dummy.next;
}
private static ListNode build(int[] arr) {
ListNode dummy = new ListNode(0), cur = dummy;
for (int v : arr) { cur.next = new ListNode(v); cur = cur.next; }
return dummy.next;
}
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[] lists = { build(new int[]{1,4,5}), build(new int[]{1,3,4}), build(new int[]{2,6}) };
System.out.println(str(s.mergeKLists(lists))); // [1,1,2,3,4,4,5,6]
System.out.println(str(s.mergeKLists(new ListNode[]{}))); // []
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 优先队列(推荐) | O(N log k) | O(k) | N=总节点数,堆大小 k |
| 分治两两合并 | O(N log k) | O(log k) | 递归栈深度 log k |
24. Swap Nodes in Pairs(两两交换链表中的节点)
题目描述
给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即只能进行节点交换)。
示例:
输入:head = [1,2,3,4]
输出:[2,1,4,3]
输入:head = []
输出:[]
约束: 链表中节点的数目在范围 [0, 100] 内,0 <= Node.val <= 100
解题思路
核心思想:三个指针 prev、a、b 完成一组交换后跳过两个节点。
解法一:迭代(推荐) 。dummy 哨兵。prev 指向"待交换对的前驱",a=prev.next, b=a.next。执行交换:a.next = b.next; b.next = a; prev.next = b,然后 prev = a 前进两步。时间 O(n),空间 O(1)。
解法二:递归 。交换后头 = b,b.next = a,a.next = swapPairs(a 的 next 之后)。时间 O(n),空间 O(n) 递归栈。
代码实现
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 swapPairs(ListNode head) {
ListNode dummy = new ListNode(0, head);
ListNode prev = dummy;
while (prev.next != null && prev.next.next != null) {
ListNode a = prev.next; // 第一节点
ListNode b = a.next; // 第二节点
a.next = b.next; // a 指向后续
b.next = a; // b 指向 a,完成交换
prev.next = b; // 前驱接到新头 b
prev = a; // 前驱前进到 a
}
return dummy.next;
}
// 递归
public ListNode swapPairsRec(ListNode head) {
if (head == null || head.next == null) return head;
ListNode b = head.next;
head.next = swapPairsRec(b.next); // 后续部分继续递归交换
b.next = head;
return b;
}
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 head = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4))));
System.out.println(str(s.swapPairs(head))); // [2,1,4,3]
System.out.println(str(s.swapPairs(null))); // []
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 迭代(推荐) | O(n) | O(1) | 只改指针 |
| 递归 | O(n) | O(n) | 递归栈深度 n/2 |
25. Reverse Nodes in k-Group(K 个一组翻转链表)
题目描述
给你链表的头节点 head,每 k 个节点一组进行翻转,请你返回修改后的链表。k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例:
输入:head = [1,2,3,4,5], k = 2
输出:[2,1,4,3,5]
输入:head = [1,2,3,4,5], k = 3
输出:[3,2,1,4,5]
约束: 链表节点数目在范围 [1, 5000] 内,0 <= Node.val <= 1000,1 <= k <= 5000
解题思路
核心思想:先确认区间内够 k 个节点,再对区间做链表反转,反复迭代。
解法:迭代切断 + 区间反转(推荐) 。维护 prev(上一段末尾)、start(本段起点)、end(本段终点)。先让 end 从 start 走 k-1 步,若中途为空则剩余节点不足,直接返回。否则 next = end.next 记下后续,end.next = null 切断区间,反转区间后接回;更新 prev 与 start 继续。时间 O(n),空间 O(1)。
解法二:递归。每次反转前 k 个,返回新头并递归处理剩余部分。时间 O(n),空间 O(n/k) 递归栈。
代码实现
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 reverseKGroup(ListNode head, int k) {
ListNode dummy = new ListNode(0, head);
ListNode prev = dummy;
while (true) {
ListNode start = prev.next;
if (start == null) break;
ListNode end = start;
for (int i = 1; i < k && end != null; i++) end = end.next; // 找区间终点
if (end == null) break; // 剩余不足 k 个
ListNode next = end.next;
end.next = null; // 切断区间
prev.next = reverse(start); // 反转区间并接回
start.next = next; // 原起点变为终点,接后续
prev = start; // 更新前驱
}
return dummy.next;
}
private ListNode reverse(ListNode node) { // 单链表反转(迭代)
ListNode pre = null, cur = node;
while (cur != null) {
ListNode nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
// 递归
public ListNode reverseKGroupRec(ListNode head, int k) {
ListNode cur = head;
int cnt = 0;
while (cur != null && cnt < k) { cur = cur.next; cnt++; }
if (cnt < k) return head; // 不足 k 个保持原序
ListNode newHead = reverse(head, k); // 反转前 k 个
head.next = reverseKGroupRec(cur, k);
return newHead;
}
private ListNode reverse(ListNode head, int k) {
ListNode pre = null, cur = head;
while (k-- > 0) {
ListNode nxt = cur.next;
cur.next = pre;
pre = cur;
cur = nxt;
}
return pre;
}
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.reverseKGroup(h1, 2))); // [2,1,4,3,5]
ListNode h2 = new ListNode(1, new ListNode(2, new ListNode(3, new ListNode(4, new ListNode(5)))));
System.out.println(str(s.reverseKGroup(h2, 3))); // [3,2,1,4,5]
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 迭代切断(推荐) | O(n) | O(1) | 每组反转总代价 O(n) |
| 递归 | O(n) | O(n/k) | 递归栈深度为组数 |
26. Remove Duplicates from Sorted Array(删除有序数组中的重复项)
题目描述
给你一个非严格递增排列 的数组 nums,请你原地删除重复出现的元素,使每个元素只出现一次,返回删除后数组的新长度。元素的相对顺序应该保持一致,然后返回 nums 中唯一元素的个数。
示例:
输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4,_,_,_,_,_]
约束: 1 <= nums.length <= 3 * 10^4,-10^4 <= nums[i] <= 10^4
解题思路
核心思想:快慢指针------慢指针指向"下一个不重复元素应放的位置"。
解法:快慢指针(推荐) 。i 慢指针,j 快指针遍历。nums[j] != nums[i-1] 时把 nums[j] 复制到 nums[i] 并 i++(也是去重判断)。最终 i 即新长度。时间 O(n),空间 O(1)。
代码实现
java
import java.util.Arrays;
class Solution {
// 快慢指针(推荐)
public int removeDuplicates(int[] nums) {
int i = 1; // i-1 是"已保留区"末尾
for (int j = 1; j < nums.length; j++) {
if (nums[j] != nums[i - 1]) { // 与上一个保留值不同则保留
nums[i++] = nums[j];
}
}
return i;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] a = {1,1,2};
int len1 = s.removeDuplicates(a);
System.out.println(len1 + ", " + Arrays.toString(Arrays.copyOf(a, len1))); // 2, [1,2]
int[] b = {0,0,1,1,1,2,2,3,3,4};
int len2 = s.removeDuplicates(b);
System.out.println(len2 + ", " + Arrays.toString(Arrays.copyOf(b, len2))); // 5, [0,1,2,3,4]
}
}
复杂度分析
- 时间复杂度:O(n) --- 单次遍历
- 空间复杂度:O(1) --- 原地修改
27. Remove Element(移除元素)
题目描述
给你一个数组 nums 和一个值 val,你需要原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。不要使用额外的数组空间,元素的顺序可以改变。
示例:
输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
约束: 0 <= nums.length <= 100,0 <= nums[i] <= 50,0 <= val <= 100
解题思路
核心思想:快慢指针覆盖非 val 元素,或右指针与 val 元素交换。
解法一:快慢指针(推荐) 。i 为新数组写入位置,j 遍历:nums[j] != val 时写 nums[i++]=nums[j]。时间 O(n),空间 O(1)。保持相对顺序。
解法二:双指针交换。左指针遇到 val 就用右指针的值覆盖(右指针左移),减少赋值次数;适合 val 出现少、数组大时。
代码实现
java
import java.util.Arrays;
class Solution {
// 快慢指针(推荐)
public int removeElement(int[] nums, int val) {
int i = 0;
for (int j = 0; j < nums.length; j++) {
if (nums[j] != val) {
nums[i++] = nums[j]; // 保留非 val 元素
}
}
return i;
}
// 双指针交换(减少赋值)
public int removeElement2(int[] nums, int val) {
int left = 0, right = nums.length;
while (left < right) {
if (nums[left] == val) {
nums[left] = nums[--right]; // 用末尾元素覆盖
} else {
left++;
}
}
return right;
}
public static void main(String[] args) {
Solution s = new Solution();
int[] a = {3,2,2,3};
int len1 = s.removeElement(a, 3);
System.out.println(len1 + ", " + Arrays.toString(Arrays.copyOf(a, len1))); // 2, [2,2]
int[] b = {0,1,2,2,3,0,4,2};
int len2 = s.removeElement(b, 2);
System.out.println(len2); // 5
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 快慢指针(推荐) | O(n) | O(1) | 保持相对顺序 |
| 双指针交换 | O(n) | O(1) | 赋值次数更少,顺序可变 |
28. Find the Index of the First Occurrence in a String(找出字符串中第一个匹配项的下标)
题目描述
给你两个字符串 haystack 和 needle,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1。
示例:
输入:haystack = "sadbutsad", needle = "sad"
输出:0
输入:haystack = "leetcode", needle = "leeto"
输出:-1
约束: 1 <= haystack.length, needle.length <= 10^4,haystack 和 needle 仅由小写英文字符组成
解题思路
核心思想:子串匹配。简单场景用 Java 内置或暴力;进阶用 KMP。
解法一:暴力滑动(推荐,简单)。外层枚举起点 i(0 到 m-n),内层逐字符比较 n 个字符,全匹配则返回 i。时间 O(m×n),n、m 均 10^4 时最坏 10^8,勉强可过,实际评测数据下常见通过。
解法二:KMP。预处理 next 数组 O(n),扫描过程 O(m),总时间 O(m+n)。适合大量重复字符的 worst-case。
代码实现
java
class Solution {
// 解法一:暴力滑动(简单直观)
public int strStr(String haystack, String needle) {
int m = haystack.length(), n = needle.length();
if (n == 0) return 0;
for (int i = 0; i <= m - n; i++) {
int j = 0;
while (j < n && haystack.charAt(i + j) == needle.charAt(j)) j++;
if (j == n) return i; // 全部匹配
}
return -1;
}
// 解法二:KMP
public int strStrKMP(String haystack, String needle) {
int m = haystack.length(), n = needle.length();
if (n == 0) return 0;
int[] next = new int[n];
// 构建失配跳转表
for (int i = 1, j = 0; i < n; i++) {
while (j > 0 && needle.charAt(i) != needle.charAt(j)) j = next[j - 1];
if (needle.charAt(i) == needle.charAt(j)) j++;
next[i] = j;
}
for (int i = 0, j = 0; i < m; i++) {
while (j > 0 && haystack.charAt(i) != needle.charAt(j)) j = next[j - 1];
if (haystack.charAt(i) == needle.charAt(j)) j++;
if (j == n) return i - n + 1;
}
return -1;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.strStr("sadbutsad", "sad")); // 0
System.out.println(s.strStr("leetcode", "leeto")); // -1
System.out.println(s.strStrKMP("sadbutsad", "sad")); // 0
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 暴力滑动 | O(m×n) | O(1) | 平均快,最坏 O(m×n) |
| KMP | O(m+n) | O(n) | next 数组 O(n),最坏线性 |
29. Divide Two Integers(两数相除)
题目描述
给你两个整数,被除数 dividend 和除数 divisor。将两数相除,要求不使用 乘法、除法和取余运算。整数除法应该向零截断,也就是截去小数部分。返回被除数除以除数得到的商。注意溢出:dividend = -2^31, divisor = -1 时商为 2^31,应返回 2^31 - 1。
示例:
输入:dividend = 10, divisor = 3
输出:3(10/3 = 3.333... 向零截断 = 3)
输入:dividend = 7, divisor = -3
输出:-2
约束: -2^31 <= dividend, divisor <= 2^31 - 1,divisor != 0
解题思路
核心思想:用减法模拟除法,但通过"倍增除数"加速(本质是二进制除法)。
解法:倍增 + 减法(推荐) 。统一转正数处理(用 long 防溢出)。定义 div(dvd, dvs) 递归/循环:从 dvs 开始倍增到不超过 dvd 的最大值 maxD(倍增次数为 k,则贡献 2^k 个 dvs),dvd -= maxD 后继续递归剩余部分,累加系数。时间 O(log² n) ~ O(log n)(倍增每轮至少减一半),空间 O(1)。
代码实现
java
class Solution {
// 倍增 + 减法(推荐)
public int divide(int dividend, int divisor) {
if (dividend == Integer.MIN_VALUE && divisor == -1) {
return Integer.MAX_VALUE; // 唯一溢出场景
}
boolean neg = (dividend < 0) ^ (divisor < 0); // 异号则结果为负
long dvd = Math.abs((long) dividend);
long dvs = Math.abs((long) divisor);
long ans = 0;
while (dvd >= dvs) {
long temp = dvs, mul = 1;
while (dvd >= (temp << 1)) { // 倍增到不超过 dvd 的最大倍数
temp <<= 1;
mul <<= 1;
}
dvd -= temp; // 扣掉这一大块
ans += mul;
}
return neg ? (int) -ans : (int) ans;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.divide(10, 3)); // 3
System.out.println(s.divide(7, -3)); // -2
System.out.println(s.divide(-2147483648, -1)); // 2147483647
}
}
复杂度分析
- 时间复杂度:O(log n) --- 每次内层倍增 O(log n),扣减至少一半以上
- 空间复杂度:O(1)
30. Substring with Concatenation of All Words(串联所有单词的子串)
题目描述
给定一个字符串 s 和一个字符串数组 words。words 中所有字符串长度相同。s 中的串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。例如,如果 words = "ab","cd","ef",那么 "abcdef"、"abefcd"、"cdabef" 等都是串联子串。
返回所有串联子串在 s 中的开始下标。你可以以任意顺序返回答案。
示例:
输入:s = "barfoothefoobarman", words = ["foo","bar"]
输出:[0,9]
输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"]
输出:[]
约束: 1 <= s.length <= 10^4,1 <= words.length <= 5000,1 <= words[i].length <= 30,words 中所有字符串长度相同
解题思路
核心思想:把 s 按"单词长度"切块,用滑动窗口在单词粒度上计数匹配。
解法:滑动窗口 + 哈希计数(推荐) 。设 wordLen = words[0].length(), total = words.length。外层按起始偏移 mod wordLen 分 wordLen 组滑动(保证窗口内单词边界对齐)。窗口内用两个 Map 统计"仍需要的词数";右边界加入新词、左边界弹出过期词,当窗口词数 == total 且计数全为 0 时记录起始下标。时间 O(n × wordLen)(实际 O(n) 级),空间 O(总单词数)。
代码实现
java
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Solution {
// 滑动窗口 + 哈希计数(推荐)
public List<Integer> findSubstring(String s, String[] words) {
List<Integer> res = new ArrayList<Integer>();
int n = s.length(), m = words.length, w = words[0].length();
if (n < m * w) return res;
Map<String, Integer> need = new HashMap<String, Integer>();
for (String word : words) need.put(word, need.getOrDefault(word, 0) + 1);
// 按起点对 w 取模分组滑动,保证单词边界对齐
for (int start = 0; start < w; start++) {
Map<String, Integer> window = new HashMap<String, Integer>();
int count = 0; // 窗口中已匹配的单词数
for (int i = start, j = start; j + w <= n; j += w) {
String in = s.substring(j, j + w);
if (need.containsKey(in)) { // 右扩
window.put(in, window.getOrDefault(in, 0) + 1);
if (window.get(in) <= need.get(in)) count++;
}
if ((j - start) / w + 1 > m) { // 超宽,左收一个单词
String out = s.substring(i, i + w);
if (need.containsKey(out)) {
if (window.get(out) <= need.get(out)) count--;
window.put(out, window.get(out) - 1);
}
i += w;
}
if (count == m) res.add(i); // 窗口恰好覆盖所有单词
}
}
return res;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.findSubstring("barfoothefoobarman", new String[]{"foo","bar"})); // [0,9]
System.out.println(s.findSubstring("wordgoodgoodgoodbestword", new String[]{"word","good","best","word"})); // []
}
}
复杂度分析
- 时间复杂度:O(n × w) --- w 为单词长度;当 s 远长于单词时近似 O(n)
- 空间复杂度:O(总单词数) --- 两个 Map
31. Next Permutation(下一个排列)
题目描述
整数数组的一个排列 就是将其所有成员以序列或线性顺序排列。整数数组的下一个排列 是指其整数的下一个字典序更大的排列。如果不存在下一个更大的排列,则将数组重新排列为最小的排列(即升序排列)。必须原地修改,只允许使用额外常数空间。
示例:
输入:nums = [1,2,3]
输出:[1,3,2]
输入:nums = [3,2,1]
输出:[1,2,3]
输入:nums = [1,1,5]
输出:[1,5,1]
约束: 1 <= nums.length <= 100,0 <= nums[i] <= 100
解题思路
核心思想:从右向左找第一个"降序"位置,将其与右侧最小大值交换,再反转右侧为升序。
解法:标准 next_permutation(推荐)。步骤:
- 从右往左找第一个
nums[i] < nums[i+1]的 i(分界点); - 若不存在 i,整个数组逆序返回(已是最大排列);
- 从右往左找第一个
nums[j] > nums[i],交换nums[i]、nums[j]; - 将 i+1 到末尾反转(此时该段是递减的,反转后即最小升序)。
时间 O(n),空间 O(1)。
代码实现
java
import java.util.Arrays;
class Solution {
// 标准 next_permutation(推荐)
public void nextPermutation(int[] nums) {
int n = nums.length, i = n - 2;
// 1. 找第一个升序分界点 nums[i] < nums[i+1]
while (i >= 0 && nums[i] >= nums[i + 1]) i--;
if (i >= 0) {
int j = n - 1;
// 2. 找右侧第一个大于 nums[i] 的数
while (j > i && nums[j] <= nums[i]) j--;
swap(nums, i, j);
}
// 3. 反转 i+1 之后为升序
reverse(nums, i + 1, n - 1);
}
private void swap(int[] a, int i, int j) { int t = a[i]; a[i] = a[j]; a[j] = t; }
private void reverse(int[] a, int l, int r) {
while (l < r) swap(a, l++, r--);
}
public static void main(String[] args) {
Solution s = new Solution();
int[] a = {1,2,3}; s.nextPermutation(a); System.out.println(Arrays.toString(a)); // [1,3,2]
int[] b = {3,2,1}; s.nextPermutation(b); System.out.println(Arrays.toString(b)); // [1,2,3]
int[] c = {1,1,5}; s.nextPermutation(c); System.out.println(Arrays.toString(c)); // [1,5,1]
}
}
复杂度分析
- 时间复杂度:O(n) --- 三次线性扫描
- 空间复杂度:O(1) --- 原地修改
32. Longest Valid Parentheses(最长有效括号)
题目描述
给你一个只包含 '(' 和 ')' 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例:
输入:s = "(()" 输出:2
输入:s = ")()())" 输出:4
输入:s = "" 输出:0
约束: 0 <= s.length <= 3 * 10^4,s 只能由 '(' 和 ')' 组成
解题思路
核心思想:栈记录"未匹配的左括号或分隔基准"。
解法一:栈(推荐) 。用栈存下标。推入 -1 作为起始基准。遍历:遇 ( 压入下标;遇 ) 弹栈------若栈空则把当前下标压入(新的基准),否则用 i - stack.peek() 更新最大长度。时间 O(n),空间 O(n)。
解法二:双向扫描 。两遍计数(左→右和右→左各一次),用 left/right 计数,当左右相等时更新长度,右多于左或左多于右时清零。时间 O(n),空间 O(1)。
代码实现
java
import java.util.ArrayDeque;
import java.util.Deque;
class Solution {
// 解法一:栈(推荐)
public int longestValidParentheses(String s) {
Deque<Integer> stack = new ArrayDeque<Integer>();
stack.push(-1); // 基准哨兵:表示"上一个未匹配位置"
int ans = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
stack.push(i);
} else {
stack.pop();
if (stack.isEmpty()) {
stack.push(i); // 右括号过多,重置基准
} else {
ans = Math.max(ans, i - stack.peek());
}
}
}
return ans;
}
// 解法二:双向扫描(空间 O(1))
public int longestValidParentheses2(String s) {
int ans = 0, left = 0, right = 0;
for (int i = 0; i < s.length(); i++) { // 左→右
if (s.charAt(i) == '(') left++; else right++;
if (left == right) ans = Math.max(ans, 2 * right);
else if (right > left) { left = 0; right = 0; }
}
left = right = 0;
for (int i = s.length() - 1; i >= 0; i--) { // 右→左(处理左括号过多)
if (s.charAt(i) == ')') right++; else left++;
if (left == right) ans = Math.max(ans, 2 * left);
else if (left > right) { left = 0; right = 0; }
}
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.longestValidParentheses("(()")); // 2
System.out.println(s.longestValidParentheses(")()())")); // 4
System.out.println(s.longestValidParentheses("")); // 0
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 栈(推荐) | O(n) | O(n) | 栈深度 O(n) |
| 双向扫描 | O(n) | O(1) | 常数空间,双 pass |
33. Search in Rotated Sorted Array(搜索旋转排序数组)
题目描述
整数数组 nums 按升序排列,数组中的值互不相同 。在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了旋转,使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ..., nums[k-1]](下标从 0 开始计数)。
给你旋转后的数组 nums 和一个整数 target,如果 nums 中存在这个目标值 target,则返回它的下标,否则返回 -1。必须设计一个时间复杂度为 O(log n) 的算法。
示例:
输入:nums = [4,5,6,7,0,1,2], target = 0
输出:4
输入:nums = [4,5,6,7,0,1,2], target = 3
输出:-1
约束: 1 <= nums.length <= 5000,-10^4 <= nums[i], target <= 10^4,nums 中的每个值都唯一
解题思路
核心思想:经典"旋转数组二分"------通过中点与左端比较判断哪一半有序,再决定搜索方向。
解法:二分 + 有序半区判断(推荐) 。二分时先看 nums[mid] 与 nums[left] 的关系:
- 若
nums[left] <= nums[mid](左半有序):target落在[left, mid)则right = mid-1,否则left = mid+1; - 否则右半有序:
target落在(mid, right]则left = mid+1,否则right = mid-1。
以此在每轮都丢弃一半。时间 O(log n),空间 O(1)。
代码实现
java
class Solution {
// 二分 + 有序半区判断(推荐)
public int search(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) return mid;
if (nums[left] <= nums[mid]) { // 左半有序
if (nums[left] <= target && target < nums[mid]) right = mid - 1;
else left = mid + 1;
} else { // 右半有序
if (nums[mid] < target && target <= nums[right]) left = mid + 1;
else right = mid - 1;
}
}
return -1;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.search(new int[]{4,5,6,7,0,1,2}, 0)); // 4
System.out.println(s.search(new int[]{4,5,6,7,0,1,2}, 3)); // -1
System.out.println(s.search(new int[]{1}, 0)); // -1
}
}
复杂度分析
- 时间复杂度:O(log n) --- 每轮排除一半
- 空间复杂度:O(1)
34. Find First and Last Position of Element in Sorted Array(在排序数组中查找元素的第一个和最后一个位置)
题目描述
给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。如果数组中不存在目标值,返回 [-1, -1]。必须设计并实现时间复杂度为 O(log n) 的算法。
示例:
输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]
输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]
约束: 0 <= nums.length <= 10^5,-10^9 <= nums[i], target <= 10^9
解题思路
核心思想:两个二分分别找"第一个 ≥ target"和"第一个 > target"的位置。
解法:两次二分(推荐) 。写辅助函数 lowerBound(nums, target) 返回第一个 >= target 的下标(即左边界),和 upperBound(nums, target) 返回第一个 > target 的下标(右边界+1)。答案即 [lower, upper-1],若越界或无匹配则 [-1,-1]。时间 O(log n),空间 O(1)。
代码实现
java
import java.util.Arrays;
class Solution {
// 两次二分(推荐)
public int[] searchRange(int[] nums, int target) {
int lower = lowerBound(nums, target);
int upper = upperBound(nums, target);
if (lower == upper) return new int[]{-1, -1}; // 不存在:lower 与 upper 重合
return new int[]{lower, upper - 1};
}
// 第一个 >= target 的下标
private int lowerBound(int[] nums, int target) {
int l = 0, r = nums.length;
while (l < r) {
int mid = (l + r) / 2;
if (nums[mid] < target) l = mid + 1;
else r = mid;
}
return l;
}
// 第一个 > target 的下标
private int upperBound(int[] nums, int target) {
int l = 0, r = nums.length;
while (l < r) {
int mid = (l + r) / 2;
if (nums[mid] <= target) l = mid + 1;
else r = mid;
}
return l;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(Arrays.toString(s.searchRange(new int[]{5,7,7,8,8,10}, 8))); // [3,4]
System.out.println(Arrays.toString(s.searchRange(new int[]{5,7,7,8,8,10}, 6))); // [-1,-1]
System.out.println(Arrays.toString(s.searchRange(new int[]{}, 0))); // [-1,-1]
}
}
复杂度分析
- 时间复杂度:O(log n) --- 两次二分各 O(log n)
- 空间复杂度:O(1)
35. Search Insert Position(搜索插入位置)
题目描述
给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。请必须使用时间复杂度为 O(log n) 的算法。
示例:
输入:nums = [1,3,5,6], target = 5 输出:2
输入:nums = [1,3,5,6], target = 2 输出:1
输入:nums = [1,3,5,6], target = 7 输出:4
约束: 1 <= nums.length <= 10^4,-10^4 <= nums[i], target <= 10^4,nums 为无重复的升序数组
解题思路
核心思想:标准二分查找下界(lower bound)。
解法:二分(推荐) 。求第一个 >= target 的位置:若 nums[mid] < target 则 left = mid+1,否则 right = mid。循环结束时 left 即答案。时间 O(log n),空间 O(1)。
代码实现
java
class Solution {
// 二分(推荐)
public int searchInsert(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + (right - left) / 2;
if (nums[mid] < target) left = mid + 1; // 仍小于 target,向右
else right = mid; // 找到第一个 >= target
}
return left;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.searchInsert(new int[]{1,3,5,6}, 5)); // 2
System.out.println(s.searchInsert(new int[]{1,3,5,6}, 2)); // 1
System.out.println(s.searchInsert(new int[]{1,3,5,6}, 7)); // 4
}
}
复杂度分析
- 时间复杂度:O(log n)
- 空间复杂度:O(1)
36. Valid Sudoku(有效的数独)
题目描述
请你判断一个 9 x 9 的数独是否有效。只需要根据以下规则,验证已经填入的数字是否有效即可:
- 数字
1-9在每一行只能出现一次 - 数字
1-9在每一列只能出现一次 - 数字
1-9在每一个以粗实线分隔的3x3宫内只能出现一次
数独部分空格内已填入了数字,空白格用 '.' 表示。注意:一个有效的数独(部分已被填充)不一定是可解的。
示例:
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:true
约束: board.length == 9,board[i].length == 9,boardij 是数字 1-9 或 '.'
解题思路
核心思想:三组哈希集合分别记录行 / 列 / 3x3 宫内已出现的数字。
解法:三次哈希检查(推荐) 。遍历每个非 . 数字 c,检查并加入三组集合:行 rows[i]、列 cols[j]、宫 boxes[(i/3)*3 + j/3]。任一重复即 false。宫编号 = (i/3)*3 + (j/3)(行块 × 3 + 列块)。时间 O(81)=O(1),空间 O(1)。
代码实现
java
import java.util.HashSet;
import java.util.Set;
class Solution {
// 三组哈希检查(推荐)
public boolean isValidSudoku(char[][] board) {
Set<Character>[] rows = new HashSet[9];
Set<Character>[] cols = new HashSet[9];
Set<Character>[] boxes = new HashSet[9];
for (int i = 0; i < 9; i++) {
rows[i] = new HashSet<Character>();
cols[i] = new HashSet<Character>();
boxes[i] = new HashSet<Character>();
}
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
char c = board[i][j];
if (c == '.') continue;
int boxIdx = (i / 3) * 3 + j / 3; // 宫编号
if (!rows[i].add(c) || !cols[j].add(c) || !boxes[boxIdx].add(c)) {
return false; // 行/列/宫任一重复
}
}
}
return true;
}
public static void main(String[] args) {
Solution s = new Solution();
char[][] board = {
{'5','3','.','.','7','.','.','.','.'},
{'6','.','.','1','9','5','.','.','.'},
{'.','9','8','.','.','.','.','6','.'},
{'8','.','.','.','6','.','.','.','3'},
{'4','.','.','8','.','3','.','.','1'},
{'7','.','.','.','2','.','.','.','6'},
{'.','6','.','.','.','.','2','8','.'},
{'.','.','.','4','1','9','.','.','5'},
{'.','.','.','.','8','.','.','7','9'}
};
System.out.println(s.isValidSudoku(board)); // true
}
}
复杂度分析
- 时间复杂度:O(81) ≈ O(1) --- 固定 9x9
- 空间复杂度:O(1) --- 三组固定大小集合
37. Sudoku Solver(解数独)
题目描述
编写一个程序,通过填充空格来解决数独问题。数独的解法需遵循如下规则:数字 1-9 在每一行/每一列/每一个 3x3 宫内只能出现一次。空白格用 '.' 表示。题目数据保证输入数独仅有一个解。
示例(节选):
输入:board = [["5","3",".",".","7",".",".",".","."],["6",".",".","1","9","5",".",".","."],[".","9","8",".",".",".",".","6","."],["8",".",".",".","6",".",".",".","3"],["4",".",".","8",".","3",".",".","1"],["7",".",".",".","2",".",".",".","6"],[".","6",".",".",".",".","2","8","."],[".",".",".","4","1","9",".",".","5"],[".",".",".",".","8",".",".","7","9"]]
输出:填充完整后的数独(结果见 LeetCode 原题)
约束: board.length == 9,board[i].length == 9,boardij 是数字 1-9 或 '.',输入保证有唯一解
解题思路
核心思想:回溯 + 剪枝,递归逐个空格尝试 1-9,冲突则回退。
解法:回溯(推荐) 。用三个 boolean 数组 row[i][d] / col[j][d] / box[b][d] 标记数字 d 是否已使用。递归遍历每个空位,尝试填入 1-9 中"行、列、宫都未用过"的数字并递归;全部填满即唯一解,立即返回。时间 O(9^空位数)(受强剪枝约束,实际远小于最坏),空间 O(81)=O(1)。
代码实现
java
class Solution {
private boolean[][] row = new boolean[9][10];
private boolean[][] col = new boolean[9][10];
private boolean[][] box = new boolean[9][10];
// 回溯(推荐)
public void solveSudoku(char[][] board) {
// 记录已填数字到三组标记
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] != '.') {
int d = board[i][j] - '0';
int b = (i / 3) * 3 + j / 3;
row[i][d] = col[j][d] = box[b][d] = true;
}
}
}
dfs(board, 0, 0);
}
private boolean dfs(char[][] board, int i, int j) {
if (i == 9) return true; // 全部填完
if (j == 9) return dfs(board, i + 1, 0); // 换行
if (board[i][j] != '.') return dfs(board, i, j + 1);
int b = (i / 3) * 3 + j / 3;
for (int d = 1; d <= 9; d++) {
if (row[i][d] || col[j][d] || box[b][d]) continue; // 冲突剪枝
board[i][j] = (char) ('0' + d);
row[i][d] = col[j][d] = box[b][d] = true;
if (dfs(board, i, j + 1)) return true;
// 回溯撤销
row[i][d] = col[j][d] = box[b][d] = false;
board[i][j] = '.';
}
return false;
}
private static String show(char[][] b) {
StringBuilder sb = new StringBuilder();
for (char[] r : b) sb.append(new String(r)).append('\n');
return sb.toString();
}
public static void main(String[] args) {
Solution s = new Solution();
char[][] board = {
{'5','3','.','.','7','.','.','.','.'},
{'6','.','.','1','9','5','.','.','.'},
{'.','9','8','.','.','.','.','6','.'},
{'8','.','.','.','6','.','.','.','3'},
{'4','.','.','8','.','3','.','.','1'},
{'7','.','.','.','2','.','.','.','6'},
{'.','6','.','.','.','.','2','8','.'},
{'.','.','.','4','1','9','.','.','5'},
{'.','.','.','.','8','.','.','7','9'}
};
s.solveSudoku(board);
System.out.println(show(board)); // 打印已解数独
}
}
复杂度分析
- 时间复杂度:O(9^m) --- m 为空格数,剪枝后远小于该上界
- 空间复杂度:O(1) --- 三组 9x10 布尔标记
38. Count and Say(外观数列)
题目描述
给定一个正整数 n,输出外观数列的第 n 项。「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述:
1→ 读作 "one 1"(一个一),记作1111→ 读作 "two 1s"(两个一),记作2121→ 读作 "one 2, then one 1",记作1211
示例:
输入:n = 1 输出:"1"
输入:n = 4 输出:"1211"
约束: 1 <= n <= 30
解题思路
核心思想:从 "1" 开始迭代 n-1 次,每次把"连续相同字符分组"转为"数量+字符"。
解法:迭代逐项生成(推荐) 。对当前串扫描,统计连续相同数字的段 count 与字符 c,下一项追加 count + c。重复 n-1 次。时间 O(n × L)(L 为序列长度,最长约 4^n 的位数级,n≤30 可控),空间 O(L)。
代码实现
java
class Solution {
// 迭代逐项生成(推荐)
public String countAndSay(int n) {
String s = "1";
for (int k = 1; k < n; k++) {
StringBuilder sb = new StringBuilder();
int i = 0;
while (i < s.length()) {
int j = i;
while (j < s.length() && s.charAt(j) == s.charAt(i)) j++; // 找同字符段
sb.append(j - i).append(s.charAt(i)); // 数量 + 字符
i = j;
}
s = sb.toString();
}
return s;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.countAndSay(1)); // 1
System.out.println(s.countAndSay(4)); // 1211
}
}
复杂度分析
- 时间复杂度:O(n × L) --- 每轮扫描当前序列长度
- 空间复杂度:O(L) --- 临时 StringBuilder
39. Combination Sum(组合总和)
题目描述
给你一个无重复元素 的整数数组 candidates 和一个目标整数 target,找出 candidates 中可以使数字和为目标数 target 的所有不同组合 ,并以列表形式返回。你可以按任意顺序 返回这些组合。candidates 中的同一个数字可以无限制重复被选取。如果至少一个数字的被选数量不同,则两种组合是不同的。
示例:
输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
输入:candidates = [2,3,5], target = 8
输出:[[2,2,2,2],[2,3,3],[3,5]]
约束: 1 <= candidates.length <= 30,2 <= candidates[i] <= 40,1 <= target <= 40,candidates 的所有元素互不相同
解题思路
核心思想:回溯 + 索引不回头(保证不产生重复组合),可无限选同一数字。
解法:回溯(推荐) 。先排序。dfs(start, remain, path):从 start 开始选数(保证组合内数字单调不降,避免重复),若 remain == 0 记录;若 remain < 0 剪枝。选 candidates[i] 后仍从 i 开始(允许重复选取)。时间 O(2^(target/min)) 级别(组合数级,target ≤ 40 可控),空间 O(target/min) 递归栈。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
private List<List<Integer>> res = new ArrayList<List<Integer>>();
// 回溯(推荐)
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
dfs(candidates, 0, target, new ArrayList<Integer>());
return res;
}
private void dfs(int[] c, int start, int remain, List<Integer> path) {
if (remain == 0) { // 恰好凑齐
res.add(new ArrayList<Integer>(path));
return;
}
for (int i = start; i < c.length; i++) {
if (c[i] > remain) break; // 剪枝:升序,后面更大
path.add(c[i]);
dfs(c, i, remain - c[i], path); // 数字可重复,下次仍从 i 开始
path.remove(path.size() - 1);
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.combinationSum(new int[]{2,3,6,7}, 7)); // [[2,2,3],[7]]
System.out.println(s.combinationSum(new int[]{2,3,5}, 8)); // [[2,2,2,2],[2,3,3],[3,5]]
}
}
复杂度分析
- 时间复杂度:O(S) --- S 为所有可行解的长度之和(上界指数级但约束小)
- 空间复杂度:O(target/min) --- 递归栈深度
40. Combination Sum II(组合总和 II)
题目描述
给定一个候选人编号的集合 candidates 和一个目标数 target,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用一次。注意:解集不能包含重复的组合。
示例:
输入:candidates = [10,1,2,7,6,1,5], target = 8
输出:[[1,1,6],[1,2,5],[1,7],[2,6]]
输入:candidates = [2,5,2,1,2], target = 5
输出:[[1,2,2],[5]]
约束: 1 <= candidates.length <= 100,1 <= candidates[i] <= 50,1 <= target <= 30
解题思路
核心思想 :与 39 题同构,但每个数字只能用一次 → 递归时 i+1;有重复数字 → 同层跳过相同值去重。
解法:回溯 + 同层去重(推荐) 。排序后使相同值相邻。dfs(start, remain, path):每层 for i = start..,若 i > start && c[i] == c[i-1] 跳过(同一层中相同值会导出相同组合)。选 c[i] 后递归 i+1(不回头 + 不重复用)。时间 O(2^n) 上界(n=100 但标准剪枝后可行),空间 O(n)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
private List<List<Integer>> res = new ArrayList<List<Integer>>();
// 回溯 + 同层去重(推荐)
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
Arrays.sort(candidates);
dfs(candidates, 0, target, new ArrayList<Integer>());
return res;
}
private void dfs(int[] c, int start, int remain, List<Integer> path) {
if (remain == 0) {
res.add(new ArrayList<Integer>(path));
return;
}
for (int i = start; i < c.length; i++) {
if (c[i] > remain) break; // 剪枝
if (i > start && c[i] == c[i - 1]) continue; // 同层去重
path.add(c[i]);
dfs(c, i + 1, remain - c[i], path); // 每个数字只能用一次
path.remove(path.size() - 1);
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.combinationSum2(new int[]{10,1,2,7,6,1,5}, 8)); // [[1,1,6],[1,2,5],[1,7],[2,6]]
System.out.println(s.combinationSum2(new int[]{2,5,2,1,2}, 5)); // [[1,2,2],[5]]
}
}
复杂度分析
- 时间复杂度:O(2^n) 上界 --- 剪枝后实际远小于
- 空间复杂度:O(n) --- 递归栈
41. First Missing Positive(缺失的第一个正数)
题目描述
给你一个未排序的整数数组 nums,请你找出其中没有出现的最小的正整数。请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
示例:
输入:nums = [1,2,0] 输出:3
输入:nums = [3,4,-1,1] 输出:2
输入:nums = [7,8,9,11,12] 输出:1
约束: 1 <= nums.length <= 5 * 10^5,-2^31 <= nums[i] <= 2^31 - 1
解题思路
核心思想 :答案必在 [1, n+1] 之间。把数组中值在 [1,n] 的数字"归位"到下标 value-1 处,再扫一遍找第一个空缺。
解法:原地哈希交换(推荐) 。遍历每个位置 i:只要 nums[i] 在 [1,n] 且 nums[nums[i]-1] != nums[i],就持续交换 nums[i] 与 nums[nums[i]-1](把每个数放到它该在的槽位)。完成后扫描:第一个 nums[i] != i+1 的位置,答案即 i+1;全部对位则答案为 n+1。时间 O(n)(每个元素最多被交换一次),空间 O(1)。
代码实现
java
class Solution {
// 原地哈希交换(推荐)
public int firstMissingPositive(int[] nums) {
int n = nums.length;
for (int i = 0; i < n; i++) {
// 把 nums[i] 放到正确槽位 nums[nums[i]-1]
while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
int t = nums[nums[i] - 1];
nums[nums[i] - 1] = nums[i];
nums[i] = t;
}
}
for (int i = 0; i < n; i++) {
if (nums[i] != i + 1) return i + 1; // 第一个错位的槽
}
return n + 1; // 1..n 全部就位
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.firstMissingPositive(new int[]{1,2,0})); // 3
System.out.println(s.firstMissingPositive(new int[]{3,4,-1,1})); // 2
System.out.println(s.firstMissingPositive(new int[]{7,8,9,11,12})); // 1
}
}
复杂度分析
- 时间复杂度:O(n) --- 每个元素最多交换一次
- 空间复杂度:O(1) --- 原地
42. Trapping Rain Water(接雨水)
题目描述
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
示例:
输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
输入:height = [4,2,0,3,2,5]
输出:9
约束: n == height.length,1 <= n <= 2 * 10^4,0 <= height[i] <= 10^5
解题思路
核心思想 :每个位置的积水量 = min(左侧最高, 右侧最高) - 该位置高度(差为正值时)。
解法一:双指针(推荐) 。维护 leftMax 和 rightMax 两个滚动最大值。左右指针向中间移动:若 leftMax < rightMax,说明当前位置蓄水由左侧决定,直接用 leftMax - height[left] 累加(右侧至少有 rightMax 兜底),然后推进左指针;否则对称处理右侧。时间 O(n),空间 O(1)。
解法二:动态规划(预计算左右最高) 。两次扫描求出每个位置的 leftMax[i] 与 rightMax[i] 数组,再求和。时间 O(n),空间 O(n)。思路直观但多 O(n) 空间。
解法三:单调栈。栈内按高度递减存下标;遇更高的柱子时,弹出凹槽并计算积水。时间 O(n),空间 O(n)。
代码实现
java
class Solution {
// 解法一:双指针(推荐)
public int trap(int[] height) {
int left = 0, right = height.length - 1;
int leftMax = 0, rightMax = 0, ans = 0;
while (left < right) {
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
if (leftMax < rightMax) { // 当前位置由左侧最高决定储水
ans += leftMax - height[left];
left++;
} else {
ans += rightMax - height[right];
right--;
}
}
return ans;
}
// 解法二:动态规划(预计算左右最高)
public int trapDP(int[] height) {
int n = height.length;
int[] leftMax = new int[n], rightMax = new int[n];
leftMax[0] = height[0];
for (int i = 1; i < n; i++) leftMax[i] = Math.max(leftMax[i - 1], height[i]);
rightMax[n - 1] = height[n - 1];
for (int i = n - 2; i >= 0; i--) rightMax[i] = Math.max(rightMax[i + 1], height[i]);
int ans = 0;
for (int i = 0; i < n; i++) ans += Math.min(leftMax[i], rightMax[i]) - height[i];
return ans;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.trap(new int[]{0,1,0,2,1,0,1,3,2,1,2,1})); // 6
System.out.println(s.trap(new int[]{4,2,0,3,2,5})); // 9
System.out.println(s.trapDP(new int[]{0,1,0,2,1,0,1,3,2,1,2,1})); // 6
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 双指针(推荐) | O(n) | O(1) | 最优:一次遍历,常数空间 |
| 动态规划 | O(n) | O(n) | 直观,两个辅助数组 |
| 单调栈 | O(n) | O(n) | 栈存下标 |
43. Multiply Strings(字符串相乘)
题目描述
给定两个以字符串形式表示的非负整数 num1 和 num2,返回 num1 和 num2 的乘积,它们的乘积也表示为字符串形式。
注意: 不能使用任何内置的 BigInteger 库或直接将输入转换为整数。
示例:
输入:num1 = "2", num2 = "3" 输出:"6"
输入:num1 = "123", num2 = "456" 输出:"56088"
约束: 1 <= num1.length, num2.length <= 200,num1 和 num2 只包含数字 0-9,均不以 0 开头(除非是 "0")
解题思路
核心思想 :模拟竖式乘法------num1[i] × num2[j] 的结果落在结果的第 i+j 和第 i+j+1 位。
解法:无进位竖式累加(推荐) 。结果数组 res[len1+len2]。双重循环:乘积 mul = (n1-num1[i]) * (n2-num2[j]),累加到 res[i+j+1](低位),进位向 res[i+j] 传播(先累加后统一进位)。全部累加完后从低位向高位逐位处理进位,最后去掉前导零转字符串。时间 O(m×n),空间 O(m+n)。
代码实现
java
class Solution {
// 无进位竖式累加(推荐)
public String multiply(String num1, String num2) {
int m = num1.length(), n = num2.length();
int[] res = new int[m + n]; // 结果最多 m+n 位
// 1. 逐位相乘并累加到对应位
for (int i = m - 1; i >= 0; i--) {
int a = num1.charAt(i) - '0';
for (int j = n - 1; j >= 0; j--) {
int b = num2.charAt(j) - '0';
res[i + j + 1] += a * b; // 低位累加
}
}
// 2. 统一处理进位
int carry = 0;
for (int i = res.length - 1; i >= 0; i--) {
int sum = res[i] + carry;
res[i] = sum % 10;
carry = sum / 10;
}
// 3. 去掉前导零
int k = 0;
while (k < res.length - 1 && res[k] == 0) k++;
StringBuilder sb = new StringBuilder();
for (; k < res.length; k++) sb.append(res[k]);
return sb.toString();
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.multiply("2", "3")); // 6
System.out.println(s.multiply("123", "456")); // 56088
System.out.println(s.multiply("0", "0")); // 0
}
}
复杂度分析
- 时间复杂度:O(m×n) --- 双重循环
- 空间复杂度:O(m+n) --- 结果数组
44. Wildcard Matching(通配符匹配)
题目描述
给你一个输入字符串 s 和一个字符模式 p,请你实现一个支持 '?' 和 '*' 的通配符匹配:
'?'可以匹配任何单个字符'*'可以匹配任意字符序列(包括空字符序列)
判定匹配成功的充要条件是:字符模式必须能够完全匹配输入字符串。
示例:
输入:s = "aa", p = "a" 输出:false
输入:s = "aa", p = "*" 输出:true
输入:s = "cb", p = "?a" 输出:false
输入:s = "adceb", p = "*a*b" 输出:true
输入:s = "acdcb", p = "a*c?b" 输出:false
约束: 0 <= s.length <= 2000,0 <= p.length <= 2000,s 只含小写字母,p 含小写字母、? 和 *
解题思路
核心思想 :动态规划,dp[i][j] 表示 s 前 i 个与 p 前 j 个是否匹配。* 可匹配空(dpij-1)或多字符(dpi-1j)。
解法:动态规划(推荐)。转移:
p[j-1]为?或等于s[i-1]:dp[i][j] = dp[i-1][j-1];p[j-1]为*:dp[i][j] = dp[i][j-1] || dp[i-1][j](匹配空序列 或 让*吃掉一个字符)。
初始化 dp[0][0]=true,p 前缀全为 * 时 dp[0][j]=true。时间 O(m×n),空间 O(m×n)(可滚动 O(n))。s、p 最长 2000,O(m×n)=4×10^6 可过。
代码实现
java
class Solution {
// 动态规划(推荐)
public boolean isMatch(String s, String p) {
int m = s.length(), n = p.length();
boolean[][] dp = new boolean[m + 1][n + 1];
dp[0][0] = true;
for (int j = 1; j <= n; j++) {
if (p.charAt(j - 1) == '*') dp[0][j] = dp[0][j - 1]; // * 可匹配空
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
char pc = p.charAt(j - 1);
if (pc == '?') {
dp[i][j] = dp[i - 1][j - 1]; // ? 匹配任意单个字符
} else if (pc == '*') {
dp[i][j] = dp[i][j - 1] || dp[i - 1][j]; // 空匹配 || 吃掉 s 的一个字符
} else {
dp[i][j] = dp[i - 1][j - 1] && (s.charAt(i - 1) == pc);
}
}
}
return dp[m][n];
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.isMatch("aa", "a")); // false
System.out.println(s.isMatch("aa", "*")); // true
System.out.println(s.isMatch("cb", "?a")); // false
System.out.println(s.isMatch("adceb", "*a*b")); // true
System.out.println(s.isMatch("acdcb", "a*c?b")); // false
}
}
复杂度分析
- 时间复杂度:O(m×n) --- 双层填充
- 空间复杂度:O(m×n) --- dp 表;可滚动数组优化为 O(n)
45. Jump Game II(跳跃游戏 II)
题目描述
给定一个长度为 n 的整数数组。初始位置为 nums[0]。每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处(0 <= j <= nums[i])。返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
示例:
输入:nums = [2,3,1,1,4]
输出:2
解释:跳到最后一个位置的最小跳跃次数是 2。从下标 0 跳到下标 1 的位置,跳 1 步,再跳 3 步到达数组末尾。
输入:nums = [2,3,0,1,4]
输出:2
约束: 1 <= nums.length <= 10^4,0 <= nums[i] <= 1000,保证可达
解题思路
核心思想:贪心------每步选择"当前可达范围内能跳到的最远位置"。
解法:贪心 + BFS 层数(推荐) 。维护 currentEnd(当前步可达的最远右边界)与 farthest(下一步可达的最远右边界)。遍历 i:更新 farthest = max(farthest, i + nums[i]);当 i == currentEnd 时说明当前步的窗口用尽,必须多跳一步,jumps++ 并把 currentEnd 更新为 farthest。时间 O(n),空间 O(1)。
代码实现
java
class Solution {
// 贪心(推荐)
public int jump(int[] nums) {
int jumps = 0, currentEnd = 0, farthest = 0;
for (int i = 0; i < nums.length - 1; i++) {
farthest = Math.max(farthest, i + nums[i]); // 更新下一步最远
if (i == currentEnd) { // 当前步窗口耗尽
jumps++;
currentEnd = farthest; // 进入下一步窗口
}
}
return jumps;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.jump(new int[]{2,3,1,1,4})); // 2
System.out.println(s.jump(new int[]{2,3,0,1,4})); // 2
}
}
复杂度分析
- 时间复杂度:O(n)
- 空间复杂度:O(1)
46. Permutations(全排列)
题目描述
给定一个不含重复数字的数组 nums,返回其所有可能的全排列 。你可以按任意顺序返回答案。
示例:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
约束: 1 <= nums.length <= 6,-10 <= nums[i] <= 10,nums 中的所有整数互不相同
解题思路
核心思想:回溯 + 已选标记(visited 数组)穷举每一位的可能值。
解法一:回溯 + visited(推荐) 。dfs(path):若 path 长度 == n 记录;否则遍历所有 nums[i],若未访问则加入 path、标记 visited、递归、撤销。时间 O(n×n!),空间 O(n)。
解法二:交换法回溯 。递归中直接交换 nums[first] 与 nums[i],省去 visited 数组,就地修改原数组。同样 O(n×n!),代码更紧凑。
代码实现
java
import java.util.ArrayList;
import java.util.List;
class Solution {
private List<List<Integer>> res = new ArrayList<List<Integer>>();
// 解法一:回溯 + visited(推荐)
public List<List<Integer>> permute(int[] nums) {
boolean[] used = new boolean[nums.length];
dfs(nums, used, new ArrayList<Integer>());
return res;
}
private void dfs(int[] nums, boolean[] used, List<Integer> path) {
if (path.size() == nums.length) { // 排列完整
res.add(new ArrayList<Integer>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
used[i] = true;
path.add(nums[i]);
dfs(nums, used, path);
path.remove(path.size() - 1);
used[i] = false; // 回溯撤销
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.permute(new int[]{1,2,3})); // [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
}
}
复杂度分析
- 时间复杂度:O(n×n!) --- n! 种排列,每种复制 O(n)
- 空间复杂度:O(n) --- 递归栈 + visited
47. Permutations II(全排列 II)
题目描述
给定一个可包含重复数字 的序列 nums,按任意顺序返回所有不重复的全排列。
示例:
输入:nums = [1,1,2]
输出:[[1,1,2],[1,2,1],[2,1,1]]
约束: 1 <= nums.length <= 8,-10 <= nums[i] <= 10
解题思路
核心思想:在 46 题基础上加"同层去重"------排序后,相同值只在第一次出现时进入递归。
解法:回溯 + 排序去重(推荐) 。先排序使相同值相邻。dfs 遍历时:若 i > 0 && nums[i] == nums[i-1] && !used[i-1] 跳过(同一层中,前一个相同值未使用说明本层已处理过该值,再选会产生重复排列)。其余与 46 题一致。时间 O(n×n!),空间 O(n)。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
private List<List<Integer>> res = new ArrayList<List<Integer>>();
// 回溯 + 排序去重(推荐)
public List<List<Integer>> permuteUnique(int[] nums) {
Arrays.sort(nums);
boolean[] used = new boolean[nums.length];
dfs(nums, used, new ArrayList<Integer>());
return res;
}
private void dfs(int[] nums, boolean[] used, List<Integer> path) {
if (path.size() == nums.length) {
res.add(new ArrayList<Integer>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) continue;
// 同层去重:前一个相同值未被使用(说明本层已试过该值)
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
used[i] = true;
path.add(nums[i]);
dfs(nums, used, path);
path.remove(path.size() - 1);
used[i] = false;
}
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.permuteUnique(new int[]{1,1,2})); // [[1,1,2],[1,2,1],[2,1,1]]
}
}
复杂度分析
- 时间复杂度:O(n×n!) --- 不重排列数是 n!/(重复度乘积),上界 n!
- 空间复杂度:O(n) --- 递归栈
48. Rotate Image(旋转图像)
题目描述
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度 。必须在原地旋转图像,即直接修改输入的二维矩阵,不要使用另一个矩阵来旋转图像。
示例:
输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[[7,4,1],[8,5,2],[9,6,3]]
约束: n == matrix.length == matrix[i].length,1 <= n <= 20,-1000 <= matrix[i][j] <= 1000
解题思路
核心思想:旋转可由"转置 + 水平翻转"两步等价实现。
解法一:转置 + 每行反转(推荐) 。顺时针旋转 90° = 先沿主对角线转置(swap(matrix[i][j], matrix[j][i])),再将每一行反转。时间 O(n²),空间 O(1)。
解法二:四元素旋转圈 。按环逐层做 4 个位置的循环替换 matrix[i][j] -> ...,一次到位。同样 O(n²)、O(1),但下标较绕。
代码实现
java
import java.util.Arrays;
class Solution {
// 解法一:转置 + 行反转(推荐)
public void rotate(int[][] matrix) {
int n = matrix.length;
// 1. 沿主对角线转置
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int t = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = t;
}
}
// 2. 每行反转
for (int i = 0; i < n; i++) {
for (int l = 0, r = n - 1; l < r; l++, r--) {
int t = matrix[i][l];
matrix[i][l] = matrix[i][r];
matrix[i][r] = t;
}
}
}
public static void main(String[] args) {
Solution s = new Solution();
int[][] m = {{1,2,3},{4,5,6},{7,8,9}};
s.rotate(m);
System.out.println(Arrays.deepToString(m)); // [[7,4,1],[8,5,2],[9,6,3]]
}
}
复杂度分析
- 时间复杂度:O(n²) --- 每个元素访问常数次
- 空间复杂度:O(1) --- 原地
49. Group Anagrams(字母异位词分组)
题目描述
给你一个字符串数组,请你将字母异位词组合在一起。可以按任意顺序返回结果列表。字母异位词是通过重新排列不同单词的字母得到的新单词。
示例:
输入:strs = ["eat","tea","tan","ate","nat","bat"]
输出:[["bat"],["nat","tan"],["ate","eat","tea"]]
约束: 1 <= strs.length <= 10^4,0 <= strs[i].length <= 100,strsi 仅包含小写字母
解题思路
核心思想:异位词排序后相同------用"排序后的字符串"或"字符计数串"作为哈希键分组。
解法一:排序键(推荐) 。对每个字符串 char[] 排序得到 key,Map<String, List<String>> 分组,最后收集 values。时间 O(n×k log k)(k=平均串长),空间 O(n×k)。
解法二:计数键。用 "1#0#..." 格式的 26 字母计数串作为 key,避免排序。时间 O(n×k),常数项略大。
代码实现
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class Solution {
// 解法一:排序键(推荐)
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String s : strs) {
char[] arr = s.toCharArray();
Arrays.sort(arr); // 异位词排序后相同
String key = new String(arr);
map.computeIfAbsent(key, k -> new ArrayList<String>()).add(s);
}
return new ArrayList<List<String>>(map.values());
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.groupAnagrams(new String[]{"eat","tea","tan","ate","nat","bat"}));
// [[bat],[nat,tan],[ate,eat,tea]](顺序不定)
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 排序键(推荐) | O(n×k log k) | O(n×k) | 实现简洁 |
| 计数键 | O(n×k) | O(n×k) | 免排序,键更长 |
50. Pow(x, n)(Pow(x, n))
题目描述
实现 pow(x, n),即计算 x 的整数 n 次幂函数。
示例:
输入:x = 2.00000, n = 10 输出:1024.00000
输入:x = 2.10000, n = 3 输出:9.26100
输入:x = 2.00000, n = -2 输出:0.25000
约束: -100.0 < x < 100.0,-2^31 <= n <= 2^31 - 1,n 是一个 32 位有符号整数,答案误差不超过 10^-5
解题思路
核心思想:快速幂------通过二进制分解指数,把乘法次数从 O(n) 降到 O(log n)。
解法一:快速幂迭代(推荐) 。n 超负数边界时用 long 保存再取绝对值。把 n 看成二进制:每次 n % 2 == 1 时结果乘上当前的 x,同时 x *= x(平方),n >>= 1。时间 O(log n),空间 O(1)。
解法二:快速幂递归 。n==0 返回 1;n<0 对称处理;n 为偶数 → pow(x*x, n/2),奇数 → x * pow(x*x, n/2)。时间 O(log n),空间 O(log n) 递归栈。
代码实现
java
class Solution {
// 解法一:快速幂迭代(推荐)
public double myPow(double x, int n) {
long N = n; // long 避免 -2^31 取绝对值溢出
if (N < 0) { x = 1 / x; N = -N; }
double ans = 1.0;
while (N > 0) {
if ((N & 1) == 1) ans *= x; // 当前二进制位为 1 则乘入
x *= x; // 底数平方
N >>= 1; // 指数右移
}
return ans;
}
// 解法二:快速幂递归
public double myPowRec(double x, int n) {
long N = n;
return N >= 0 ? pow(x, N) : 1.0 / pow(x, -N);
}
private double pow(double x, long n) {
if (n == 0) return 1.0;
double half = pow(x * x, n / 2); // 折半递归
return (n % 2 == 0) ? half : half * x;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.myPow(2.0, 10)); // 1024.0
System.out.println(s.myPow(2.1, 3)); // 9.261
System.out.println(s.myPow(2.0, -2)); // 0.25
System.out.println(s.myPow(2.0, -2147483648)); // 0.0(极小值)
}
}
复杂度分析
| 解法 | 时间复杂度 | 空间复杂度 | 说明 |
|---|---|---|---|
| 迭代快速幂(推荐) | O(log n) | O(1) | 二进制分解指数 |
| 递归快速幂 | O(log n) | O(log n) | 递归栈深度 log n |