LeetCode76~100题目解析

LeetCode 76-100 题解合集

文章目录


76. Minimum Window Substring(最小覆盖子串)

题目描述

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

注意: 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。

示例:

复制代码
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"

约束: 1 <= s.length, t.length <= 10^5,s 和 t 由英文字母组成

解题思路

核心思想:滑动窗口 + 欠账计数------窗口收缩时找出所有覆盖串中的最短。

解法:滑动窗口(推荐) 。先统计 t 的字符需求 need 与欠账数 needCount。右指针扩展窗口补欠账,当 needCount == 0(完全覆盖)时尝试收缩左指针:若左移后仍覆盖则更新最短区间并继续左移。时间 O(n)(左右指针各移动 n 次),空间 O(字符集)。

代码实现

java 复制代码
class Solution {
    // 滑动窗口(推荐)
    public String minWindow(String s, String t) {
        int[] need = new int[128];
        for (char c : t.toCharArray()) need[c]++;
        int needCount = t.length();
        int left = 0, minLen = Integer.MAX_VALUE, minStart = 0;
        for (int right = 0; right < s.length(); right++) {
            char rc = s.charAt(right);
            if (need[rc] > 0) needCount--; // 是需要的字符,欠账减少
            need[rc]--;
            while (needCount == 0) {       // 覆盖 t 全部字符
                if (right - left + 1 < minLen) { // 更新最短
                    minLen = right - left + 1;
                    minStart = left;
                }
                char lc = s.charAt(left);
                need[lc]++;
                if (need[lc] > 0) needCount++; // 移出后欠账恢复
                left++;
            }
        }
        return minLen == Integer.MAX_VALUE ? "" : s.substring(minStart, minStart + minLen);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.minWindow("ADOBECODEBANC", "ABC")); // BANC
        System.out.println(s.minWindow("a", "a"));               // a
        System.out.println(s.minWindow("a", "aa"));              // (空)
    }
}

复杂度分析

  • 时间复杂度:O(n) --- 双指针各扫一遍
  • 空间复杂度:O(1) --- 固定 128 计数数组

77. Combinations(组合)

题目描述

给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。你可以按任何顺序返回答案。

示例:

复制代码
输入:n = 4, k = 2
输出:[[2,4],[3,4],[2,3],[1,2],[1,3],[1,4]]

约束: 1 <= n <= 20,1 <= k <= n

解题思路

核心思想:回溯------从 start 开始选数,保证组合内升序不重复。

解法:回溯(推荐) 。dfs(start, k, path):path 满 k 个记录;从 start...n 选数加入并递归 start+1。剪枝:还需元素数 > 剩余可选数时可提前终止。时间 O(C(n,k)×k),空间 O(k)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.List;

class Solution {
    private List<List<Integer>> res = new ArrayList<List<Integer>>();

    // 回溯(推荐)
    public List<List<Integer>> combine(int n, int k) {
        dfs(n, k, 1, new ArrayList<Integer>());
        return res;
    }

    private void dfs(int n, int k, int start, List<Integer> path) {
        if (path.size() == k) {
            res.add(new ArrayList<Integer>(path));
            return;
        }
        for (int i = start; i <= n; i++) {
            path.add(i);
            dfs(n, k, i + 1, path); // 不回头,保证不重复
            path.remove(path.size() - 1);
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.combine(4, 2)); // [[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]]
    }
}

复杂度分析

  • 时间复杂度:O(C(n,k)×k) --- 组合数 × 复制成本
  • 空间复杂度:O(k) --- 递归栈

78. Subsets(子集)

题目描述

给你一个整数数组 nums,数组中的元素互不相同 。返回该数组所有可能的子集(幂集),解集不能 包含重复的子集。你可以按任意顺序返回解集。

示例:

复制代码
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

约束: 1 <= nums.length <= 10,-10 <= nums[i] <= 10,nums 中所有元素互不相同

解题思路

核心思想:每个元素"选或不选",可回溯或按位枚举。

解法一:回溯(推荐) 。对每个位置可选可不选:加入 path 递归选,再撤销递归不选;也可按 start 遍历式选入路径(每进入一层就记录当前 path 为子集)。时间 O(2^n),空间 O(n)。

解法二:位运算。2^n 个子集,每个 mask 的二进制位表示是否取 numsi。时间 O(n×2^n),空间 O(1)(不含结果)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.List;

class Solution {
    private List<List<Integer>> res = new ArrayList<List<Integer>>();

    // 解法一:回溯(推荐)
    public List<List<Integer>> subsets(int[] nums) {
        dfs(nums, 0, new ArrayList<Integer>());
        return res;
    }

    private void dfs(int[] nums, int idx, List<Integer> path) {
        res.add(new ArrayList<Integer>(path)); // 当前路径即一个子集
        for (int i = idx; i < nums.length; i++) {
            path.add(nums[i]);
            dfs(nums, i + 1, path);
            path.remove(path.size() - 1);
        }
    }

    // 解法二:位运算
    public List<List<Integer>> subsetsBit(int[] nums) {
        int n = nums.length;
        List<List<Integer>> res = new ArrayList<List<Integer>>();
        for (int mask = 0; mask < (1 << n); mask++) {
            List<Integer> list = new ArrayList<Integer>();
            for (int i = 0; i < n; i++) {
                if ((mask & (1 << i)) != 0) list.add(nums[i]); // 第 i 位为 1 则取
            }
            res.add(list);
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.subsets(new int[]{1,2,3})); // 8 个子集
        System.out.println(s.subsetsBit(new int[]{1,2,3}).size()); // 8
    }
}

复杂度分析

解法 时间复杂度 空间复杂度 说明
回溯(推荐) O(2^n) O(n) 累加路径
位运算 O(n×2^n) O(1)(不含结果) 枚举 mask

79. Word Search(单词搜索)

题目描述

给定一个 m x n 二维字符网格 board 和一个字符串单词 word。如果 word 存在于网格中,返回 true;否则返回 false。单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

复制代码
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
输入:同一网格, word = "ABCB"
输出:false

约束: 1 <= m, n <= 6,1 <= word.length <= 15,board 和 word 仅由大小写英文字母组成

解题思路

核心思想:DFS 回溯------从每个格子作为起点搜单词,用"访问标记"防重复使用。

解法:DFS + 回溯(推荐) 。对每个格子,若首字母匹配则进入 dfs:向上下左右四个方向扩展,匹配单词下一字符;不匹配或越界返回 false;匹配完整个单词返回 true。用临时把当前格子字符改为 # 作为访问标记(原地),回溯时还原。时间 O(m×n×4^L)(L=单词长度),空间 O(L)(递归栈)。

代码实现

java 复制代码
class Solution {
    private int m, n;
    private int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};

    // DFS + 回溯(推荐)
    public boolean exist(char[][] board, String word) {
        m = board.length; n = board[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (dfs(board, word, i, j, 0)) return true; // 任意起点成功即可
            }
        }
        return false;
    }

    private boolean dfs(char[][] board, String word, int i, int j, int idx) {
        if (idx == word.length()) return true; // 全部匹配
        if (i < 0 || i >= m || j < 0 || j >= n || board[i][j] != word.charAt(idx)) return false;
        char tmp = board[i][j];
        board[i][j] = '#'; // 访问标记
        for (int[] d : dirs) {
            if (dfs(board, word, i + d[0], j + d[1], idx + 1)) return true;
        }
        board[i][j] = tmp; // 回溯还原
        return false;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        char[][] b = {{'A','B','C','E'},{'S','F','C','S'},{'A','D','E','E'}};
        System.out.println(s.exist(b, "ABCCED")); // true
        System.out.println(s.exist(b, "ABCB"));   // false
    }
}

复杂度分析

  • 时间复杂度:O(m×n×4^L) --- 每次最多四方向分支
  • 空间复杂度:O(L) --- 递归深度

80. Remove Duplicates from Sorted Array II(删除有序数组中的重复项 II)

题目描述

给你一个有序数组 nums,请你原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次,返回删除后数组的新长度。不要使用额外的数组空间,必须保持相对顺序。

示例:

复制代码
输入:nums = [1,1,1,2,2,3]
输出:5, nums = [1,1,2,2,3]
输入:nums = [0,0,1,1,1,1,2,3,3]
输出:7, nums = [0,0,1,1,2,3,3]

约束: 1 <= nums.length <= 3 * 10^4,-10^4 <= nums[i] <= 10^4

解题思路

核心思想:快慢指针------每个元素最多保留 2 个,慢指针指向下一个可写位置。

解法:快慢指针(推荐) 。i 慢指针(已保留区终点),j 快指针。nums[j] 与 nums[i-2] 比较:不同则可以保留(因为 i-2 之前是"第 i-2 个的计数边界",相同则说明 numsj 同类已出现过 2 次)。时间 O(n),空间 O(1)。

代码实现

java 复制代码
import java.util.Arrays;

class Solution {
    // 快慢指针(推荐)
    public int removeDuplicates(int[] nums) {
        int i = 0;
        for (int j = 0; j < nums.length; j++) {
            if (i < 2 || nums[j] != nums[i - 2]) { // 与"上上个保留值"不同才保留
                nums[i++] = nums[j];
            }
        }
        return i;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] a = {1,1,1,2,2,3};
        int len = s.removeDuplicates(a);
        System.out.println(len + ", " + Arrays.toString(Arrays.copyOf(a, len))); // 5, [1,1,2,2,3]
    }
}

复杂度分析

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

题目描述

已知存在一个按非降序排列的整数数组 nums,数组中的值不必互不相同 。在传递给函数之前,nums 在预先未知的某个下标上进行了旋转,使数组变为有序数组旋转若干次的形态。给你旋转后的数组 nums 和一个整数 target,请你编写一个函数来判断给定的目标值是否存在于数组中。如果 nums 中存在这个目标值 target,则返回 true,否则返回 false。你必须尽可能减少整个操作步骤。

示例:

复制代码
输入:nums = [2,5,6,0,0,1,2], target = 0
输出:true
输入:nums = [2,5,6,0,0,1,2], target = 3
输出:false

约束: 1 <= nums.length <= 5000,-10^4 <= nums[i], target <= 10^4

解题思路

核心思想:与 33 题类似,但有重复元素后边界条件失效,需要先"去重"(左右端重复时收缩)再二分。

解法:二分 + 重复处理(推荐) 。当 nums[left] == nums[mid] 时无法判断哪边有序,直接 left++ 收缩一步。其余情况沿用 33 题的"有序半区判断"。最坏情况全是相同元素退化为 O(n),平均 O(log n)。时间 O(log n)~O(n),空间 O(1)。

代码实现

java 复制代码
class Solution {
    // 二分 + 重复处理(推荐)
    public boolean search(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) return true;
            if (nums[left] == nums[mid] && nums[mid] == nums[right]) {
                left++; right--; // 三个相等无法判断,两端收缩
            } else if (nums[left] <= nums[mid]) { // 左半有序
                if (nums[left] <= target && target < nums[mid]) right = mid - 1;
                else left = mid + 1;
            } else { // 右半有序
                if (nums[mid] < target && target <= nums[right]) left = mid + 1;
                else right = mid - 1;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.search(new int[]{2,5,6,0,0,1,2}, 0)); // true
        System.out.println(s.search(new int[]{2,5,6,0,0,1,2}, 3)); // false
        System.out.println(s.search(new int[]{1,1,1,1,1}, 0));     // false
    }
}

复杂度分析

  • 时间复杂度:O(log n) 平均,O(n) 最坏(大量重复)
  • 空间复杂度:O(1)

82. Remove Duplicates from Sorted List II(删除排序链表中的重复元素 II)

题目描述

给定一个已排序的链表的头 head,删除原始链表中所有重复数字的节点,只留下不同的数字。返回已排序的链表。

示例:

复制代码
输入:head = [1,2,3,3,4,4,5]
输出:[1,2,5]
输入:head = [1,1,1,2,3]
输出:[2,3]

约束: 链表中节点数目在范围 [0, 300] 内,-100 <= Node.val <= 100,链表已按升序排列

解题思路

核心思想:用前驱指针扫描,发现重复段整段跳过。

解法:哑节点 + 前驱扫描(推荐) 。dummy 哨兵,prev 指向前驱。扫描中若 cur.next 与 cur 值相同,则继续后移直到跳过整段重复值,再 prev.next = 下一个不同值;否则 prev = prev.next。时间 O(n),空间 O(1)。

代码实现

java 复制代码
class ListNode {
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

class Solution {
    // 哑节点 + 前驱扫描(推荐)
    public ListNode deleteDuplicates(ListNode head) {
        ListNode dummy = new ListNode(0, head);
        ListNode prev = dummy, cur = head;
        while (cur != null) {
            // 发现重复段
            if (cur.next != null && cur.next.val == cur.val) {
                int v = cur.val;
                while (cur != null && cur.val == v) cur = cur.next; // 跳过整段
                prev.next = cur; // 前驱直接接到段后
            } else {
                prev = cur; // 无重复,正常前进
                cur = cur.next;
            }
        }
        return dummy.next;
    }

    private static ListNode build(int[] arr) {
        ListNode d = new ListNode(0), c = d;
        for (int v : arr) { c.next = new ListNode(v); c = c.next; }
        return d.next;
    }
    private static String str(ListNode h) {
        StringBuilder sb = new StringBuilder("[");
        while (h != null) { sb.append(h.val).append(h.next == null ? "" : ","); h = h.next; }
        return sb.append("]").toString();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(str(s.deleteDuplicates(build(new int[]{1,2,3,3,4,4,5})))); // [1,2,5]
        System.out.println(str(s.deleteDuplicates(build(new int[]{1,1,1,2,3}))));    // [2,3]
    }
}

复杂度分析

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

83. Remove Duplicates from Sorted List(删除排序链表中的重复元素)

题目描述

给定一个已排序的链表的头 head,删除所有重复的元素,使每个元素只出现一次。返回已排序的链表。

示例:

复制代码
输入:head = [1,1,2]
输出:[1,2]
输入:head = [1,1,2,3,3]
输出:[1,2,3]

约束: 链表中节点数目在范围 [0, 300] 内,-100 <= Node.val <= 100,链表已按升序排列

解题思路

核心思想:单指针遍历,值相同则跳过,否则前进。

解法:单指针(推荐) 。cur = head,若 cur.next.val == cur.val 则 cur.next = cur.next.next(删除重复),否则 cur = cur.next。时间 O(n),空间 O(1)。

代码实现

java 复制代码
class ListNode {
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

class Solution {
    // 单指针(推荐)
    public ListNode deleteDuplicates(ListNode head) {
        ListNode cur = head;
        while (cur != null && cur.next != null) {
            if (cur.val == cur.next.val) {
                cur.next = cur.next.next; // 删掉重复节点
            } else {
                cur = cur.next;
            }
        }
        return head;
    }

    private static ListNode build(int[] arr) {
        ListNode d = new ListNode(0), c = d;
        for (int v : arr) { c.next = new ListNode(v); c = c.next; }
        return d.next;
    }
    private static String str(ListNode h) {
        StringBuilder sb = new StringBuilder("[");
        while (h != null) { sb.append(h.val).append(h.next == null ? "" : ","); h = h.next; }
        return sb.append("]").toString();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(str(s.deleteDuplicates(build(new int[]{1,1,2}))));    // [1,2]
        System.out.println(str(s.deleteDuplicates(build(new int[]{1,1,2,3,3})))); // [1,2,3]
    }
}

复杂度分析

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

84. Largest Rectangle in Histogram(柱状图中最大的矩形)

题目描述

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例:

复制代码
输入:heights = [2,1,5,6,2,3]
输出:10

约束: 1 <= heights.length <= 10^5,0 <= heights[i] <= 10^4

解题思路

核心思想:单调递增栈------对每个柱子找到"左边第一个比它矮"和"右边第一个比它矮",两者之间就是它能撑起的最大宽度。

解法:单调栈(推荐) 。栈内按下标递增、高度递增。遍历每个高度:heights[i] 比栈顶矮时,弹出栈顶作为"高",其左右边界分别是栈内下一个元素与 i,面积 = 高 × (i - 左界 - 1)。末尾补一个高度 0 的哨兵,保证所有柱子出栈。时间 O(n)(每个下标进出栈一次),空间 O(n)。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Deque;

class Solution {
    // 单调栈(推荐)
    public int largestRectangleArea(int[] heights) {
        Deque<Integer> stack = new ArrayDeque<Integer>();
        int ans = 0;
        int n = heights.length;
        for (int i = 0; i <= n; i++) {
            int h = (i == n) ? 0 : heights[i]; // 末尾哨兵 0,强制出栈
            while (!stack.isEmpty() && heights[stack.peek()] > h) {
                int height = heights[stack.pop()];       // 以弹出柱高为矩形高
                int width = stack.isEmpty() ? i : i - stack.peek() - 1; // 左右矮柱之间
                ans = Math.max(ans, height * width);
            }
            stack.push(i);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.largestRectangleArea(new int[]{2,1,5,6,2,3})); // 10
        System.out.println(s.largestRectangleArea(new int[]{2,4}));         // 4
    }
}

复杂度分析

  • 时间复杂度:O(n) --- 每个下标进出栈各一次
  • 空间复杂度:O(n) --- 栈

85. Maximal Rectangle(最大矩形)

题目描述

给定一个仅包含 0 和 1、大小为 rows x cols 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。

示例:

复制代码
输入:matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]
输出:6

约束: 1 <= rows, cols <= 200,matrixij 为 '0' 或 '1'

解题思路

核心思想:把每一行当作"以该行为底的柱状图",逐行调用 84 题的单调栈算法。

解法:逐行柱状图 + 单调栈(推荐) 。维护 heights[col](当前行到顶部的连续 1 的高度):遇到 '1' 则 heights[j]++,遇 '0' 则归零。对每行用 84 题的单调栈求最大矩形,全局取最大值。时间 O(rows×cols),空间 O(cols)。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Deque;

class Solution {
    // 逐行柱状图 + 单调栈(推荐)
    public int maximalRectangle(char[][] matrix) {
        if (matrix.length == 0) return 0;
        int cols = matrix[0].length, ans = 0;
        int[] heights = new int[cols];
        for (char[] row : matrix) {
            for (int j = 0; j < cols; j++) {
                heights[j] = row[j] == '1' ? heights[j] + 1 : 0; // 累计高度
            }
            ans = Math.max(ans, largestArea(heights)); // 复用 84 题算法
        }
        return ans;
    }

    private int largestArea(int[] heights) {
        Deque<Integer> stack = new ArrayDeque<Integer>();
        int ans = 0, n = heights.length;
        for (int i = 0; i <= n; i++) {
            int h = (i == n) ? 0 : heights[i];
            while (!stack.isEmpty() && heights[stack.peek()] > h) {
                int height = heights[stack.pop()];
                int width = stack.isEmpty() ? i : i - stack.peek() - 1;
                ans = Math.max(ans, height * width);
            }
            stack.push(i);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        char[][] m = {{'1','0','1','0','0'},{'1','0','1','1','1'},{'1','1','1','1','1'},{'1','0','0','1','0'}};
        System.out.println(s.maximalRectangle(m)); // 6
        System.out.println(s.maximalRectangle(new char[][]{{'0'}})); // 0
    }
}

复杂度分析

  • 时间复杂度:O(rows×cols) --- 每行一次单调栈
  • 空间复杂度:O(cols) --- 高度数组与栈

86. Partition List(分隔链表)

题目描述

给你一个链表的头节点 head 和一个特定值 x,请你对链表进行分隔,使得所有小于 x 的节点都出现在大于或等于 x 的节点之前。你应当保留两个分区中每个节点的初始相对位置。

示例:

复制代码
输入:head = [1,4,3,2,5,2], x = 3
输出:[1,2,2,4,3,5]

约束: 链表中节点数目在范围 [0, 200] 内,-100 <= Node.val <= 100,-200 <= x <= 200

解题思路

核心思想:拆成"小链表"和"大链表"两条,最后拼接。

解法:双链表拼接(推荐) 。small 与 big 两个哨兵链表。遍历原链表,节点值 < x 挂到 small 尾,否则挂到 big 尾。最后 smallTail.next = big.next,bigTail.next = null。时间 O(n),空间 O(1)。

代码实现

java 复制代码
class ListNode {
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

class Solution {
    // 双链表拼接(推荐)
    public ListNode partition(ListNode head, int x) {
        ListNode small = new ListNode(0), big = new ListNode(0);
        ListNode sTail = small, bTail = big;
        for (ListNode cur = head; cur != null; cur = cur.next) {
            if (cur.val < x) { sTail.next = cur; sTail = cur; } // 小链表
            else { bTail.next = cur; bTail = cur; }             // 大链表
        }
        bTail.next = null;      // 尾部断开,防成环
        sTail.next = big.next;  // 拼接
        return small.next;
    }

    private static String str(ListNode h) {
        StringBuilder sb = new StringBuilder("[");
        while (h != null) { sb.append(h.val).append(h.next == null ? "" : ","); h = h.next; }
        return sb.append("]").toString();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        ListNode h = new ListNode(1, new ListNode(4, new ListNode(3, new ListNode(2, new ListNode(5, new ListNode(2))))));
        System.out.println(str(s.partition(h, 3))); // [1,2,2,4,3,5]
    }
}

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1) --- 仅哨兵

87. Scramble String(扰乱字符串)

题目描述

使用下面描述的算法可以扰乱字符串 s 得到字符串 t:如果字符串长度为 1,算法停止;否则把字符串分成两个非空子串;交换或不交换两个子串后拼起来;对两个子串递归执行。给定两个长度相等的字符串 s1 和 s2,判断 s2 是否是 s1 的扰乱字符串。

示例:

复制代码
输入:s1 = "great", s2 = "rgeat"
输出:true
输入:s1 = "abcde", s2 = "caebd"
输出:false

约束: s1.length == s2.length,1 <= s1.length <= 30,s1 和 s2 由小写英文字母组成

解题思路

核心思想:递归 + 记忆化------对每个分割点,(s1左,s2左) 且 (s1右,s2右) 都匹配,或 (s1左,s2右) 且 (s1右,s2左) 都匹配。

解法:递归 + 记忆化(推荐) 。dfs(s1的i1..i1+len, s2的i2..i2+len) 判断子串是否可扰乱。对长度 len 的每个分割点 k:①不交换:dfs(left,left) && dfs(right,right);②交换:dfs(s1左, s2右) && dfs(s1右, s2左)。用 memo[i1][i2][len] 记忆(Integer 三态)。剪枝:两子串字符集合不同直接 false。时间 O(n⁴),空间 O(n³)。

代码实现

java 复制代码
class Solution {
    private String s1, s2;
    private int[][][] memo; // -1 未算,0 false,1 true

    // 递归 + 记忆化(推荐)
    public boolean isScramble(String s1, String s2) {
        this.s1 = s1; this.s2 = s2;
        int n = s1.length();
        memo = new int[n][n][n + 1];
        return dfs(0, 0, n);
    }

    private boolean dfs(int i1, int i2, int len) {
        if (memo[i1][i2][len] != 0) return memo[i1][i2][len] == 1;
        if (s1.substring(i1, i1 + len).equals(s2.substring(i2, i2 + len))) {
            memo[i1][i2][len] = 1;
            return true;
        }
        // 字符频率不一致直接剪枝
        int[] cnt = new int[26];
        for (int i = 0; i < len; i++) {
            cnt[s1.charAt(i1 + i) - 'a']++;
            cnt[s2.charAt(i2 + i) - 'a']--;
        }
        for (int c : cnt) if (c != 0) { memo[i1][i2][len] = -1; return false; }
        // 尝试每个分割点
        for (int k = 1; k < len; k++) {
            if (dfs(i1, i2, k) && dfs(i1 + k, i2 + k, len - k)) {       // 不交换
                memo[i1][i2][len] = 1;
                return true;
            }
            if (dfs(i1, i2 + len - k, k) && dfs(i1 + k, i2, len - k)) { // 交换
                memo[i1][i2][len] = 1;
                return true;
            }
        }
        memo[i1][i2][len] = -1;
        return false;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.isScramble("great", "rgeat")); // true
        System.out.println(s.isScramble("abcde", "caebd")); // false
    }
}

复杂度分析

  • 时间复杂度:O(n⁴) --- 长度 × 起点组合 × 分割点
  • 空间复杂度:O(n³) --- 记忆表

88. Merge Sorted Array(合并两个有序数组)

题目描述

给你两个按非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n,分别表示 nums1 和 nums2 中的元素数目。请你合并 nums2 到 nums1 中,使合并后的数组同样按非递减顺序排列。注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0,应忽略。nums2 的长度为 n。

示例:

复制代码
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3
输出:[1,2,2,3,5,6]

约束: 1 <= m + n <= 200,-10^9 <= nums1[i], nums2[j] <= 10^9

解题思路

核心思想:从后往前填充,避免覆盖 nums1 前部的元素。

解法:逆向双指针(推荐) 。i=m-1, j=n-1, k=m+n-1。比较 nums1[i] 与 nums2[j] 取较大者放 nums1[k--],直到 nums2 全部处理完。时间 O(m+n),空间 O(1)。

代码实现

java 复制代码
import java.util.Arrays;

class Solution {
    // 逆向双指针(推荐)
    public void merge(int[] nums1, int m, int[] nums2, int n) {
        int i = m - 1, j = n - 1, k = m + n - 1;
        while (j >= 0) {
            if (i >= 0 && nums1[i] > nums2[j]) {
                nums1[k--] = nums1[i--]; // 取 nums1 较大者
            } else {
                nums1[k--] = nums2[j--]; // 取 nums2 较大者
            }
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] a = {1,2,3,0,0,0};
        s.merge(a, 3, new int[]{2,5,6}, 3);
        System.out.println(Arrays.toString(a)); // [1,2,2,3,5,6]
    }
}

复杂度分析

  • 时间复杂度:O(m+n)
  • 空间复杂度:O(1)

89. Gray Code(格雷编码)

题目描述

n 位格雷码序列是一个由 2^n 个整数组成的序列,其中:每个整数都在范围 [0, 2^n - 1] 内(含 0 和 2^n - 1);第一个整数是 0;一个整数序列在序列中不超过一次 ;每对相邻整数的二进制表示恰好一位不同,且第一个和最后一个整数的二进制表示恰好一位不同。给你一个整数 n,返回任一有效的 n 位格雷码序列。

示例:

复制代码
输入:n = 2
输出:[0,1,3,2]
解释:[0,1,3,2] 的二进制表示是 [00,01,11,10],相邻恰好一位不同。

约束: 1 <= n <= 16

解题思路

核心思想 :格雷码构造公式------第 i 个格雷码 = i ^ (i >> 1)。

解法一:公式构造(推荐) 。直接对 0...2^n-1 逐个计算 i ^ (i >> 1) 生成。时间 O(2^n),空间 O(2^n)(结果)。

解法二:镜像反射递归。n 位格雷码 = 0 前缀的 (n-1) 位格雷码 + 1 前缀的逆序 (n-1) 位格雷码。适合讲解但实现略绕。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.List;

class Solution {
    // 公式构造:i ^ (i >> 1)(推荐)
    public List<Integer> grayCode(int n) {
        List<Integer> res = new ArrayList<Integer>();
        int total = 1 << n;
        for (int i = 0; i < total; i++) {
            res.add(i ^ (i >> 1)); // 著名格雷码公式
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.grayCode(2)); // [0,1,3,2]
        System.out.println(s.grayCode(1)); // [0,1]
    }
}

复杂度分析

  • 时间复杂度:O(2^n)
  • 空间复杂度:O(2^n) --- 结果集

90. Subsets II(子集 II)

题目描述

给你一个整数数组 nums,其中可能包含重复元素 ,请你返回该数组所有可能的子集(幂集)。解集不能 包含重复的子集。返回的解集中,子集可以按任意顺序排列。

示例:

复制代码
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

约束: 1 <= nums.length <= 10,-10 <= nums[i] <= 10

解题思路

核心思想:与 78 题相同回溯,但排序 + 同层去重(与 40/47 题同一套路)。

解法:回溯 + 排序去重(推荐) 。先排序使相同值相邻。递归中:i > idx && nums[i] == nums[i-1] 跳过(同一层不要重复选择相同值)。时间 O(2^n),空间 O(n)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    private List<List<Integer>> res = new ArrayList<List<Integer>>();

    // 回溯 + 排序去重(推荐)
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        dfs(nums, 0, new ArrayList<Integer>());
        return res;
    }

    private void dfs(int[] nums, int idx, List<Integer> path) {
        res.add(new ArrayList<Integer>(path)); // 每个状态都是子集
        for (int i = idx; i < nums.length; i++) {
            if (i > idx && nums[i] == nums[i - 1]) continue; // 同层去重
            path.add(nums[i]);
            dfs(nums, i + 1, path);
            path.remove(path.size() - 1);
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.subsetsWithDup(new int[]{1,2,2})); // [[],[1],[1,2],[1,2,2],[2],[2,2]]
    }
}

复杂度分析

  • 时间复杂度:O(2^n)
  • 空间复杂度:O(n) --- 递归栈

91. Decode Ways(解码方法)

题目描述

一条包含字母 A-Z 的消息通过以下映射进行了编码:'A' -> "1"、'B' -> "2" ... 'Z' -> "26"。要解码已编码的消息,所有数字必须基于上述映射的方法,反向映射回字母(可能有多种方法)。例如,"11106" 可以映射为:"AAJF",将消息分组为 (1 1 10 6);"KJF",将消息分组为 (11 10 6)。注意,消息不能分组为 (1 11 06),因为 "06" 不能映射为 "F",这是由于 "6" 和 "06" 在映射中并不等价。给你一个只含数字的非空字符串 s,请计算并返回解码方法的总数。题目数据保证答案肯定是一个 32 位整数。

示例:

复制代码
输入:s = "12"    输出:2("AB" 或 "L")
输入:s = "226"   输出:3("BZ"、"VF"、"BBF")
输入:s = "06"    输出:0("06" 无法解码)

约束: 1 <= s.length <= 100,s 只含数字且可能含前导零

解题思路

核心思想 :一维 DP------dp[i] 表示前 i 个字符的解码方案数,每步考虑"取 1 位"和"取 2 位"两种可能。

解法:一维 DP(推荐) 。dp[0]=1(空串)。对 i 从 1...n:若 s[i-1] != '0',dp[i] += dp[i-1](1 位数 1-9 可解码);若 i>=2 且 s[i-2..i-1] 在 10..26,dp[i] += dp[i-2]。可滚动只需前两个 dp 值。时间 O(n),空间 O(1)。

代码实现

java 复制代码
class Solution {
    // 一维 DP(推荐)
    public int numDecodings(String s) {
        int n = s.length();
        int prev2 = 1, prev1 = 0; // prev2=dp[i-2], prev1=dp[i-1]
        // 初始化 dp[1]
        prev1 = (s.charAt(0) == '0') ? 0 : 1;
        for (int i = 2; i <= n; i++) {
            int cur = 0;
            if (s.charAt(i - 1) != '0') cur += prev1; // 取 1 位 [1-9]
            int two = (s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0');
            if (two >= 10 && two <= 26) cur += prev2; // 取 2 位 [10-26]
            prev2 = prev1;
            prev1 = cur;
        }
        return prev1;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.numDecodings("12"));  // 2
        System.out.println(s.numDecodings("226")); // 3
        System.out.println(s.numDecodings("06"));  // 0
    }
}

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(1) --- 滚动变量

92. Reverse Linked List II(反转链表 II)

题目描述

给你单链表的头指针 head 和两个整数 left 和 right,其中 left <= right。请你反转从位置 left 到位置 right 的链表节点,返回反转后的链表。

示例:

复制代码
输入:head = [1,2,3,4,5], left = 2, right = 4
输出:[1,4,3,2,5]

约束: 链表中节点数目为 n,1 <= n <= 500,-500 <= Node.val <= 500,1 <= left <= right <= n

解题思路

核心思想:头插法------定位 left 前驱,把区间内节点逐个"头插"到区间头部,实现区间反转。

解法:头插法(推荐) 。dummy 哨兵;pre 走到 left 的前驱;cur = pre.next。执行 right-left 次:把 cur.next 摘出来插到 pre.next,cur 不动保持指向段内节点。时间 O(n),空间 O(1)。

代码实现

java 复制代码
class ListNode {
    int val;
    ListNode next;
    ListNode() {}
    ListNode(int val) { this.val = val; }
    ListNode(int val, ListNode next) { this.val = val; this.next = next; }
}

class Solution {
    // 头插法(推荐)
    public ListNode reverseBetween(ListNode head, int left, int right) {
        ListNode dummy = new ListNode(0, head);
        ListNode pre = dummy;
        for (int i = 1; i < left; i++) pre = pre.next; // 走到 left 前驱
        ListNode cur = pre.next;
        for (int i = left; i < right; i++) {
            ListNode nxt = cur.next;      // 待头插的节点
            cur.next = nxt.next;          // 摘除
            nxt.next = pre.next;          // 头插
            pre.next = nxt;
        }
        return dummy.next;
    }

    private static ListNode build(int[] arr) {
        ListNode d = new ListNode(0), c = d;
        for (int v : arr) { c.next = new ListNode(v); c = c.next; }
        return d.next;
    }
    private static String str(ListNode h) {
        StringBuilder sb = new StringBuilder("[");
        while (h != null) { sb.append(h.val).append(h.next == null ? "" : ","); h = h.next; }
        return sb.append("]").toString();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(str(s.reverseBetween(build(new int[]{1,2,3,4,5}), 2, 4))); // [1,4,3,2,5]
        System.out.println(str(s.reverseBetween(build(new int[]{5}), 1, 1)));        // [5]
    }
}

复杂度分析

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

93. Restore IP Addresses(复原 IP 地址)

题目描述

有效 IP 地址正好由四个整数(每个位于 0 到 255 之间组成,且不能含有前导零)构成。例如,"0.1.2.201" 和 "192.168.1.1" 是有效 IP 地址;除了数字 0 本身,其他数字不能含前导零。给你一个只含数字的字符串 s,表示一个 IP 地址,返回所有可能的有效 IP 地址 ,这些地址可以通过在 s 中插入 '.' 来形成。你不能重新排序或删除 s 中的任何数字,也不能以任何方式重新排序。

示例:

复制代码
输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

约束: 1 <= s.length <= 20,s 仅由数字组成

解题思路

核心思想:回溯切 4 段------每段取 1~3 位并校验合法(0-255、无前导零)。

解法:回溯(推荐) 。dfs(start, seg, path):当前已切 seg 段、从 start 开始选下一段。校验 isValid(段):长度 1-3、无前导零(除非是 "0")、值 ≤ 255。切满 4 段且用完全部字符时记录。时间 O(3^4)=O(1)(最多 4 段每段 3 种长度),空间 O(1)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.List;

class Solution {
    private List<String> res = new ArrayList<String>();

    // 回溯(推荐)
    public List<String> restoreIpAddresses(String s) {
        dfs(s, 0, 0, new StringBuilder());
        return res;
    }

    private void dfs(String s, int start, int seg, StringBuilder sb) {
        if (seg == 4) { // 已切 4 段
            if (start == s.length()) res.add(sb.toString());
            return;
        }
        for (int len = 1; len <= 3 && start + len <= s.length(); len++) {
            String part = s.substring(start, start + len);
            if (!isValid(part)) continue;
            int oldLen = sb.length();
            sb.append(part);
            if (seg < 3) sb.append('.'); // 非最后一段加分隔点
            dfs(s, start + len, seg + 1, sb);
            sb.setLength(oldLen); // 撤销
        }
    }

    private boolean isValid(String part) {
        if (part.length() > 1 && part.charAt(0) == '0') return false; // 前导零
        return Integer.parseInt(part) <= 255;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.restoreIpAddresses("25525511135")); // [255.255.11.135, 255.255.111.35]
        System.out.println(s.restoreIpAddresses("0000"));        // [0.0.0.0]
    }
}

复杂度分析

  • 时间复杂度:O(3^4) --- 固定常数上界
  • 空间复杂度:O(1) --- 递归深度 4

94. Binary Tree Inorder Traversal(二叉树的中序遍历)

题目描述

给定一个二叉树的根节点 root,返回它的中序遍历。中序遍历顺序:左子树 → 根 → 右子树。

示例:

复制代码
输入:root = [1,null,2,3]
输出:[1,3,2]

约束: 树中节点数目在范围 [0, 100] 内,-100 <= Node.val <= 100

解题思路

核心思想:中序 = 左-根-右。可递归或显式栈迭代(Morris 可做到 O(1) 空间)。

解法一:递归(推荐) 。inorder(root.left) → 记录 root → inorder(root.right)。时间 O(n),空间 O(h)(h 为树高,递归栈)。

解法二:显式栈迭代。不断向左压栈,弹栈访问后转右子树。时间 O(n),空间 O(h)。

解法三:Morris 遍历 。利用线索指针,时间 O(n)、空间 O(1)。完整可运行文件 放在 94/Solution.java(含类 TreeNode)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.List;
import java.util.ArrayDeque;
import java.util.Deque;

// 树节点定义(LeetCode 预置,提交时勿粘贴)
class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }
}

class Solution {
    // 解法一:递归
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        inorder(root, res);
        return res;
    }
    private void inorder(TreeNode node, List<Integer> res) {
        if (node == null) return;
        inorder(node.left, res);    // 左
        res.add(node.val);          // 根
        inorder(node.right, res);   // 右
    }

    // 解法二:显式栈迭代
    public List<Integer> inorderTraversalIter(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        Deque<TreeNode> stack = new ArrayDeque<TreeNode>();
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()) {
            while (cur != null) { stack.push(cur); cur = cur.left; } // 左入栈
            cur = stack.pop();
            res.add(cur.val);      // 访问根
            cur = cur.right;       // 转右
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        TreeNode root = new TreeNode(1, null, new TreeNode(2, new TreeNode(3), null));
        System.out.println(s.inorderTraversal(root));      // [1,3,2]
        System.out.println(s.inorderTraversalIter(root));  // [1,3,2]
    }
}

复杂度分析

解法 时间复杂度 空间复杂度 说明
递归(推荐) O(n) O(h) 最直观
显式栈 O(n) O(h) 避免递归栈溢出
Morris O(n) O(1) 线索指针,进阶

题目描述

给你一个整数 n,请你生成并返回所有由 n 个节点组成且节点值从 1 到 n 互不相同的不同二叉搜索树。可以按任意顺序返回答案。

示例:

复制代码
输入:n = 3
输出:[[1,null,2,null,3],[1,null,3,2],[2,1,3],[3,1,null,null,2],[3,2,null,1]]

约束: 1 <= n <= 8

解题思路

核心思想:递归分治+回溯------以每个数为根,左右子树分别是比它小/大的数能形成的所有 BST 组合。

解法:递归构造(推荐) 。generate(start, end):对区间 start, end 内每个 i 作为根,递归生成左子树集合(start...i-1)与右子树集合(i+1...end),然后两两交叉组合所有 (左, 根, 右)。空区间返回含 null 的列表。时间 O(卡特兰数),空间 O(卡特兰数 × n)。

代码实现

java 复制代码
import java.util.ArrayList;
import java.util.List;

// 树节点定义(LeetCode 预置,提交时勿粘贴)
class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }
}

class Solution {
    // 递归分治(推荐)
    public List<TreeNode> generateTrees(int n) {
        return build(1, n);
    }

    private List<TreeNode> build(int start, int end) {
        List<TreeNode> res = new ArrayList<TreeNode>();
        if (start > end) { res.add(null); return res; } // 空子树
        for (int i = start; i <= end; i++) {
            List<TreeNode> leftList = build(start, i - 1);  // 左子树候选
            List<TreeNode> rightList = build(i + 1, end);   // 右子树候选
            for (TreeNode left : leftList) {
                for (TreeNode right : rightList) {
                    res.add(new TreeNode(i, left, right));  // 组合成树
                }
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.generateTrees(3).size()); // 5
        System.out.println(s.generateTrees(1).size()); // 1
    }
}

复杂度分析

  • 时间复杂度:O(4ⁿ/√n) --- 卡特兰数级
  • 空间复杂度:O(4ⁿ/√n × n) --- 结果集

题目描述

给你一个整数 n,求恰由 n 个节点组成且节点值从 1 到 n 互不相同的二叉搜索树有多少种?返回满足题意的二叉搜索树的种数。

示例:

复制代码
输入:n = 3
输出:5

约束: 1 <= n <= 19

解题思路

核心思想 :卡特兰数------G(n) = Σ_{i=1..n} G(i-1) * G(n-i)。

解法一:DP(推荐) 。dp[i] 表示 i 个节点能构成的 BST 数量(节点值只影响结构不影响数量)。对每个根 j,dp[i] += dp[j-1] * dp[i-j]。时间 O(n²),空间 O(n)。

解法二:公式 。G(n) = C(2n, n) / (n+1),用 long 计算组合数。时间 O(n),空间 O(1)。

代码实现

java 复制代码
class Solution {
    // 解法一:DP(推荐)
    public int numTrees(int n) {
        int[] dp = new int[n + 1];
        dp[0] = 1; // 空树
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                dp[i] += dp[j - 1] * dp[i - j]; // 左子树 j-1 个,右子树 i-j 个
            }
        }
        return dp[n];
    }

    // 解法二:卡特兰公式 C(2n,n)/(n+1)
    public int numTreesFormula(int n) {
        long c = 1; // C(2n, n) 递推
        for (int i = 0; i < n; i++) {
            c = c * 2 * (2 * i + 1) / (i + 2); // 卡塔兰递推 c_(i+1)
        }
        return (int) c;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.numTrees(3));         // 5
        System.out.println(s.numTreesFormula(3));  // 5
        System.out.println(s.numTrees(19));        // 1767263190
    }
}

复杂度分析

解法 时间复杂度 空间复杂度 说明
DP(推荐) O(n²) O(n) 递推清晰
卡特兰公式 O(n) O(1) 公式迭代

97. Interleaving String(交错字符串)

题目描述

给定三个字符串 s1、s2、s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错组成的。交错是指交替使用 s1 和 s2 中的字符(原顺序保持),例如 s1="aabcc",s2="dbbca",交错结果之一为 "aadbbcbcac"。

示例:

复制代码
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false

约束: 0 <= s1.length, s2.length <= 100,0 <= s3.length <= 200,s1、s2、s3 由小写英文字母组成

解题思路

核心思想 :双串 DP------dp[i][j] 表示 s3 前 i+j 个字符能否由 s1 前 i 个 + s2 前 j 个交错组成。

解法:二维 DP(推荐) 。先判长度 len1+len2 == len3。转移:dp[i][j] 为 true 当 (i>0 && dp[i-1][j] && s3[i+j-1]==s1[i-1]) || (j>0 && dp[i][j-1] && s3[i+j-1]==s2[j-1])。时间 O(m×n),空间 O(m×n)(可滚动 O(n))。

代码实现

java 复制代码
class Solution {
    // 二维 DP(推荐)
    public boolean isInterleave(String s1, String s2, String s3) {
        int m = s1.length(), n = s2.length();
        if (m + n != s3.length()) return false;
        boolean[][] dp = new boolean[m + 1][n + 1];
        dp[0][0] = true;
        for (int i = 0; i <= m; i++) {
            for (int j = 0; j <= n; j++) {
                int k = i + j - 1; // 对应 s3 下标
                if (i > 0 && dp[i - 1][j] && s3.charAt(k) == s1.charAt(i - 1)) dp[i][j] = true;
                if (j > 0 && dp[i][j - 1] && s3.charAt(k) == s2.charAt(j - 1)) dp[i][j] = true;
            }
        }
        return dp[m][n];
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        System.out.println(s.isInterleave("aabcc", "dbbca", "aadbbcbcac")); // true
        System.out.println(s.isInterleave("aabcc", "dbbca", "aadbbbaccc")); // false
    }
}

复杂度分析

  • 时间复杂度:O(m×n)
  • 空间复杂度:O(m×n)(可滚动优化为 O(n))

题目描述

给你一个二叉树的根节点 root,判断其是否是一个有效的二叉搜索树。有效二叉搜索树定义如下:节点的左子树只包含小于 当前节点的数;节点的右子树只包含大于当前节点的数;所有左子树和右子树自身必须也是二叉搜索树。

示例:

复制代码
输入:root = [2,1,3]
输出:true
输入:root = [5,1,4,null,null,3,6]
输出:false

约束: 树中节点数目范围在 [1, 10^4] 内,-2^31 <= Node.val <= 2^31 - 1

解题思路

核心思想:中序遍历应严格递增;或用区间约束递归校验(用 long 规避 int 边界)。

解法一:区间约束递归(推荐) 。dfs(node, low, high):节点值必须满足 low < val < high,左子树用 (low, val),右子树用 (val, high)。用 Long 类型上下界处理 int 边界。时间 O(n),空间 O(h)。

解法二:中序遍历。中序序列必须严格递增,迭代版记录 prev。时间 O(n),空间 O(h)。

代码实现

java 复制代码
class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }
}

class Solution {
    // 解法一:区间约束递归(推荐)
    public boolean isValidBST(TreeNode root) {
        return dfs(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    private boolean dfs(TreeNode node, long low, long high) {
        if (node == null) return true;
        if (node.val <= low || node.val >= high) return false; // 超出区间
        // 左子树收紧上界,右子树收紧下界
        return dfs(node.left, low, node.val) && dfs(node.right, node.val, high);
    }

    // 解法二:中序遍历判断递增
    private Integer prev = null;
    public boolean isValidBSTInorder(TreeNode root) {
        return inorder(root);
    }
    private boolean inorder(TreeNode node) {
        if (node == null) return true;
        if (!inorder(node.left)) return false;
        if (prev != null && node.val <= prev) return false; // 必须严格大于
        prev = node.val;
        return inorder(node.right);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        TreeNode t1 = new TreeNode(2, new TreeNode(1), new TreeNode(3));
        System.out.println(s.isValidBST(t1));  // true
        TreeNode t2 = new TreeNode(5, new TreeNode(1), new TreeNode(4, new TreeNode(3), new TreeNode(6)));
        System.out.println(s.isValidBST(t2));  // false
    }
}

复杂度分析

解法 时间复杂度 空间复杂度 说明
区间约束(推荐) O(n) O(h) 传入高低界
中序遍历 O(n) O(h) 依赖"严格递增"性质

题目描述

给你二叉搜索树的根节点 root,该树中的恰好 两个节点的值被错误地交换。请在不改变其结构的情况下,恢复这棵树。进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用 O(1) 空间的解决方案吗?

示例:

复制代码
输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2](恢复后 3 和 1 交换)

约束: 树上节点数目在范围 [2, 1000] 内,-2^31 <= Node.val <= 2^31 - 1

解题思路

核心思想:BST 中序遍历应递增;恰好两个节点错位会在序列中形成 1~2 组"前大后小"。用中序遍历找出这两处,交换第一个大值和最后一个小值。

解法:中序遍历 + 交换(推荐) 。迭代中序(或递归),记录 prev。当出现 node.val < prev.val 时:第一次记录 first=prev,第二次记录 second=node,最后交换 first 与 second 的 val(若只有一组错位,second 就是那个后点)。时间 O(n),空间 O(h)(Morris 可做到 O(1))。

代码实现

java 复制代码
class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }
}

class Solution {
    private TreeNode prev = null, first = null, second = null;

    // 中序遍历定位错位节点(推荐)
    public void recoverTree(TreeNode root) {
        prev = null; first = null; second = null;
        inorder(root);
        // 交换两个错位节点的值
        int tmp = first.val;
        first.val = second.val;
        second.val = tmp;
    }

    private void inorder(TreeNode node) {
        if (node == null) return;
        inorder(node.left);
        if (prev != null && node.val < prev.val) { // 出现逆序
            if (first == null) first = prev;       // 第一处:记录前点
            second = node;                         // 第二处(或同一处):记录后点
        }
        prev = node;
        inorder(node.right);
    }

    private static String serialize(TreeNode r) {
        if (r == null) return "null";
        return r.val + "," + serialize(r.left) + "," + serialize(r.right);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        TreeNode root = new TreeNode(3, new TreeNode(1), new TreeNode(2)); // 3,2 错位(应为1,2,3)
        s.recoverTree(root);
        System.out.println(serialize(root)); // 2,1,3 结构应等价于合法 BST
    }
}

复杂度分析

  • 时间复杂度:O(n) --- 单次中序遍历
  • 空间复杂度:O(h) --- 递归栈(用 Morris 可 O(1))

100. Same Tree(相同的树)

题目描述

给你两棵二叉树的根节点 p 和 q,编写一个函数来检验这两棵树是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。

示例:

复制代码
输入:p = [1,2,3], q = [1,2,3]
输出:true
输入:p = [1,2], q = [1,null,2]
输出:false

约束: 两棵树上的节点数目都在范围 [0, 100] 内,-10^4 <= Node.val <= 10^4

解题思路

核心思想:递归比较------两节点都空则 true,一空一非空则 false,值不等则 false,否则递归比较左右子树。

解法一:递归(推荐) 。isSameTree(p,q):三方判断同值 + 左左递归 + 右右递归。时间 O(n),空间 O(h)。

解法二:迭代 BFS。用队列同时入队两棵树节点,成对取出比较。时间 O(n),空间 O(n)。

代码实现

java 复制代码
import java.util.ArrayDeque;
import java.util.Deque;

class TreeNode {
    int val;
    TreeNode left, right;
    TreeNode() {}
    TreeNode(int val) { this.val = val; }
    TreeNode(int val, TreeNode left, TreeNode right) { this.val = val; this.left = left; this.right = right; }
}

class Solution {
    // 解法一:递归(推荐)
    public boolean isSameTree(TreeNode p, TreeNode q) {
        if (p == null && q == null) return true;         // 均空
        if (p == null || q == null) return false;        // 一空
        if (p.val != q.val) return false;                // 值不同
        return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); // 递归子树
    }

    // 解法二:迭代 BFS
    public boolean isSameTreeIter(TreeNode p, TreeNode q) {
        Deque<TreeNode> queue = new ArrayDeque<TreeNode>();
        queue.offer(p); queue.offer(q);
        while (!queue.isEmpty()) {
            TreeNode a = queue.poll(), b = queue.poll();
            if (a == null && b == null) continue;
            if (a == null || b == null || a.val != b.val) return false;
            queue.offer(a.left); queue.offer(b.left);
            queue.offer(a.right); queue.offer(b.right);
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        TreeNode p = new TreeNode(1, new TreeNode(2), new TreeNode(3));
        TreeNode q = new TreeNode(1, new TreeNode(2), new TreeNode(3));
        System.out.println(s.isSameTree(p, q)); // true
        TreeNode p2 = new TreeNode(1, new TreeNode(2), null);
        TreeNode q2 = new TreeNode(1, null, new TreeNode(2));
        System.out.println(s.isSameTree(p2, q2)); // false
    }
}

复杂度分析

解法 时间复杂度 空间复杂度 说明
递归(推荐) O(n) O(h) 最直观
迭代 BFS O(n) O(n) 队列宽度
相关推荐
她的男孩44 分钟前
页面已发布用户仍进不去:Forge Admin 门户入口与系统菜单挂载解耦
java·spring boot·低代码
不像程序员的程序媛1 小时前
mybatisplus insert dto未指定id时会如何
java·开发语言
我命由我123451 小时前
5-2.Python 编码进阶 - 字符串进阶(拼接、普通格式化、普通格式化数字精度控制、快速格式化、遍历、常用方法)
java·开发语言·后端·python·学习·java-ee·学习方法
Pniubi9 小时前
HashMap的扩容和树化边界
java·开发语言
Mikko79 小时前
用 Java 复刻了高中时在塞班上玩的「熔岩矿车」:纯 Swing、零依赖、没有一张图片,一个 jar 双击就能玩
java·游戏开发·swing
知了学历孙老师9 小时前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
Java爱好狂.10 小时前
Java初学者如何一周速通SpringBoot!
java·spring boot·后端·spring·高并发·架构师·java面试
秋饼11 小时前
LLM API密钥动态轮换实战:Spring Vault + Spring AI + 多Provider路由
java·ai·技术分享·后端开发
福兮说12 小时前
JSON 格式正确,数据就可靠吗?我用一份自编配置检查类型与空值
java·前端·json