参考资料:
1. 无重复字符的最长子串

解答: 这段代码使用滑动窗口算法来高效地找到不包含重复字符的最长子串长度
算法思路:
- 滑动窗口:使用两个指针left和right表示当前窗口的左右边界。
- 字符位置记录:使用数组
index记录每个 字符最后一次出现的位置+1。 - 窗口调整:当遇到重复字符时,将左边界移动到重复字符上次出现位置的下一个位置。
- 长度计算:每次右边界移动时,计算当前窗口长度并更新最大值。
1.1. 解答
class Solution {
public int lengthOfLongestSubstring(String s) {
// 创建一个数组来记录每个ASCII字符最后一次出现的位置+1
// 使用128是因为ASCII字符共有128个
int[] index = new int[128];
// 初始化最大长度为0
int maxLen = 0;
// 滑动窗口的左边界,初始为0
int left = 0;
// 遍历字符串,right是滑动窗口的右边界
for(int right = 0; right < s.length(); right++) {
// 获取当前字符
char ch = s.charAt(right);
// 如果当前字符之前出现过,则更新左边界到上次出现位置的下一个位置
// 使用Math.max确保left不会向左移动
left = Math.max(left, index[ch]);
// 计算当前窗口大小,并更新最大长度
maxLen = Math.max(maxLen, right - left + 1);
// 记录当前字符的位置+1(方便下次直接跳转到不重复的位置)
index[ch] = right + 1;
}
// 返回找到的最大长度
return maxLen;
}
}
2. LRU缓存机制

算法思路:
- 数据结构选择:
-
- 双向链表:维护节点的访问顺序,头部是最近使用的节点,尾部是最久未使用的节点
- 哈希表:提供O(1)时间复杂度的键值查找
- 关键操作:
-
addNode(Node node):将节点添加到链表头部removeNode(Node node):从链表中移除指定节点moveToHead(Node node):将节点移动到头部(表示最近使用)popTail():移除并返回链表尾部节点(最近最少使用)
- LRU策略实现:
-
- get操作:如果键存在,将对应节点移到链表头部并返回值
- put操作:
-
-
- 键不存在时:创建新节点,添加到链表头部和哈希表,如果超过容量则移除尾部节点
- 键存在时:更新值并将节点移到链表头部
-
- 虚拟头尾节点:
-
- 简化链表边界条件的处理
- 不需要检查null指针,代码更简洁
- 时间复杂度:
-
- 所有操作(get/put/add/remove)都是O(1)时间复杂度
- 哈希表提供快速查找
- 链表操作维护访问顺序
2.1. 解答
import java.util.HashMap;
class LRUCache {
// 定义双向链表节点类
class Node {
int key; // 键
int value; // 值
Node prev; // 前驱节点
Node next; // 后继节点
// 节点构造函数
Node(int key, int value) {
this.key = key;
this.value = value;
}
}
// 添加节点到链表头部(最近使用)
private void addNode(Node node) {
node.prev = head; // 新节点的前驱指向头节点
node.next = head.next; // 新节点的后继指向原第一个节点
head.next.prev = node; // 原第一个节点的前驱指向新节点
head.next = node; // 头节点的后继指向新节点
}
// 从链表中移除节点
private void removeNode(Node node) {
Node prev = node.prev; // 获取前驱节点
Node next = node.next; // 获取后继节点
prev.next = next; // 前驱节点的后继指向当前节点的后继
next.prev = prev; // 后继节点的前驱指向当前节点的前驱
}
// 将节点移动到链表头部(表示最近使用)
private void moveToHead(Node node) {
removeNode(node); // 先从链表中移除
addNode(node); // 再添加到头部
}
// 移除链表尾部节点(最近最少使用)
private Node popTail() {
Node res = tail.prev; // 获取尾节点的前驱(实际最后一个节点)
removeNode(res); // 从链表中移除
return res; // 返回被移除的节点
}
// 哈希表用于快速查找节点
private HashMap<Integer, Node> cache = new HashMap<>();
private int size; // 当前缓存大小
private int capacity; // 缓存容量
private Node head, tail; // 虚拟头尾节点(哨兵节点)
// 构造函数,初始化LRU缓存
public LRUCache(int capacity) {
this.size = 0; // 初始大小为0
this.capacity = capacity; // 设置容量
// 初始化虚拟头尾节点
head = new Node(-1, -1); // 头节点(不存储实际数据)
tail = new Node(-1, -1); // 尾节点(不存储实际数据)
// 头尾节点互相连接
head.next = tail;
tail.prev = head;
}
// 获取键对应的值
public int get(int key) {
Node node = cache.get(key); // 从哈希表获取节点
if (node == null) {
return -1; // 不存在返回-1
}
moveToHead(node); // 移动到头部(表示最近使用)
return node.value; // 返回值
}
// 插入或更新键值对
public void put(int key, int value) {
Node node = cache.get(key); // 检查键是否已存在
if (node == null) {
// 键不存在,创建新节点
Node newNode = new Node(key, value);
cache.put(key, newNode); // 加入哈希表
addNode(newNode); // 添加到链表头部
size++; // 大小增加
// 如果超过容量,移除最近最少使用的节点
if (size > capacity) {
Node tail = popTail(); // 移除尾部节点
cache.remove(tail.key); // 从哈希表移除
size--; // 大小减少
}
} else {
// 键已存在,更新值并移动到头部
node.value = value;
moveToHead(node);
}
}
}
3. 反转链表

解题思路:
- 递归思想:
-
- 采用"分而治之"的思想,将大问题分解为小问题
- 假设剩余部分的链表已经反转好,只需要处理当前节点
- 关键步骤:
-
- 终止条件:当链表为空或只有一个节点时,无需反转,直接返回
- 递归调用:先反转后续部分的链表(head.next之后的部分)
- 指针调整:将当前节点的下一个节点的next指向自己,实现局部反转
- 断开原指针:避免形成循环链表
- 递归过程示例(以输入[1,2,3,4,5]为例):
-
- 递归到最后一个节点5时返回5
- 回到节点4时:4.next(5).next = 4,形成5→4
- 回到节点3时:3.next(4).next = 3,形成5→4→3
- 依此类推,最终形成5→4→3→2→1
- 复杂度分析:
-
- 时间复杂度:O(n),需要遍历每个节点一次
- 空间复杂度:O(n),递归调用栈的深度为链表长度
3.1. 解答
class Solution {
public ListNode reverseList(ListNode head) {
// 递归终止条件:当前节点为空或只有一个节点时,直接返回
if (head == null || head.next == null) {
return head;
}
// 递归调用,反转剩余部分的链表
ListNode newHead = reverseList(head.next);
// 将当前节点的下一个节点的next指针指向当前节点(实现反转)
head.next.next = head;
// 断开当前节点的next指针,避免循环
head.next = null;
// 返回新的头节点(原链表的尾节点)
return newHead;
}
}
4. 数组中的第K个最大元素

解题思路:
- 随机选择基准值(pivot):从当前数组中随机选取一个元素作为基准
- 分区:将数组分为三部分
-
- 大于pivot的元素
- 等于pivot的元素
- 小于pivot的元素
- 递归选择:
-
- 如果第k大元素在大于pivot的部分,递归处理这部分
- 如果第k大元素在小于pivot的部分,递归处理这部分并调整k值
- 如果第k大元素在等于pivot的部分,直接返回pivot
- 时间复杂度:该算法的时间复杂度为 O(n)(平均情况),最坏情况下为 O(n²),
4.1. 解答
public class Solution {
// 快速选择的核心递归方法
private int quickSelect(List<Integer> nums, int k) {
// 随机选择基准数 - 这是保证算法平均O(n)时间复杂度的关键
Random rand = new Random();
int pivot = nums.get(rand.nextInt(nums.size()));
// 创建三个列表用于分区
List<Integer> big = new ArrayList<>(); // 存储大于pivot的元素
List<Integer> equal = new ArrayList<>(); // 存储等于pivot的元素
List<Integer> small = new ArrayList<>(); // 存储小于pivot的元素
// 遍历数组,将元素分配到对应的分区
for (int num : nums) {
if (num > pivot)
big.add(num); // 大于pivot的元素放入big
else if (num < pivot)
small.add(num); // 小于pivot的元素放入small
else
equal.add(num); // 等于pivot的元素放入equal
}
// 判断第k大元素在哪个分区
if (k <= big.size())
// 第k大元素在big分区中,递归处理big分区
return quickSelect(big, k);
if (nums.size() - small.size() < k)
// 第k大元素在small分区中,需要调整k值后递归处理
// 新k值 = k - (nums.size() - small.size())
// nums.size() - small.size() = big.size() + equal.size()
return quickSelect(small, k - (nums.size() - small.size()));
// 如果上面两个条件都不满足,说明第k大元素在equal分区中
// 因为equal中的所有元素都等于pivot,所以直接返回pivot
return pivot;
}
// 对外提供的接口方法
public int findKthLargest(int[] nums, int k) {
// 将原始数组转换为List,方便操作
List<Integer> numList = new ArrayList<>();
for (int num : nums) {
numList.add(num);
}
// 调用快速选择算法
return quickSelect(numList, k);
}
}
5. 三数之和


解题思路:
- 排序数组:首先将数组排序,这样可以方便地跳过重复元素和使用双指针技巧
- 固定一个数:遍历数组,每次固定一个数nums[i]作为三元组的第一个元素
- 双指针查找:在固定nums[i]后,使用双指针在剩余数组中查找两个数,使得三数之和为0
- 跳过重复元素:在每一步都检查并跳过重复元素,避免结果中出现重复的三元组
- 时间复杂度:时间复杂度为 O(n²),其中
n是数组nums的长度。
5.1. 解答
class Solution {
public List<List<Integer>> threeSum(int[] nums) {
// 首先对数组进行排序,这是双指针法的基础
Arrays.sort(nums);
// 创建结果列表
List<List<Integer>> res = new ArrayList<>();
// 遍历数组,i作为三元组的第一个元素的索引
for (int i = 0; i < nums.length; i++) {
// 跳过重复元素:如果当前元素与前一个相同,则跳过以避免重复结果
if (i > 0 && nums[i] == nums[i - 1]) continue;
// 初始化双指针:左指针在i的下一个位置,右指针在数组末尾
int l = i + 1, r = nums.length - 1;
// 计算目标值:我们需要找nums[l] + nums[r] = -nums[i]
int target = -nums[i];
// 双指针循环
while (l < r) {
int sum = nums[l] + nums[r];
if (sum == target) {
// 找到满足条件的三元组,加入结果列表
res.add(Arrays.asList(nums[i], nums[l], nums[r]));
// 移动双指针
l++;
r--;
// 跳过左指针的重复元素
while (l < r && nums[l] == nums[l - 1]) l++;
// 跳过右指针的重复元素
while (l < r && nums[r] == nums[r + 1]) r--;
} else if (sum < target) {
// 和太小,需要增大,所以移动左指针
l++;
} else {
// 和太大,需要减小,所以移动右指针
r--;
}
}
}
// 返回所有找到的三元组
return res;
}
}
6. 最大子数组和

解题思路:
- 动态规划思想:维护两个变量,
pre表示以当前元素结尾的子数组的最大和,maxAns表示全局最大子数组和 - 遍历数组:对于每个元素,决定是将其加入前面的子数组,还是以它作为新子数组的开头
- 更新最大值:每次迭代后更新全局最大值
- 时间复杂度:O(n),其中
n是数组nums的长度。
6.1. 解答
class Solution {
public int maxSubArray(int[] nums) {
// pre 表示以当前元素结尾的子数组的最大和
// 初始化为0,因为第一个元素可以自己作为子数组
int pre = 0;
// maxAns 存储全局最大子数组和
// 初始化为数组第一个元素,处理数组只有一个元素的情况
int maxAns = nums[0];
// 遍历数组中的每个元素
for (int x : nums) {
// 计算以当前元素结尾的子数组的最大和:
// 选择1:将当前元素加入前面的子数组(pre + x)
// 选择2:以当前元素作为新子数组的开头(x)
// 取两者中较大的值
pre = Math.max(pre + x, x);
// 更新全局最大子数组和
// 比较之前记录的最大值和当前计算出的最大值
maxAns = Math.max(maxAns, pre);
}
// 返回最终找到的最大子数组和
return maxAns;
}
}
7. 合并两个有序链表

解题思路:
- 创建虚拟头节点:创建一个虚拟头节点
ans作为新链表的起点,并用指针i跟踪当前节点 - 比较并合并:同时遍历两个链表,每次比较当前节点的值,将较小值的节点接入新链表
- 处理剩余节点:当其中一个链表遍历完后,将另一个链表的剩余部分直接接在新链表后面
- 返回结果:返回虚拟头节点的下一个节点,即合并后链表的真正头节点
- 时间复杂度:O(n+m),其中 n 和 m 分别是两个链表的长度。
7.1. 解答
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
// 创建一个虚拟头节点(dummy node),简化边界条件处理
// 这个节点的值无关紧要,最终我们会返回它的next节点
ListNode ans = new ListNode();
// 指针i用于构建新链表,初始指向虚拟头节点
ListNode i = ans;
// 当两个链表都还有节点时,进行比较和合并
while (list1 != null && list2 != null) {
// 比较两个链表当前节点的值
if (list1.val > list2.val) {
// 如果list2的当前节点值较小,将其加入新链表
i.next = new ListNode(list2.val);
// list2指针后移
list2 = list2.next;
} else {
// 如果list1的当前节点值较小或相等,将其加入新链表
i.next = new ListNode(list1.val);
// list1指针后移
list1 = list1.next;
}
// 新链表指针后移
i = i.next;
}
// 处理剩余节点:将未遍历完的链表直接接在新链表后面
// 三元运算符判断哪个链表还有剩余节点
i.next = (list1 == null) ? list2 : list1;
// 返回虚拟头节点的下一个节点,即合并后链表的真正头节点
return ans.next;
}
}
8. 最长回文子串

解题思路:
- 动态规划定义:
dp[j]表示子串s[j...i]是否是回文 - 状态转移:
-
- 当
s[j] == s[i]且内部子串s[j+1...i-1]是回文时,s[j...i]也是回文 - 特殊情况:当子串长度为1或2时,只需比较两端字符
- 当
- 记录最长回文:在计算过程中记录找到的最长回文子串的起始和结束位置
- 时间复杂度:O(n²),其中
n是字符串s的长度。
8.1. 解答
class Solution {
public String longestPalindrome(String s) {
// 初始化最长回文子串的起始和结束索引
int ans1 = 0, ans2 = 0;
// dp数组:dp[j]表示s[j...i]是否是回文
boolean[] dp = new boolean[s.length()];
// 外层循环:i作为子串的结束索引
for (int i = 0; i < s.length(); i++) {
// 单个字符总是回文
dp[i] = true;
// 内层循环:j作为子串的起始索引
for (int j = 0; j < i; j++) {
// 状态转移方程:
// 1. 两端字符必须相等
// 2. 内部子串(j+1到i-1)必须是回文(或者子串长度<=2)
dp[j] = (s.charAt(j) == s.charAt(i)) && dp[j + 1];
// 如果当前子串是回文且比之前记录的最长回文更长
if (dp[j] && (i - j > ans2 - ans1)) {
// 更新最长回文的起始和结束索引
ans1 = j;
ans2 = i;
}
}
}
// 返回最长回文子串(substring的结束索引是exclusive的,所以需要+1)
return s.substring(ans1, ans2 + 1);
}
}
9. 两数之和

解题思路:
- 哈希表存储:使用哈希表存储已经遍历过的数字及其索引
- 一次遍历:在遍历数组时,检查哈希表中是否存在
target - current_num - 立即返回:如果找到匹配的对,立即返回两个索引
- 无解处理:遍历完成后若无解,返回空数组或抛出异常
- 时间复杂度:O(n),其中
n是数组nums的长度。
9.1. 解答
class Solution {
public int[] twoSum(int[] nums, int target) {
// 创建哈希表,用于存储数值和对应的索引
// key: 数组中的数字
// value: 该数字对应的索引
Map<Integer, Integer> map = new HashMap<>();
// 遍历数组中的每个元素
for(int i = 0; i < nums.length; i++) {
// 检查哈希表中是否存在与当前数字配对的数字
// 即是否存在一个数字等于 target - nums[i]
if(map.containsKey(target - nums[i])) {
// 如果存在,返回这两个数字的索引
// map.get(target - nums[i]) 是之前存储的配对数字的索引
// i 是当前数字的索引
return new int[] {map.get(target - nums[i]), i};
}
// 将当前数字和它的索引存入哈希表
// 这样后续的数字就可以检查是否能与它配对
map.put(nums[i], i);
}
// 如果没有找到符合条件的两个数字,返回空数组
// 根据题目描述,这种情况理论上不会出现
// 也可以选择抛出异常:throw new IllegalArgumentException("No two sum solution");
return new int[0];
}
}
10. 二叉树书的层序遍历

解题思路:
- 队列初始化:使用队列来存储待访问的节点,初始时放入根节点
- 逐层处理:
-
- 每次处理一层的所有节点
- 将当前层节点的值存入列表
- 将下一层的子节点加入队列
- 结果存储:将每层的节点值列表添加到最终结果中
- 时间复杂度:O(n),其中
n是二叉树的节点总数。
10.1. 广度优先遍历(BFS)
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
// 创建结果列表,用于存储各层节点值
List<List<Integer>> ret = new ArrayList<List<Integer>>();
// 处理空树情况
if (root == null) {
return ret;
}
// 创建队列用于广度优先搜索,初始放入根节点
Queue<TreeNode> queue = new LinkedList<TreeNode>();
queue.offer(root);
// 当队列不为空时继续处理
while (!queue.isEmpty()) {
// 创建当前层的节点值列表
List<Integer> level = new ArrayList<Integer>();
// 获取当前层的节点数量
int currentLevelSize = queue.size();
// 处理当前层的所有节点
for (int i = 1; i <= currentLevelSize; ++i) {
// 从队列取出节点
TreeNode node = queue.poll();
// 将节点值加入当前层列表
level.add(node.val);
// 如果有左子节点,加入队列(下一层)
if (node.left != null) {
queue.offer(node.left);
}
// 如果有右子节点,加入队列(下一层)
if (node.right != null) {
queue.offer(node.right);
}
}
// 将当前层的结果加入最终结果列表
ret.add(level);
}
// 返回层序遍历结果
return ret;
}
}
10.2. 递归解法
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<>();
if(root==null){
return ans;
}
levelOrderHelper(root,ans,1);
return ans;
}
public void levelOrderHelper(TreeNode root,List<List<Integer>> ans,int level){
if(root == null){
return;
}
List<Integer> tempAns = new ArrayList<>();
if(ans.size()<level){
ans.add(tempAns);
}
ans.get(level-1).add(root.val);
levelOrderHelper(root.left,ans,level+1);
levelOrderHelper(root.right,ans,level+1);
}
}
11. 搜索旋转排序数组

解题思路:
- 二分查找框架:使用标准的二分查找循环结构
- 旋转点判断:通过比较中间元素与首元素,确定哪一半是有序的
- 目标值定位:在确定的有序半区中进行常规二分查找
- 边界调整:根据目标值与有序半区的关系调整搜索边界
- 时间复杂度:O(log n),其中
n是数组nums的长度。
11.1. 解答
class Solution {
public int search(int[] nums, int target) {
// 获取数组长度
int n = nums.length;
// 处理空数组情况
if (n == 0) {
return -1;
}
// 处理单元素数组情况
if (n == 1) {
return nums[0] == target ? 0 : -1;
}
// 初始化二分查找的左右指针
int l = 0, r = n - 1;
// 开始二分查找循环
while (l <= r) {
// 计算中间索引
int mid = (l + r) / 2;
// 如果中间元素正好是目标值,直接返回
if (nums[mid] == target) {
return mid;
}
// 判断左半部分是否有序(旋转点在右半部分)
if (nums[0] <= nums[mid]) {
// 如果目标值在有序的左半部分范围内
if (nums[0] <= target && target < nums[mid]) {
// 调整右指针,在左半部分继续搜索
r = mid - 1;
} else {
// 否则调整左指针,在右半部分搜索
l = mid + 1;
}
}
// 右半部分有序(旋转点在左半部分)
else {
// 如果目标值在有序的右半部分范围内
if (nums[mid] < target && target <= nums[n - 1]) {
// 调整左指针,在右半部分继续搜索
l = mid + 1;
} else {
// 否则调整右指针,在左半部分搜索
r = mid - 1;
}
}
}
// 如果没有找到目标值,返回-1
return -1;
}
}
12. 岛屿数量

解题思路:
- 网格遍历:逐个检查网格中的每个单元格
- 发现岛屿:当遇到'1'时,表示发现新岛屿,计数器加1
- DFS淹没岛屿:使用深度优先搜索将与当前陆地相连的所有陆地标记为已访问(改为'0')
- 继续搜索:完成一个岛屿的搜索后,继续扫描网格寻找下一个岛屿
- 时间复杂度:O(m×n),其中
m是网格的行数,n是网格的列数。
12.1. DFS深度优先
class Solution {
// 深度优先搜索辅助函数,用于"淹没"已发现的岛屿
void dfs(char[][] grid, int r, int c) {
// 获取网格的行数和列数
int nr = grid.length;
int nc = grid[0].length;
// 边界检查:确保位置(r,c)在网格范围内,且当前单元格是陆地('1')
if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] == '0') {
return;
}
// 将当前陆地标记为已访问(改为水域'0')
grid[r][c] = '0';
// 递归访问四个相邻方向(上、下、左、右)
dfs(grid, r - 1, c); // 上
dfs(grid, r + 1, c); // 下
dfs(grid, r, c - 1); // 左
dfs(grid, r, c + 1); // 右
}
// 主函数:计算岛屿数量
public int numIslands(char[][] grid) {
// 处理空网格或空数组的情况
if (grid == null || grid.length == 0) {
return 0;
}
// 获取网格的行数和列数
int nr = grid.length;
int nc = grid[0].length;
// 初始化岛屿计数器
int num_islands = 0;
// 双重循环遍历整个网格
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
// 当发现未被访问的陆地('1')时
if (grid[r][c] == '1') {
// 增加岛屿计数
++num_islands;
// 使用DFS"淹没"整个岛屿
dfs(grid, r, c);
}
}
}
// 返回岛屿总数
return num_islands;
}
}
12.2. BFS广度优先
class Solution {
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;
grid[r][c] = '0';
Queue<Integer> neighbors = new LinkedList<>();
neighbors.add(r * nc + c);
while (!neighbors.isEmpty()) {
int id = neighbors.remove();
int row = id / nc;
int col = id % nc;
if (row - 1 >= 0 && grid[row-1][col] == '1') {
neighbors.add((row-1) * nc + col);
grid[row-1][col] = '0';
}
if (row + 1 < nr && grid[row+1][col] == '1') {
neighbors.add((row+1) * nc + col);
grid[row+1][col] = '0';
}
if (col - 1 >= 0 && grid[row][col-1] == '1') {
neighbors.add(row * nc + col-1);
grid[row][col-1] = '0';
}
if (col + 1 < nc && grid[row][col+1] == '1') {
neighbors.add(row * nc + col+1);
grid[row][col+1] = '0';
}
}
}
}
}
return num_islands;
}
}
12.3. 并查集
class Solution {
class UnionFind {
int count;
int[] parent;
int[] rank;
public UnionFind(char[][] grid) {
count = 0;
int m = grid.length;
int n = grid[0].length;
parent = new int[m * n];
rank = new int[m * n];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '1') {
parent[i * n + j] = i * n + j;
++count;
}
rank[i * n + j] = 0;
}
}
}
public int find(int i) {
if (parent[i] != i) parent[i] = find(parent[i]);
return parent[i];
}
public void union(int x, int y) {
int rootx = find(x);
int rooty = find(y);
if (rootx != rooty) {
if (rank[rootx] > rank[rooty]) {
parent[rooty] = rootx;
} else if (rank[rootx] < rank[rooty]) {
parent[rootx] = rooty;
} else {
parent[rooty] = rootx;
rank[rootx] += 1;
}
--count;
}
}
public int getCount() {
return count;
}
}
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int num_islands = 0;
UnionFind uf = new UnionFind(grid);
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
grid[r][c] = '0';
if (r - 1 >= 0 && grid[r-1][c] == '1') {
uf.union(r * nc + c, (r-1) * nc + c);
}
if (r + 1 < nr && grid[r+1][c] == '1') {
uf.union(r * nc + c, (r+1) * nc + c);
}
if (c - 1 >= 0 && grid[r][c-1] == '1') {
uf.union(r * nc + c, r * nc + c - 1);
}
if (c + 1 < nc && grid[r][c+1] == '1') {
uf.union(r * nc + c, r * nc + c + 1);
}
}
}
}
return uf.getCount();
}
}
13. 全排列

解题思路:
- 主方法 permute:
-
- 初始化结果列表
res和当前排列output - 将输入数组转换为列表形式便于操作
- 调用回溯方法生成所有排列
- 初始化结果列表
- 回溯方法 backtrack:
-
- 当所有位置都填完时 (
first == n),将当前排列加入结果 - 通过交换元素生成不同的排列组合
- 递归处理下一个位置
- 回溯时撤销交换操作
- 当所有位置都填完时 (
- 时间复杂度:O(n×n!),其中
n是数组nums的长度。
13.1. 解答-回溯法
class Solution {
public List<List<Integer>> permute(int[] nums) {
// 初始化结果列表
List<List<Integer>> res = new ArrayList<List<Integer>>();
// 将输入数组转换为ArrayList便于操作
List<Integer> output = new ArrayList<Integer>();
for (int num : nums) {
output.add(num);
}
int n = nums.length;
// 调用回溯方法,从第0个位置开始
backtrack(n, output, res, 0);
return res;
}
public void backtrack(int n, List<Integer> output, List<List<Integer>> res, int first) {
// 当所有位置都填完时,将当前排列加入结果
if (first == n) {
res.add(new ArrayList<Integer>(output)); // 注意要创建新列表
}
// 遍历从first开始的所有位置
for (int i = first; i < n; i++) {
// 交换当前位置和first位置的元素
Collections.swap(output, first, i);
// 递归处理下一个位置
backtrack(n, output, res, first + 1);
// 撤销交换操作(回溯)
Collections.swap(output, first, i);
}
}
}
14. 有效的括号

解题思路:
- 预处理检查:字符串长度为奇数直接返回false
- 建立括号映射:用哈希表存储右括号到左括号的映射关系
- 栈处理:
-
- 遇到左括号入栈
- 遇到右括号检查栈顶是否匹配
- 不匹配立即返回false
- 最终检查:栈为空才说明所有括号都正确匹配
- 时间复杂度:O(n),其中
n是字符串s的长度
14.1. 解答
class Solution {
public boolean isValid(String s) {
// 获取字符串长度
int n = s.length();
// 如果长度是奇数,肯定不匹配,直接返回false
if (n % 2 == 1) {
return false;
}
// 创建括号映射表,右括号作为key,对应的左括号作为value
Map<Character, Character> pairs = new HashMap<Character, Character>() {{
put(')', '('); // 小括号匹配
put(']', '['); // 中括号匹配
put('}', '{'); // 大括号匹配
}};
// 使用双端队列作为栈结构
Deque<Character> stack = new LinkedList<Character>();
// 遍历字符串中的每个字符
for (int i = 0; i < n; i++) {
char ch = s.charAt(i);
// 如果是右括号
if (pairs.containsKey(ch)) {
// 栈为空(没有对应的左括号)或者栈顶不匹配
if (stack.isEmpty() || stack.peek() != pairs.get(ch)) {
return false;
}
// 匹配成功,弹出栈顶的左括号
stack.pop();
} else {
// 左括号直接入栈
stack.push(ch);
}
}
// 最后检查栈是否为空(所有左括号都有匹配)
return stack.isEmpty();
}
}
15. 买卖股票的最佳时机

解题思路:
- minprice:记录遍历过程中遇到的最低价格(最佳买入点)
- maxprofit:记录当前能获得的最大利润(当前价格与minprice的差值)
- 时间复杂度:O(n),其中
n是数组prices的长度。
15.1. 解答
public class Solution {
public int maxProfit(int prices[]) {
// 初始化最小价格为整型最大值
int minprice = Integer.MAX_VALUE;
// 初始化最大利润为0
int maxprofit = 0;
// 遍历每一天的价格
for (int i = 0; i < prices.length; i++) {
// 如果当前价格比记录的最小价格还小
if (prices[i] < minprice) {
// 更新最小价格(找到更佳的买入点)
minprice = prices[i];
}
// 否则检查当前卖出是否能获得更大利润
else if (prices[i] - minprice > maxprofit) {
// 更新最大利润
maxprofit = prices[i] - minprice;
}
}
// 返回最终的最大利润
return maxprofit;
}
}
16. 二叉树的最近公共先祖

解题思路:
- 如果当前节点是p或q,且其子树包含另一个节点
- 如果p和q分别位于当前节点的左右子树中
- 递归过程中第一个满足以上条件的节点就是LCA
- 时间复杂度:O(n),其中
n是二叉树的节点数。
16.1. 解答
class Solution {
// 存储最终结果的节点
private TreeNode ans;
// 构造函数初始化ans为null
public Solution() {
this.ans = null;
}
// 深度优先搜索递归函数
private boolean dfs(TreeNode root, TreeNode p, TreeNode q) {
// 递归终止条件:到达空节点
if (root == null) return false;
// 递归查找左子树是否包含p或q
boolean lson = dfs(root.left, p, q);
// 递归查找右子树是否包含p或q
boolean rson = dfs(root.right, p, q);
// LCA判断条件:
// 1. p和q分别位于左右子树中
// 2. 当前节点是p或q,且另一个节点在子树中
if ((lson && rson) ||
((root.val == p.val || root.val == q.val) && (lson || rson))) {
ans = root; // 找到LCA
}
// 返回当前子树是否包含p或q:
// 1. 左子树包含
// 2. 右子树包含
// 3. 当前节点就是p或q
return lson || rson || (root.val == p.val || root.val == q.val);
}
// 主方法:启动DFS并返回结果
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
this.dfs(root, p, q);
return this.ans;
}
}
17. 环形链表

解题思路:
算法使用哈希集合(HashSet)来记录已经访问过的节点,通过遍历链表并检查每个节点是否已被访问过来判断是否存在环。如果遇到已经存在于集合中的节点,说明链表有环;如果遍历到链表末尾(null)则无环。
时间复杂度:O(n),其中 n 是链表的节点数。
17.1. 哈希表
public class Solution {
public boolean hasCycle(ListNode head) {
// 创建哈希集合用于存储已访问的节点
Set<ListNode> seen = new HashSet<ListNode>();
// 遍历链表
while (head != null) {
// 尝试将当前节点加入集合,如果已存在则返回true(有环)
if (!seen.add(head)) {
return true;
}
// 移动到下一个节点
head = head.next;
}
// 遍历完成未发现重复节点,返回false(无环)
return false;
}
}
17.2. 快慢指针
public class Solution {
public boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}
ListNode slow = head;
ListNode fast = head.next;
while (slow != fast) {
if (fast == null || fast.next == null) {
return false;
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}
18. 最长递增子序列

解题思路:
- 定义状态:
dp[i]表示以nums[i]结尾的最长递增子序列的长度 - 状态转移:对于每个
nums[i],检查前面所有比它小的元素nums[j],取最大的dp[j]+1 - 初始化:每个元素本身至少构成长度为1的子序列
- 结果获取:
dp数组中的最大值即为答案 - 时间复杂度:O(n²),其中
n是数组nums的长度。
18.1. 动态规划
class Solution {
public int lengthOfLIS(int[] nums) {
// 处理空数组特殊情况
if (nums.length == 0) {
return 0;
}
// dp数组:dp[i]表示以nums[i]结尾的LIS长度
int[] dp = new int[nums.length];
// 初始状态:第一个元素自身构成长度为1的子序列
dp[0] = 1;
// 记录全局最大值
int maxans = 1;
// 遍历数组中的每个元素
for (int i = 1; i < nums.length; i++) {
// 初始化当前元素的LIS长度为1(至少包含自己)
dp[i] = 1;
// 检查前面所有元素
for (int j = 0; j < i; j++) {
// 如果当前元素大于前面的某个元素
if (nums[i] > nums[j]) {
// 更新dp[i]为较大值(保持当前值或取dp[j]+1)
dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
// 更新全局最大值
maxans = Math.max(maxans, dp[i]);
}
// 返回最终结果
return maxans;
}
}
18.2. 贪心+二分查找
class Solution {
public int lengthOfLIS(int[] nums) {
int len = 1, n = nums.length;
if (n == 0) {
return 0;
}
int[] d = new int[n + 1];
d[len] = nums[0];
for (int i = 1; i < n; ++i) {
if (nums[i] > d[len]) {
d[++len] = nums[i];
} else {
int l = 1, r = len, pos = 0; // 如果找不到说明所有的数都比 nums[i] 大,此时要更新 d[1],所以这里将 pos 设为 0
while (l <= r) {
int mid = (l + r) >> 1;
if (d[mid] < nums[i]) {
pos = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
d[pos + 1] = nums[i];
}
}
return len;
}
}
19. 合并K个升序链表

解题思路:
- 两两合并:使用
mergeTwoLists方法合并两个有序链表 - 顺序合并:将链表数组中的链表依次与当前结果合并
- 边界处理:处理空链表的情况
- 时间复杂度:O(k²n),其中
k是链表个数,n是平均链表长度。
19.1. 解答
class Solution {
// 主方法:合并K个有序链表
public ListNode mergeKLists(ListNode[] lists) {
// 初始化结果链表为空
ListNode ans = null;
// 遍历链表数组,逐个合并
for (int i = 0; i < lists.length; ++i) {
ans = mergeTwoLists(ans, lists[i]);
}
// 返回最终合并结果
return ans;
}
// 辅助方法:合并两个有序链表
public ListNode mergeTwoLists(ListNode a, ListNode b) {
// 处理空链表情况
if (a == null || b == null) {
return a != null ? a : b; // 返回非空的链表
}
// 创建哑节点作为合并后链表的头部
ListNode head = new ListNode(0);
// tail指向当前合并链表的尾部
// aPtr和bPtr分别指向两个链表的当前节点
ListNode tail = head, aPtr = a, bPtr = b;
// 遍历两个链表,比较节点值
while (aPtr != null && bPtr != null) {
if (aPtr.val < bPtr.val) {
// 将a的当前节点接入合并链表
tail.next = aPtr;
aPtr = aPtr.next;
} else {
// 将b的当前节点接入合并链表
tail.next = bPtr;
bPtr = bPtr.next;
}
// 移动tail指针
tail = tail.next;
}
// 将剩余部分直接接入合并链表
tail.next = (aPtr != null ? aPtr : bPtr);
// 返回哑节点的下一个节点(真正的头节点)
return head.next;
}
}
20. 合并区间

解题思路:
- 检查空输入:如果输入的区间数组为空,直接返回一个空的二维数组。
- 排序区间:根据区间的起始值对所有区间进行升序排序,以便后续合并操作。
- 合并区间:遍历排序后的区间,逐个检查当前区间是否可以与已合并的最后一个区间合并。如果可以合并,则更新合并后的区间的结束值;如果不能合并,则将当前区间添加到合并结果中。
- 返回结果:将合并后的结果从列表转换为二维数组并返回。
- 时间复杂度:
- 排序:使用快速排序,时间复杂度为 O(n log n),其中 n 是区间的数量。
- 合并:遍历所有区间一次,时间复杂度为 O(n)。
- 总时间复杂度:O(n log n) + O(n) = O(n log n)。
20.1. 解答
class Solution {
public int[][] merge(int[][] intervals) {
// 如果输入为空,直接返回空的二维数组
if (intervals.length == 0) {
return new int[0][2];
}
// 根据区间的起始值进行升序排序
Arrays.sort(intervals, new Comparator<int[]>() {
public int compare(int[] interval1, int[] interval2) {
return interval1[0] - interval2[0];
}
});
// 使用列表存储合并后的区间
List<int[]> merged = new ArrayList<int[]>();
// 遍历所有区间
for (int i = 0; i < intervals.length; ++i) {
int L = intervals[i][0], R = intervals[i][1];
// 如果列表为空或当前区间与已合并的最后一个区间不重叠,直接添加
if (merged.size() == 0 || merged.get(merged.size() - 1)[1] < L) {
merged.add(new int[]{L, R});
}
// 否则,合并区间,更新结束值为较大的那个
else {
merged.get(merged.size() - 1)[1] = Math.max(merged.get(merged.size() - 1)[1], R);
}
}
// 将列表转换为二维数组并返回
return merged.toArray(new int[merged.size()][]);
}
}
21. 相交链表

解题思路:
该代码使用哈希集合来存储链表A的所有节点,然后遍历链表B,检查链表B的节点是否存在于哈希集合中。如果存在,则该节点就是两个链表的相交起始节点;如果遍历完链表B都没有找到相交节点,则返回null。
时间复杂度:
- 遍历链表A的时间复杂度为O(m),其中m是链表A的长度。
- 遍历链表B的时间复杂度为O(n),其中n是链表B的长度。
- 哈希集合的插入和查询操作的平均时间复杂度为O(1)。
- 因此,总的时间复杂度为O(m + n)。
21.1. 哈希集合
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
// 创建一个哈希集合,用于存储链表A的节点
Set<ListNode> visited = new HashSet<ListNode>();
// 遍历链表A,将所有节点加入哈希集合
ListNode temp = headA;
while (temp != null) {
visited.add(temp); // 将当前节点加入集合
temp = temp.next; // 移动到下一个节点
}
// 遍历链表B,检查节点是否存在于哈希集合中
temp = headB;
while (temp != null) {
// 如果当前节点在集合中,说明是相交节点
if (visited.contains(temp)) {
return temp; // 返回该节点
}
temp = temp.next; // 移动到下一个节点
}
// 如果遍历完链表B都没有找到相交节点,返回null
return null;
}
}
21.2. 双指针
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
22. 接雨水

解题思路:
该代码使用双指针法来计算接雨水的总量。通过维护左右指针 left 和 right,以及左右两侧的最大高度 leftMax 和 rightMax,动态计算每个位置能接的雨水量。具体来说,如果 height[left] < height[right],则处理左指针,否则处理右指针。每次移动指针时,更新当前侧的最大高度,并计算当前位置能接的雨水量。
时间复杂度:只需遍历数组一次,因此时间复杂度为 O(n),其中 n 是数组的长度。
22.1. 解答
class Solution {
public int trap(int[] height) {
int ans = 0; // 初始化接雨水的总量
int left = 0, right = height.length - 1; // 初始化左右指针
int leftMax = 0, rightMax = 0; // 初始化左右两侧的最大高度
while (left < right) { // 当左指针小于右指针时循环
leftMax = Math.max(leftMax, height[left]); // 更新左侧最大高度
rightMax = Math.max(rightMax, height[right]); // 更新右侧最大高度
if (height[left] < height[right]) { // 如果左侧高度小于右侧高度
// 计算左侧当前位置能接的雨水量
ans += leftMax - height[left];
++left; // 左指针右移
} else { // 否则(右侧高度小于等于左侧高度)
// 计算右侧当前位置能接的雨水量
ans += rightMax - height[right];
--right; // 右指针左移
}
}
return ans; // 返回接雨水的总量
}
}
23. 编辑距离

解题思路:
该代码使用动态规划(DP)来解决编辑距离问题。通过构建一个二维数组 D,其中 D[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。初始化边界条件后,填充 DP 数组,根据字符是否相等来决定替换操作的代价。
时间复杂度:
- 需要填充一个大小为
(n+1) × (m+1)的 DP 数组,其中n和m分别是word1和word2的长度。 - 因此,时间复杂度为 O(n × m)。
23.1. 解答
class Solution {
public int minDistance(String word1, String word2) {
int n = word1.length();
int m = word2.length();
// 如果其中一个字符串为空,直接返回另一个字符串的长度(全插入或全删除)
if (n * m == 0) {
return n + m;
}
// DP 数组,D[i][j] 表示 word1 前 i 个字符转换为 word2 前 j 个字符的最小操作数
int[][] D = new int[n + 1][m + 1];
// 初始化边界条件:当 word2 为空时,需要删除 word1 的所有字符
for (int i = 0; i < n + 1; i++) {
D[i][0] = i;
}
// 初始化边界条件:当 word1 为空时,需要插入 word2 的所有字符
for (int j = 0; j < m + 1; j++) {
D[0][j] = j;
}
// 填充 DP 数组
for (int i = 1; i < n + 1; i++) {
for (int j = 1; j < m + 1; j++) {
int left = D[i - 1][j] + 1; // 从 word1 删除一个字符(操作数 +1)
int down = D[i][j - 1] + 1; // 向 word1 插入一个字符(操作数 +1)
int left_down = D[i - 1][j - 1]; // 替换或匹配操作
if (word1.charAt(i - 1) != word2.charAt(j - 1)) {
left_down += 1; // 字符不相等时,替换操作数 +1
}
// 取三种操作的最小值
D[i][j] = Math.min(left, Math.min(down, left_down));
}
}
return D[n][m]; // 返回最终结果
}
}
24. 二叉树中的最大路径和

解题思路:
该代码使用递归方法计算二叉树中的最大路径和。对于每个节点,计算其左右子树的最大贡献值(如果贡献值为负则忽略),然后计算以当前节点为根的最大路径和(当前节点值加上左右子树的最大贡献值),并更新全局最大路径和。最后返回当前节点的最大贡献值(当前节点值加上左右子树中较大的贡献值)。
时间复杂度:每个节点仅被访问一次,因此时间复杂度为 O(n),其中 n 是二叉树的节点数。
24.1. 解答
class Solution {
int maxSum = Integer.MIN_VALUE; // 初始化全局最大路径和为最小整数值
public int maxPathSum(TreeNode root) {
maxGain(root); // 调用递归函数计算最大路径和
return maxSum; // 返回全局最大路径和
}
public int maxGain(TreeNode node) {
if (node == null) {
return 0; // 空节点的贡献值为0
}
// 递归计算左右子节点的最大贡献值,若为负则取0(不选该子树)
int leftGain = Math.max(maxGain(node.left), 0);
int rightGain = Math.max(maxGain(node.right), 0);
// 当前节点作为根的最大路径和(左+右+当前节点值)
int priceNewpath = node.val + leftGain + rightGain;
// 更新全局最大路径和
maxSum = Math.max(maxSum, priceNewpath);
// 返回当前节点的最大贡献值(当前节点值+较大的子树贡献值)
return node.val + Math.max(leftGain, rightGain);
}
}
25. 环形链表Ⅱ

解题思路:
该代码使用哈希集合来检测链表中是否存在环,并返回环的起始节点。通过遍历链表,将每个访问过的节点加入集合中。如果在遍历过程中遇到已经存在于集合中的节点,则该节点即为环的起始节点;如果遍历结束仍未发现重复节点,则链表无环,返回 null。
时间复杂度:
- 遍历链表的时间复杂度为 O(n),其中
n是链表的节点数。 - 哈希集合的插入和查询操作的平均时间复杂度为 O(1)。
- 因此,总的时间复杂度为 O(n)。
25.1. 解答
public class Solution {
public ListNode detectCycle(ListNode head) {
ListNode pos = head; // 初始化指针 pos 指向链表头节点
Set<ListNode> visited = new HashSet<ListNode>(); // 创建哈希集合存储已访问的节点
while (pos != null) { // 遍历链表直到 pos 为 null
if (visited.contains(pos)) { // 如果当前节点已存在于集合中
return pos; // 返回该节点(环的起始节点)
} else {
visited.add(pos); // 否则将当前节点加入集合
}
pos = pos.next; // 移动指针到下一个节点
}
return null; // 遍历结束未发现环,返回 null
}
}
26. 删除链表的倒数第N个结点

解题思路:
该代码通过两次遍历链表来删除倒数第 nn 个节点。第一次遍历计算链表的长度,第二次遍历找到倒数第 nn 个节点的前驱节点,然后修改指针跳过该节点,实现删除操作。使用虚拟头节点 dummy 简化边界条件处理(如删除头节点的情况)。
时间复杂度:
- 两次遍历链表的时间复杂度均为 O(L),其中 LL 是链表的长度。
- 因此,总的时间复杂度为 O(L)。
26.1. 解答
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode dummy = new ListNode(0, head); // 创建虚拟头节点,简化边界条件处理
int length = getLength(head); // 获取链表长度
ListNode cur = dummy; // 初始化当前指针指向虚拟头节点
// 移动指针到倒数第 n 个节点的前驱节点
for (int i = 1; i < length - n + 1; ++i) {
cur = cur.next;
}
cur.next = cur.next.next; // 删除倒数第 n 个节点
ListNode ans = dummy.next; // 获取新链表的头节点
return ans; // 返回结果
}
// 辅助函数:计算链表长度
public int getLength(ListNode head) {
int length = 0;
while (head != null) {
++length;
head = head.next;
}
return length;
}
}
27. 寻找两个正序数组中的中位数

解题思路:
该代码使用二分查找的思想来高效地找到两个有序数组的中位数。通过比较两个数组的中间元素,逐步缩小搜索范围,最终定位到中位数的位置。主要分为两个步骤:处理奇数长度和偶数长度的情况,以及实现查找第k小元素的辅助函数。
时间复杂度:
- 每次递归或迭代都将k的值减少一半,因此时间复杂度为 O(log(m+n)),其中m和n分别是两个数组的长度。
27.1. 解答
class Solution {
public double findMedianSortedArrays(int[] nums1, int[] nums2) {
int length1 = nums1.length, length2 = nums2.length;
int totalLength = length1 + length2;
if (totalLength % 2 == 1) { // 如果总长度为奇数,直接返回中间的那个数
int midIndex = totalLength / 2;
double median = getKthElement(nums1, nums2, midIndex + 1); // 查找第k小的元素
return median;
} else { // 如果总长度为偶数,返回中间两个数的平均值
int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
double median = (getKthElement(nums1, nums2, midIndex1 + 1) + getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
return median;
}
}
// 辅助函数:找到两个有序数组中第k小的元素
public int getKthElement(int[] nums1, int[] nums2, int k) {
int length1 = nums1.length, length2 = nums2.length;
int index1 = 0, index2 = 0; // 初始化两个数组的起始索引
int kthElement = 0; // 存储第k小的元素
while (true) {
// 边界情况处理
if (index1 == length1) { // nums1已经全部排除,直接从nums2中取第k小的元素
return nums2[index2 + k - 1];
}
if (index2 == length2) { // nums2已经全部排除,直接从nums1中取第k小的元素
return nums1[index1 + k - 1];
}
if (k == 1) { // 当k=1时,直接比较当前两个数组的最小元素
return Math.min(nums1[index1], nums2[index2]);
}
// 正常情况:比较两个数组的中间元素
int half = k / 2;
int newIndex1 = Math.min(index1 + half, length1) - 1; // 防止数组越界
int newIndex2 = Math.min(index2 + half, length2) - 1;
int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
if (pivot1 <= pivot2) { // 排除nums1中较小的部分
k -= (newIndex1 - index1 + 1); // 更新k的值
index1 = newIndex1 + 1; // 移动nums1的起始索引
} else { // 排除nums2中较小的部分
k -= (newIndex2 - index2 + 1); // 更新k的值
index2 = newIndex2 + 1; // 移动nums2的起始索引
}
}
}
}
28. 二叉树的中序遍历

解题思路:
该代码使用递归方法实现二叉树的中序遍历。中序遍历的顺序是:左子树 → 根节点 → 右子树。通过递归访问左子树,将当前节点的值加入结果列表,再递归访问右子树,最终完成整个二叉树的中序遍历。
时间复杂度:
- 每个节点被访问一次,因此时间复杂度为 O(n),其中
n是二叉树的节点数。
28.1. 解答
class Solution {
public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<Integer>(); // 初始化结果列表
inorder(root, res); // 调用递归函数进行中序遍历
return res; // 返回遍历结果
}
// 递归函数:中序遍历二叉树
public void inorder(TreeNode root, List<Integer> res) {
if (root == null) { // 如果当前节点为空,直接返回
return;
}
inorder(root.left, res); // 递归遍历左子树
res.add(root.val); // 将当前节点的值加入结果列表
inorder(root.right, res); // 递归遍历右子树
}
}
29. 括号生成

解题思路:
该代码使用回溯法生成所有有效的括号组合。通过递归地添加左括号和右括号,确保在任何时候右括号的数量不超过左括号的数量,并且左括号的数量不超过给定的对数 n。当生成的字符串长度达到 2n 时,将其加入结果列表。
时间复杂度:
- 每个有效的括号组合都需要被构造一次,时间复杂度为 O(4^n / sqrt(n)),这是第
n个卡特兰数的渐进行为。
29.1. 解答
class Solution {
public List<String> generateParenthesis(int n) {
List<String> ans = new ArrayList<String>(); // 存储所有有效的括号组合
backtrack(ans, new StringBuilder(), 0, 0, n); // 调用回溯函数生成括号组合
return ans; // 返回结果列表
}
public void backtrack(List<String> ans, StringBuilder cur, int open, int close, int max) {
if (cur.length() == max * 2) { // 当前字符串长度达到 2n,说明找到一个有效组合
ans.add(cur.toString()); // 将当前组合加入结果列表
return;
}
if (open < max) { // 如果左括号数量未达到 n,可以添加左括号
cur.append('('); // 添加左括号
backtrack(ans, cur, open + 1, close, max); // 递归处理下一层
cur.deleteCharAt(cur.length() - 1); // 回溯,移除最后一个字符
}
if (close < open) { // 如果右括号数量小于左括号数量,可以添加右括号
cur.append(')'); // 添加右括号
backtrack(ans, cur, open, close + 1, max); // 递归处理下一层
cur.deleteCharAt(cur.length() - 1); // 回溯,移除最后一个字符
}
}
}
30. 排序链表

解题思路:
自顶向下归并排序
时间复杂度:O (log n)
30.1. 解答
class Solution {
public ListNode sortList(ListNode head) {
return sortList(head, null);
}
public ListNode sortList(ListNode head, ListNode tail) {
if (head == null) {
return head;
}
if (head.next == tail) {
head.next = null;
return head;
}
ListNode slow = head, fast = head;
while (fast != tail) {
slow = slow.next;
fast = fast.next;
if (fast != tail) {
fast = fast.next;
}
}
ListNode mid = slow;
ListNode list1 = sortList(head, mid);
ListNode list2 = sortList(mid, tail);
ListNode sorted = merge(list1, list2);
return sorted;
}
public ListNode merge(ListNode head1, ListNode head2) {
ListNode dummyHead = new ListNode(0);
ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
while (temp1 != null && temp2 != null) {
if (temp1.val <= temp2.val) {
temp.next = temp1;
temp1 = temp1.next;
} else {
temp.next = temp2;
temp2 = temp2.next;
}
temp = temp.next;
}
if (temp1 != null) {
temp.next = temp1;
} else if (temp2 != null) {
temp.next = temp2;
}
return dummyHead.next;
}
}
31. 下一个排列

解题思路:
- 从后向前查找:找到第一个满足
nums[i] < nums[i+1]的索引i。 - 交换元素:在
i的右侧找到第一个大于nums[i]的元素nums[j],并交换它们。 - 反转子数组:将
i+1到数组末尾的子数组反转,以确保这一部分是最小的字典序排列。
时间复杂度:
- 最多需要两次线性扫描(查找
i和j)和一次部分反转操作,因此时间复杂度为 O(n),其中n是数组的长度。
31.1. 解答
class Solution {
public void nextPermutation(int[] nums) {
// 从后向前查找第一个满足 nums[i] < nums[i+1] 的索引 i
int i = nums.length - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
// 如果找到这样的 i,则在右侧找到第一个大于 nums[i] 的元素并交换
if (i >= 0) {
int j = nums.length - 1;
while (j >= 0 && nums[i] >= nums[j]) {
j--;
}
swap(nums, i, j); // 交换 nums[i] 和 nums[j]
}
// 反转 i+1 到末尾的子数组,确保这部分是最小字典序
reverse(nums, i + 1);
}
// 交换数组中两个元素的位置
public void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
// 反转数组中从 start 到末尾的部分
public void reverse(int[] nums, int start) {
int left = start, right = nums.length - 1;
while (left < right) {
swap(nums, left, right);
left++;
right--;
}
}
}
32. 滑动窗口最大值

解题思路:
该代码使用优先队列(最大堆)来维护滑动窗口中的最大值。通过将元素及其索引存入优先队列,确保堆顶始终是当前窗口的最大值。在滑动窗口移动时,移除超出窗口范围的元素,保持堆顶的有效性。
32.1. 解答
class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
// 创建优先队列,按元素值降序排序,值相同时按索引降序排序
PriorityQueue<int[]> pq = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] pair1, int[] pair2) {
return pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1];
}
});
// 初始化第一个窗口
for (int i = 0; i < k; ++i) {
pq.offer(new int[]{nums[i], i}); // 存入元素值和索引
}
int[] ans = new int[n - k + 1]; // 存储结果
ans[0] = pq.peek()[0]; // 第一个窗口的最大值
// 滑动窗口
for (int i = k; i < n; ++i) {
pq.offer(new int[]{nums[i], i}); // 添加新元素
// 移除超出窗口范围的堆顶元素
while (pq.peek()[1] <= i - k) {
pq.poll();
}
ans[i - k + 1] = pq.peek()[0]; // 当前窗口的最大值
}
return ans;
}
}
33. 两数相加

解题思路:
该代码用于将两个逆序存储的非负整数链表相加,返回表示和的链表。通过同时遍历两个链表,逐位相加并处理进位,生成新的链表节点。如果遍历结束后仍有进位,则额外创建一个节点存储进位值。
时间复杂度:O(max(m,n)),其中 m 和 n 分别为两个链表的长度
33.1. 解答
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = null, tail = null; // 初始化结果链表的头和尾
int carry = 0; // 初始化进位值为0
// 遍历两个链表,直到两个链表都为空
while (l1 != null || l2 != null) {
// 获取当前节点的值,如果节点为空则取0
int n1 = l1 != null ? l1.val : 0;
int n2 = l2 != null ? l2.val : 0;
// 计算当前位的和(包括进位)
int sum = n1 + n2 + carry;
// 创建新节点存储当前位的值(sum % 10)
if (head == null) {
head = tail = new ListNode(sum % 10); // 如果是第一个节点,初始化头和尾
} else {
tail.next = new ListNode(sum % 10); // 否则添加到尾部
tail = tail.next;
}
carry = sum / 10; // 更新进位值
// 移动到下一个节点
if (l1 != null) {
l1 = l1.next;
}
if (l2 != null) {
l2 = l2.next;
}
}
// 如果最后还有进位,添加一个新节点
if (carry > 0) {
tail.next = new ListNode(carry);
}
return head; // 返回结果链表的头节点
}
}
34. 最长有效括号

解题思路:
该代码使用动态规划(DP)来求解最长有效括号子串的长度。通过维护一个 dp 数组,其中 dp[i] 表示以字符 s[i] 结尾的最长有效括号子串的长度。根据当前字符是否为右括号 ')' 以及前一个字符的情况,更新 dp 数组的值,并记录全局最大值 maxans。
时间复杂度: O(n),其中 n 为字符串的长度。
34.1. 解答
class Solution {
public int longestValidParentheses(String s) {
int maxans = 0; // 全局最大值,记录最长有效括号子串的长度
int[] dp = new int[s.length()]; // dp数组,dp[i]表示以s[i]结尾的最长有效括号子串的长度
for (int i = 1; i < s.length(); i++) { // 从第二个字符开始遍历
if (s.charAt(i) == ')') { // 当前字符是右括号时才可能形成有效括号
if (s.charAt(i - 1) == '(') { // 情况1:前一个字符是左括号,形如 "()"
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2; // 更新dp[i],加上前一个有效子串的长度(如果存在)
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
// 情况2:前一个字符是右括号,且前面有匹配的左括号,形如 "(())"
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
// 更新dp[i],加上前一个有效子串的长度和更前面的有效子串的长度(如果存在)
}
maxans = Math.max(maxans, dp[i]); // 更新全局最大值
}
}
return maxans; // 返回最长有效括号子串的长度
}
}
35. 爬楼梯

解题思路:
该代码使用动态规划(DP)来计算爬楼梯的不同方法数。通过维护三个变量 p、q 和 r,分别表示前两步、前一步和当前步的方法数。每次迭代更新这三个变量,最终得到爬到第 n 阶楼梯的方法数。
时间复杂度:循环执行 n 次,每次花费常数的时间代价,故渐进时间复杂度为 O(n)。
35.1. 解答
class Solution {
public int climbStairs(int n) {
// 初始化变量:p表示前两步的方法数,q表示前一步的方法数,r表示当前步的方法数
int p = 0, q = 0, r = 1;
for (int i = 1; i <= n; ++i) { // 从第1阶遍历到第n阶
p = q; // 更新前两步的方法数为前一步的方法数
q = r; // 更新前一步的方法数为当前步的方法数
// 当前步的方法数等于前两步和前一步方法数之和(因为每次可以爬1或2阶)
r = p + q;
}
return r; // 返回爬到第n阶的方法数
}
}
36. 零钱兑换

解题思路:
该代码使用动态规划(DP)和记忆化搜索来计算凑成指定金额所需的最少硬币数。通过递归分解问题,并使用数组 count 存储中间结果以避免重复计算,从而高效地找到最优解。
时间复杂度:O(Sn),其中 S 是金额,n 是面额数。
36.1. 记忆化搜索
public class Solution {
public int coinChange(int[] coins, int amount) {
if (amount < 1) { // 处理金额为0或负数的特殊情况
return 0;
}
return coinChange(coins, amount, new int[amount]); // 调用辅助方法,传入硬币数组、剩余金额和记忆化数组
}
private int coinChange(int[] coins, int rem, int[] count) {
if (rem < 0) { // 剩余金额为负,无法凑出,返回-1
return -1;
}
if (rem == 0) { // 剩余金额为0,表示已凑出,返回0个硬币
return 0;
}
if (count[rem - 1] != 0) { // 如果当前剩余金额的结果已计算过,直接返回
return count[rem - 1];
}
int min = Integer.MAX_VALUE; // 初始化最小硬币数为最大值
for (int coin : coins) { // 遍历每种硬币
int res = coinChange(coins, rem - coin, count); // 递归计算剩余金额所需硬币数
if (res >= 0 && res < min) { // 如果结果有效且更优,更新最小硬币数
min = 1 + res; // 当前硬币数(1)加上剩余金额所需硬币数
}
}
count[rem - 1] = (min == Integer.MAX_VALUE) ? -1 : min; // 存储结果到记忆化数组
return count[rem - 1]; // 返回当前剩余金额的最小硬币数
}
}
36.2. 动态规划
public class Solution {
public int coinChange(int[] coins, int amount) {
int max = amount + 1;
int[] dp = new int[amount + 1];
Arrays.fill(dp, max);
dp[0] = 0;
for (int i = 1; i <= amount; i++) {
for (int j = 0; j < coins.length; j++) {
if (coins[j] <= i) {
dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
37. 最小覆盖字串

解题思路:
该代码使用滑动窗口技术来寻找字符串 s 中包含字符串 t 所有字符的最小子串。通过维护两个哈希表 ori 和 cnt 分别记录 t 中字符的频率和当前窗口中字符的频率,动态调整窗口的左右边界以找到满足条件的最小窗口。
时间复杂度:最坏情况下左右指针对 s 的每个元素各遍历一遍,哈希表中对 s 中的每个元素各插入、删除一次,对 t 中的元素各插入一次。每次检查是否可行会遍历整个 t 的哈希表,哈希表的大小与字符集的大小有关,设字符集大小为 C,则渐进时间复杂度为 O(C⋅∣s∣+∣t∣)。
37.1. 解答
class Solution {
Map<Character, Integer> ori = new HashMap<Character, Integer>(); // 存储t中字符的频率
Map<Character, Integer> cnt = new HashMap<Character, Integer>(); // 存储当前窗口中字符的频率
public String minWindow(String s, String t) {
int tLen = t.length();
// 初始化ori,统计t中每个字符的出现次数
for (int i = 0; i < tLen; i++) {
char c = t.charAt(i);
ori.put(c, ori.getOrDefault(c, 0) + 1);
}
int l = 0, r = -1; // 滑动窗口的左右指针
int len = Integer.MAX_VALUE, ansL = -1, ansR = -1; // 记录最小窗口的长度和位置
int sLen = s.length();
while (r < sLen) {
++r; // 右指针右移
// 如果当前字符在t中,更新cnt中的频率
if (r < sLen && ori.containsKey(s.charAt(r))) {
cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);
}
// 检查当前窗口是否包含t的所有字符
while (check() && l <= r) {
// 更新最小窗口
if (r - l + 1 < len) {
len = r - l + 1;
ansL = l;
ansR = l + len;
}
// 左指针右移,缩小窗口
if (ori.containsKey(s.charAt(l))) {
cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
}
++l;
}
}
// 返回结果,若未找到则返回空字符串
return ansL == -1 ? "" : s.substring(ansL, ansR);
}
// 检查当前窗口是否包含t的所有字符
public boolean check() {
Iterator iter = ori.entrySet().iterator();
while (iter.hasNext()) {
Map.Entry entry = (Map.Entry) iter.next();
Character key = (Character) entry.getKey();
Integer val = (Integer) entry.getValue();
// 如果当前窗口中该字符的数量不足,返回false
if (cnt.getOrDefault(key, 0) < val) {
return false;
}
}
return true;
}
}
38. 从前序与中序遍历序列构造二叉树

解题思路:
该代码通过递归的方式,利用先序遍历和中序遍历的特点来重建二叉树。先序遍历的第一个元素是根节点,中序遍历中根节点左侧是左子树,右侧是右子树。通过哈希表快速定位中序遍历中根节点的位置,从而确定左右子树的边界,递归构建左右子树。
时间复杂度:O(n),其中 n 是树中的节点个数。
38.1. 递归
class Solution {
private Map<Integer, Integer> indexMap; // 用于存储中序遍历的值与索引的映射
public TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left, int inorder_right) {
if (preorder_left > preorder_right) { // 递归终止条件:当前子树区间为空
return null;
}
// 前序遍历的第一个节点是当前子树的根节点
int preorder_root = preorder_left;
// 在中序遍历中找到根节点的位置
int inorder_root = indexMap.get(preorder[preorder_root]);
// 创建当前根节点
TreeNode root = new TreeNode(preorder[preorder_root]);
// 计算左子树的节点数量
int size_left_subtree = inorder_root - inorder_left;
// 递归构建左子树
root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left, inorder_root - 1);
// 递归构建右子树
root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right, inorder_root + 1, inorder_right);
return root; // 返回当前根节点
}
public TreeNode buildTree(int[] preorder, int[] inorder) {
int n = preorder.length;
// 初始化哈希表,存储中序遍历的值与索引的对应关系
indexMap = new HashMap<Integer, Integer>();
for (int i = 0; i < n; i++) {
indexMap.put(inorder[i], i);
}
// 调用递归函数构建二叉树
return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
}
}
38.2. 迭代
class Solution {
public TreeNode buildTree(int[] preorder, int[] inorder) {
if (preorder == null || preorder.length == 0) {
return null;
}
TreeNode root = new TreeNode(preorder[0]);
Deque<TreeNode> stack = new LinkedList<TreeNode>();
stack.push(root);
int inorderIndex = 0;
for (int i = 1; i < preorder.length; i++) {
int preorderVal = preorder[i];
TreeNode node = stack.peek();
if (node.val != inorder[inorderIndex]) {
node.left = new TreeNode(preorderVal);
stack.push(node.left);
} else {
while (!stack.isEmpty() && stack.peek().val == inorder[inorderIndex]) {
node = stack.pop();
inorderIndex++;
}
node.right = new TreeNode(preorderVal);
stack.push(node.right);
}
}
return root;
}
}
39. 子集

解题思路:
该代码使用位运算的方法生成数组的所有子集。通过遍历所有可能的位掩码(从 0 到 2^n - 1),每个掩码对应一个子集,其中掩码的每一位表示是否包含数组中对应位置的元素。
时间复杂度:O(n×2n)。一共 2n 个状态,每种状态需要 O(n) 的时间来构造子集。
39.1. 解答
class Solution {
List<Integer> t = new ArrayList<Integer>(); // 临时存储当前子集
List<List<Integer>> ans = new ArrayList<List<Integer>>(); // 存储所有子集的结果列表
public List<List<Integer>> subsets(int[] nums) {
int n = nums.length; // 数组长度
for (int mask = 0; mask < (1 << n); ++mask) { // 遍历所有可能的掩码(0 到 2^n - 1)
t.clear(); // 清空临时列表
for (int i = 0; i < n; ++i) { // 检查掩码的每一位
if ((mask & (1 << i)) != 0) { // 如果第i位为1,表示包含nums[i]
t.add(nums[i]); // 将nums[i]添加到当前子集
}
}
ans.add(new ArrayList<Integer>(t)); // 将当前子集添加到结果列表
}
return ans; // 返回所有子集
}
}
40. 最小栈

解题思路:
该代码使用双栈结构实现最小栈功能。xStack 存储所有元素,minStack 同步存储当前栈中的最小值。通过维护这两个栈,确保在常数时间内获取最小元素。
时间复杂度:对于题目中的所有操作,时间复杂度均为 O(1)。因为栈的插入、删除与读取操作都是 O(1),我们定义的每个操作最多调用栈操作两次。
40.1. 解答
class MinStack {
Deque<Integer> xStack; // 主栈,存储所有元素
Deque<Integer> minStack; // 辅助栈,栈顶始终存储当前最小值
public MinStack() {
xStack = new LinkedList<Integer>(); // 初始化主栈
minStack = new LinkedList<Integer>(); // 初始化辅助栈
minStack.push(Integer.MAX_VALUE); // 预先压入最大值,避免空栈判断
}
public void push(int x) {
xStack.push(x); // 元素压入主栈
minStack.push(Math.min(minStack.peek(), x)); // 当前最小值压入辅助栈
}
public void pop() {
xStack.pop(); // 主栈弹出栈顶
minStack.pop(); // 辅助栈同步弹出(维护最小值)
}
public int top() {
return xStack.peek(); // 返回主栈栈顶元素
}
public int getMin() {
return minStack.peek(); // 返回辅助栈栈顶(当前最小值)
}
}
41. 在排序数组中查找元素的第一个和最后一个位置

解题思路:
该代码使用二分查找算法来定位目标值在数组中的起始和结束位置。通过两次二分查找(分别查找左边界和右边界),确保在 O(log n) 时间复杂度内完成搜索。如果目标值不存在,返回 [-1, -1]。
时间复杂度: O(logn) ,其中 n 为数组的长度。二分查找的时间复杂度为 O(logn),一共会执行两次,因此总时间复杂度为 O(logn)。
41.1. 解答
class Solution {
public int[] searchRange(int[] nums, int target) {
// 查找左边界(第一个等于target的位置)
int leftIdx = binarySearch(nums, target, true);
// 查找右边界(最后一个等于target的位置的后一位,需要减1)
int rightIdx = binarySearch(nums, target, false) - 1;
// 检查边界有效性:左边界不超过右边界,且右边界不越界,且两个边界值确实等于target
if (leftIdx <= rightIdx && rightIdx < nums.length && nums[leftIdx] == target && nums[rightIdx] == target) {
return new int[]{leftIdx, rightIdx}; // 返回有效的起始和结束位置
}
return new int[]{-1, -1}; // 目标值不存在,返回[-1, -1]
}
// 二分查找辅助函数:查找左边界或右边界
public int binarySearch(int[] nums, int target, boolean lower) {
int left = 0, right = nums.length - 1, ans = nums.length; // 初始化左右指针和默认答案
while (left <= right) {
int mid = (left + right) / 2; // 计算中间位置
// 如果当前值大于target,或者(查找左边界且当前值等于target),则向左收缩
if (nums[mid] > target || (lower && nums[mid] >= target)) {
right = mid - 1;
ans = mid; // 更新可能的边界位置
} else {
left = mid + 1; // 否则向右收缩
}
}
return ans; // 返回边界位置
}
}
42. 对称二叉树

解题思路:
该代码使用递归方法检查二叉树是否轴对称。通过比较左右子树的镜像节点(左子树的左节点与右子树的右节点,左子树的右节点与右子树的左节点),确保每个对称位置的节点值相等且结构一致
时间复杂度:这里遍历了这棵树,渐进时间复杂度为 O(n)。
42.1. 递归
class Solution {
public boolean isSymmetric(TreeNode root) {
// 从根节点的左右子树开始检查
return check(root.left, root.right);
}
// 递归检查两个节点是否对称
public boolean check(TreeNode p, TreeNode q) {
// 两个节点都为空,对称
if (p == null && q == null) {
return true;
}
// 一个为空一个不为空,不对称
if (p == null || q == null) {
return false;
}
// 当前节点值相等,且左子树的左节点与右子树的右节点对称,左子树的右节点与右子树的左节点对称
return p.val == q.val && check(p.left, q.right) && check(p.right, q.left);
}
}
42.2. 迭代
class Solution {
public boolean isSymmetric(TreeNode root) {
return check(root, root);
}
public boolean check(TreeNode u, TreeNode v) {
Queue<TreeNode> q = new LinkedList<TreeNode>();
q.offer(u);
q.offer(v);
while (!q.isEmpty()) {
u = q.poll();
v = q.poll();
if (u == null && v == null) {
continue;
}
if ((u == null || v == null) || (u.val != v.val)) {
return false;
}
q.offer(u.left);
q.offer(v.right);
q.offer(u.right);
q.offer(v.left);
}
return true;
}
}
43. 字符串解码

解题思路:
该代码使用栈结构来处理嵌套的解码操作。通过遍历编码字符串,遇到数字、字母或括号时分别处理:数字入栈作为重复次数,字母或左括号直接入栈,右括号时出栈直到遇到左括号,将中间的字符串与重复次数结合生成解码后的字符串并入栈。最终栈中的字符串拼接即为解码结果。
时间复杂度:记解码后得出的字符串长度为 S,除了遍历一次原字符串 s,我们还需要将解码后的字符串中的每个字符都入栈,并最终拼接进答案中,故渐进时间复杂度为 O(S+∣s∣),即 O(S)
43.1. 栈操作
class Solution {
int ptr; // 字符串遍历指针
public String decodeString(String s) {
LinkedList<String> stk = new LinkedList<String>(); // 使用栈存储数字和字符串片段
ptr = 0;
while (ptr < s.length()) {
char cur = s.charAt(ptr);
if (Character.isDigit(cur)) {
// 获取连续数字并入栈(如 "12" -> 12)
String digits = getDigits(s);
stk.addLast(digits);
} else if (Character.isLetter(cur) || cur == '[') {
// 字母或左括号直接入栈,指针后移
stk.addLast(String.valueOf(s.charAt(ptr++)));
} else { // 遇到右括号
++ptr; // 跳过右括号
LinkedList<String> sub = new LinkedList<String>();
// 出栈直到遇到左括号,获取待重复的字符串片段
while (!"[".equals(stk.peekLast())) {
sub.addLast(stk.removeLast());
}
Collections.reverse(sub); // 反转顺序(因为出栈是逆序的)
stk.removeLast(); // 左括号出栈
// 获取重复次数
int repTime = Integer.parseInt(stk.removeLast());
StringBuffer t = new StringBuffer();
String o = getString(sub); // 拼接待重复的字符串
// 生成重复后的字符串
while (repTime-- > 0) {
t.append(o);
}
stk.addLast(t.toString()); // 结果入栈
}
}
return getString(stk); // 最终栈内拼接即为解码结果
}
// 提取连续数字
public String getDigits(String s) {
StringBuffer ret = new StringBuffer();
while (Character.isDigit(s.charAt(ptr))) {
ret.append(s.charAt(ptr++));
}
return ret.toString();
}
// 拼接字符串列表
public String getString(LinkedList<String> v) {
StringBuffer ret = new StringBuffer();
for (String s : v) {
ret.append(s);
}
return ret.toString();
}
}
43.2. 递归
class Solution {
String src;
int ptr;
public String decodeString(String s) {
src = s;
ptr = 0;
return getString();
}
public String getString() {
if (ptr == src.length() || src.charAt(ptr) == ']') {
// String -> EPS
return "";
}
char cur = src.charAt(ptr);
int repTime = 1;
String ret = "";
if (Character.isDigit(cur)) {
// String -> Digits [ String ] String
// 解析 Digits
repTime = getDigits();
// 过滤左括号
++ptr;
// 解析 String
String str = getString();
// 过滤右括号
++ptr;
// 构造字符串
while (repTime-- > 0) {
ret += str;
}
} else if (Character.isLetter(cur)) {
// String -> Char String
// 解析 Char
ret = String.valueOf(src.charAt(ptr++));
}
return ret + getString();
}
public int getDigits() {
int ret = 0;
while (ptr < src.length() && Character.isDigit(src.charAt(ptr))) {
ret = ret * 10 + src.charAt(ptr++) - '0';
}
return ret;
}
}
44. 二叉树最大深度

解题思路:
该代码使用递归方法计算二叉树的最大深度。通过分别计算左右子树的最大深度,取较大值并加1(当前节点深度),最终得到整棵树的最大深度。
时间复杂度:O(n),其中 n 为二叉树节点的个数。每个节点在递归中只被遍历一次。
44.1. 解答
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) { // 递归终止条件:空节点深度为0
return 0;
} else {
int leftHeight = maxDepth(root.left); // 递归计算左子树深度
int rightHeight = maxDepth(root.right); // 递归计算右子树深度
return Math.max(leftHeight, rightHeight) + 1; // 当前节点深度 = 较大子树深度 + 1
}
}
}
45. 组合总和

解题思路:
该代码使用回溯算法(DFS)来寻找所有可能的组合,使得它们的和等于目标值 target。通过递归遍历候选数组,每次可以选择跳过当前数字或重复选择当前数字(如果剩余目标值允许),直到目标值为0或遍历完所有候选数字。
时间复杂度:O(S),其中 S 为所有可行解的长度之和。
45.1. 解答
class Solution {
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> ans = new ArrayList<List<Integer>>(); // 存储所有符合条件的组合
List<Integer> combine = new ArrayList<Integer>(); // 当前组合
dfs(candidates, target, ans, combine, 0); // 从第0个候选数字开始回溯
return ans;
}
public void dfs(int[] candidates, int target, List<List<Integer>> ans, List<Integer> combine, int idx) {
if (idx == candidates.length) { // 终止条件:遍历完所有候选数字
return;
}
if (target == 0) { // 终止条件:找到和为target的组合
ans.add(new ArrayList<Integer>(combine)); // 添加到结果集
return;
}
// 选择1:跳过当前候选数字,直接处理下一个数字
dfs(candidates, target, ans, combine, idx + 1);
// 选择2:使用当前候选数字(如果剩余目标值允许)
if (target - candidates[idx] >= 0) {
combine.add(candidates[idx]); // 将当前数字加入组合
dfs(candidates, target - candidates[idx], ans, combine, idx); // 继续处理当前数字(可重复使用)
combine.remove(combine.size() - 1); // 回溯,移除当前数字
}
}
}
46. 最小路径和

解题思路:
该代码使用动态规划(DP)来求解网格中的最小路径和。通过构建一个与输入网格大小相同的 dp 数组,其中 dp[i][j] 表示从起点 (0,0) 到 (i,j) 的最小路径和。初始化第一行和第一列后,逐步填充 dp 数组,每个位置的值由其上方或左方的最小值加上当前网格值得到。
时间复杂度:O(mn),其中 m 和 n 分别是网格的行数和列数。
46.1. 解答
class Solution {
public int minPathSum(int[][] grid) {
// 处理空网格的情况
if (grid == null || grid.length == 0 || grid[0].length == 0) {
return 0;
}
int rows = grid.length, columns = grid[0].length;
int[][] dp = new int[rows][columns]; // 初始化dp数组
dp[0][0] = grid[0][0]; // 起点路径和即为网格值
// 初始化第一列:只能从上方下来
for (int i = 1; i < rows; i++) {
dp[i][0] = dp[i - 1][0] + grid[i][0];
}
// 初始化第一行:只能从左方过来
for (int j = 1; j < columns; j++) {
dp[0][j] = dp[0][j - 1] + grid[0][j];
}
// 填充剩余dp数组:取上方或左方的最小路径和加上当前网格值
for (int i = 1; i < rows; i++) {
for (int j = 1; j < columns; j++) {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j];
}
}
return dp[rows - 1][columns - 1]; // 返回右下角的最小路径和
}
}
47. 最长连续序列

解题思路:
该代码使用哈希集合来高效地查找数字是否存在,并通过遍历集合中的每个数字,检查其是否为某个连续序列的起点(即不存在比它小1的数字),然后向后扩展计算连续序列的长度。最终返回最长连续序列的长度。
时间复杂度:O(n),其中 n 为数组的长度。具体分析已在上面正文中给出。
47.1. 解答
class Solution {
public int longestConsecutive(int[] nums) {
// 将数组中的数字存入哈希集合,实现O(1)时间复杂度的查找
Set<Integer> num_set = new HashSet<Integer>();
for (int num : nums) {
num_set.add(num);
}
int longestStreak = 0; // 记录最长连续序列的长度
// 遍历哈希集合中的每个数字
for (int num : num_set) {
// 如果当前数字的前一个数字不在集合中,说明它是某个连续序列的起点
if (!num_set.contains(num - 1)) {
int currentNum = num; // 当前数字作为序列起点
int currentStreak = 1; // 当前连续序列的长度初始化为1
// 向后查找连续的数字,并增加当前序列长度
while (num_set.contains(currentNum + 1)) {
currentNum += 1;
currentStreak += 1;
}
// 更新最长连续序列的长度
longestStreak = Math.max(longestStreak, currentStreak);
}
}
return longestStreak; // 返回最长连续序列的长度
}
}
48. 旋转图像

解题思路:
该代码通过创建一个新的二维数组 matrix_new,按照顺时针旋转90度的规则将原矩阵 matrix 中的元素重新排列到新矩阵中,然后将新矩阵的值复制回原矩阵。虽然题目要求原地旋转,但此方法通过辅助数组实现了旋转效果。
时间复杂度:O(N2),其中 N 是 matrix 的边长。
48.1. 使用辅助数组
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length; // 获取矩阵的维度n
int[][] matrix_new = new int[n][n]; // 创建一个新的n×n矩阵用于存储旋转后的结果
// 遍历原矩阵,将元素按顺时针旋转90度的规则放入新矩阵
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
// 新矩阵的第j行第n-i-1列 = 原矩阵的第i行第j列
matrix_new[j][n - i - 1] = matrix[i][j];
}
}
// 将新矩阵的值复制回原矩阵
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
matrix[i][j] = matrix_new[i][j];
}
}
}
}
48.2. 原地旋转
class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
for (int i = 0; i < n / 2; ++i) {
for (int j = 0; j < (n + 1) / 2; ++j) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - j - 1][i];
matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
matrix[j][n - i - 1] = temp;
}
}
}
}
49. 搜索二维矩阵

解题思路:
我们直接遍历整个矩阵 matrix,判断 target 是否出现即可。
时间复杂度:O(mn)。
49.1. 直接查找
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
for (int[] row : matrix) {
for (int element : row) {
if (element == target) {
return true;
}
}
}
return false;
}
}
49.2. 二分查找
class Solution {
public boolean searchMatrix(int[][] matrix, int target) {
for (int[] row : matrix) {
int index = search(row, target);
if (index >= 0) {
return true;
}
}
return false;
}
public int search(int[] nums, int target) {
int low = 0, high = nums.length - 1;
while (low <= high) {
int mid = (high - low) / 2 + low;
int num = nums[mid];
if (num == target) {
return mid;
} else if (num > target) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
}
50. 验证二叉搜索树

解题思路:
该代码使用递归方法验证二叉树是否为有效的二叉搜索树(BST)。通过为每个节点设置上下界(lower 和 upper),确保节点的值在允许范围内,并递归检查其左右子树是否满足BST的性质。
时间复杂度:O(n),其中 n 为二叉树的节点个数。在递归调用的时候二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
50.1. 递归
class Solution {
public boolean isValidBST(TreeNode root) {
// 初始调用,上下界设置为Long的最小值和最大值
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}
public boolean isValidBST(TreeNode node, long lower, long upper) {
if (node == null) { // 空节点是有效的BST
return true;
}
// 当前节点值必须在(lower, upper)范围内
if (node.val <= lower || node.val >= upper) {
return false;
}
// 递归检查左子树(上界更新为当前节点值)和右子树(下界更新为当前节点值)
return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);
}
}
50.2. 中序遍历
class Solution {
public boolean isValidBST(TreeNode root) {
Deque<TreeNode> stack = new LinkedList<TreeNode>();
double inorder = -Double.MAX_VALUE;
while (!stack.isEmpty() || root != null) {
while (root != null) {
stack.push(root);
root = root.left;
}
root = stack.pop();
// 如果中序遍历得到的节点的值小于等于前一个 inorder,说明不是二叉搜索树
if (root.val <= inorder) {
return false;
}
inorder = root.val;
root = root.right;
}
return true;
}
}
51. 二叉树的直径

解题思路:
该代码通过递归计算二叉树的深度,并在递归过程中更新直径的最大值。直径的定义是任意两个节点间最长路径的边数,可以通过计算每个节点的左右子树深度之和来得到当前节点的直径。
时间复杂度:O(N),其中 N 为二叉树的节点数,即遍历一棵二叉树的时间复杂度,每个结点只被访问一次。
51.1. 解答
class Solution {
int ans; // 全局变量,用于记录最大直径(初始时设为1,因为直径是边数,而ans计算的是节点数)
public int diameterOfBinaryTree(TreeNode root) {
ans = 1; // 初始化ans为1(至少有一个节点时直径为0,但ans计算的是节点数)
depth(root); // 调用递归函数计算深度并更新ans
return ans - 1; // 直径 = 节点数 - 1(边数)
}
public int depth(TreeNode node) {
if (node == null) {
return 0; // 空节点的深度为0
}
int L = depth(node.left); // 递归计算左子树的深度
int R = depth(node.right); // 递归计算右子树的深度
ans = Math.max(ans, L + R + 1); // 更新最大直径(当前节点的直径 = L + R + 1)
return Math.max(L, R) + 1; // 返回当前节点的深度(左右子树深度的最大值 + 1)
}
}
52. 最大正方形

解题思路:
该代码使用动态规划(DP)来寻找二维矩阵中由 '1' 组成的最大正方形的面积。通过构建一个与输入矩阵大小相同的 dp 数组,其中 dp[i][j] 表示以 (i,j) 为右下角的最大正方形的边长。初始化边界条件后,逐步填充 dp 数组,每个位置的值由其上方、左方和左上方的值决定,最终返回最大边长的平方作为面积。
时间复杂度:O(mn),其中 m 和 n 是矩阵的行数和列数。需要遍历原始矩阵中的每个元素计算 dp 的值。
52.1. 解答
class Solution {
public int maximalSquare(char[][] matrix) {
int maxSide = 0; // 初始化最大边长为0
// 处理矩阵为空或行列数为0的情况
if (matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return maxSide;
}
int rows = matrix.length, columns = matrix[0].length;
int[][] dp = new int[rows][columns]; // 初始化dp数组
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (matrix[i][j] == '1') { // 当前元素为'1'时才处理
if (i == 0 || j == 0) { // 边界条件:第一行或第一列
dp[i][j] = 1; // 边长至少为1
} else {
// 状态转移:取左、上、左上三个方向的最小值加1
dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
}
maxSide = Math.max(maxSide, dp[i][j]); // 更新最大边长
}
}
}
int maxSquare = maxSide * maxSide; // 计算最大面积
return maxSquare; // 返回结果
}
}
53. 回文链表

解题思路:
该代码通过将链表的值复制到数组中,然后使用双指针法从数组的两端向中间遍历,比较对应位置的元素是否相等,从而判断链表是否为回文结构。
时间复杂度:O(n),其中 n 指的是链表的元素个数。
第一步: 遍历链表并将值复制到数组中,O(n)。
第二步:双指针判断是否为回文,执行了 O(n/2) 次的判断,即 O(n)。
总的时间复杂度:O(2n)=O(n)。
53.1. 将值复制到数组后用双指针法
class Solution {
public boolean isPalindrome(ListNode head) {
// 创建一个ArrayList来存储链表节点的值
List<Integer> vals = new ArrayList<Integer>();
// 遍历链表,将每个节点的值添加到ArrayList中
ListNode currentNode = head;
while (currentNode != null) {
vals.add(currentNode.val);
currentNode = currentNode.next;
}
// 初始化双指针:front指向数组头部,back指向数组尾部
int front = 0;
int back = vals.size() - 1;
// 使用双指针检查数组是否为回文
while (front < back) {
// 如果头尾指针指向的值不相等,则链表不是回文
if (!vals.get(front).equals(vals.get(back))) {
return false;
}
// 移动指针:front向后移动,back向前移动
front++;
back--;
}
// 如果所有对应位置的值都相等,则链表是回文
return true;
}
}
53.2. 递归
class Solution {
private ListNode frontPointer;
private boolean recursivelyCheck(ListNode currentNode) {
if (currentNode != null) {
if (!recursivelyCheck(currentNode.next)) {
return false;
}
if (currentNode.val != frontPointer.val) {
return false;
}
frontPointer = frontPointer.next;
}
return true;
}
public boolean isPalindrome(ListNode head) {
frontPointer = head;
return recursivelyCheck(head);
}
}
54. 不同路径

解题思路:
该代码使用动态规划(DP)来计算机器人从网格左上角到右下角的不同路径数。通过优化空间复杂度,仅使用一维数组 f 来存储中间结果,其中 f[j] 表示到达当前位置 (i,j) 的路径数。初始化第一行和第一列的路径数为1后,逐步填充数组,最终得到右下角的路径总数。
时间复杂度:O(mn)。
54.1. 动态规划
class Solution {
public int uniquePaths(int m, int n) {
int[] f = new int[n]; // 初始化一维DP数组,f[j]表示到达当前行的第j列的路径数
for (int i = 0; i < n; ++i) {
f[i] = 1; // 第一行的每个位置只有1条路径(只能向右)
}
for (int i = 1; i < m; ++i) { // 从第二行开始遍历
for (int j = 1; j < n; ++j) { // 从第二列开始遍历
f[j] += f[j - 1]; // 状态转移:路径数 = 上方路径数 + 左方路径数
}
}
return f[n - 1]; // 返回右下角的路径总数
}
}
54.2. 组合数学
class Solution {
public int uniquePaths(int m, int n) {
long ans = 1;
for (int x = n, y = 1; y < m; ++x, ++y) {
ans = ans * x / y;
}
return (int) ans;
}
}
55. 打家劫舍

解题思路:
该代码使用动态规划(DP)来解决房屋偷窃问题。通过维护两个变量 first 和 second,分别表示前一个房屋和前两个房屋的最大偷窃金额,逐步更新当前房屋的最大偷窃金额,最终得到整个街道的最大偷窃金额。
时间复杂度:O(n),其中 n 是数组长度。只需要对数组遍历一次。
55.1. 解答
class Solution {
public int rob(int[] nums) {
// 处理空数组或长度为0的情况
if (nums == null || nums.length == 0) {
return 0;
}
int length = nums.length;
// 如果只有一个房屋,直接返回该房屋的金额
if (length == 1) {
return nums[0];
}
// 初始化前两个房屋的最大偷窃金额
int first = nums[0]; // 前两个房屋的最大金额
int second = Math.max(nums[0], nums[1]); // 前一个房屋的最大金额
// 从第三个房屋开始遍历
for (int i = 2; i < length; i++) {
int temp = second; // 临时保存前一个房屋的最大金额
// 更新当前房屋的最大金额:偷窃当前房屋(前两个房屋金额 + 当前金额)或不偷窃(前一个房屋金额)
second = Math.max(first + nums[i], second);
first = temp; // 更新前两个房屋的最大金额
}
// 返回最后一个房屋的最大偷窃金额
return second;
}
}
56. 乘积最大子数组

解题思路:
该代码使用动态规划(DP)来寻找数组中乘积最大的非空连续子数组。通过维护两个变量 maxF 和 minF,分别记录以当前元素结尾的子数组的最大乘积和最小乘积(用于处理负数相乘的情况),并在遍历过程中更新全局最大值 ans。
时间复杂度:程序一次循环遍历了 nums,故渐进时间复杂度为 O(n)。
56.1. 解答
class Solution {
public int maxProduct(int[] nums) {
// 初始化:maxF和minF分别表示以当前元素结尾的子数组的最大和最小乘积
long maxF = nums[0], minF = nums[0];
int ans = nums[0]; // 全局最大值
int length = nums.length;
for (int i = 1; i < length; ++i) {
// 保存前一步的maxF和minF,避免被覆盖
long mx = maxF, mn = minF;
// 更新maxF:取三者中的最大值(mx * nums[i], nums[i], mn * nums[i])
maxF = Math.max(mx * nums[i], Math.max(nums[i], mn * nums[i]));
// 更新minF:取三者中的最小值(mn * nums[i], nums[i], mx * nums[i])
minF = Math.min(mn * nums[i], Math.min(nums[i], mx * nums[i]));
// 处理可能的整数溢出(题目保证答案在32位整数范围内)
if (minF < -1 << 31) {
minF = nums[i];
}
// 更新全局最大值
ans = Math.max((int)maxF, ans);
}
return ans; // 返回全局最大值
}
}
57. 和为K的子数组

解题思路:
该代码通过双重循环遍历所有可能的子数组,计算每个子数组的和,并统计和等于 k 的子数组个数。外层循环固定子数组的起始位置,内层循环从起始位置向前遍历,累加元素值并检查是否等于 k。
时间复杂度:O(n2),其中 n 为数组的长度。枚举子数组开头和结尾需要 O(n2 ) 的时间,其中求和需要 O(1) 的时间复杂度,因此总时间复杂度为 O(n 2 )。
57.1. 枚举
public class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0; // 初始化计数器,用于统计和为k的子数组个数
for (int start = 0; start < nums.length; ++start) { // 外层循环:固定子数组的起始位置
int sum = 0; // 初始化当前子数组的和
for (int end = start; end >= 0; --end) { // 内层循环:从起始位置向前遍历
sum += nums[end]; // 累加当前元素到子数组的和
if (sum == k) { // 如果子数组的和等于k
count++; // 计数器加1
}
}
}
return count; // 返回和为k的子数组个数
}
}
57.2. 前缀和+哈希表优化
public class Solution {
public int subarraySum(int[] nums, int k) {
int count = 0, pre = 0;
HashMap < Integer, Integer > mp = new HashMap < > ();
mp.put(0, 1);
for (int i = 0; i < nums.length; i++) {
pre += nums[i];
if (mp.containsKey(pre - k)) {
count += mp.get(pre - k);
}
mp.put(pre, mp.getOrDefault(pre, 0) + 1);
}
return count;
}
}
58. 多数元素

解题思路:
该代码通过统计数组中每个元素的出现次数,然后遍历统计结果,找到出现次数最多的元素作为多数元素。由于题目保证数组非空且存在多数元素,最终可以直接返回出现次数最多的元素。
时间复杂度:O(n),其中 n 是数组 nums 的长度。
58.1. 哈希表
class Solution {
// 统计数组中每个元素的出现次数
private Map<Integer, Integer> countNums(int[] nums) {
Map<Integer, Integer> counts = new HashMap<Integer, Integer>(); // 用于存储元素及其出现次数
for (int num : nums) {
if (!counts.containsKey(num)) { // 如果元素不在Map中,初始化次数为1
counts.put(num, 1);
} else { // 否则,次数加1
counts.put(num, counts.get(num) + 1);
}
}
return counts; // 返回统计结果
}
public int majorityElement(int[] nums) {
Map<Integer, Integer> counts = countNums(nums); // 获取元素出现次数的统计结果
Map.Entry<Integer, Integer> majorityEntry = null; // 初始化多数元素的Entry
for (Map.Entry<Integer, Integer> entry : counts.entrySet()) { // 遍历统计结果
if (majorityEntry == null || entry.getValue() > majorityEntry.getValue()) { // 找到出现次数最多的元素
majorityEntry = entry;
}
}
return majorityEntry.getKey(); // 返回多数元素
}
}
58.2. 排序
class Solution {
public int majorityElement(int[] nums) {
Arrays.sort(nums);
return nums[nums.length / 2];
}
}
59. 翻转二叉树

解题思路:
该代码使用递归方法翻转二叉树。对于每个节点,递归地翻转其左右子树,然后交换左右子节点的位置,最终返回翻转后的根节点。
时间复杂度:O(N),其中 N 为二叉树节点的数目。我们会遍历二叉树中的每一个节点,对每个节点而言,我们在常数时间内交换其两棵子树。
59.1. 解答
class Solution {
public TreeNode invertTree(TreeNode root) {
if (root == null) { // 递归终止条件:当前节点为空时返回null
return null;
}
TreeNode left = invertTree(root.left); // 递归翻转左子树
TreeNode right = invertTree(root.right); // 递归翻转右子树
root.left = right; // 交换当前节点的左右子节点
root.right = left;
return root; // 返回翻转后的当前节点
}
}
60. 单词拆分

解题思路;
该代码使用动态规划(DP)来判断字符串 s 是否能被字典 wordDict 中的单词拼接而成。通过维护一个布尔数组 dp,其中 dp[i] 表示字符串 s 的前 i 个字符是否能被字典中的单词拼接而成。遍历字符串 s 的每个子串,检查是否存在于字典中,并更新 dp 数组。
时间复杂度:O(n2) ,其中 n 为字符串 s 的长度。
60.1. 解答
public class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> wordDictSet = new HashSet(wordDict); // 将字典转换为集合,便于快速查找
boolean[] dp = new boolean[s.length() + 1]; // dp[i]表示s的前i个字符是否能被拼接
dp[0] = true; // 空字符串可以被拼接
for (int i = 1; i <= s.length(); i++) { // 遍历字符串s的每个位置
for (int j = 0; j < i; j++) { // 检查所有可能的分割点j
// 如果前j个字符能被拼接,且子串s[j..i)在字典中
if (dp[j] && wordDictSet.contains(s.substring(j, i))) {
dp[i] = true; // 标记前i个字符能被拼接
break; // 找到一种可能即可跳出内层循环
}
}
}
return dp[s.length()]; // 返回整个字符串是否能被拼接
}
}
61. 移动零

解题思路;
该代码使用双指针技术来移动数组中的零元素。通过维护两个指针 left 和 right,right 指针用于遍历数组,left 指针用于标记非零元素的位置。当 right 指针遇到非零元素时,将其与 left 指针位置的元素交换,并移动 left 指针。这样可以确保所有非零元素的相对顺序不变,同时将所有零元素移动到数组末尾。
时间复杂度:O(n),其中 n 为序列长度。每个位置至多被遍历两次。
61.1. 解答
class Solution {
public void moveZeroes(int[] nums) {
int n = nums.length, left = 0, right = 0; // 初始化指针和数组长度
while (right < n) { // 遍历数组
if (nums[right] != 0) { // 如果当前元素不为零
swap(nums, left, right); // 交换当前元素和left指针位置的元素
left++; // 移动left指针
}
right++; // 移动right指针
}
}
// 交换数组中两个位置的元素
public void swap(int[] nums, int left, int right) {
int temp = nums[left];
nums[left] = nums[right];
nums[right] = temp;
}
}
62. 每日温度

解题思路:
该代码使用单调栈来高效地找到每一天的下一个更高温度的天数。通过维护一个栈,栈中存储的是尚未找到更高温度的日期索引。遍历温度数组时,如果当前温度高于栈顶索引对应的温度,则计算天数差并更新结果数组,直到栈为空或当前温度不再高于栈顶温度。最后将当前日期索引入栈。
时间复杂度:O(n),其中 n 是温度列表的长度。正向遍历温度列表一遍,对于温度列表中的每个下标,最多有一次进栈和出栈的操作。
62.1. 解答
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int length = temperatures.length;
int[] ans = new int[length]; // 初始化结果数组,默认值为0
Deque<Integer> stack = new LinkedList<Integer>(); // 使用栈存储日期索引
for (int i = 0; i < length; i++) { // 遍历每一天的温度
int temperature = temperatures[i];
// 当栈不为空且当前温度高于栈顶索引对应的温度时
while (!stack.isEmpty() && temperature > temperatures[stack.peek()]) {
int prevIndex = stack.pop(); // 弹出栈顶索引
ans[prevIndex] = i - prevIndex; // 计算天数差并存入结果数组
}
stack.push(i); // 将当前日期索引入栈
}
return ans; // 返回结果数组
}
}
63. 二叉树的序列化和反序列化

解题思路:
该代码通过递归的前序遍历方式实现二叉树的序列化和反序列化。序列化时,将二叉树转换为以逗号分隔的字符串,空节点用 "None" 表示;反序列化时,根据字符串重建二叉树,处理 "None" 标记以恢复空节点。
时间复杂度:在序列化和反序列化函数中,我们只访问每个节点一次,因此时间复杂度为 O(n),其中 n 是节点数,即树的大小。
63.1. 解答
public class Codec {
// 序列化:将二叉树转换为字符串
public String serialize(TreeNode root) {
return rserialize(root, ""); // 调用递归函数进行序列化
}
// 反序列化:将字符串转换为二叉树
public TreeNode deserialize(String data) {
String[] dataArray = data.split(","); // 按逗号分割字符串
List<String> dataList = new LinkedList<>(Arrays.asList(dataArray)); // 转换为链表便于操作
return rdeserialize(dataList); // 调用递归函数进行反序列化
}
// 递归序列化辅助函数
public String rserialize(TreeNode root, String str) {
if (root == null) {
str += "None,"; // 空节点标记为"None"
} else {
str += root.val + ","; // 非空节点记录值
str = rserialize(root.left, str); // 递归序列化左子树
str = rserialize(root.right, str); // 递归序列化右子树
}
return str;
}
// 递归反序列化辅助函数
public TreeNode rdeserialize(List<String> dataList) {
if (dataList.get(0).equals("None")) { // 遇到"None"表示空节点
dataList.remove(0); // 移除已处理的标记
return null;
}
TreeNode root = new TreeNode(Integer.parseInt(dataList.get(0))); // 创建当前节点
dataList.remove(0); // 移除已处理的值
root.left = rdeserialize(dataList); // 递归反序列化左子树
root.right = rdeserialize(dataList); // 递归反序列化右子树
return root;
}
}
64. 课程表

解题思路;
该代码使用深度优先搜索(DFS)来检测课程安排图中是否存在环,从而判断是否能完成所有课程。通过构建邻接表表示课程依赖关系,并使用 visited 数组标记节点的访问状态(0:未访问;1:访问中;2:已访问),在DFS过程中检测是否存在环(即是否存在循环依赖)。
时间复杂度: O(n+m),其中 n 为课程数,m 为先修课程的要求数。这其实就是对图进行深度优先搜索的时间复杂度。
64.1. 深度优先搜索
class Solution {
List<List<Integer>> edges; // 邻接表,存储课程依赖关系
int[] visited; // 访问状态数组:0-未访问,1-访问中,2-已访问
boolean valid = true; // 标记是否存在环
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 初始化邻接表
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
visited = new int[numCourses]; // 初始化访问状态数组
// 构建邻接表:b_i -> a_i(学习a_i前需完成b_i)
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
}
// 对每个未访问的节点启动DFS
for (int i = 0; i < numCourses && valid; ++i) {
if (visited[i] == 0) {
dfs(i);
}
}
return valid; // 返回是否存在环
}
public void dfs(int u) {
visited[u] = 1; // 标记当前节点为“访问中”
for (int v : edges.get(u)) { // 遍历当前节点的所有邻接节点
if (visited[v] == 0) { // 如果邻接节点未访问,递归处理
dfs(v);
if (!valid) return; // 发现环则提前退出
} else if (visited[v] == 1) { // 如果邻接节点正在访问中,说明存在环
valid = false;
return;
}
}
visited[u] = 2; // 标记当前节点为“已访问”
}
}
64.2. 广度优先搜索
class Solution {
List<List<Integer>> edges;
int[] indeg;
public boolean canFinish(int numCourses, int[][] prerequisites) {
edges = new ArrayList<List<Integer>>();
for (int i = 0; i < numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
indeg = new int[numCourses];
for (int[] info : prerequisites) {
edges.get(info[1]).add(info[0]);
++indeg[info[0]];
}
Queue<Integer> queue = new LinkedList<Integer>();
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0) {
queue.offer(i);
}
}
int visited = 0;
while (!queue.isEmpty()) {
++visited;
int u = queue.poll();
for (int v: edges.get(u)) {
--indeg[v];
if (indeg[v] == 0) {
queue.offer(v);
}
}
}
return visited == numCourses;
}
}
65. 单词搜索

解题思路;
该代码使用深度优先搜索(DFS)和回溯法来在二维字符网格中查找是否存在目标单词。通过遍历网格中的每个单元格作为起点,递归检查其相邻单元格是否能匹配单词的下一个字符,并使用 visited 数组标记已访问的单元格以避免重复使用。
时间复杂度:一个非常宽松的上界为 O(MN⋅3 L),其中 M,N 为网格的长度与宽度,L 为字符串 word 的长度。
65.1. 解答
class Solution {
public boolean exist(char[][] board, String word) {
int h = board.length, w = board[0].length; // 获取网格的行数和列数
boolean[][] visited = new boolean[h][w]; // 标记单元格是否被访问过
// 遍历网格中的每个单元格作为起点
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
boolean flag = check(board, visited, i, j, word, 0); // 从当前单元格开始检查
if (flag) {
return true; // 找到匹配则直接返回true
}
}
}
return false; // 遍历完所有单元格未找到匹配则返回false
}
public boolean check(char[][] board, boolean[][] visited, int i, int j, String s, int k) {
// 当前单元格字符不匹配单词的第k个字符
if (board[i][j] != s.charAt(k)) {
return false;
}
// 已匹配到单词的最后一个字符
else if (k == s.length() - 1) {
return true;
}
visited[i][j] = true; // 标记当前单元格为已访问
int[][] directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; // 四个可能的移动方向
boolean result = false;
// 遍历四个方向
for (int[] dir : directions) {
int newi = i + dir[0], newj = j + dir[1]; // 计算新位置
// 检查新位置是否在网格范围内且未被访问
if (newi >= 0 && newi < board.length && newj >= 0 && newj < board[0].length) {
if (!visited[newi][newj]) {
// 递归检查新位置是否能匹配单词的下一个字符
boolean flag = check(board, visited, newi, newj, s, k + 1);
if (flag) {
result = true;
break; // 找到匹配则提前退出循环
}
}
}
}
visited[i][j] = false; // 回溯,恢复当前单元格的未访问状态
return result; // 返回当前路径是否匹配
}
}
66. 只出现一次的数字

解题思路;
该代码利用异或运算(XOR)的性质来找出数组中只出现一次的元素。异或运算满足交换律和结合律,且任何数与自身异或结果为0,与0异或结果为该数本身。因此,通过遍历数组并对所有元素进行异或操作,最终结果即为只出现一次的元素。
时间复杂度:O(n),其中 n 是数组长度。只需要对数组遍历一次。
66.1. 解答
class Solution {
public int singleNumber(int[] nums) {
int single = 0; // 初始化结果为0
for (int num : nums) { // 遍历数组中的每个元素
single ^= num; // 对当前元素进行异或操作
}
return single; // 返回最终结果(只出现一次的元素)
}
}
67. 盛最多水的容器

解题思路:
该代码使用双指针法来寻找能容纳最多水的容器。通过初始化两个指针 l 和 r 分别指向数组的首尾,计算当前指针位置的容器面积,并移动较小高度的指针以寻找更大的面积。最终返回最大的容器面积。
时间复杂度:O(N),双指针总计最多遍历整个数组一次。
67.1. 解答
public class Solution {
public int maxArea(int[] height) {
int l = 0, r = height.length - 1; // 初始化双指针,l指向首元素,r指向尾元素
int ans = 0; // 初始化最大面积为0
while (l < r) { // 当左指针小于右指针时循环
// 计算当前指针位置的容器面积(高度取较小值,宽度为指针间距)
int area = Math.min(height[l], height[r]) * (r - l);
ans = Math.max(ans, area); // 更新最大面积
// 移动较小高度的指针(保留较高的边以寻找更大面积)
if (height[l] <= height[r]) {
++l; // 左指针右移
} else {
--r; // 右指针左移
}
}
return ans; // 返回最大面积
}
}
68. 跳跃游戏

解题思路:
该代码使用贪心算法来判断是否能从数组的第一个位置跳到最后一个位置。通过维护一个变量 rightmost 来记录当前能够到达的最远位置,遍历数组时更新 rightmost,如果在某个位置 i 时 i 超过了 rightmost,则说明无法继续前进,否则检查是否能直接到达最后一个位置。
时间复杂度:O(n),其中 n 为数组的大小。只需要访问 nums 数组一遍,共 n 个位置。
68.1. 解答
public class Solution {
public boolean canJump(int[] nums) {
int n = nums.length; // 数组长度
int rightmost = 0; // 当前能够到达的最远位置
for (int i = 0; i < n; ++i) { // 遍历数组
if (i <= rightmost) { // 如果当前位置可以到达
rightmost = Math.max(rightmost, i + nums[i]); // 更新最远位置
if (rightmost >= n - 1) { // 如果最远位置已经覆盖最后一个位置
return true; // 可以到达
}
}
}
return false; // 遍历结束后仍无法到达
}
}
69. 二叉树展开为列表

解题思路:
该代码通过先序遍历(根-左-右)将二叉树的所有节点按顺序存入列表,然后遍历列表,将每个节点的左指针设为 null,右指针指向下一个节点,从而将二叉树展开为单链表。
时间复杂度:O(n),其中 n 是二叉树的节点数。前序遍历的时间复杂度是 O(n),前序遍历之后,需要对每个节点更新左右子节点的信息,时间复杂度也是 O(n)。
69.1. 前序遍历
class Solution {
public void flatten(TreeNode root) {
List<TreeNode> list = new ArrayList<TreeNode>(); // 存储先序遍历的节点
preorderTraversal(root, list); // 先序遍历二叉树,将节点存入列表
int size = list.size(); // 获取列表长度
// 遍历列表,将每个节点的left设为null,right指向下一个节点
for (int i = 1; i < size; i++) {
TreeNode prev = list.get(i - 1), curr = list.get(i);
prev.left = null; // 左指针置空
prev.right = curr; // 右指针指向下一个节点
}
}
// 先序遍历辅助函数
public void preorderTraversal(TreeNode root, List<TreeNode> list) {
if (root != null) { // 当前节点不为空时处理
list.add(root); // 将当前节点加入列表
preorderTraversal(root.left, list); // 递归遍历左子树
preorderTraversal(root.right, list); // 递归遍历右子树
}
}
}
69.2. 前序遍历和展开同步
class Solution {
public void flatten(TreeNode root) {
if (root == null) {
return;
}
Deque<TreeNode> stack = new LinkedList<TreeNode>();
stack.push(root);
TreeNode prev = null;
while (!stack.isEmpty()) {
TreeNode curr = stack.pop();
if (prev != null) {
prev.left = null;
prev.right = curr;
}
TreeNode left = curr.left, right = curr.right;
if (right != null) {
stack.push(right);
}
if (left != null) {
stack.push(left);
}
prev = curr;
}
}
}
70. 颜色分类

解题思路:
该代码使用两次遍历的计数排序方法,原地对数组进行排序。第一次遍历将所有 0 移动到数组的前部,第二次遍历将所有 1 移动到 0 之后的位置,剩下的 2 自然就位于数组末尾。
时间复杂度:O(n),其中 n 是数组 nums 的长度。
70.1. 单指针
class Solution {
public void sortColors(int[] nums) {
int n = nums.length; // 数组长度
int ptr = 0; // 指针,用于标记当前应插入的位置
// 第一次遍历:将所有0移动到数组前部
for (int i = 0; i < n; ++i) {
if (nums[i] == 0) { // 如果当前元素是0
// 交换当前元素和ptr位置的元素
int temp = nums[i];
nums[i] = nums[ptr];
nums[ptr] = temp;
++ptr; // 移动指针
}
}
// 第二次遍历:将所有1移动到0之后的位置
for (int i = ptr; i < n; ++i) {
if (nums[i] == 1) { // 如果当前元素是1
// 交换当前元素和ptr位置的元素
int temp = nums[i];
nums[i] = nums[ptr];
nums[ptr] = temp;
++ptr; // 移动指针
}
}
// 剩余的2自然位于数组末尾
}
}
70.2. 双指针
class Solution {
public void sortColors(int[] nums) {
int n = nums.length;
int p0 = 0, p1 = 0;
for (int i = 0; i < n; ++i) {
if (nums[i] == 1) {
int temp = nums[i];
nums[i] = nums[p1];
nums[p1] = temp;
++p1;
} else if (nums[i] == 0) {
int temp = nums[i];
nums[i] = nums[p0];
nums[p0] = temp;
if (p0 < p1) {
temp = nums[i];
nums[i] = nums[p1];
nums[p1] = temp;
}
++p0;
++p1;
}
}
}
}
71. 正则表达式匹配

解题思路:
- 初始化动态规划表:创建一个二维布尔数组
f,其中f[i][j]表示字符串s的前i个字符和模式p的前j个字符是否匹配。 - 基本情况:空字符串和空模式匹配,所以
f[0][0] = true。 - 填充动态规划表:
-
- 遍历字符串
s的所有可能长度(包括空字符串) - 遍历模式
p的所有可能长度(不包括空模式) - 处理两种主要情况:
- 遍历字符串
-
-
- 当前模式字符是
*:考虑匹配零次或多次前一个字符 - 当前模式字符不是
*:直接匹配当前字符
- 当前模式字符是
-
- 匹配辅助函数:检查当前字符是否匹配(考虑
.的通配情况) - 时间复杂度:O(mn),其中 m 和 n 分别是字符串 s 和 p 的长度。我们需要计算出所有的状态,并且每个状态在进行转移时的时间复杂度为 O(1)。
71.1. 解答
class Solution {
public boolean isMatch(String s, String p) {
int m = s.length();
int n = p.length();
// 创建动态规划表,f[i][j]表示s的前i个字符和p的前j个字符是否匹配
boolean[][] f = new boolean[m + 1][n + 1];
// 空字符串和空模式匹配
f[0][0] = true;
// 遍历s的所有可能长度(包括空字符串)
for (int i = 0; i <= m; ++i) {
// 遍历p的所有可能长度(不包括空模式)
for (int j = 1; j <= n; ++j) {
if (p.charAt(j - 1) == '*') {
// 处理'*'情况:匹配零个或多个前一个字符
// 首先考虑匹配零个的情况(跳过前一个字符和'*')
f[i][j] = f[i][j - 2];
if (matches(s, p, i, j - 1)) {
// 如果可以匹配一个或多个前一个字符,则考虑s去掉当前字符的情况
f[i][j] = f[i][j] || f[i - 1][j];
}
} else {
// 处理非'*'情况:直接匹配当前字符
if (matches(s, p, i, j)) {
f[i][j] = f[i - 1][j - 1];
}
}
}
}
return f[m][n];
}
// 辅助函数:检查s的第i个字符和p的第j个字符是否匹配
public boolean matches(String s, String p, int i, int j) {
if (i == 0) {
return false; // s为空,无法匹配
}
if (p.charAt(j - 1) == '.') {
return true; // '.'可以匹配任意字符
}
return s.charAt(i - 1) == p.charAt(j - 1); // 直接比较字符
}
}
72. 前K个高频元素

解题思路:
- 统计频率:首先遍历数组,使用哈希表统计每个数字出现的频率。
- 维护最小堆:创建一个大小为 k 的最小堆,用于存储当前频率最高的 k 个元素。
-
- 当堆大小小于 k 时,直接添加元素
- 当堆大小等于 k 时,比较堆顶元素(当前最小频率)与新元素的频率,保留频率更高的
- 提取结果:最后将堆中的元素取出,得到前 k 个高频元素
时间复杂度:O(Nlogk),其中 N 为数组的长度。我们首先遍历原数组,并使用哈希表记录出现次数,每个元素需要 O(1) 的时间,共需 O(N) 的时间。随后,我们遍历「出现次数数组」,由于堆的大小至多为 k,因此每次堆操作需要 O(logk) 的时间,共需 O(Nlogk) 的时间。二者之和为 O(Nlogk)。
73. 堆
class Solution {
public int[] topKFrequent(int[] nums, int k) {
// 1. 使用哈希表统计每个数字出现的频率
Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
for (int num : nums) {
occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
}
// 2. 创建最小堆(优先队列),按频率排序
// 堆中存储数组,[0]是数字,[1]是频率
PriorityQueue<int[]> queue = new PriorityQueue<int[]>(new Comparator<int[]>() {
public int compare(int[] m, int[] n) {
return m[1] - n[1]; // 按频率升序排序
}
});
// 3. 遍历频率哈希表,维护大小为k的最小堆
for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
int num = entry.getKey(), count = entry.getValue();
if (queue.size() == k) {
// 如果堆已满,比较堆顶频率与当前频率
if (queue.peek()[1] < count) {
queue.poll(); // 移除频率最小的元素
queue.offer(new int[]{num, count}); // 添加当前元素
}
} else {
queue.offer(new int[]{num, count}); // 堆未满,直接添加
}
}
// 4. 从堆中提取结果
int[] ret = new int[k];
for (int i = 0; i < k; ++i) {
ret[i] = queue.poll()[0]; // 取出堆顶元素(频率从低到高)
}
return ret;
}
}
73.1. 基于快速排序
class Solution {
public int[] topKFrequent(int[] nums, int k) {
Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
for (int num : nums) {
occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);
}
// 获取每个数字出现次数
List<int[]> values = new ArrayList<int[]>();
for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
int num = entry.getKey(), count = entry.getValue();
values.add(new int[]{num, count});
}
int[] ret = new int[k];
qsort(values, 0, values.size() - 1, ret, 0, k);
return ret;
}
public void qsort(List<int[]> values, int start, int end, int[] ret, int retIndex, int k) {
int picked = (int) (Math.random() * (end - start + 1)) + start;
Collections.swap(values, picked, start);
int pivot = values.get(start)[1];
int index = start;
for (int i = start + 1; i <= end; i++) {
// 使用双指针把不小于基准值的元素放到左边,
// 小于基准值的元素放到右边
if (values.get(i)[1] >= pivot) {
Collections.swap(values, index + 1, i);
index++;
}
}
Collections.swap(values, start, index);
if (k <= index - start) {
// 前 k 大的值在左侧的子数组里
qsort(values, start, index - 1, ret, retIndex, k);
} else {
// 前 k 大的值等于左侧的子数组全部元素
// 加上右侧子数组中前 k - (index - start + 1) 大的值
for (int i = start; i <= index; i++) {
ret[retIndex++] = values.get(i)[0];
}
if (k > index - start + 1) {
qsort(values, index + 1, end, ret, retIndex, k - (index - start + 1));
}
}
}
}
74. 实现Trie(前缀树)

解题思路:
- 数据结构设计:
-
- 每个Trie节点包含一个子节点数组(26个字母)和一个结束标志
isEnd标记表示从根节点到当前节点的路径是否构成一个完整单词
- 核心操作:
-
- 插入:沿着单词字符逐个创建/访问子节点,最后标记结束
- 搜索:检查是否能完整匹配单词路径且结束标志为true
- 前缀匹配:只需检查是否存在对应的路径,不考虑结束标志
- 辅助方法:
searchPrefix用于共享前缀搜索逻辑
时间复杂度:初始化为 O(1),其余操作为 O(∣S∣),其中 ∣S∣ 是每次插入或查询的字符串的长度。
74.1. 解答
class Trie {
// 每个节点包含26个子节点(对应26个小写字母)
private Trie[] children;
// 标记当前节点是否是某个单词的结束
private boolean isEnd;
// 初始化Trie
public Trie() {
children = new Trie[26]; // 26个字母的子节点
isEnd = false; // 初始时不是单词结尾
}
// 插入单词到Trie中
public void insert(String word) {
Trie node = this; // 从根节点开始
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a'; // 计算字母对应的索引(0-25)
if (node.children[index] == null) {
// 如果对应字母的子节点不存在,则创建
node.children[index] = new Trie();
}
// 移动到子节点
node = node.children[index];
}
// 标记单词结束
node.isEnd = true;
}
// 搜索完整单词
public boolean search(String word) {
Trie node = searchPrefix(word);
// 节点存在且是单词结尾才返回true
return node != null && node.isEnd;
}
// 检查是否存在以prefix为前缀的单词
public boolean startsWith(String prefix) {
// 只需检查前缀路径是否存在
return searchPrefix(prefix) != null;
}
// 辅助方法:搜索前缀对应的节点
private Trie searchPrefix(String prefix) {
Trie node = this; // 从根节点开始
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
// 如果路径中断,返回null
return null;
}
// 继续向下搜索
node = node.children[index];
}
// 返回前缀最后的节点
return node;
}
}
75. 寻找重复数

解题思路:
- 二分查找范围:在数字范围 [1, n] 内进行二分查找
- 统计计数:对于每个中间值 mid,统计数组中 ≤ mid 的数字个数
- 判断重复:
-
- 如果计数 ≤ mid,说明重复数在右半部分
- 否则重复数在左半部分,并记录当前 mid 为候选答案
- 缩小范围:不断调整查找范围直到找到重复数
时间复杂度:O(nlogn),其中 n 为 nums 数组的长度。二分查找最多需要二分 O(logn) 次,每次判断的时候需要O(n) 遍历 nums 数组求解小于等于 mid 的数的个数,因此总时间复杂度为 O(nlogn)。
75.1. 二分查找
class Solution {
public int findDuplicate(int[] nums) {
int n = nums.length; // 数组长度是n+1,所以n实际上是最大值
int l = 1, r = n - 1, ans = -1; // 初始化左右边界和答案
while (l <= r) { // 标准二分查找循环
int mid = (l + r) >> 1; // 计算中间值,等同于(l + r)/2
int cnt = 0; // 统计≤mid的数字个数
// 遍历整个数组统计≤mid的数字个数
for (int i = 0; i < n; ++i) {
if (nums[i] <= mid) {
cnt++;
}
}
// 判断重复数在哪一侧
if (cnt <= mid) {
// 如果≤mid的数字个数≤mid,说明重复数在右半部分
l = mid + 1; // 调整左边界
} else {
// 否则重复数在左半部分,记录当前mid为候选答案
r = mid - 1; // 调整右边界
ans = mid; // 更新候选答案
}
}
return ans; // 返回最终找到的重复数
}
}
75.2. 二进制
class Solution {
public int findDuplicate(int[] nums) {
int n = nums.length, ans = 0;
int bit_max = 31;
while (((n - 1) >> bit_max) == 0) {
bit_max -= 1;
}
for (int bit = 0; bit <= bit_max; ++bit) {
int x = 0, y = 0;
for (int i = 0; i < n; ++i) {
if ((nums[i] & (1 << bit)) != 0) {
x += 1;
}
if (i >= 1 && ((i & (1 << bit)) != 0)) {
y += 1;
}
}
if (x > y) {
ans |= 1 << bit;
}
}
return ans;
}
}
76. 不同的二叉搜索树

解题思路:
- 动态规划数组:
G[i]表示 i 个节点能组成的二叉搜索树数量 - 基本情况:
-
- 0 个节点:空树算 1 种
- 1 个节点:只有 1 种树
- 递推关系:对于每个节点数 i,遍历每个可能的根节点 j,计算左子树和右子树的组合数
- 结果计算:通过逐步填充动态规划数组,最终得到 G[n] 的值
时间复杂度 : O(n 2 ),其中 n 表示二叉搜索树的节点个数。G(n) 函数一共有 n 个值需要求解,每次求解需要 O(n) 的时间复杂度,因此总时间复杂度为 O(n
2)。
76.1. 动态规划
class Solution {
public int numTrees(int n) {
// G[i] 表示i个节点能组成的BST数量
int[] G = new int[n + 1];
// 基本情况
G[0] = 1; // 空树算1种
G[1] = 1; // 单节点树只有1种
// 计算2到n个节点的情况
for (int i = 2; i <= n; ++i) {
// 遍历每个可能的根节点位置j
for (int j = 1; j <= i; ++j) {
// 左子树有j-1个节点,右子树有i-j个节点
// 组合数为左右子树数量的乘积
G[i] += G[j - 1] * G[i - j];
}
}
// 返回n个节点的BST数量
return G[n];
}
}
76.2. 数学
class Solution {
public int numTrees(int n) {
// 提示:我们在这里需要用 long 类型防止计算过程中的溢出
long C = 1;
for (int i = 0; i < n; ++i) {
C = C * 2 * (2 * i + 1) / (i + 2);
}
return (int) C;
}
}
77. 最大矩形

解题思路:
- 预处理:计算每个位置(i,j)左侧连续1的数量(包括自己)
- 查找最大矩形:对于每个位置(i,j),向上扩展计算可能的最大矩形面积
- 更新结果:在遍历过程中不断更新最大面积值
时间复杂度:O(m2n),其中 m 和 n 分别是矩阵的行数和列数。计算 left 矩阵需要 O(mn) 的时间。随后对于矩阵的每个点,需要 O(m) 的时间枚举高度。故总的时间复杂度为 O(mn)+O(mn)⋅O(m)=O(m2 n)。
77.1. 暴力解法
class Solution {
public int maximalRectangle(char[][] matrix) {
int m = matrix.length;
if (m == 0) {
return 0; // 空矩阵直接返回0
}
int n = matrix[0].length;
// left[i][j]表示matrix[i][j]左侧连续1的个数(包括自己)
int[][] left = new int[m][n];
// 预处理:计算每行每个位置左侧连续1的个数
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
}
}
}
int ret = 0; // 存储最大面积
// 遍历每个位置,计算以该位置为右下角的最大矩形面积
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '0') {
continue; // 遇到0则跳过
}
int width = left[i][j]; // 当前行的宽度
int area = width; // 初始面积为1行的高度
// 向上扩展,计算多行形成的矩形面积
for (int k = i - 1; k >= 0; k--) {
width = Math.min(width, left[k][j]); // 取最小宽度
area = Math.max(area, (i - k + 1) * width); // 计算新面积
}
ret = Math.max(ret, area); // 更新最大面积
}
}
return ret; // 返回最终结果
}
}
77.2. 单调栈
class Solution {
public int maximalRectangle(char[][] matrix) {
int m = matrix.length;
if (m == 0) {
return 0;
}
int n = matrix[0].length;
int[][] left = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
left[i][j] = (j == 0 ? 0 : left[i][j - 1]) + 1;
}
}
}
int ret = 0;
for (int j = 0; j < n; j++) { // 对于每一列,使用基于柱状图的方法
int[] up = new int[m];
int[] down = new int[m];
Deque<Integer> stack = new LinkedList<Integer>();
for (int i = 0; i < m; i++) {
while (!stack.isEmpty() && left[stack.peek()][j] >= left[i][j]) {
stack.pop();
}
up[i] = stack.isEmpty() ? -1 : stack.peek();
stack.push(i);
}
stack.clear();
for (int i = m - 1; i >= 0; i--) {
while (!stack.isEmpty() && left[stack.peek()][j] >= left[i][j]) {
stack.pop();
}
down[i] = stack.isEmpty() ? m : stack.peek();
stack.push(i);
}
for (int i = 0; i < m; i++) {
int height = down[i] - up[i] - 1;
int area = height * left[i][j];
ret = Math.max(ret, area);
}
}
return ret;
}
}
78. 柱状图中最大的矩形

解题思路:
- 预处理左右边界:
-
- 使用单调栈计算每个柱子左侧第一个比它矮的柱子位置(left数组)
- 使用单调栈计算每个柱子右侧第一个比它矮的柱子位置(right数组)
- 计算最大面积:
-
- 对于每个柱子,计算以它为高的最大矩形宽度(right[i] - left[i] - 1)
- 更新最大面积值
时间复杂度:O(N)。
78.1. 单调栈
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
// left[i] 表示第i根柱子左侧第一个比它矮的柱子的下标
int[] left = new int[n];
// right[i] 表示第i根柱子右侧第一个比它矮的柱子的下标
int[] right = new int[n];
// 单调栈:用于计算left数组(栈底到栈顶单调递增)
Deque<Integer> mono_stack = new ArrayDeque<Integer>();
for (int i = 0; i < n; ++i) {
// 弹出栈顶所有比当前柱子高的柱子
while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
mono_stack.pop();
}
// 如果栈为空,说明左侧没有更矮的柱子,记为-1
// 否则栈顶元素就是左侧第一个比当前矮的柱子
left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
mono_stack.push(i); // 当前柱子入栈
}
// 清空栈,准备计算right数组
mono_stack.clear();
// 从右向左遍历计算right数组
for (int i = n - 1; i >= 0; --i) {
// 弹出栈顶所有比当前柱子高的柱子
while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
mono_stack.pop();
}
// 如果栈为空,说明右侧没有更矮的柱子,记为n
// 否则栈顶元素就是右侧第一个比当前矮的柱子
right[i] = (mono_stack.isEmpty() ? n : mono_stack.peek());
mono_stack.push(i); // 当前柱子入栈
}
// 计算最大矩形面积
int ans = 0;
for (int i = 0; i < n; ++i) {
// 宽度 = 右边界 - 左边界 - 1
// 面积 = 高度 * 宽度
ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);
}
return ans;
}
}
78.2. 单调栈+常数优化
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int[] left = new int[n];
int[] right = new int[n];
Arrays.fill(right, n);
Deque<Integer> mono_stack = new ArrayDeque<Integer>();
for (int i = 0; i < n; ++i) {
while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
right[mono_stack.peek()] = i;
mono_stack.pop();
}
left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
mono_stack.push(i);
}
int ans = 0;
for (int i = 0; i < n; ++i) {
ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);
}
return ans;
}
}
79. 分割等和子集

解题思路:
- 初步检查:
-
- 数组长度小于2时直接返回false
- 计算数组总和,如果为奇数则无法平分,返回false
- 检查最大元素是否超过总和的一半,超过则返回false
- 动态规划求解:
-
- 创建布尔型dp数组,dp[j]表示能否组成和为j的子集
- 初始化dp[0]为true(和为0的子集总是存在)
- 遍历数组元素,更新dp数组
- 返回结果:检查dp[target]是否为true
时间复杂度:O(n×target),其中 n 是数组的长度,target 是整个数组的元素和的一半。需要计算出所有的状态,每个状态在进行转移时的时间复杂度为 O(1)。
79.1. 解答
class Solution {
public boolean canPartition(int[] nums) {
int n = nums.length;
if (n < 2) {
return false; // 数组长度小于2无法分割
}
// 计算数组总和和最大值
int sum = 0, maxNum = 0;
for (int num : nums) {
sum += num;
maxNum = Math.max(maxNum, num);
}
// 总和为奇数无法平分
if (sum % 2 != 0) {
return false;
}
int target = sum / 2; // 目标子集和
// 如果最大元素超过目标值,无法平分
if (maxNum > target) {
return false;
}
// dp[j]表示能否组成和为j的子集
boolean[] dp = new boolean[target + 1];
dp[0] = true; // 和为0的子集总是存在
// 动态规划填充dp数组
for (int i = 0; i < n; i++) {
int num = nums[i];
// 从后往前更新,避免重复计算
for (int j = target; j >= num; --j) {
// 当前j能否组成 = 之前就能组成j 或 能组成j-num
dp[j] |= dp[j - num];
}
}
return dp[target]; // 返回能否组成和为target的子集
}
}
80. 目标和

解题思路:
- 回溯框架:
-
- 从数组第一个元素开始,对每个元素尝试加号和减号两种情况
- 递归探索所有可能的组合路径
- 当处理完所有元素时,检查当前和是否等于目标值
- 结果统计:
-
- 每当找到一个满足条件的组合时,计数器加1
- 最终返回所有满足条件的组合数目
时间复杂度:O(2n),其中 n 是数组 nums 的长度。回溯需要遍历所有不同的表达式,共有 2n种不同的表达式,每种表达式计算结果需要 O(1) 的时间,因此总时间复杂度是 O(2n)。
80.1. 回溯
class Solution {
int count = 0; // 用于记录满足条件的表达式数目
public int findTargetSumWays(int[] nums, int target) {
// 从第0个元素开始回溯,初始和为0
backtrack(nums, target, 0, 0);
return count; // 返回最终结果
}
// 回溯方法
public void backtrack(int[] nums, int target, int index, int sum) {
// 基准情况:处理完所有数字
if (index == nums.length) {
// 如果当前和等于目标值,增加计数
if (sum == target) {
count++;
}
} else {
// 尝试给当前数字添加+号,递归处理下一个数字
backtrack(nums, target, index + 1, sum + nums[index]);
// 尝试给当前数字添加-号,递归处理下一个数字
backtrack(nums, target, index + 1, sum - nums[index]);
}
}
}
80.2. 动态规划
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int num : nums) {
sum += num;
}
int diff = sum - target;
if (diff < 0 || diff % 2 != 0) {
return 0;
}
int n = nums.length, neg = diff / 2;
int[][] dp = new int[n + 1][neg + 1];
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
for (int j = 0; j <= neg; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= num) {
dp[i][j] += dp[i - 1][j - num];
}
}
}
return dp[n][neg];
}
}
81. 完全平方数

解题思路:
- 动态规划数组:
f[i]表示和为i的完全平方数的最少数量 - 初始化:
f[0]初始化为0(虽然代码中未显式设置,但f[0]默认为0) - 递推关系:对于每个数字i,找到所有可能的完全平方数j²,使得i-j²≥0,然后取最小值加1
- 结果获取:最终返回
f[n]的值
时间复杂度:O(n根号n),其中 n 为给定的正整数。状态转移方程的时间复杂度为 O(n),共需要计算 n 个状态,因此总时间复杂度为 O(n根号n)。
81.1. 动态规划
class Solution {
public int numSquares(int n) {
// f[i]表示和为i的完全平方数的最少数量
int[] f = new int[n + 1];
// 从1到n依次计算每个数字的最少完全平方数数量
for (int i = 1; i <= n; i++) {
int minn = Integer.MAX_VALUE; // 初始化最小值
// 遍历所有可能的完全平方数j²(j² ≤ i)
for (int j = 1; j * j <= i; j++) {
// 比较当前最小值和使用j²后的结果
minn = Math.min(minn, f[i - j * j]);
}
// 当前数字i的最少数量为最小值+1(加上当前j²)
f[i] = minn + 1;
}
// 返回和为n的完全平方数的最少数量
return f[n];
}
}
81.2. 数学
class Solution {
public int numSquares(int n) {
if (isPerfectSquare(n)) {
return 1;
}
if (checkAnswer4(n)) {
return 4;
}
for (int i = 1; i * i <= n; i++) {
int j = n - i * i;
if (isPerfectSquare(j)) {
return 2;
}
}
return 3;
}
// 判断是否为完全平方数
public boolean isPerfectSquare(int x) {
int y = (int) Math.sqrt(x);
return y * y == x;
}
// 判断是否能表示为 4^k*(8m+7)
public boolean checkAnswer4(int x) {
while (x % 4 == 0) {
x /= 4;
}
return x % 8 == 7;
}
}
82. 打家劫舍Ⅲ

解题思路:
- 后序遍历DFS:采用深度优先搜索遍历二叉树
- 状态数组:每个节点返回一个长度为2的数组
-
selected:选择当前节点时的最大金额notSelected:不选择当前节点时的最大金额
- 状态转移:
-
- 选择当前节点时,不能选择子节点
- 不选择当前节点时,可以选择子节点的最大可能值(无论子节点是否被选择)
- 结果获取:比较根节点的两种状态,取最大值
时间复杂度:O(n)。
82.1. 解答
class Solution {
public int rob(TreeNode root) {
// 获取根节点的两种状态值:选择和不选择
int[] rootStatus = dfs(root);
// 返回两种状态中的最大值
return Math.max(rootStatus[0], rootStatus[1]);
}
// 深度优先搜索,返回当前节点的两种状态
public int[] dfs(TreeNode node) {
// 空节点返回[0, 0]
if (node == null) {
return new int[]{0, 0};
}
// 递归获取左右子节点的状态
int[] l = dfs(node.left);
int[] r = dfs(node.right);
// 选择当前节点时,不能选择直接子节点
int selected = node.val + l[1] + r[1];
// 不选择当前节点时,可以选择子节点的最大可能值
int notSelected = Math.max(l[0], l[1]) + Math.max(r[0], r[1]);
// 返回当前节点的两种状态
return new int[]{selected, notSelected};
}
}
83. 回文子串

解题思路:
- 中心扩展法:考虑所有可能的回文中心(包括奇数和偶数长度)
- 双重中心处理:通过巧妙的下标处理同时处理奇数和偶数长度的回文
- 扩展计数:从每个中心向两边扩展,统计所有可能的回文子串
时间复杂度:O(n2)。
83.1. 中心拓展
class Solution {
public int countSubstrings(String s) {
int n = s.length(), ans = 0; // 字符串长度和结果计数器
// 遍历所有可能的回文中心(共2n-1个)
for (int i = 0; i < 2 * n - 1; ++i) {
// 计算左右起始位置
// 偶数i对应奇数长度回文(中心为一个字符)
// 奇数i对应偶数长度回文(中心为两个字符之间)
int l = i / 2; // 左起始位置
int r = i / 2 + i % 2; // 右起始位置
// 向两边扩展,统计回文子串
while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
--l; // 向左扩展
++r; // 向右扩展
++ans; // 发现一个回文子串
}
}
return ans; // 返回总回文子串数
}
}
83.2. Manacher算法
class Solution {
public int countSubstrings(String s) {
int n = s.length();
StringBuffer t = new StringBuffer("$#");
for (int i = 0; i < n; ++i) {
t.append(s.charAt(i));
t.append('#');
}
n = t.length();
t.append('!');
int[] f = new int[n];
int iMax = 0, rMax = 0, ans = 0;
for (int i = 1; i < n; ++i) {
// 初始化 f[i]
f[i] = i <= rMax ? Math.min(rMax - i + 1, f[2 * iMax - i]) : 1;
// 中心拓展
while (t.charAt(i + f[i]) == t.charAt(i - f[i])) {
++f[i];
}
// 动态维护 iMax 和 rMax
if (i + f[i] - 1 > rMax) {
iMax = i;
rMax = i + f[i] - 1;
}
// 统计答案, 当前贡献为 (f[i] - 1) / 2 上取整
ans += f[i] / 2;
}
return ans;
}
}
84. 删除无效的括号

解题思路:
- 计算需要删除的括号数量:首先统计需要删除的多余左括号和右括号的数量
- 递归回溯处理:尝试删除每个可能的括号,跳过连续相同的括号以避免重复
- 有效性验证:检查生成的字符串是否是有效的括号组合
- 结果收集:将所有有效的字符串结果收集到列表中
- 时间复杂度:O(n×2n),其中 n 为字符串的长度。考虑到一个字符串最多可能有 2n个子序列,每个子序列可能需要进行一次合法性检测,因此时间复杂度为 O(n×2n)。
84.1. 回溯+剪枝
class Solution {
private List<String> res = new ArrayList<String>(); // 存储有效结果
public List<String> removeInvalidParentheses(String s) {
int lremove = 0; // 需要删除的多余左括号数
int rremove = 0; // 需要删除的多余右括号数
// 计算需要删除的左右括号数量
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
lremove++;
} else if (s.charAt(i) == ')') {
if (lremove == 0) {
rremove++; // 没有匹配的左括号,需要删除右括号
} else {
lremove--; // 有匹配的左括号,减少需要删除的左括号数
}
}
}
helper(s, 0, lremove, rremove); // 开始递归处理
return res; // 返回所有有效结果
}
private void helper(String str, int start, int lremove, int rremove) {
// 当不需要删除任何括号时,检查当前字符串是否有效
if (lremove == 0 && rremove == 0) {
if (isValid(str)) {
res.add(str); // 有效则加入结果列表
}
return;
}
for (int i = start; i < str.length(); i++) {
// 跳过连续相同的括号,避免重复计算
if (i != start && str.charAt(i) == str.charAt(i - 1)) {
continue;
}
// 如果剩余字符不够删除,直接返回
if (lremove + rremove > str.length() - i) {
return;
}
// 尝试删除左括号
if (lremove > 0 && str.charAt(i) == '(') {
helper(str.substring(0, i) + str.substring(i + 1), i, lremove - 1, rremove);
}
// 尝试删除右括号
if (rremove > 0 && str.charAt(i) == ')') {
helper(str.substring(0, i) + str.substring(i + 1), i, lremove, rremove - 1);
}
}
}
// 检查字符串是否是有效的括号组合
private boolean isValid(String str) {
int cnt = 0;
for (int i = 0; i < str.length(); i++) {
if (str.charAt(i) == '(') {
cnt++;
} else if (str.charAt(i) == ')') {
cnt--;
if (cnt < 0) { // 右括号多于左括号
return false;
}
}
}
return cnt == 0; // 左右括号数量相等
}
}
84.2. 广度优先算法
class Solution {
public List<String> removeInvalidParentheses(String s) {
List<String> ans = new ArrayList<String>();
Set<String> currSet = new HashSet<String>();
currSet.add(s);
while (true) {
for (String str : currSet) {
if (isValid(str)) {
ans.add(str);
}
}
if (ans.size() > 0) {
return ans;
}
Set<String> nextSet = new HashSet<String>();
for (String str : currSet) {
for (int i = 0; i < str.length(); i ++) {
if (i > 0 && str.charAt(i) == str.charAt(i - 1)) {
continue;
}
if (str.charAt(i) == '(' || str.charAt(i) == ')') {
nextSet.add(str.substring(0, i) + str.substring(i + 1));
}
}
}
currSet = nextSet;
}
}
private boolean isValid(String str) {
char[] ss = str.toCharArray();
int count = 0;
for (char c : ss) {
if (c == '(') {
count++;
} else if (c == ')') {
count--;
if (count < 0) {
return false;
}
}
}
return count == 0;
}
}
85. 电话号码的字母组合

解题思路:
- 初始化映射:建立数字到字母的映射关系
- 回溯生成组合:
-
- 从第一个数字开始,逐个处理每个数字
- 对于每个数字对应的字母,递归生成所有可能的组合
- 当处理完所有数字时,将当前组合加入结果列表
- 回溯处理:在递归返回时删除当前选择的字母,尝试其他可能性

85.1. 解答
class Solution {
public List<String> letterCombinations(String digits) {
List<String> combinations = new ArrayList<String>();
// 处理空输入情况
if (digits.length() == 0) {
return combinations;
}
// 建立数字到字母的映射关系
Map<Character, String> phoneMap = new HashMap<Character, String>() {{
put('2', "abc");
put('3', "def");
put('4', "ghi");
put('5', "jkl");
put('6', "mno");
put('7', "pqrs");
put('8', "tuv");
put('9', "wxyz");
}};
// 开始回溯生成组合
backtrack(combinations, phoneMap, digits, 0, new StringBuffer());
return combinations;
}
// 回溯方法
public void backtrack(List<String> combinations, Map<Character, String> phoneMap,
String digits, int index, StringBuffer combination) {
// 基准情况:已处理完所有数字
if (index == digits.length()) {
combinations.add(combination.toString()); // 将当前组合加入结果列表
} else {
char digit = digits.charAt(index); // 当前处理的数字
String letters = phoneMap.get(digit); // 当前数字对应的字母
int lettersCount = letters.length();
// 遍历当前数字对应的所有字母
for (int i = 0; i < lettersCount; i++) {
combination.append(letters.charAt(i)); // 选择当前字母
// 递归处理下一个数字
backtrack(combinations, phoneMap, digits, index + 1, combination);
combination.deleteCharAt(index); // 回溯,移除当前字母
}
}
}
}
86. 找到字符串中所有字母异位词

解题思路;
- 初始化检查:如果s长度小于p,直接返回空列表
- 初始化计数数组:统计p中字符频率,并初始化第一个窗口的字符频率差
- 差异度计算:统计当前窗口中字符频率与p的差异度
- 滑动窗口处理:
-
- 移动窗口时调整离开和进入窗口的字符计数
- 动态更新差异度
- 当差异度为0时记录当前窗口起始位置
- 返回结果:收集所有满足条件的起始位置
- 时间复杂度为O(n),其中n是字符串s的长度
86.1. 解答
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int sLen = s.length(), pLen = p.length();
// 如果s比p短,直接返回空列表
if (sLen < pLen) {
return new ArrayList<Integer>();
}
List<Integer> ans = new ArrayList<Integer>();
int[] count = new int[26]; // 用于统计字符频率差
// 初始化第一个窗口的字符频率差
for (int i = 0; i < pLen; ++i) {
++count[s.charAt(i) - 'a']; // s中的字符加1
--count[p.charAt(i) - 'a']; // p中的字符减1
}
// 计算初始差异度(有多少字符的频率不同)
int differ = 0;
for (int j = 0; j < 26; ++j) {
if (count[j] != 0) {
++differ;
}
}
// 如果初始窗口就是异位词,记录位置0
if (differ == 0) {
ans.add(0);
}
// 滑动窗口处理剩余部分
for (int i = 0; i < sLen - pLen; ++i) {
// 处理离开窗口的字符s[i]
if (count[s.charAt(i) - 'a'] == 1) { // 该字符从不同变为相同
--differ;
} else if (count[s.charAt(i) - 'a'] == 0) { // 该字符从相同变为不同
++differ;
}
--count[s.charAt(i) - 'a']; // 更新计数
// 处理进入窗口的字符s[i+pLen]
if (count[s.charAt(i + pLen) - 'a'] == -1) { // 该字符从不同变为相同
--differ;
} else if (count[s.charAt(i + pLen) - 'a'] == 0) { // 该字符从相同变为不同
++differ;
}
++count[s.charAt(i + pLen) - 'a']; // 更新计数
// 如果差异度为0,记录当前窗口起始位置
if (differ == 0) {
ans.add(i + 1);
}
}
return ans;
}
}
87. 合并二叉树

解题思路:
- 可以使用深度优先搜索合并两个二叉树。从根节点开始同时遍历两个二叉树,并将对应的节点进行合并。
- 两个二叉树的对应节点可能存在以下三种情况,对于每种情况使用不同的合并方式。
- 如果两个二叉树的对应节点都为空,则合并后的二叉树的对应节点也为空;
- 如果两个二叉树的对应节点只有一个为空,则合并后的二叉树的对应节点为其中的非空节点;
- 如果两个二叉树的对应节点都不为空,则合并后的二叉树的对应节点的值为两个二叉树的对应节点的值之和,此时需要显性合并两个节点。
时间复杂度:O(min(m,n)),其中m和n分别是两棵树的节点数量。我们需要遍历两棵树中较小的那棵的所有节点。
87.1. 深度优先搜索
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
// 如果t1为空,直接返回t2(包括t2为null的情况)
if (t1 == null) {
return t2;
}
// 如果t2为空,直接返回t1
if (t2 == null) {
return t1;
}
// 创建新节点,值为两个节点值之和
TreeNode merged = new TreeNode(t1.val + t2.val);
// 递归合并左子树
merged.left = mergeTrees(t1.left, t2.left);
// 递归合并右子树
merged.right = mergeTrees(t1.right, t2.right);
// 返回合并后的节点
return merged;
}
}
87.2. 广度优先搜索
class Solution {
public TreeNode mergeTrees(TreeNode t1, TreeNode t2) {
if (t1 == null) {
return t2;
}
if (t2 == null) {
return t1;
}
TreeNode merged = new TreeNode(t1.val + t2.val);
Queue<TreeNode> queue = new LinkedList<TreeNode>();
Queue<TreeNode> queue1 = new LinkedList<TreeNode>();
Queue<TreeNode> queue2 = new LinkedList<TreeNode>();
queue.offer(merged);
queue1.offer(t1);
queue2.offer(t2);
while (!queue1.isEmpty() && !queue2.isEmpty()) {
TreeNode node = queue.poll(), node1 = queue1.poll(), node2 = queue2.poll();
TreeNode left1 = node1.left, left2 = node2.left, right1 = node1.right, right2 = node2.right;
if (left1 != null || left2 != null) {
if (left1 != null && left2 != null) {
TreeNode left = new TreeNode(left1.val + left2.val);
node.left = left;
queue.offer(left);
queue1.offer(left1);
queue2.offer(left2);
} else if (left1 != null) {
node.left = left1;
} else if (left2 != null) {
node.left = left2;
}
}
if (right1 != null || right2 != null) {
if (right1 != null && right2 != null) {
TreeNode right = new TreeNode(right1.val + right2.val);
node.right = right;
queue.offer(right);
queue1.offer(right1);
queue2.offer(right2);
} else if (right1 != null) {
node.right = right1;
} else {
node.right = right2;
}
}
}
return merged;
}
}
88. 字母异位词分组

解题思路:
- 利用排序后的字符串作为字母异位词的统一标识(键)
- 使用哈希表来分组存储具有相同标识的原始字符串
- 最后将哈希表中的所有值(分组结果)转换为列表返回
时间复杂度:O(nklogk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要遍历 n 个字符串,对于每个字符串,需要 O(klogk) 的时间进行排序以及 O(1) 的时间更新哈希表,因此总时间复杂度是 O(nklogk)。
88.1. 排序
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
// 创建一个哈希表,键是排序后的字符串,值是对应的字母异位词列表
Map<String, List<String>> map = new HashMap<String, List<String>>();
// 遍历输入字符串数组中的每个字符串
for (String str : strs) {
// 将字符串转换为字符数组以便排序
char[] array = str.toCharArray();
// 对字符数组进行排序,使得字母异位词会有相同的排序结果
Arrays.sort(array);
// 将排序后的字符数组转换回字符串,作为哈希表的键
String key = new String(array);
// 获取该键对应的列表,如果不存在则创建新列表
List<String> list = map.getOrDefault(key, new ArrayList<String>());
// 将当前字符串添加到对应的列表中
list.add(str);
// 更新哈希表中的键值对
map.put(key, list);
}
// 将哈希表中的所有值(即分组后的字母异位词列表)转换为ArrayList返回
return new ArrayList<List<String>>(map.values());
}
}
88.2. 计数
class Solution {
public List<List<String>> groupAnagrams(String[] strs) {
Map<String, List<String>> map = new HashMap<String, List<String>>();
for (String str : strs) {
int[] counts = new int[26];
int length = str.length();
for (int i = 0; i < length; i++) {
counts[str.charAt(i) - 'a']++;
}
// 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串,作为哈希表的键
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 26; i++) {
if (counts[i] != 0) {
sb.append((char) ('a' + i));
sb.append(counts[i]);
}
}
String key = sb.toString();
List<String> list = map.getOrDefault(key, new ArrayList<String>());
list.add(str);
map.put(key, list);
}
return new ArrayList<List<String>>(map.values());
}
}
89. 路径总和Ⅲ

解题思路:
- 主函数 pathSum:
-
- 处理空树情况,直接返回0。
- 计算以当前节点为起点的路径数(调用rootSum)。
- 递归计算左子树和右子树中的路径数。
- 返回三者之和。
- 辅助函数 rootSum:
-
- 处理空节点情况,返回0。
- 检查当前节点值是否等于目标值,如果是则计数+1。
- 递归计算左右子树中剩余目标值(targetSum - val)的路径数。
- 返回总路径数。
时间复杂度:O(N^2),其中 N 为该二叉树节点的个数。对于每一个节点,求以该节点为起点的路径数目时,则需要遍历以该节点为根节点的子树的所有节点,因此求该路径所花费的最大时间为 O(N),我们会对每个节点都求一次以该节点为起点的路径数目,因此时间复杂度为 O(N^2)。
89.1. 深度优先算法
class Solution {
public int pathSum(TreeNode root, long targetSum) {
// 如果当前节点为空,返回0(递归终止条件)
if (root == null) {
return 0;
}
// 计算以当前节点为起点的路径数
int ret = rootSum(root, targetSum);
// 递归计算左子树中的路径数(不限定起点)
ret += pathSum(root.left, targetSum);
// 递归计算右子树中的路径数(不限定起点)
ret += pathSum(root.right, targetSum);
// 返回总路径数
return ret;
}
public int rootSum(TreeNode root, long targetSum) {
int ret = 0;
// 如果当前节点为空,返回0(递归终止条件)
if (root == null) {
return 0;
}
int val = root.val;
// 如果当前节点值等于目标值,计数+1
if (val == targetSum) {
ret++;
}
// 递归计算左子树中剩余目标值(targetSum - val)的路径数
ret += rootSum(root.left, targetSum - val);
// 递归计算右子树中剩余目标值(targetSum - val)的路径数
ret += rootSum(root.right, targetSum - val);
// 返回以当前节点为起点的总路径数
return ret;
}
}
89.2. 前缀和
class Solution {
public int pathSum(TreeNode root, int targetSum) {
Map<Long, Integer> prefix = new HashMap<Long, Integer>();
prefix.put(0L, 1);
return dfs(root, prefix, 0, targetSum);
}
public int dfs(TreeNode root, Map<Long, Integer> prefix, long curr, int targetSum) {
if (root == null) {
return 0;
}
int ret = 0;
curr += root.val;
ret = prefix.getOrDefault(curr - targetSum, 0);
prefix.put(curr, prefix.getOrDefault(curr, 0) + 1);
ret += dfs(root.left, prefix, curr, targetSum);
ret += dfs(root.right, prefix, curr, targetSum);
prefix.put(curr, prefix.getOrDefault(curr, 0) - 1);
return ret;
}
}
90. 除自身以外数组的乘积

解题思路:
- 对于数组中的每一个元素
nums[i],其对应的结果可以分解为:左侧所有元素的乘积 × 右侧所有元素的乘积。 - 因此,我们可以预先计算每个元素的左侧乘积和右侧乘积,然后将两者相乘得到最终结果。
- 算左边积
-
- 从左到右遍历,
L[i] = L[i-1] * nums[i-1](首元素左边积为1)
- 从左到右遍历,
- 算右边积
-
- 从右到左遍历,
R[i] = R[i+1] * nums[i+1](末元素右边积为1)
- 从右到左遍历,
- 合并结果
-
answer[i] = L[i] * R[i]
时间复杂度:O(N),其中 N 指的是数组 nums 的大小。预处理 L 和 R 数组以及最后的遍历计算都是 O(N) 的时间复杂度。
90.1. 左右乘积列表
class Solution {
public int[] productExceptSelf(int[] nums) {
int length = nums.length;
// L 和 R 分别表示左右两侧的乘积列表
int[] L = new int[length];
int[] R = new int[length];
int[] answer = new int[length];
// L[i] 为索引 i 左侧所有元素的乘积
// 对于索引为 '0' 的元素,因为左侧没有元素,所以 L[0] = 1
L[0] = 1;
for (int i = 1; i < length; i++) {
L[i] = nums[i - 1] * L[i - 1];
}
// R[i] 为索引 i 右侧所有元素的乘积
// 对于索引为 'length-1' 的元素,因为右侧没有元素,所以 R[length-1] = 1
R[length - 1] = 1;
for (int i = length - 2; i >= 0; i--) {
R[i] = nums[i + 1] * R[i + 1];
}
// 对于索引 i,除 nums[i] 之外其余各元素的乘积就是左侧所有元素的乘积乘以右侧所有元素的乘积
for (int i = 0; i < length; i++) {
answer[i] = L[i] * R[i];
}
return answer;
}
}
90.2. 空间复杂度O(1)的方法
class Solution {
public int[] productExceptSelf(int[] nums) {
int length = nums.length;
int[] answer = new int[length];
// answer[i] 表示索引 i 左侧所有元素的乘积
// 因为索引为 '0' 的元素左侧没有元素, 所以 answer[0] = 1
answer[0] = 1;
for (int i = 1; i < length; i++) {
answer[i] = nums[i - 1] * answer[i - 1];
}
// R 为右侧所有元素的乘积
// 刚开始右边没有元素,所以 R = 1
int R = 1;
for (int i = length - 1; i >= 0; i--) {
// 对于索引 i,左边的乘积为 answer[i],右边的乘积为 R
answer[i] = answer[i] * R;
// R 需要包含右边所有的乘积,所以计算下一个结果时需要将当前值乘到 R 上
R *= nums[i];
}
return answer;
}
}
91. 戳气球

解题思路:
- 问题分析:
-
- 戳破气球的顺序会影响最终获得的硬币总数,需要找到最优顺序。
- 采用动态规划(DP)+ 记忆化搜索的方法,逆向思考:考虑最后一个被戳破的气球。
- 关键步骤:
-
- 扩展数组:在原数组首尾添加值为1的虚拟气球,处理边界情况。
- DP定义:
rec[left][right]表示戳破区间(left, right)内所有气球能获得的最大硬币数。 - 递归求解:枚举区间内每个气球作为最后一个戳破的气球,计算分治后的左右区间值加上当前气球的硬币数。
- 优化:
-
- 使用记忆化存储
rec避免重复计算。
- 使用记忆化存储
时间复杂度:O(n^3),其中 n 是气球数量。区间数为 n^2,区间迭代复杂度为 O(n),最终复杂度为 O(n^2×n)=O(n^3)。
91.1. 记忆化搜索
class Solution {
public int[][] rec; // 记忆化数组,存储区间[left][right]的最大硬币数
public int[] val; // 扩展后的气球数组,首尾为1
public int maxCoins(int[] nums) {
int n = nums.length;
val = new int[n + 2]; // 扩展数组,首尾补1
for (int i = 1; i <= n; i++) {
val[i] = nums[i - 1]; // 原数组赋值到扩展数组中间
}
val[0] = val[n + 1] = 1; // 边界虚拟气球值为1
rec = new int[n + 2][n + 2];
for (int i = 0; i <= n + 1; i++) {
Arrays.fill(rec[i], -1); // 初始化记忆化数组为-1(未计算)
}
return solve(0, n + 1); // 求解整个区间[0, n+1]
}
public int solve(int left, int right) {
if (left >= right - 1) {
return 0; // 区间内无气球可戳,返回0
}
if (rec[left][right] != -1) {
return rec[left][right]; // 直接返回已计算的结果
}
for (int i = left + 1; i < right; i++) {
// 分治:最后戳破i,计算左右区间+当前硬币
int sum = val[left] * val[i] * val[right]; // 当前气球硬币
sum += solve(left, i) + solve(i, right); // 左右子区间
rec[left][right] = Math.max(rec[left][right], sum); // 更新最大值
}
return rec[left][right]; // 返回区间最优解
}
}
91.2. 动态规划
class Solution {
public int maxCoins(int[] nums) {
int n = nums.length;
int[][] rec = new int[n + 2][n + 2];
int[] val = new int[n + 2];
val[0] = val[n + 1] = 1;
for (int i = 1; i <= n; i++) {
val[i] = nums[i - 1];
}
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 2; j <= n + 1; j++) {
for (int k = i + 1; k < j; k++) {
int sum = val[i] * val[k] * val[j];
sum += rec[i][k] + rec[k][j];
rec[i][j] = Math.max(rec[i][j], sum);
}
}
}
return rec[0][n + 1];
}
}
92. 最短无序连续子数组

解题思路:
- 问题分析:
-
- 需要找到一个最短的连续子数组,排序该子数组后能使整个数组有序。
- 直接思路:将原数组排序后,比较原数组和排序后数组的差异,确定无序子数组的左右边界。
- 关键步骤:
-
- 检查是否已排序:若数组本身有序,直接返回0。
- 复制并排序数组:得到排序后的数组用于比较。
- 确定左右边界:
-
-
- 从左往右找到第一个不匹配的位置(
left)。 - 从右往左找到第一个不匹配的位置(
right)。
- 从左往右找到第一个不匹配的位置(
-
-
- 计算子数组长度:
right - left + 1。
- 计算子数组长度:
时间复杂度:O(nlogn),其中 n 为给定数组的长度。我们需要 O(nlogn) 的时间进行排序,以及 O(n) 的时间遍历数组,因此总时间复杂度为 O(nlogn)。
92.1. 排序
class Solution {
public int findUnsortedSubarray(int[] nums) {
// 检查数组是否已经有序,若是则直接返回0
if (isSorted(nums)) {
return 0;
}
// 复制原数组并排序,用于比较
int[] numsSorted = new int[nums.length];
System.arraycopy(nums, 0, numsSorted, 0, nums.length);
Arrays.sort(numsSorted);
// 从左往右找到第一个不匹配的位置(左边界)
int left = 0;
while (nums[left] == numsSorted[left]) {
left++;
}
// 从右往左找到第一个不匹配的位置(右边界)
int right = nums.length - 1;
while (nums[right] == numsSorted[right]) {
right--;
}
// 返回无序子数组的长度
return right - left + 1;
}
// 辅助方法:检查数组是否已经有序
public boolean isSorted(int[] nums) {
for (int i = 1; i < nums.length; i++) {
if (nums[i] < nums[i - 1]) {
return false;
}
}
return true;
}
}
92.2. 一次遍历
class Solution {
public int findUnsortedSubarray(int[] nums) {
int n = nums.length;
int maxn = Integer.MIN_VALUE, right = -1;
int minn = Integer.MAX_VALUE, left = -1;
for (int i = 0; i < n; i++) {
if (maxn > nums[i]) {
right = i;
} else {
maxn = nums[i];
}
if (minn < nums[n - i - 1]) {
left = n - i - 1;
} else {
minn = nums[n - i - 1];
}
}
return right == -1 ? 0 : right - left + 1;
}
}
93. 任务调度器

解题思路:
- 问题分析:
-
- 任务列表中有多种任务,相同任务之间需要间隔
n个时间单位。 - 目标是找到完成任务的最短时间,合理安排任务顺序以避免冷却时间冲突。
- 任务列表中有多种任务,相同任务之间需要间隔
- 关键步骤:
- 统计任务频率:使用哈希表记录每种任务的出现次数。
- 初始化任务状态:
-
nextValid记录每个任务下一次可执行的时间。rest记录每个任务的剩余执行次数。
- 模拟时间流逝:
-
- 每次选择剩余次数最多且可执行的任务执行。
- 更新该任务的
nextValid和rest。 - 若无可执行任务,时间直接跳到下一个可执行任务的最早时间。
时间复杂度:O(∣tasks∣⋅∣Σ∣),其中 ∣Σ∣ 是数组 task 中出现任务的种类,在本题中任务用大写字母表示,因此 ∣Σ∣ 不会超过 26。在对 time 的更新进行优化后,每一次遍历中我们都可以安排一个任务,因此会进行 ∣tasks∣ 次遍历,每次遍历的时间复杂度为 O(∣Σ∣),相乘即可得到总时间复杂度。
93.1. 模拟
class Solution {
public int leastInterval(char[] tasks, int n) {
// 统计每个任务的频率
Map<Character, Integer> freq = new HashMap<Character, Integer>();
for (char ch : tasks) {
freq.put(ch, freq.getOrDefault(ch, 0) + 1);
}
int m = freq.size(); // 任务种类数
List<Integer> nextValid = new ArrayList<Integer>(); // 记录每个任务下一次可执行的时间
List<Integer> rest = new ArrayList<Integer>(); // 记录每个任务的剩余执行次数
// 初始化nextValid和rest
for (Map.Entry<Character, Integer> entry : freq.entrySet()) {
nextValid.add(1); // 初始时所有任务都可以在第1个时间执行
rest.add(entry.getValue()); // 剩余次数为任务频率
}
int time = 0; // 当前时间
for (int i = 0; i < tasks.length; ++i) {
++time; // 时间推进
// 找到剩余任务中最早可执行的时间
int minNextValid = Integer.MAX_VALUE;
for (int j = 0; j < m; ++j) {
if (rest.get(j) != 0) {
minNextValid = Math.min(minNextValid, nextValid.get(j));
}
}
time = Math.max(time, minNextValid); // 跳过空闲时间
// 选择剩余次数最多且可执行的任务
int best = -1;
for (int j = 0; j < m; ++j) {
if (rest.get(j) != 0 && nextValid.get(j) <= time) {
if (best == -1 || rest.get(j) > rest.get(best)) {
best = j;
}
}
}
// 更新选中任务的状态
nextValid.set(best, time + n + 1); // 下一次可执行时间为当前时间 + n + 1
rest.set(best, rest.get(best) - 1); // 剩余次数减1
}
return time; // 返回总时间
}
}
93.2. 构造
class Solution {
public int leastInterval(char[] tasks, int n) {
Map<Character, Integer> freq = new HashMap<Character, Integer>();
// 最多的执行次数
int maxExec = 0;
for (char ch : tasks) {
int exec = freq.getOrDefault(ch, 0) + 1;
freq.put(ch, exec);
maxExec = Math.max(maxExec, exec);
}
// 具有最多执行次数的任务数量
int maxCount = 0;
Set<Map.Entry<Character, Integer>> entrySet = freq.entrySet();
for (Map.Entry<Character, Integer> entry : entrySet) {
int value = entry.getValue();
if (value == maxExec) {
++maxCount;
}
}
return Math.max((maxExec - 1) * (n + 1) + maxCount, tasks.length);
}
}
93.3. 贪心算法(编译有问题)
public int leastInterval(char[] tasks, int n) {
//统计每个任务出现的次数,找到出现次数最多的任务
int[] hash = new int[26];
for(int i = 0; i < tasks.length; ++i) {
hash[tasks[i] - 'A'] += 1;
}
Arrays.sort(hash);
//因为相同元素必须有n个冷却时间,假设A出现3次,n = 2,任务要执行完,至少形成AXX AXX A序列(X看作预占位置)
//该序列长度为
int minLen = (n+1) * (hash[25] - 1) + 1;
//此时为了尽量利用X所预占的空间(贪心)使得整个执行序列长度尽量小,将剩余任务往X预占的空间插入
//剩余的任务次数有两种情况:
//1.与A出现次数相同,比如B任务最优插入结果是ABX ABX AB,中间还剩两个空位,当前序列长度+1
//2.比A出现次数少,若还有X,则按序插入X位置,比如C出现两次,形成ABC ABC AB的序列
//直到X预占位置还没插满,剩余元素逐个放入X位置就满足冷却时间至少为n
for(int i = 24; i >= 0; --i){
if(hash[i] == hash[25]) ++ minLen;
}
//当所有X预占的位置插满了怎么办?
//在任意插满区间(这里是ABC)后面按序插入剩余元素,比如ABCD ABCD发现D之间距离至少为n+1,肯定满足冷却条件
//因此,当X预占位置能插满时,最短序列长度就是task.length,不能插满则取最少预占序列长度
return Math.max(minLen, tasks.length);
}
94. 除法求值

时间复杂度:O((N+Q)logA),
- 构建并查集 O(NlogA) ,这里 N 为输入方程 equations 的长度,每一次执行合并操作的时间复杂度是 O(logA),这里 A 是 equations 里不同字符的个数;
- 查询并查集 O(QlogA),这里 Q 为查询数组 queries 的长度,每一次查询时执行「路径压缩」的时间复杂度是 O(logA)。
94.1. 并查集
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class Solution {
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
int equationsSize = equations.size();
UnionFind unionFind = new UnionFind(2 * equationsSize);
// 第 1 步:预处理,将变量的值与 id 进行映射,使得并查集的底层使用数组实现,方便编码
Map<String, Integer> hashMap = new HashMap<>(2 * equationsSize);
int id = 0;
for (int i = 0; i < equationsSize; i++) {
List<String> equation = equations.get(i);
String var1 = equation.get(0);
String var2 = equation.get(1);
if (!hashMap.containsKey(var1)) {
hashMap.put(var1, id);
id++;
}
if (!hashMap.containsKey(var2)) {
hashMap.put(var2, id);
id++;
}
unionFind.union(hashMap.get(var1), hashMap.get(var2), values[i]);
}
// 第 2 步:做查询
int queriesSize = queries.size();
double[] res = new double[queriesSize];
for (int i = 0; i < queriesSize; i++) {
String var1 = queries.get(i).get(0);
String var2 = queries.get(i).get(1);
Integer id1 = hashMap.get(var1);
Integer id2 = hashMap.get(var2);
if (id1 == null || id2 == null) {
res[i] = -1.0d;
} else {
res[i] = unionFind.isConnected(id1, id2);
}
}
return res;
}
private class UnionFind {
private int[] parent;
/**
* 指向的父结点的权值
*/
private double[] weight;
public UnionFind(int n) {
this.parent = new int[n];
this.weight = new double[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
weight[i] = 1.0d;
}
}
public void union(int x, int y, double value) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return;
}
parent[rootX] = rootY;
// 关系式的推导请见「参考代码」下方的示意图
weight[rootX] = weight[y] * value / weight[x];
}
/**
* 路径压缩
*
* @param x
* @return 根结点的 id
*/
public int find(int x) {
if (x != parent[x]) {
int origin = parent[x];
parent[x] = find(parent[x]);
weight[x] *= weight[origin];
}
return parent[x];
}
public double isConnected(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX == rootY) {
return weight[x] / weight[y];
} else {
return -1.0d;
}
}
}
}
作者:LeetCode
链接:https://leetcode.cn/problems/evaluate-division/solutions/548634/399-chu-fa-qiu-zhi-nan-du-zhong-deng-286-w45d/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
95. 最佳买卖股票时机含冷冻期

解题思路:
- 问题分析:
-
- 股票交易问题,带有一天冷冻期限制(卖出后第二天不能买入)。
- 需要动态规划(DP)来跟踪每天的不同状态下的最大收益。
- 状态定义:
-
f0:当前持有股票时的最大收益。f1:当前不持有股票且处于冷冻期(即当天卖出)的最大收益。f2:当前不持有股票且不处于冷冻期(可买入)的最大收益。
- 状态转移:
-
- 持有股票(
newf0):
- 持有股票(
-
-
- 可能是前一天已持有,或者今天买入(需前一天不处于冷冻期)。
-
-
- 冷冻期(
newf1):
- 冷冻期(
-
-
- 当天卖出股票,收益为前一天持有股票的收益加上当天股价。
-
-
- 非冷冻期(
newf2):
- 非冷冻期(
-
-
- 可能是前一天处于冷冻期,或者前一天已处于非冷冻期。
-
- 初始条件:
-
- 第一天只能买入股票,因此
f0 = -prices[0],其他状态初始为0。
- 第一天只能买入股票,因此
时间复杂度:O(n),其中 n 为数组 prices 的长度。
95.1. 解答
class Solution {
public int maxProfit(int[] prices) {
if (prices.length == 0) {
return 0; // 空数组直接返回0
}
int n = prices.length;
// 初始化状态:
// f0: 持有股票的最大收益(第一天只能买入)
// f1: 不持有股票且处于冷冻期的最大收益(初始为0)
// f2: 不持有股票且不处于冷冻期的最大收益(初始为0)
int f0 = -prices[0];
int f1 = 0;
int f2 = 0;
for (int i = 1; i < n; ++i) {
// 计算新状态:
// newf0: 继续持有股票或今天买入(需前一天不处于冷冻期)
int newf0 = Math.max(f0, f2 - prices[i]);
// newf1: 今天卖出股票,进入冷冻期
int newf1 = f0 + prices[i];
// newf2: 保持不持有股票状态(可能是前一天冷冻期或非冷冻期)
int newf2 = Math.max(f1, f2);
// 更新状态
f0 = newf0;
f1 = newf1;
f2 = newf2;
}
// 最后一天不能持有股票,取冷冻期或非冷冻期的最大值
return Math.max(f1, f2);
}
}
96. 把二叉搜索树转为累加树


解题思路:
- 问题分析:
-
- 给定一个二叉搜索树(BST),要求将每个节点的值替换为所有大于或等于该节点值的节点值之和(包括自身)。
- 利用BST的中序遍历特性(左-根-右是升序),反向中序遍历(右-根-左)可以得到降序序列。
- 关键步骤:
-
- 反向中序遍历:先遍历右子树,再处理当前节点,最后遍历左子树。
- 累加和:维护一个全局变量
sum,在遍历过程中累加节点值,并将当前节点的值更新为sum。
- 实现方式:
-
- 递归实现反向中序遍历,确保先处理较大值的节点。
- 在访问每个节点时,先处理右子树,然后更新当前节点的值,最后处理左子树。
时间复杂度:O(n),其中 n 是二叉搜索树的节点数。每一个节点恰好被遍历一次。
96.1. 反序中序遍历
class Solution {
int sum = 0; // 全局变量,用于累加节点值
public TreeNode convertBST(TreeNode root) {
if (root != null) {
convertBST(root.right); // 递归处理右子树(先处理较大值)
sum += root.val; // 累加当前节点值
root.val = sum; // 更新当前节点值为累加和
convertBST(root.left); // 递归处理左子树(后处理较小值)
}
return root; // 返回转换后的树
}
}
96.2. Morris遍历
class Solution {
public TreeNode convertBST(TreeNode root) {
int sum = 0;
TreeNode node = root;
while (node != null) {
if (node.right == null) {
sum += node.val;
node.val = sum;
node = node.left;
} else {
TreeNode succ = getSuccessor(node);
if (succ.left == null) {
succ.left = node;
node = node.right;
} else {
succ.left = null;
sum += node.val;
node.val = sum;
node = node.left;
}
}
}
return root;
}
public TreeNode getSuccessor(TreeNode node) {
TreeNode succ = node.right;
while (succ.left != null && succ.left != node) {
succ = succ.left;
}
return succ;
}
}
97. 根据身高重建队列

解题思路:
- 问题分析:
-
- 需要根据每个人的身高
h_i和前面比他高或相等的人数k_i重新排列队列。 - 高个子的人不受矮个子的人影响,因此应该先处理高个子的人。
- 需要根据每个人的身高
- 关键步骤:
-
- 排序:按身高降序排列,身高相同的按
k_i升序排列。 - 插入队列:根据
k_i的值将每个人插入到队列的指定位置。
- 排序:按身高降序排列,身高相同的按
- 实现方式:
-
- 使用自定义排序对
people数组进行排序。 - 遍历排序后的数组,将每个人插入到
ans列表的k_i位置。
- 使用自定义排序对
时间复杂度:O(n^2),其中 n 是数组 people 的长度。我们需要 O(nlogn) 的时间进行排序,随后需要 O(n^2) 的时间遍历每一个人并将他们放入队列中。由于前者在渐近意义下小于后者,因此总时间复杂度为O(n^2)。
97.1. 从高到低考虑
class Solution {
public int[][] reconstructQueue(int[][] people) {
// 自定义排序:先按身高降序,身高相同则按k_i升序
Arrays.sort(people, new Comparator<int[]>() {
public int compare(int[] person1, int[] person2) {
if (person1[0] != person2[0]) {
return person2[0] - person1[0]; // 身高降序
} else {
return person1[1] - person2[1]; // k_i升序
}
}
});
List<int[]> ans = new ArrayList<int[]>();
// 遍历排序后的数组,按k_i插入到指定位置
for (int[] person : people) {
ans.add(person[1], person); // 在k_i位置插入当前person
}
// 将List转换为二维数组并返回
return ans.toArray(new int[ans.size()][]);
}
}
97.2. 从低到高考虑
class Solution {
public int[][] reconstructQueue(int[][] people) {
Arrays.sort(people, new Comparator<int[]>() {
public int compare(int[] person1, int[] person2) {
if (person1[0] != person2[0]) {
return person1[0] - person2[0];
} else {
return person2[1] - person1[1];
}
}
});
int n = people.length;
int[][] ans = new int[n][];
for (int[] person : people) {
int spaces = person[1] + 1;
for (int i = 0; i < n; ++i) {
if (ans[i] == null) {
--spaces;
if (spaces == 0) {
ans[i] = person;
break;
}
}
}
}
return ans;
}
}
98. 找到所有数组中消失的数字

解题思路:
- 问题分析:
-
- 需要找出数组中缺失的数字,这些数字应在
[1, n]范围内但未出现在数组中。 - 要求时间复杂度为 O(n)O(n),空间复杂度为 O(1)O(1)(不考虑返回结果占用的空间)。
- 需要找出数组中缺失的数字,这些数字应在
- 关键步骤:
-
- 标记出现过的数字:利用数组索引和值的映射关系,通过修改原数组来标记数字是否出现过。
- 收集缺失的数字:遍历数组,未被标记的索引对应的数字即为缺失的数字。
- 实现方式:
-
- 第一次遍历:将数字对应的索引位置的值加上
n,标记该数字出现过。 - 第二次遍历:检查哪些位置的值未被标记(即值仍小于等于
n),这些位置的索引加1即为缺失的数字。
- 第一次遍历:将数字对应的索引位置的值加上
时间复杂度:O(n)。其中 n 是数组 nums 的长度。
98.1. 解答
class Solution {
public List<Integer> findDisappearedNumbers(int[] nums) {
int n = nums.length;
// 第一次遍历:标记出现过的数字
for (int num : nums) {
int x = (num - 1) % n; // 将数字映射到索引范围 [0, n-1]
if (nums[x] <= n) {
nums[x] += n; // 标记该数字出现过(通过增加n)
}
}
List<Integer> ret = new ArrayList<Integer>();
// 第二次遍历:收集未被标记的数字
for (int i = 0; i < n; i++) {
if (nums[i] <= n) {
ret.add(i + 1); // 索引i对应的数字是i+1
}
}
return ret;
}
}
99. 比特位计数

解题思路:
- 问题分析:
-
- 需要计算从0到n的每个整数的二进制表示中1的个数。
- 直接遍历每个数并计算其二进制中1的个数。
- 关键步骤:
-
- 初始化数组:创建一个长度为n+1的数组
bits,用于存储结果。 - 遍历每个数:对于每个数i(0 ≤ i ≤ n),调用
countOnes方法计算其二进制中1的个数。 - 计算1的个数:利用位运算
x &= (x - 1)快速消除最低位的1,直到x变为0。
- 初始化数组:创建一个长度为n+1的数组
- 实现方式:
-
- 使用循环遍历每个数,逐个计算其二进制中1的个数。
- 利用位运算优化计算过程,提高效率。
时间复杂度:O(nlogn)。需要对从 0 到 n 的每个整数使用计算「一比特数」,对于每个整数计算「一比特数」的时间都不会超过 O(logn)。
99.1. Brian Kernighan 算法
class Solution {
public int[] countBits(int n) {
int[] bits = new int[n + 1]; // 初始化结果数组,长度为n+1
for (int i = 0; i <= n; i++) {
bits[i] = countOnes(i); // 计算i的二进制中1的个数并存入数组
}
return bits; // 返回结果数组
}
// 计算整数x的二进制中1的个数
public int countOnes(int x) {
int ones = 0; // 计数器,记录1的个数
while (x > 0) {
x &= (x - 1); // 消除x的最低位的1
ones++; // 每消除一个1,计数器加1
}
return ones; // 返回1的个数
}
}
99.2. 动态规划-最高有效位
class Solution {
public int[] countBits(int n) {
int[] bits = new int[n + 1];
int highBit = 0;
for (int i = 1; i <= n; i++) {
if ((i & (i - 1)) == 0) {
highBit = i;
}
bits[i] = bits[i - highBit] + 1;
}
return bits;
}
}
100. 汉明距离

解题思路:
- 问题分析:
-
- 汉明距离是指两个整数在二进制表示下对应位不同的数量。
- 可以通过异或运算(XOR)快速找到不同的位,然后统计结果中1的个数。
- 关键步骤:
-
- 异或运算:计算
x ^ y,结果中为1的位表示x和y在该位不同。 - 统计1的个数:使用
Integer.bitCount()方法直接统计异或结果中1的个数。
- 异或运算:计算
- 实现方式:
-
- 一行代码解决问题,简洁高效。
- 时间复杂度:O(1)。不同语言的实现方法不一,我们可以近似认为其时间复杂度为 O(1)。
100.1. 内置计数器功能
class Solution {
public int hammingDistance(int x, int y) {
// 1. 对x和y进行异或运算,结果中为1的位表示不同的位
// 2. 使用Integer.bitCount()统计异或结果中1的个数
return Integer.bitCount(x ^ y);
}
}
100.2. 移位实现位计数
class Solution {
public int hammingDistance(int x, int y) {
int s = x ^ y, ret = 0;
while (s != 0) {
ret += s & 1;
s >>= 1;
}
return ret;
}
}
4175

被折叠的 条评论
为什么被折叠?



