当前位置: 首页 > news >正文

快速学完 LeetCode top 1~50 [特殊字符]

文章目录

  • [LeetCode top 1~50](https://leetcode.cn/studyplan/top-100-liked/)
  • 「哈希」
    • [1. 两数之和](https://leetcode.cn/problems/two-sum/)
      • 题目描述
      • 核心思路
      • 题解代码
      • 时空分析
    • [49. 字母异位词分组](https://leetcode.cn/problems/group-anagrams/)
      • 题目描述
      • 核心思路
      • 题解代码
      • 时空分析
    • [128. 最长连续序列](https://leetcode.cn/problems/longest-consecutive-sequence/)
      • 题目描述
      • 核心思路
      • 题解代码
      • 时空分析
  • 「双指针」
    • [283. 移动零](https://leetcode.cn/problems/move-zeroes/)
      • 题目描述
      • 核心思路
      • 题解代码
      • 时空分析
    • [11. 盛最多水的容器](https://leetcode.cn/problems/container-with-most-water/)
      • 题目描述
      • 核心思路
      • 题解代码
      • 时空分析
    • [15. 三数之和](https://leetcode.cn/problems/3sum/)
      • 题目描述
      • 核心思路
      • 题解代码
      • 时空分析
    • [42. 接雨水](https://leetcode.cn/problems/trapping-rain-water/)
      • 题目描述
      • 核心思路
      • 题解代码
      • 时空分析
  • 「滑动窗口」
    • [3. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/)
      • 题目描述
      • 核心思路
      • 题解代码
      • 时空分析
    • [438. 找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/)
      • 题目描述
      • 核心思路
      • 题解代码
  • 「子串」
    • [560. 和为 K 的子数组](https://leetcode.cn/problems/subarray-sum-equals-k/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [239. 滑动窗口最大值](https://leetcode.cn/problems/sliding-window-maximum/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [76. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/)
      • 题目描述
      • 核心思路
      • 题解代码
  • 「普通数组」
    • [53. 最大子数组和](https://leetcode.cn/problems/maximum-subarray/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [56. 合并区间](https://leetcode.cn/problems/merge-intervals/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [189. 轮转数组](https://leetcode.cn/problems/rotate-array/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [238. 除自身以外数组的乘积](https://leetcode.cn/problems/product-of-array-except-self/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [41. 缺失的第一个正数](https://leetcode.cn/problems/first-missing-positive/)
      • 题目描述
      • 核心思路
      • 题解代码
  • 「矩阵」
    • [73. 矩阵置零](https://leetcode.cn/problems/set-matrix-zeroes/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [54. 螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [48. 旋转图像](https://leetcode.cn/problems/rotate-image/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [240. 搜索二维矩阵 II](https://leetcode.cn/problems/search-a-2d-matrix-ii/)
      • 题目描述
      • 核心思路
      • 题解代码
  • 「链表」
    • [160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [206. 反转链表](https://leetcode.cn/problems/reverse-linked-list/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [234. 回文链表](https://leetcode.cn/problems/palindrome-linked-list/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [2. 两数相加](https://leetcode.cn/problems/add-two-numbers/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [19. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [24. 两两交换链表中的节点](https://leetcode.cn/problems/swap-nodes-in-pairs/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [25. K 个一组翻转链表](https://leetcode.cn/problems/reverse-nodes-in-k-group/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [138. 随机链表的复制](https://leetcode.cn/problems/copy-list-with-random-pointer/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [148. 排序链表](https://leetcode.cn/problems/sort-list/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [23. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [146. LRU 缓存](https://leetcode.cn/problems/lru-cache/)
      • 题目描述
      • 核心思路
      • 题解代码
  • 「二叉树」
    • [94. 二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/)
      • 题目描述
      • 核心思路
      • 题解代码
      • 时空分析
    • [104. 二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/)
      • 题目描述
      • 核心思路
      • 题解代码
      • 时空分析
    • [226. 翻转二叉树](https://leetcode.cn/problems/invert-binary-tree/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [101. 对称二叉树](https://leetcode.cn/problems/symmetric-tree/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [543. 二叉树的直径](https://leetcode.cn/problems/diameter-of-binary-tree/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [102. 二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [108. 将有序数组转换为二叉搜索树](https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [98. 验证二叉搜索树](https://leetcode.cn/problems/validate-binary-search-tree/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [230. 二叉搜索树中第 K 小的元素](https://leetcode.cn/problems/kth-smallest-element-in-a-bst/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [199. 二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [114. 二叉树展开为链表](https://leetcode.cn/problems/flatten-binary-tree-to-linked-list/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [437. 路径总和 III](https://leetcode.cn/problems/path-sum-iii/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [236. 二叉树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/)
      • 题目描述
      • 核心思路
      • 题解代码
    • [124. 二叉树中的最大路径和](https://leetcode.cn/problems/binary-tree-maximum-path-sum/)
      • 题目描述
      • 核心思路
      • 题解代码

LeetCode top 1~50

Alt

「哈希」

1. 两数之和

题目描述

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。

你可以按任意顺序返回答案。

核心思路

使用 HashMap 存储 val 到 Index 的映射。

题解代码

class Solution {public int[] twoSum(int[] nums, int target) {// 1.由于返回的是索引,所以需要用一个 Map 存储 val 到 Index 的映射HashMap<Integer, Integer> valToIndex = new HashMap<>();// 2.遍历迭代for(int i = 0; i < nums.length; i++){int need = target - nums[i];if(valToIndex.containsKey(need)){return new int[]{valToIndex.get(need), i};}valToIndex.put(nums[i], i);}return null;}
}

时空分析

  • 时间复杂度:(O(n))
  • 空间复杂度:(O(n))
  • 原因分析
    • 时间复杂度:代码包含一个 for 循环,该循环遍历数组 nums 一次。在每次循环中,执行的操作如计算差值、在 HashMap 中查找和插入元素,平均情况下,这些操作的时间复杂度均为 (O(1))。所以总的时间复杂度为 (O(n)\times O(1)= O(n)) 。
    • 空间复杂度:代码中使用了一个 HashMap 来存储数组中的元素及其索引。在最坏情况下,数组中的所有元素都需要存储到 HashMap 中,因此空间复杂度与数组的长度 n 成正比,即 (O(n)) 。

49. 字母异位词分组

题目描述

给你一个字符串数组,请你将字母异位词组合在一起。可以按任意顺序返回结果列表。

核心思路

数据编码和 HashMap -> 找到一种编码方法,使得字母异位词的编码都相同。找到这种编码方式之后,就可以用一个哈希表存储编码相同的所有异位词,得到最终的答案。

编码方法:利用每个字符出现的次数进行编码。 “10001…000”

题解代码

class Solution {public List<List<String>> groupAnagrams(String[] strs) {// 编码到分组的映射HashMap<String, List<String>> codeToGroup = new HashMap<>();for (String s : strs) {// 对字符串进行编码String code = encode(s);// 把编码相同的字符串放在一起codeToGroup.putIfAbsent(code, new LinkedList<>());codeToGroup.get(code).add(s);}// 获取结果List<List<String>> res = new LinkedList<>();for (List<String> group : codeToGroup.values()) {res.add(group);}return res;}// 利用每个字符的出现次数进行编码String encode(String s) {char[] count = new char[26];for (char c : s.toCharArray()) {int delta = c - 'a';count[delta]++;}return new String(count);}
}

时空分析

  • 时间复杂度:(O(n \cdot k))
  • 空间复杂度:(O(n \cdot k))
  • 原因分析
    • 时间复杂度
      • 外层有一个 for 循环遍历 strs 数组,循环次数为 n,n 是 strs 数组的长度。
      • 对于 strs 中的每一个字符串,都会调用 encode 方法。在 encode 方法中,又有一个 for 循环遍历字符串 s,假设字符串平均长度为 k,这个循环时间复杂度为 (O(k))。
      • 此外,在 groupAnagrams 方法的 for 循环中,每次循环还有平均时间复杂度为 (O(1)) 的 putIfAbsentadd 操作。所以整体时间复杂度为 (O(n)\times O(k + 1)= O(n \cdot k)) 。
    • 空间复杂度
      • 代码中使用了一个 HashMap 来存储编码后的字符串及其对应的字符串列表。在最坏情况下,所有不同的编码字符串都不同,且每个编码字符串对应一个长度为 k 的字符串列表。每个编码字符串长度为 26(常数),假设平均每个编码字符串对应一个长度为 k 的字符串列表,所以 HashMap 占用空间为 (O(n \cdot k))。
      • 此外,res 列表存储所有分组后的结果,在最坏情况下,它也占用 (O(n \cdot k)) 的空间。因此,总的空间复杂度为 (O(n \cdot k)) 。

128. 最长连续序列

题目描述

给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。

请你设计并实现时间复杂度为 O(n) 的算法解决此问题。

核心思路

HashSet -> 想找连续序列,首先要找到这个连续序列的开头元素,然后递增,看看之后有多少个元素还在 nums 中,即可得到最长连续序列的长度了。

题解代码

class Solution {public int longestConsecutive(int[] nums) {// 转化成哈希集合,方便快速查找是否存在某个元素HashSet<Integer> set = new HashSet<Integer>();for (int num : nums) {set.add(num);}int res = 0;for (int num : set) {if (set.contains(num - 1)) {// num 不是连续子序列的第一个,跳过continue;}// num 是连续子序列的第一个,开始向上计算连续子序列的长度int curNum = num;int curLen = 1;while (set.contains(curNum + 1)) {curNum += 1;curLen += 1;}// 更新最长连续序列的长度res = Math.max(res, curLen);}return res;}
}

时空分析

  • 时间复杂度:(O(n))
  • 空间复杂度:(O(n))
  • 原因分析
    • 时间复杂度
      • 首先,有一个 for 循环遍历数组 nums 并将元素添加到 HashSet 中,这个过程的时间复杂度为 (O(n)),因为向 HashSet 中添加元素平均时间复杂度是 (O(1)),总共添加 n 个元素。
      • 然后,又有一个 for 循环遍历 HashSet 中的元素。在这个循环中,对于每个元素,首先检查其前驱元素是否存在,如果不存在,则从该元素开始查找连续序列的长度。虽然这里有一个 while 循环,但对于每个连续序列,只会从序列的起始元素进入 while 循环一次。例如,对于序列 [1, 2, 3],只有 1 会进入 while 循环来计算这个序列的长度。因此,这个 while 循环的总时间复杂度也是 (O(n))。所以整体时间复杂度为 (O(n + n)= O(n)) 。
    • 空间复杂度
      • 代码中使用了一个 HashSet 来存储数组中的元素。在最坏情况下,数组中的所有元素都不相同,HashSet 需要存储 n 个元素,所以空间复杂度为 (O(n))。

「双指针」

283. 移动零

题目描述

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

核心思路

快慢指针 -> 先移除所有 0,然后把最后的元素都置为 0,就相当于移动 0 的效果。

题解代码

class Solution {public void moveZeroes(int[] nums) {// 去除 nums 中的所有 0// 返回去除 0 之后的数组长度int p = removeElement(nums, 0);// 将 p 之后的所有元素赋值为 0for (; p < nums.length; p++) {nums[p] = 0;}}// 双指针技巧,复用 [27. 移除元素] 的解法。int removeElement(int[] nums, int val) {int fast = 0, slow = 0;while (fast < nums.length) {if (nums[fast] != val) {nums[slow] = nums[fast];slow++;}fast++;}return slow;}
}

时空分析

  • 时间复杂度:(O(n))

  • 空间复杂度:(O(1))

  • 原因分析

    • 时间复杂度:代码的核心操作集中在两个方法的循环中:

      • removeElement 方法中,fast 指针从 0 遍历到数组末尾(共 n 次,n 为数组长度),循环内操作均为常数时间 (O(1)),因此该方法时间复杂度为 (O(n));

      • moveZeroes 方法中,除调用 removeElement 外,还包含一个从 (i = r) 到 (i = n-1) 的循环,该循环执行次数为 (n - r)(r 为 removeElement 的返回值),最多为 n 次,时间复杂度为 (O(n))。

        总操作次数为 (O(n) + O(n) = O(n)),忽略常数系数后,整体时间复杂度为 (O(n))。

    • 空间复杂度:两个方法均仅使用固定数量的临时变量(如 fastslowri),未申请额外数组或动态内存,空间占用不随输入规模 n 变化,因此空间复杂度为常数级 (O(1))。

11. 盛最多水的容器

题目描述

给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0)(i, height[i])

找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。

返回容器可以储存的最大水量。

说明: 你不能倾斜容器。

核心思路

左右指针 -> 用 leftright 两个指针从两端向中心收缩,一边收缩一边计算 [left, right] 之间的矩形面积,取最大的面积值即是答案。

题解代码

class Solution {public int maxArea(int[] height) {int left = 0, right = height.length - 1;int res = 0;while (left < right) {// [left, right] 之间的矩形面积int cur_area = Math.min(height[left], height[right]) * (right - left);res = Math.max(res, cur_area);// 双指针技巧,移动较低的一边if (height[left] < height[right]) {left++;} else {right--;}}return res;}
}

时空分析

  • 时间复杂度:(O(n))

  • 空间复杂度:(O(1))

  • 原因分析

    • 时间复杂度:代码使用双指针法,核心是一个 while 循环:

      • 初始时左指针 left = 0,右指针 right = n - 1(n 为数组长度);

      • 每次循环中,指针只会向中间移动一次(left++right--);

      • 循环终止条件为 left >= right,最多执行(n-1)次(指针从两端移动到相邻位置);

      • 循环内部操作(计算面积、更新结果、移动指针)均为常数时间 (O(1))。

        总操作次数为 (O(n)),因此时间复杂度为 (O(n))。

    • 空间复杂度:仅使用固定数量的临时变量(leftrightresarea),未申请额外的数组或动态内存空间,空间占用不随输入规模 n 变化,因此空间复杂度为常数级 (O(1))。

15. 三数之和

题目描述

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。

注意: 答案中不可以包含重复的三元组。

核心思路

nSumTarget 模版。

排序 + 双指针 -> 先给数组从小到大排序,然后双指针 lohi 分别在数组开头和结尾,这样就可以控制 nums[lo]nums[hi] 这两数之和的大小。如果你想让它俩的和大一些,就让 lo++,如果你想让它俩的和小一些,就让 hi--

题解代码

class Solution {public List<List<Integer>> threeSum(int[] nums) {Arrays.sort(nums);// n 为 3,从 nums[0] 开始计算和为 0 的三元组return nSumTarget(nums, 3, 0, 0);}// 注意:调用这个函数之前一定要先给 nums 排序// n 填写想求的是几数之和,start 从哪个索引开始计算(一般填 0),target 填想凑出的目标和List<List<Integer>> nSumTarget(int[] nums, int n, int start, long target) {int sz = nums.length;List<List<Integer>> res = new ArrayList<>();// 至少是 2Sum,且数组大小不应该小于 nif (n < 2 || sz < n) return res;// 2Sum 是 base caseif (n == 2) {// 双指针那一套操作int lo = start, hi = sz - 1;while (lo < hi) {int sum = nums[lo] + nums[hi];int left = nums[lo], right = nums[hi];if (sum < target) {while (lo < hi && nums[lo] == left) lo++;} else if (sum > target) {while (lo < hi && nums[hi] == right) hi--;} else {res.add(new ArrayList<>(Arrays.asList(left, right)));while (lo < hi && nums[lo] == left) lo++;while (lo < hi && nums[hi] == right) hi--;}}} else {// n > 2 时,递归计算 (n-1)Sum 的结果for (int i = start; i < sz; i++) {List<List<Integer>> sub = nSumTarget(nums, n - 1, i + 1, target - nums[i]);for (List<Integer> arr : sub) {// (n-1)Sum 加上 nums[i] 就是 nSumarr.add(nums[i]);res.add(arr);}while (i < sz - 1 && nums[i] == nums[i + 1]) i++;}}return res;}
}

时空分析

  • 时间复杂度:(O(n^2))

  • 空间复杂度:(O(\log n))

  • 原因分析:

    • 时间复杂度

      • 首先对数组进行排序,排序操作的时间复杂度为 (O(n \log n))(其中 n 为数组长度)。
      • 核心逻辑通过递归实现 nSum 计算,对于本题的 threeSum(即 (n = 3)):
        • 外层有一个遍历数组的循环,时间复杂度为 (O(n));
        • 每次循环内部调用 2Sum 逻辑,2Sum 通过双指针实现,时间复杂度为 (O(n));
        • 因此 3Sum 的整体计算复杂度为 (O(n \times n) = O(n^2))。
      • 由于 (O(n^2)) 的增长速度快于排序的 (O(n \log n)),最终时间复杂度由 (O(n^2)) 主导。
    • 空间复杂度

      • 主要来源于两部分:

        • 排序过程中(Java 的 Arrays.sort 对基本类型使用双轴快排)的递归栈空间,复杂度为 (O(\log n));
        • 递归调用 nSumTarget 的栈深度,对于 threeSum 仅从 3Sum 递归到 2Sum,深度为常数级((O(1)))。
      • 因此整体空间复杂度由排序的递归栈空间主导,为 (O(\log n))。

42. 接雨水

题目描述

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

核心思路

对于任意一个位置 i,能够装的水为:

water[i] = min(# 左边最高的柱子max(height[0..i]),# 右边最高的柱子max(height[i..end])) - height[i]

然后将这些位置求和即可。

题解代码

class Solution {public int trap(int[] height) {if (height.length == 0) {return 0;}int n = height.length;int res = 0;// 数组充当备忘录int[] l_max = new int[n];int[] r_max = new int[n];// 初始化 base casel_max[0] = height[0];r_max[n - 1] = height[n - 1];// 从左向右计算 l_maxfor (int i = 1; i < n; i++)l_max[i] = Math.max(height[i], l_max[i - 1]);// 从右向左计算 r_maxfor (int i = n - 2; i >= 0; i--)r_max[i] = Math.max(height[i], r_max[i + 1]);// 计算答案for (int i = 1; i < n - 1; i++)res += Math.min(l_max[i], r_max[i]) - height[i];return res;}
}

时空分析

  • 时间复杂度:(O(n))

  • 空间复杂度:(O(n))

  • 原因分析

    • 时间复杂度:代码包含三个独立的线性循环,均基于数组长度 n 执行:

      • 第一个循环(计算左侧最大值数组 (l_max))从 (i = 1) 到 (i = n-1),共执行 (n-1) 次;

      • 第二个循环(计算右侧最大值数组 (r_max))从 (j = n-2) 到 (j = 0),共执行 (n-1) 次;

      • 第三个循环(累加总接水量)从 (i = 0) 到 (i = n-1),共执行 n 次。

        总操作次数为 ((n-1) + (n-1) + n = 3n - 2),根据大 O 表示法的定义,忽略常数项和系数后,时间复杂度为(O(n))。

    • 空间复杂度:额外使用了两个长度为 n 的数组 (l_max) 和 (r_max),用于存储每个位置的左侧最大值和右侧最大值,总额外空间为 2n;其他变量(如 n、i、j、res)仅占用常数空间。忽略系数后,空间复杂度为 (O(n))。


「滑动窗口」

3. 无重复字符的最长子串

题目描述

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

核心思路

用一个 HashMap(CharToCount) 统计窗口中出现的字符的数量。当 window[c] 值大于 1 时,说明窗口中存在重复字符,不符合条件,就该移动 left 缩小窗口了。

题解代码

class Solution {public int lengthOfLongestSubstring(String s) {Map<Character, Integer> window = new HashMap<>();int left = 0, right = 0;// 记录结果int res = 0;while (right < s.length()) {char c = s.charAt(right);right++;// 进行窗口内数据的一系列更新window.put(c, window.getOrDefault(c, 0) + 1);// 判断左侧窗口是否要收缩while (window.get(c) > 1) {char d = s.charAt(left);left++;// 进行窗口内数据的一系列更新window.put(d, window.get(d) - 1);}// 在这里更新答案res = Math.max(res, right - left);}return res;}
}

时空分析

  • 时间复杂度:(O(n))
  • 空间复杂度:(O(min(n, m))),其中 n 是字符串 s 的长度,m 是字符集的大小
  • 原因分析
    • 时间复杂度
      • 代码使用滑动窗口算法,通过 while 循环遍历字符串 sright 指针从字符串开头开始,逐步向右移动,最多移动 n 次,其中 n 是字符串 s 的长度。
      • 对于每个 right 指针移动,虽然存在一个内层 while 循环用于收缩左侧窗口,但从整体来看,left 指针也最多向右移动 n 次。也就是说,两个指针移动的总次数最多为 2n 次。因此,整体时间复杂度为 (O(n))。
    • 空间复杂度
      • 代码中使用了一个 HashMap 来存储窗口内的字符及其出现的次数。在最坏情况下,窗口内可能包含所有不同的字符。如果字符集大小为 m,那么 HashMap 最多会存储 m 个不同的字符。当 (m < n) 时,空间复杂度为 (O(m));当 (m \geq n) 时,窗口内最多只能容纳 n 个不同字符,此时空间复杂度为 (O(n))。所以空间复杂度为 (O(min(n, m)))。

438. 找到字符串中所有字母异位词

题目描述

给定两个字符串 sp,找到 s 中所有 p异位词 的子串,返回这些子串的起始索引。不考虑答案输出的顺序。

核心思路

先用一个 HashMap 统计 p 中各个字符出现的次数,need。然后在用一个 HashMap 统计 window 中各字符出现的次数。如果 window 满足了 need(用一个 valid 实现),就将索引添加到结果列表中。

什么时候扩大窗口? right 一直往右遍历。

什么时候缩小窗口? right - left >= p.length() 时缩小窗口。

什么时候更新答案? 当缩小窗口时,如果满足 valid 则更新答案。

note:Integer 包装器类型,判断值是否相等要使用 .equals()

题解代码

class Solution {public List<Integer> findAnagrams(String s, String t) {Map<Character, Integer> need = new HashMap<>();Map<Character, Integer> window = new HashMap<>();for (char c : t.toCharArray()) {need.put(c, need.getOrDefault(c, 0) + 1);}int left = 0, right = 0;int valid = 0;// 记录结果List<Integer> res = new ArrayList<>();while (right < s.length()) {char c = s.charAt(right);right++;// 进行窗口内数据的一系列更新if (need.containsKey(c)) {window.put(c, window.getOrDefault(c, 0) + 1);if (window.get(c).equals(need.get(c))) {valid++;}}// 判断左侧窗口是否要收缩while (right - left >= t.length()) {// 当窗口符合条件时,把起始索引加入 resif (valid == need.size()) {res.add(left);}char d = s.charAt(left);left++;// 进行窗口内数据的一系列更新if (need.containsKey(d)) {if (window.get(d).equals(need.get(d))) {valid--;}window.put(d, window.get(d) - 1);}}}return res;}
}

「子串」

560. 和为 K 的子数组

题目描述

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数

子数组是数组中元素的连续非空序列。

核心思路

前缀和 和 HashMap -> 需要在维护 preSum 前缀和数组的同时动态维护 count 映射,而不能等到 preSum 计算完成后再处理 count,因为 count[need] 应该维护 preSum[0..i] 中值为 need 的元素个数。

题解代码

class Solution {public int subarraySum(int[] nums, int k) {int n = nums.length;// 前缀和数组int[] preSum = new int[n + 1];preSum[0] = 0;// 前缀和到该前缀和出现次数的映射,方便快速查找所需的前缀和HashMap<Integer, Integer> count = new HashMap<>();count.put(0, 1);// 记录和为 k 的子数组个数int res = 0;// 计算 nums 的前缀和for (int i = 1; i <= n; i++) {preSum[i] = preSum[i - 1] + nums[i - 1];// 如果之前存在值为 need 的前缀和// 说明存在以 nums[i-1] 结尾的子数组的和为 kint need = preSum[i] - k;if (count.containsKey(need)) {res += count.get(need);}// 将当前前缀和存入哈希表if (!count.containsKey(preSum[i])) {count.put(preSum[i], 1);} else {count.put(preSum[i], count.get(preSum[i]) + 1);}}return res;}
}

239. 滑动窗口最大值

题目描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

核心思路

单调队列作为滑动窗口。

使用一个队列充当不断滑动的窗口,每次滑动记录其中的最大值:

Alt

如何在 O(1) 时间计算最大值,只需要一个特殊的数据结构「单调队列」,push 方法依然在队尾添加元素,但是要把前面比自己小的元素都删掉,直到遇到更大的元素才停止删除。

Alt

题解代码

class Solution {// 单调队列的实现class MonotonicQueue {LinkedList<Integer> q = new LinkedList<>();public void push(int n) {// 将小于 n 的元素全部删除while (!q.isEmpty() && q.getLast() < n) {q.pollLast();}// 然后将 n 加入尾部q.addLast(n);}public int max() {return q.getFirst();}public void pop(int n) {if (n == q.getFirst()) {q.pollFirst();}}}// 解题函数的实现public int[] maxSlidingWindow(int[] nums, int k) {MonotonicQueue window = new MonotonicQueue();List<Integer> res = new ArrayList<>();for (int i = 0; i < nums.length; i++) {if (i < k - 1) {// 先填满窗口的前 k - 1window.push(nums[i]);} else {// 窗口向前滑动,加入新数字window.push(nums[i]);// 记录当前窗口的最大值res.add(window.max());// 移出旧数字window.pop(nums[i - k + 1]);}}// 需要转成 int[] 数组再返回int[] arr = new int[res.size()];for (int i = 0; i < res.size(); i++) {arr[i] = res.get(i);}return arr;}
}

76. 最小覆盖子串

题目描述

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

核心思路

滑动窗口 -> 标准滑动窗口框架,用两个 HashMap 分别充当 window 和 need,统计每个字符的数量。

什么时候扩大窗口?right < n 时扩大窗口。

什么时候缩小窗口?当 valid >= need.size() 时缩小窗口,也就是窗口中覆盖了 t 的所有字符。

什么时候更新答案?当缩小窗口的时候更新答案。

题解代码

class Solution {public String minWindow(String s, String t) {Map<Character, Integer> need = new HashMap<>();Map<Character, Integer> window = new HashMap<>();for (char c : t.toCharArray()) {need.put(c, need.getOrDefault(c, 0) + 1);}int left = 0, right = 0;int valid = 0;// 记录最小覆盖子串的起始索引及长度int start = 0, len = Integer.MAX_VALUE;while (right < s.length()) {// c 是将移入窗口的字符char c = s.charAt(right);// 扩大窗口right++;// 进行窗口内数据的一系列更新if (need.containsKey(c)) {window.put(c, window.getOrDefault(c, 0) + 1);if (window.get(c).equals(need.get(c)))valid++;}// 判断左侧窗口是否要收缩while (valid == need.size()) {// 在这里更新最小覆盖子串if (right - left < len) {start = left;len = right - left;}// d 是将移出窗口的字符char d = s.charAt(left);// 缩小窗口left++;// 进行窗口内数据的一系列更新if (need.containsKey(d)) {if (window.get(d).equals(need.get(d)))valid--;window.put(d, window.get(d) - 1);}                    }}// 返回最小覆盖子串return len == Integer.MAX_VALUE ? "" : s.substring(start, start + len);}
}

「普通数组」

53. 最大子数组和

题目描述

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

核心思路

动态规划 -> dp 数组的含义:nums[i] 为结尾的「最大子数组和」为 dp[i]dp[i] 有两种「选择」,要么与前面的相邻子数组连接,形成一个和更大的子数组;要么不与前面的子数组连接,自成一派,自己作为一个子数组。在这两种选择中择优,就可以计算出最大子数组。

滑动窗口 -> right < n 时扩大窗口,windowSum < 0 时缩小窗口,扩大窗口的时候同时更新答案。

题解代码

class Solution {public int maxSubArray(int[] nums) {int n = nums.length;int[] dp = new int[n];// base case// 第一个元素前面没有子数组dp[0] = nums[0];// 状态转移方程for (int i = 1; i < n; i++) {dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);}// 得到 nums 的最大子数组int res = Integer.MIN_VALUE;for (int i = 0; i < n; i++) {res = Math.max(res, dp[i]);}return res;}
}
class Solution {public int maxSubArray(int[] nums) {int n = nums.length;int windowSum = 0;int left = 0, right = 0;int res = Integer.MIN_VALUE;while(right < n){windowSum += nums[right];right++;res = Math.max(res, windowSum);while(windowSum < 0){windowSum -= nums[left];left++;}}return res;}
}

56. 合并区间

题目描述

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

核心思路

技巧 -> 一个区间可以表示为 [start, end],先按区间的 start 排序。对于几个相交区间合并后的结果区间 xx.start 一定是这些相交区间中 start 最小的,x.end 一定是这些相交区间中 end 最大的。

题解代码

class Solution {public int[][] merge(int[][] intervals) {LinkedList<int[]> res = new LinkedList<>();// 按区间的 start 升序排列Arrays.sort(intervals, (a, b) -> {return a[0] - b[0];});res.add(intervals[0]);for (int i = 1; i < intervals.length; i++) {int[] curr = intervals[i];// res 中最后一个元素的引用int[] last = res.getLast();if (curr[0] <= last[1]) {last[1] = Math.max(last[1], curr[1]);} else {// 处理下一个待合并区间res.add(curr);}}return res.toArray(new int[0][0]);}
}

189. 轮转数组

题目描述

给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。

核心思路

技巧 -> 先把整个数组反转,再分别反转前 k 个和后 n-k 个即可。

例如 [1,2,3,4,5], k = 3, 先反转整个数组得 [5,4,3,2,1], 再反转前 k 个得 [3,4,5,2,1], 再反转后 n-k 个得 [3,4,5,1,2] 即为答案。

note:k 需要小于 n。(取模)

题解代码

class Solution {public void rotate(int[] nums, int k) {int n = nums.length;k %= n;reverse(nums, 0, n - 1);reverse(nums, 0, k - 1);reverse(nums, k, n - 1);}public void reverse(int[] nums, int start, int end){while(start < end){int temp = nums[start];nums[start] = nums[end];nums[end] = temp;start++;end--;}}
}

238. 除自身以外数组的乘积

题目描述

给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。

题目数据 保证 数组 nums 之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。

不要使用除法, 且在 O(n) 时间复杂度内完成此题。

核心思路

前缀积 -> 前缀和数组中两个元素之差是子数组元素之和,那么如果构造「前缀积」数组,两个元素相除就是子数组元素之积。

构造一个 prefix 数组记录「前缀积」,再用一个 suffix 记录「后缀积」,根据前缀和后缀积就能计算除了当前元素之外其他元素的积。

题解代码

class Solution {public int[] productExceptSelf(int[] nums) {int n = nums.length;// 从左到右的前缀积,prefix[i] 是 nums[0..i] 的元素积int[] prefix = new int[n];prefix[0] = nums[0];for (int i = 1; i < nums.length; i++) {prefix[i] = prefix[i - 1] * nums[i];}// 从右到左的前缀积,suffix[i] 是 nums[i..n-1] 的元素积int[] suffix = new int[n];suffix[n - 1] = nums[n - 1];for (int i = n - 2; i >= 0; i--) {suffix[i] = suffix[i + 1] * nums[i];}// 结果数组int[] res = new int[n];res[0] = suffix[1];res[n - 1] = prefix[n - 2];for (int i = 1; i < n - 1; i++) {// 除了 nums[i] 自己的元素积就是 nums[i] 左侧和右侧所有元素之积res[i] = prefix[i - 1] * suffix[i + 1];}return res;}
}

41. 缺失的第一个正数

题目描述

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。

请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。

核心思路

对于一个长度为 N 的数组,其中没有出现的最小正整数只能在 [1, N+1] 中。这是因为如果 [1, N] 都出现了,那么答案是 N+1,否则答案是 [1, N] 中没有出现的最小正整数。

数组设计成哈希表 -> 我们对数组进行遍历,对于遍历到的数 x,如果它在 [1, N] 的范围内,那么就将数组中的第 x−1 个位置(注意:数组下标从 0 开始)打上「标记」。在遍历结束之后,如果所有的位置都被打上了标记,那么答案是 N+1,否则答案是最小的没有打上标记的位置加 1。

算法的流程如下:

  • 我们将数组中所有小于等于 0 的数修改为 N+1;
  • 我们遍历数组中的每一个数 x,它可能已经被打了标记,因此原本对应的数为 ∣x∣,其中 ∣∣ 为绝对值符号。如果 ∣x∣∈ [1, N],那么我们给数组中的第 ∣x∣−1 个位置的数添加一个负号。注意如果它已经有负号,不需要重复添加;
  • 在遍历完成之后,如果数组中的每一个数都是负数,那么答案是 N+1,否则答案是第一个正数的位置加 1。

fig1

题解代码

class Solution {public int firstMissingPositive(int[] nums) {int n = nums.length;for (int i = 0; i < n; i++) {if (nums[i] <= 0) {nums[i] = n + 1;}}for (int i = 0; i < n; i++) {int num = Math.abs(nums[i]);if (1 <= num && num <= n) {nums[num - 1] = -Math.abs(nums[num - 1]);}}for (int i = 0; i < n; i++) {if (nums[i] > 0) {return i + 1;}}return n + 1;}
}

「矩阵」

73. 矩阵置零

题目描述

给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用原地算法。

核心思路

二维数组遍历 -> 用两个标记数组分别记录每一行和每一列是否有零出现 -> 首先遍历数组一次,如果某个元素为 0,那么就将该元素所在的行和列所对应标记数组的位置置为 true。最后再次遍历该数组,用标记数组更新原数组即可。

题解代码

class Solution {public void setZeroes(int[][] matrix) {int m = matrix.length, n = matrix[0].length;boolean[] row = new boolean[m];boolean[] col = new boolean[n];for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (matrix[i][j] == 0) {row[i] = col[j] = true;}}}for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (row[i] || col[j]) {matrix[i][j] = 0;}}}}
}

54. 螺旋矩阵

题目描述

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

核心思路

Alt

二维数组遍历 -> 按照 右、下、左、上 的顺序遍历数组,并使用四个变量圈定未遍历元素的边界。

题解代码

class Solution {public List<Integer> spiralOrder(int[][] matrix) {int m = matrix.length, n = matrix[0].length;int upper_bound = 0, lower_bound = m - 1;int left_bound = 0, right_bound = n - 1;List<Integer> res = new LinkedList<>();// res.size() == m * n 则遍历完整个数组while (res.size() < m * n) {if (upper_bound <= lower_bound) {// 在顶部从左向右遍历for (int j = left_bound; j <= right_bound; j++) {res.add(matrix[upper_bound][j]);}// 上边界下移upper_bound++;}if (left_bound <= right_bound) {// 在右侧从上向下遍历for (int i = upper_bound; i <= lower_bound; i++) {res.add(matrix[i][right_bound]);}// 右边界左移right_bound--;}if (upper_bound <= lower_bound) {// 在底部从右向左遍历for (int j = right_bound; j >= left_bound; j--) {res.add(matrix[lower_bound][j]);}// 下边界上移lower_bound--;}if (left_bound <= right_bound) {// 在左侧从下向上遍历for (int i = lower_bound; i >= upper_bound; i--) {res.add(matrix[i][left_bound]);}// 左边界右移left_bound++;}}return res;}
}

48. 旋转图像

题目描述

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

核心思路

二维数组遍历 -> 先把二维矩阵沿对角线反转,然后反转矩阵的每一行,结果就是顺时针反转整个矩阵。

题解代码

class Solution {public void rotate(int[][] matrix) {int n = matrix.length;// 先沿对角线反转二维矩阵for (int i = 0; i < n; i++) {for (int j = i; j < n; j++) {// swap(matrix[i][j], matrix[j][i]);int temp = matrix[i][j];matrix[i][j] = matrix[j][i];matrix[j][i] = temp;}}// 然后反转二维矩阵的每一行for (int[] row : matrix) {reverse(row);}}// 反转一维数组void reverse(int[] arr) {int i = 0, j = arr.length - 1;while (j > i) {// swap(arr[i], arr[j]);int temp = arr[i];arr[i] = arr[j];arr[j] = temp;i++;j--;}}
}

240. 搜索二维矩阵 II

题目描述

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:

  • 每行的元素从左到右升序排列。
  • 每列的元素从上到下升序排列。

核心思路

二维数组遍历 -> 从右上角开始遍历,规定只能向左或向下移动。如果向左移动,元素在减小,如果向下移动,元素在增大,这样的话就可以根据当前位置的元素和 target 的相对大小来判断应该往哪移动,不断接近从而找到 target 的位置。

题解代码

class Solution {public boolean searchMatrix(int[][] matrix, int target) {int m = matrix.length, n = matrix[0].length;// 初始化在右上角int i = 0, j = n - 1;while (i < m && j >= 0) {if (matrix[i][j] == target) {return true;}if (matrix[i][j] < target) {// 需要大一点,往下移动i++;} else {// 需要小一点,往左移动j--;}}// while 循环中没有找到,则 target 不存在return false;}
}

「链表」

160. 相交链表

题目描述

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

图示两个链表在节点 c1 开始相交:

Alt

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

核心思路

两个指针 p1p2 分别在两条链表上前进,我们可以让 p1 遍历完链表 A 之后开始遍历链表 B,让 p2 遍历完链表 B 之后开始遍历链表 A,这样相当于「逻辑上」两条链表接在了一起。

如果这样进行拼接,就可以让 p1p2 同时进入公共部分,也就是同时到达相交节点 c1

image-20250919160111148

题解代码

public class Solution {public ListNode getIntersectionNode(ListNode headA, ListNode headB) {// p1 指向 A 链表头结点,p2 指向 B 链表头结点ListNode p1 = headA, p2 = headB;while (p1 != p2) {// p1 走一步,如果走到 A 链表末尾,转到 B 链表if (p1 == null) p1 = headB;else            p1 = p1.next;// p2 走一步,如果走到 B 链表末尾,转到 A 链表if (p2 == null) p2 = headA;else            p2 = p2.next;}return p1;}
}

206. 反转链表

题目描述

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

核心思路

迭代 -> 三个结点 cur,pre 与 next。

题解代码

class Solution {public ListNode reverseList(ListNode head) {ListNode cur = head, pre = null;while(cur != null){ListNode next = cur.next;cur.next = pre;pre = cur;cur = next;}return pre;}
}

234. 回文链表

题目描述

给你一个单链表的头节点 head ,请你判断该链表是否为回文链表。如果是,返回 true ;否则,返回 false

核心思路

Alt

  1. 先通过快慢指针找到链表的中点。(注意奇偶长度)
  2. 从 slow 开始反转后面的链表,现在就可以开始比较回文串了。

题解代码

class Solution {public boolean isPalindrome(ListNode head) {ListNode slow, fast;slow = fast = head;while (fast != null && fast.next != null) {slow = slow.next;fast = fast.next.next;}if (fast != null)slow = slow.next;ListNode left = head;ListNode right = reverse(slow);while (right != null) {if (left.val != right.val)return false;left = left.next;right = right.next;}return true;}ListNode reverse(ListNode head) {ListNode pre = null, cur = head;while (cur != null) {ListNode next = cur.next;cur.next = pre;pre = cur;cur = next;}return pre;}
}

141. 环形链表

题目描述

给你一个链表的头节点 head ,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。

如果链表中存在环 ,则返回 true 。 否则,返回 false

核心思路

快慢指针 -> 每当慢指针 slow 前进一步,快指针 fast 就前进两步。如果 fast 最终遇到空指针,说明链表中没有环;如果 fast 最终和 slow 相遇,那肯定是 fast 超过了 slow 一圈,说明链表中含有环。

题解代码

public class Solution {public boolean hasCycle(ListNode head) {// 快慢指针初始化指向 headListNode slow = head, fast = head;// 快指针走到末尾时停止while (fast != null && fast.next != null) {// 慢指针走一步,快指针走两步slow = slow.next;fast = fast.next.next;// 快慢指针相遇,说明含有环if (slow == fast) {return true;}}// 不包含环return false;}
}

142. 环形链表 II

题目描述

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

核心思路

基于环形链表的解法,直观地来说就是当快慢指针相遇时,让其中任一个指针指向头节点,然后让它俩以相同速度前进,再次相遇时所在的节点位置就是环开始的位置。

题解代码

class Solution {public ListNode detectCycle(ListNode head) {ListNode fast, slow;fast = slow = head;while (fast != null && fast.next != null) {fast = fast.next.next;slow = slow.next;if (fast == slow) break;}// 上面的代码类似 hasCycle 函数if (fast == null || fast.next == null) {// fast 遇到空指针说明没有环return null;}// 重新指向头结点slow = head;// 快慢指针同步前进,相交点就是环起点while (slow != fast) {fast = fast.next;slow = slow.next;}return slow;}
}

21. 合并两个有序链表

题目描述

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

核心思路

双指针和虚拟头结点

Alt

题解代码

class Solution {public ListNode mergeTwoLists(ListNode l1, ListNode l2) {// 虚拟头结点ListNode dummy = new ListNode(-1), p = dummy;ListNode p1 = l1, p2 = l2;while (p1 != null && p2 != null) {// 比较 p1 和 p2 两个指针// 将值较小的的节点接到 p 指针if (p1.val > p2.val) {p.next = p2;p2 = p2.next;} else {p.next = p1;p1 = p1.next;}// p 指针不断前进p = p.next;}if (p1 != null) {p.next = p1;}if (p2 != null) {p.next = p2;}return dummy.next;}
}

2. 两数相加

题目描述

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

核心思路

image-20250921204244604

用一个 carry 变量记录进位,然后通过 (valA + valB + carry) % 10 计算当前位的值,通过 (valA + valB + carry) / 10 计算进位。

题解代码

class Solution {public ListNode addTwoNumbers(ListNode l1, ListNode l2) {// 在两条链表上的指针ListNode p1 = l1, p2 = l2;// 虚拟头结点(构建新链表时的常用技巧)ListNode dummy = new ListNode(-1);// 指针 p 负责构建新链表ListNode p = dummy;// 记录进位int carry = 0;// 开始执行加法,两条链表走完且没有进位时才能结束循环while (p1 != null || p2 != null || carry > 0) {// 先加上上次的进位int val = carry;if (p1 != null) {val += p1.val;p1 = p1.next;}if (p2 != null) {val += p2.val;p2 = p2.next;}// 处理进位情况carry = val / 10;val = val % 10;// 构建新节点p.next = new ListNode(val);p = p.next;}// 返回结果链表的头结点(去除虚拟头结点)return dummy.next;}
}

19. 删除链表的倒数第 N 个结点

题目描述

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

核心思路

要删除倒数第 n 个节点,就得获得倒数第 n + 1 个节点的引用。

第一步,我们先让一个指针 p1 指向链表的头节点 head,然后走 k 步。

第二步,用一个指针 p2 指向链表头节点 head

第三步,让 p1p2 同时向前走,p1 走到链表末尾的空指针时走了 n - k 步,p2 也走了 n - k 步,也就是链表的倒数第 k 个节点。

这样,只遍历了一次链表,就获得了倒数第 k 个节点 p2

题解代码

class Solution {// 主函数public ListNode removeNthFromEnd(ListNode head, int n) {// 虚拟头结点ListNode dummy = new ListNode(-1);dummy.next = head;// 删除倒数第 n 个,要先找倒数第 n + 1 个节点ListNode x = findFromEnd(dummy, n + 1);// 删掉倒数第 n 个节点x.next = x.next.next;return dummy.next;}// 返回链表的倒数第 k 个节点ListNode findFromEnd(ListNode head, int k) {ListNode p1 = head;// p1 先走 k 步for (int i = 0; i < k; i++) {p1 = p1.next;}ListNode p2 = head;// p1 和 p2 同时走 n - k 步while (p1 != null) {p2 = p2.next;p1 = p1.next;}// p2 现在指向第 n - k 个节点return p2;}
}

24. 两两交换链表中的节点

题目描述

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

核心思路

递归

题解代码

class Solution {// 定义:输入以 head 开头的单链表,将这个单链表中的每两个元素翻转,// 返回翻转后的链表头结点public ListNode swapPairs(ListNode head) {if (head == null || head.next == null) {return head;}ListNode first = head;ListNode second = head.next;ListNode others = head.next.next;// 先把前两个元素翻转second.next = first;// 利用递归定义,将剩下的链表节点两两翻转,接到后面first.next = swapPairs(others);// 现在整个链表都成功翻转了,返回新的头结点return second;}
}

25. K 个一组翻转链表

题目描述

给你链表的头节点 head ,每 k 个节点一组进行翻转,请你返回修改后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。

你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

核心思路

image-20250922095216779

  • 先反转以 head 开头的 k 个元素。这里可以复用前面实现的 reverseN 函数。
  • 将第 k + 1 个元素作为 head 递归调用 reverseKGroup 函数
  • 将上述两个过程的结果连接起来

题解代码

class Solution {public ListNode reverseKGroup(ListNode head, int k) {ListNode p1 = head, p2 = head;// 找到第 k + 1 个结点for(int i = 0; i < k; i++){// 如果不够 K 个,不需要反转if(p2 == null) return head;p2 = p2.next;}// 反转前 k 个元素ListNode pre = reverseK(p1, k);// 递归反转后续链表,并连接起来head.next = reverseKGroup(p2, k);return pre;}// 反转前 k 个链表public ListNode reverseK(ListNode head, int k){ListNode pre = null, cur = head;while(k != 0){ListNode next = cur.next;cur.next = pre;pre = cur;cur = next;k--;}return pre;}
}

138. 随机链表的复制

题目描述

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

构造这个链表的 深拷贝。 深拷贝应该正好由 n全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点

例如,如果原链表中有 XY 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 xy ,同样有 x.random --> y

返回复制链表的头节点。

用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:

  • val:一个表示 Node.val 的整数。
  • random_index:随机指针指向的节点索引(范围从 0n-1);如果不指向任何节点,则为 null

你的代码 接受原链表的头节点 head 作为传入参数。

核心思路

一个哈希表 + 两次遍历。

第一次遍历专门克隆节点,借助哈希表把原始节点和克隆节点的映射存储起来;第二次专门组装节点,照着原数据结构的样子,把克隆节点的指针组装起来。

题解代码

class Solution {public Node copyRandomList(Node head) {HashMap<Node, Node> originToClone = new HashMap<>();// 第一次遍历,先把所有节点克隆出来for (Node p = head; p != null; p = p.next) {if (!originToClone.containsKey(p)) {originToClone.put(p, new Node(p.val));}}// 第二次遍历,把克隆节点的结构连接好for (Node p = head; p != null; p = p.next) {if (p.next != null) {originToClone.get(p).next = originToClone.get(p.next);}if (p.random != null) {originToClone.get(p).random = originToClone.get(p.random);}}// 返回克隆之后的头结点return originToClone.get(head);}
}

148. 排序链表

题目描述

给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表

核心思路

Alt

归并排序(分治) ->

  • 找到 链表的中间结点 head2 的 前一个节点,并断开 head2 与其前一个节点的连接。这样我们就把原链表均分成了两段更短的链表。
  • 分治,递归调用 sortList,分别排序 head(只有前一半)和 head2。
  • 排序后,我们得到了两个有序链表,那么 合并两个有序链表,得到排序后的链表,返回链表头节点。

题解代码

class Solution {public ListNode sortList(ListNode head) {// 如果链表为空或者只有一个节点,无需排序if (head == null || head.next == null) {return head;}// 找到中间节点 head2,并断开 head2 与其前一个节点的连接// 比如 head=[4,2,1,3],那么 middleNode 调用结束后 head=[4,2] head2=[1,3]ListNode head2 = findMiddleNode(head);// 分治head = sortList(head);head2 = sortList(head2);// 合并return mergeTwoLists(head, head2);}// 快慢指针寻找链表中点,并将中点与前一个结点断开连接public ListNode findMiddleNode(ListNode head){ListNode fast = head, slow = head, pre = head;while(fast != null && fast.next != null){pre = slow;slow = slow.next;fast = fast.next.next;}pre.next = null;return slow;}// 合并两个有序列表public ListNode mergeTwoLists(ListNode head1, ListNode head2){ListNode dummy = new ListNode(-1), p = dummy;ListNode p1 = head1, p2 = head2;while(p1 != null && p2 != null){if(p1.val < p2.val){p.next = p1;p1 = p1.next;}else{p.next = p2;p2 = p2.next;}p = p.next;}if(p1 == null) p.next = p2;if(p2 == null) p.next = p1;return dummy.next;}
}

23. 合并 K 个升序链表

题目描述

给你一个链表数组,每个链表都已经按升序排列。

请你将所有链表合并到一个升序链表中,返回合并后的链表。

核心思路

PriorityQueue -> 合并 k 个有序链表的逻辑类似合并两个有序链表,难点在于,如何快速得到 k 个节点中的最小节点,接到结果链表上?这里我们就要用到优先级队列这种数据结构,把链表节点放入一个最小堆,就可以每次获得 k 个节点中的最小节点。

题解代码

class Solution {public ListNode mergeKLists(ListNode[] lists) {ListNode dummy = new ListNode(-1), p = dummy;PriorityQueue<ListNode> q = new PriorityQueue<>((a,b) -> (a.val - b.val));// 将 K 个链表的头节点加入优先级队列for(ListNode head : lists){if(head != null){q.add(head);}}while(!q.isEmpty()){// 获取最小节点,接到结果链表中ListNode cur = q.poll();p.next = cur;p = p.next;if(cur.next != null){q.add(cur.next);}}return dummy.next;}
}

146. LRU 缓存

题目描述

请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。

实现 LRUCache 类:

  • LRUCache(int capacity)正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

函数 getput 必须以 O(1) 的平均时间复杂度运行。

核心思路

要让 putget 方法的时间复杂度为 O(1),可以总结出 cache 这个数据结构必要的条件:

1、显然 cache 中的元素必须有时序,以区分最近使用的和久未使用的数据,当容量满了之后要删除最久未使用的那个元素腾位置。

2、我们要在 cache 中快速找某个 key 是否已存在并得到对应的 val

3、每次访问 cache 中的某个 key,需要将这个元素变为最近使用的,也就是说 cache 要支持在任意位置快速插入和删除元素。

哈希表查找快,但是数据无固定顺序;链表有顺序之分,插入删除快,但是查找慢,所以结合二者的长处,可以形成一种新的数据结构:哈希链表 LinkedHashMap

Alt

putget 的具体逻辑,可以画出这样一个流程图:

Alt

题解代码

class LRUCache {int cap;LinkedHashMap<Integer, Integer> cache = new LinkedHashMap<>();public LRUCache(int capacity) {this.cap = capacity;}public int get(int key) {if (!cache.containsKey(key)) {return -1;}// 将 key 变为最近使用makeRecently(key);return cache.get(key);}public void put(int key, int val) {if (cache.containsKey(key)) {// 修改 key 的值cache.put(key, val);// 将 key 变为最近使用makeRecently(key);return;}if (cache.size() >= this.cap) {// 链表头部就是最久未使用的 keyint oldestKey = cache.keySet().iterator().next();cache.remove(oldestKey);}// 将新的 key 添加链表尾部cache.put(key, val);}private void makeRecently(int key) {int val = cache.get(key);// 删除 key,重新插入到队尾cache.remove(key);cache.put(key, val);}
}

「二叉树」

94. 二叉树的中序遍历

题目描述

给定一个二叉树的根节点 root ,返回它的中序遍历。

核心思路

二叉树的 DFS 遍历模版。

/// 二叉树的遍历框架
void traverse(TreeNode root) {if (root == null) {return;}// 前序位置traverse(root.left);// 中序位置traverse(root.right);// 后序位置
}

题解代码

class Solution {List<Integer> res = new LinkedList<>();public List<Integer> inorderTraversal(TreeNode root) {traverse(root);return res;}public void traverse(TreeNode root){if(root == null){return;}traverse(root.left);res.add(root.val);traverse(root.right);}
}

时空分析

  • 时间复杂度:(O(n))
  • 空间复杂度:(O(h))
  • 原因分析
    • 时间复杂度
      • 该代码实现了二叉树的中序遍历。在遍历过程中,每个节点都会被访问一次且仅一次。假设二叉树节点数为 n,每次访问节点时执行的操作(如添加节点值到结果列表)时间复杂度为常数 (O(1))。所以总的时间复杂度为 (O(n\times1)= O(n))。
    • 空间复杂度
      • 空间复杂度主要来源于递归调用栈。在最坏情况下,二叉树是一条链,递归深度达到树的高度 (h = n),此时空间复杂度为 (O(n))。而在平衡二叉树中,树的高度 (h =\log n),空间复杂度为 (O(\log n))。一般情况下,空间复杂度取决于树的高度 h,因此空间复杂度为 (O(h))。这里不考虑存储结果 res 所占用的空间,如果考虑 res,其空间复杂度为 (O(n)),因为最终 res 会存储 n 个节点的值。但按照常规分析递归算法空间复杂度的方式,不把存储结果的空间计算在内,所以空间复杂度为 (O(h))。

104. 二叉树的最大深度

题目描述

给定一个二叉树 root ,返回其最大深度。

二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。

核心思路

递归(分解问题) -> 输入一个节点,返回以该节点为根的二叉树的最大深度。

题解代码

class Solution {// 定义:输入一个节点,返回以该节点为根的二叉树的最大深度public int maxDepth(TreeNode root) {if (root == null) {return 0;}int leftMax = maxDepth(root.left);int rightMax = maxDepth(root.right);// 根据左右子树的最大深度推出原二叉树的最大深度return 1 + Math.max(leftMax, rightMax);}
}

时空分析

  • 时间复杂度:(O(n))
  • 空间复杂度:(O(h))
  • 原因分析
    • 时间复杂度
      • 此代码通过递归方式计算二叉树的最大深度。每个节点在递归过程中都会被访问一次,且仅一次。设二叉树的节点数为 n,每次访问节点执行的操作(如递归调用、比较等)时间复杂度为常数 (O(1))。所以整体时间复杂度为 (O(n\times1)= O(n))。
    • 空间复杂度
      • 空间复杂度主要由递归调用栈的深度决定。在最坏情况下,二叉树为一条链,此时树的高度 (h = n),递归调用栈深度也为 n,空间复杂度为 (O(n))。对于平衡二叉树,树的高度 (h = \log n),递归调用栈深度为 (\log n),空间复杂度为 (O(\log n))。一般地,空间复杂度取决于树的高度 h,因此空间复杂度为 (O(h))。

226. 翻转二叉树

题目描述

给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。

核心思路

递归(分解问题)-> 把二叉树上的每个节点的左右子节点都交换一下。

题解代码

class Solution {// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点TreeNode invertTree(TreeNode root) {if (root == null) {return null;}// 利用函数定义,先翻转左右子树TreeNode left = invertTree(root.left);TreeNode right = invertTree(root.right);// 然后交换左右子节点root.left = right;root.right = left;// 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 rootreturn root;}
}

101. 对称二叉树

题目描述

给你一个二叉树的根节点 root , 检查它是否轴对称。

核心思路

递归(分解问题) -> 判断两棵树是否镜像对称,只要判断两棵子树都是镜像对称的就行了。

题解代码

class Solution {public boolean isSymmetric(TreeNode root) {if (root == null) return true;// 检查两棵子树是否对称return check(root.left, root.right);}// 定义:判断输入的两棵树是否是镜像对称的boolean check(TreeNode left, TreeNode right) {if (left == null || right == null) {return left == right;}// 两个根节点需要相同if (left.val != right.val) return false;// 左右子树也需要镜像对称return check(left.right, right.left) && check(left.left, right.right);}
}

543. 二叉树的直径

题目描述

给你一棵二叉树的根节点,返回该树的 直径

二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root

两节点之间路径的 长度 由它们之间边数表示。

核心思路

二叉树的直径,就是左右子树的最大深度之和 -> 在求最大深度时,运用二叉树的后序遍历,在 maxDepth 的后序遍历位置顺便计算最大直径。

题解代码

class Solution {int maxDiameter = 0;public int diameterOfBinaryTree(TreeNode root) {maxDepth(root);return maxDiameter;}int maxDepth(TreeNode root) {if (root == null) {return 0;}int leftMax = maxDepth(root.left);int rightMax = maxDepth(root.right);// 后序遍历位置顺便计算最大直径maxDiameter = Math.max(maxDiameter, leftMax + rightMax);return 1 + Math.max(leftMax, rightMax);}
}

102. 二叉树的层序遍历

题目描述

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

核心思路

二叉树 BFS 遍历模版。

void levelOrderTraverse(TreeNode root) {if (root == null) {return;}Queue<TreeNode> q = new LinkedList<>();q.offer(root);// 记录当前遍历到的层数(根节点视为第 1 层)int depth = 1;while (!q.isEmpty()) {int sz = q.size();for (int i = 0; i < sz; i++) {TreeNode cur = q.poll();// 访问 cur 节点,同时知道它所在的层数System.out.println("depth = " + depth + ", val = " + cur.val);// 把 cur 的左右子节点加入队列if (cur.left != null) {q.offer(cur.left);}if (cur.right != null) {q.offer(cur.right);}}depth++;}
}

题解代码

class Solution {public List<List<Integer>> levelOrder(TreeNode root) {List<List<Integer>> res = new LinkedList<>();if (root == null) {return res;}Queue<TreeNode> q = new LinkedList<>();q.offer(root);// while 循环控制从上向下一层层遍历while (!q.isEmpty()) {int sz = q.size();// 记录这一层的节点值List<Integer> level = new LinkedList<>();// for 循环控制每一层从左向右遍历for (int i = 0; i < sz; i++) {TreeNode cur = q.poll();level.add(cur.val);if (cur.left != null)q.offer(cur.left);if (cur.right != null)q.offer(cur.right);}res.add(level);}return res;}
}

108. 将有序数组转换为二叉搜索树

题目描述

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 平衡 二叉搜索树。(平衡二叉树 是指该树所有节点的左右子树的高度相差不超过 1。)

核心思路

递归(分解问题)-> 二叉树的构建问题遵循固定的套路,构造整棵树可以分解成:先构造根节点,然后构建左右子树。一个有序数组对于 BST 来说就是中序遍历结果,根节点在数组中心,数组左侧是左子树元素,右侧是右子树元素。

题解代码

class Solution {public TreeNode sortedArrayToBST(int[] nums) {return build(nums, 0, nums.length - 1);}// 将闭区间 [left, right] 中的元素转化成 BST,返回根节点TreeNode build(int[] nums, int left, int right) {if (left > right) {// 区间为空return null;}// 构造根节点// BST 节点左小右大,中间的元素就是根节点int mid = (left + right) / 2;TreeNode root = new TreeNode(nums[mid]);// 递归构建左子树root.left = build(nums, left, mid - 1);// 递归构造右子树root.right = build(nums, mid + 1, right);return root;}
}

98. 验证二叉搜索树

题目描述

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 严格小于 当前节点的数。
  • 节点的右子树只包含 严格大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

核心思路

递归(分解问题)-> BST 左小右大的特性是指 root.val 要比左子树的所有节点都更大,要比右子树的所有节点都小。通过使用辅助函数,增加函数参数列表,在参数中携带额外信息,将这种约束传递给子树的所有节点。

题解代码

class Solution {public boolean isValidBST(TreeNode root) {return isValidBST(root, null, null);}// 限定以 root 为根的子树节点必须满足 max.val > root.val > min.valboolean isValidBST(TreeNode root, TreeNode min, TreeNode max) {// base caseif (root == null) return true;// 若 root.val 不符合 max 和 min 的限制,说明不是合法 BSTif (min != null && root.val <= min.val) return false;if (max != null && root.val >= max.val) return false;// 限定左子树的最大值是 root.val,右子树的最小值是 root.valreturn isValidBST(root.left, min, root)&& isValidBST(root.right, root, max);}
}

230. 二叉搜索树中第 K 小的元素

题目描述

给定一个二叉搜索树的根节点 root ,和一个整数 k ,请你设计一个算法查找其中第 k 小的元素(从 1 开始计数)。

核心思路

递归(遍历)-> BST 的中序遍历结果是有序的(升序),所以用一个外部变量记录中序遍历结果第 k 个元素即是第 k 小的元素。

题解代码

class Solution {public int kthSmallest(TreeNode root, int k) {// 利用 BST 的中序遍历特性traverse(root, k);return res;}// 记录结果int res = 0;// 记录当前元素的排名int rank = 0;void traverse(TreeNode root, int k) {if (root == null) {return;}traverse(root.left, k);// 中序代码位置rank++;if (k == rank) {// 找到第 k 小的元素res = root.val;return;}traverse(root.right, k);}
}

199. 二叉树的右视图

题目描述

给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。

核心思路

BFS 层序遍历 -> 每一层的最后一个节点就是二叉树的右侧视图。我们可以把 BFS 反过来,从右往左遍历每一行,进一步提升效率。

题解代码

class Solution {public List<Integer> rightSideView(TreeNode root) {List<Integer> res = new LinkedList<>();if(root == null) return res;Queue<TreeNode> q = new LinkedList<>();q.offer(root);while(!q.isEmpty()){int sz = q.size();TreeNode last = q.peek();for(int i = 0; i < sz; i++){TreeNode cur = q.poll();if(cur.right != null){q.offer(cur.right);}if(cur.left != null){q.offer(cur.left);}}res.add(last.val);}return res;}
}

114. 二叉树展开为链表

题目描述

给你二叉树的根结点 root ,请你将它展开为一个单链表:

  • 展开后的单链表应该同样使用 TreeNode ,其中 right 子指针指向链表中下一个结点,而左子指针始终为 null
  • 展开后的单链表应该与二叉树 先序遍历 顺序相同。

核心思路

递归(分解问题)-> 递归函数 flatten 的定义:flatten 函数输入一个节点 root,那么以 root 为根的二叉树就会被拉平为一条链表

如何利用这个定义来完成算法?想想怎么把以 root 为根的二叉树拉平为一条链表?很简单,以下流程:

1、将 root 的左子树和右子树拉平。

2、将 root 的右子树接到左子树下方,然后将整个左子树作为右子树。

image-20250925091445523

题解代码

class Solution {// 定义:将以 root 为根的树拉平为链表public void flatten(TreeNode root) {// base caseif (root == null) return;// 先递归拉平左右子树flatten(root.left);flatten(root.right);// ***后序遍历位置***// 1、左右子树已经被拉平成一条链表TreeNode left = root.left;TreeNode right = root.right;// 2、将左子树作为右子树root.left = null;root.right = left;// 3、将原先的右子树接到当前右子树的末端TreeNode p = root;while (p.right != null) {p = p.right;}p.right = right;}
}

105. 从前序与中序遍历序列构造二叉树

题目描述

给定两个整数数组 preorderinorder ,其中 preorder 是二叉树的 先序遍历inorder 是同一棵树的 中序遍历,请构造二叉树并返回其根节点。

核心思路

构造二叉树,第一件事一定是找根节点,然后想办法构造左右子树。

二叉树的前序和中序遍历结果的特点如下:

image-20250925094605484

前序遍历结果第一个就是根节点的值,然后再根据中序遍历结果确定左右子树的节点。

image-20250925094653174

题解代码

class Solution {// 存储 inorder 中值到索引的映射HashMap<Integer, Integer> valToIndex = new HashMap<>();public TreeNode buildTree(int[] preorder, int[] inorder) {for (int i = 0; i < inorder.length; i++) {valToIndex.put(inorder[i], i);}return build(preorder, 0, preorder.length - 1,inorder, 0, inorder.length - 1);}// 定义:前序遍历数组为 preorder[preStart..preEnd]// 中序遍历数组为 inorder[inStart..inEnd]// 构造这个二叉树并返回该二叉树的根节点TreeNode build(int[] preorder, int preStart, int preEnd,int[] inorder, int inStart, int inEnd) {if (preStart > preEnd) {return null;}// root 节点对应的值就是前序遍历数组的第一个元素int rootVal = preorder[preStart];// rootVal 在中序遍历数组中的索引int index = valToIndex.get(rootVal);int leftSize = index - inStart;// 先构造出当前根节点TreeNode root = new TreeNode(rootVal);// 递归构造左右子树root.left = build(preorder, preStart + 1, preStart + leftSize,inorder, inStart, index - 1);root.right = build(preorder, preStart + leftSize + 1, preEnd,inorder, index + 1, inEnd);return root;}
}

437. 路径总和 III

题目描述

给定一个二叉树的根节点 root ,和一个整数 targetSum ,求该二叉树里节点值之和等于 targetSum路径 的数目。

路径 不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。

核心思路

递归(遍历) + 前缀和 + HashMap -> 这道题与 560. 和为 K 的子数组 思想类似答题步骤:

  1. 求前缀和
  2. 查看 HashMap(定义为:preSumToCount) 中是否有 preSum-target 的 key,有则加上对应的 count。
  3. 将该 preSum 放入到 HashMap 中,并更新 count 次数。

note:数据大小,preSum 应该定义为 long 类型。

题解代码

class Solution {// 记录前缀和// 定义:从二叉树的根节点开始,路径和为 pathSum 的路径有 preSumCount.get(pathSum) 个HashMap<Long, Integer> preSumCount = new HashMap<>();long pathSum, targetSum;int res = 0;public int pathSum(TreeNode root, int targetSum) {if (root == null) {return 0;}this.pathSum = 0;this.targetSum = targetSum;this.preSumCount.put(0L, 1);traverse(root);return res;}void traverse(TreeNode root) {if (root == null) {return;}// 前序遍历位置pathSum += root.val;// 从二叉树的根节点开始,路径和为 pathSum - targetSum 的路径条数// 就是路径和为 targetSum 的路径条数res += preSumCount.getOrDefault(pathSum - targetSum, 0);// 记录从二叉树的根节点开始,路径和为 pathSum 的路径条数preSumCount.put(pathSum, preSumCount.getOrDefault(pathSum, 0) + 1);traverse(root.left);traverse(root.right);// 后序遍历位置preSumCount.put(pathSum, preSumCount.get(pathSum) - 1);pathSum -= root.val;}
}

236. 二叉树的最近公共祖先

题目描述

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科 中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

核心思路

递归(分解问题) -> 最近公共祖先模板

递归函数的定义:给该函数输入三个参数 rootpq,它会返回一个节点:

情况 1,如果 pq 都在以 root 为根的树中,函数返回的即使 pq 的最近公共祖先节点。

情况 2,那如果 pq 都不在以 root 为根的树中怎么办呢?函数理所当然地返回 null 呗。

情况 3,那如果 pq 只有一个存在于 root 为根的树中呢?函数就会返回那个节点。

// 定义:在以 root 为根的二叉树中寻找值为 val1 或 val2 的节点
TreeNode find(TreeNode root, int val1, int val2) {// base caseif (root == null) {return null;}// 前序位置,看看 root 是不是目标值if (root.val == val1 || root.val == val2) {return root;}// 去左右子树寻找TreeNode left = find(root.left, val1, val2);TreeNode right = find(root.right, val1, val2);// 后序位置,已经知道左右子树是否存在目标值return left != null ? left : right;
}

题解代码

class Solution {public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {return find(root, p.val, q.val);}public TreeNode find(TreeNode root, int val1, int val2){if(root == null) return null;if(root.val == val1 || root.val == val2) return root;TreeNode left = find(root.left, val1, val2);TreeNode right = find(root.right, val1, val2);if(left != null && right != null){return root;}return left != null ? left : right;}
}

124. 二叉树中的最大路径和

题目描述

二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和

核心思路

递归(分解问题) -> 分解问题的思想:定义一个 oneSideMax 函数来计算从根节点 root 为起点的最大单边路径和。oneSideMax 函数和 maxDepth 函数原理类似,只不过 maxDepth 计算最大深度,oneSideMax 计算「单边」最大路径和。然后在计算单边路径和时顺便计算最大路径和。(后序遍历位置)

Alt

题解代码

class Solution {int res = Integer.MIN_VALUE;public int maxPathSum(TreeNode root) {if (root == null) {return 0;}// 计算单边路径和时顺便计算最大路径和oneSideMax(root);return res;}// 定义:计算从根节点 root 为起点的最大单边路径和int oneSideMax(TreeNode root) {if (root == null) {return 0;}int leftMaxSum = Math.max(0, oneSideMax(root.left));int rightMaxSum = Math.max(0, oneSideMax(root.right));// 后序遍历位置,顺便更新最大路径和int pathMaxSum = root.val + leftMaxSum + rightMaxSum;res = Math.max(res, pathMaxSum);// 实现函数定义,左右子树的最大单边路径和加上根节点的值// 就是从根节点 root 为起点的最大单边路径和return Math.max(leftMaxSum, rightMaxSum) + root.val;}
}

🐮🐴

http://www.dtcms.com/a/524256.html

相关文章:

  • 河南网站开发培训价格商丘哪里做网站比较好
  • 【常用设计模式全解析】创建型模式(聚焦对象创建机制)、结构型模式(优化类与对象的组合关系)、行为型模式(规范对象间的交互行为)
  • MFF-YOLOv8:基于多尺度特征融合的无人机遥感图像小目标检测
  • SSM框架-MyBatis1
  • 从一开始部署Android项目Sonarqube的自动化扫码+通知+增量扫描功能(Win环境、Docker,基于Jenkins)
  • 对我单位网站进行改版苏州网站建设开发公司
  • 网站架构文案软文推广去哪个平台好
  • 【Cuda C 编程指南第7章 硬件实现】
  • 低代码开发,如何让企业应用搭建化繁为简?
  • npm使用国内淘宝镜像的方法
  • pnpm + webpack + vue 项目依赖缺失错误排查与解决
  • 北斗GNSS变形监测一体机在地质灾害和桥梁安全中的应用解析
  • C++ 中的 initializer_list 详解
  • 网站建设与管理好处网站 流量 不够用
  • 2025年第六届MathorCup大数据竞赛赛题浅析-助攻快速选题
  • 网站注册转化率网站建设营销
  • 做食品网站的素材海南省建设注册执业资格中心网站
  • 烧结工序的“隐形守护者”:在线监测如何成为钢铁制造的关键支柱
  • Vue2下项目集成DeepSeek API
  • Mysql作业3
  • 指定列交集内容合并-Rscript_v1.0
  • 基于单片机的牧场奶牛养殖系统设计(论文+源码)
  • 市场上有哪些主流的 MFT 管理软件?
  • webrtc弱网-PccNetworkController类源码分析与算法原理
  • 做任务得得q币的网站临沂seo排名外包
  • 豆浆机破壁机MCU控制方案开发设计-基于国产单片机开发
  • 基于开源链动2+1模式、AI智能名片与S2B2C商城小程序的预搜索机制优化研究
  • 基于开源AI智能名片链动2+1模式S2B2C商城小程序的微商本地化发展研究
  • 从“事后抢险”到“事前防控”:江西水投用 TDengine 时序数据库重塑防汛模式
  • 【Https】Received fatal alert: internal_error