LeetCode226~250题目解析

LeetCode 226-250 题解合集

文章目录


226. Invert Binary Tree(翻转二叉树)

题目描述

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

(把每个节点的左右子树互换)

  • 输入 :TreeNode root(树中节点数目范围在 0, 100 内,-100 <= Node.val <= 100)
  • 输出 :TreeNode(翻转后的根节点)

示例 1:

复制代码
root=[4,2,7,1,3,6,9] -> [4,7,2,9,6,3,1]

示例 2:

复制代码
root=[2,1,3] -> [2,3,1]

示例 3:

复制代码
root=[] -> []

约束:

  • 树中节点数目范围在 0, 100 内;-100 <= Node.val <= 100。

解题思路

自顶向下交换 / 递归 / 层序迭代

  • 解法一(递归 DFS):交换当前节点的左右子树,再递归翻转左右子树。
  • 解法二(迭代 BFS 层序):用队列逐层访问,每个节点交换左右孩子再入队。
  • 解法三(前序/后序栈):与 BFS 等价,用栈遍历。
    为什么交换后各自子树仍需翻转:子树内部的结构也要镜像,只交换根节点左右孩子
    无法翻转更深层结构。

代码实现

java 复制代码
public class Solution {

    public TreeNode invertTree(TreeNode root) {
        if (root == null) return null;
        TreeNode tmp = root.left;      // 交换左右孩子
        root.left = root.right;
        root.right = tmp;
        invertTree(root.left);         // 递归翻转左右子树
        invertTree(root.right);
        return root;
    }

    public TreeNode invertTreeBFS(TreeNode root) {
        if (root == null) return null;
        java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
        q.offer(root);
        while (!q.isEmpty()) {
            TreeNode cur = q.poll();
            TreeNode tmp = cur.left;
            cur.left = cur.right;
            cur.right = tmp;
            if (cur.left != null) q.offer(cur.left);
            if (cur.right != null) q.offer(cur.right);
        }
        return root;
    }

    public TreeNode invertTreeStack(TreeNode root) {
        if (root == null) return null;
        java.util.Deque<TreeNode> st = new java.util.ArrayDeque<>();
        st.push(root);
        while (!st.isEmpty()) {
            TreeNode cur = st.pop();
            TreeNode tmp = cur.left;
            cur.left = cur.right;
            cur.right = tmp;
            if (cur.left != null) st.push(cur.left);
            if (cur.right != null) st.push(cur.right);
        }
        return root;
    }

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

    public static void main(String[] args) {
        Solution s = new Solution();
        check(serialize(s.invertTree(build(new Integer[]{4,2,7,1,3,6,9}))), "[4,7,2,9,6,3,1]", "示例1");
        check(serialize(s.invertTree(build(new Integer[]{2,1,3}))), "[2,3,1]", "示例2");
        check(serialize(s.invertTree(build(new Integer[]{}))), "[]", "示例3");
        check(serialize(s.invertTreeBFS(build(new Integer[]{4,2,7,1,3,6,9}))), "[4,7,2,9,6,3,1]", "BFS示例1");
        check(serialize(s.invertTreeBFS(build(new Integer[]{2,1,3}))), "[2,3,1]", "BFS示例2");
        check(serialize(s.invertTreeStack(build(new Integer[]{4,2,7,1,3,6,9}))), "[4,7,2,9,6,3,1]", "栈示例1");
        check(serialize(s.invertTreeStack(build(new Integer[]{2,1,3}))), "[2,3,1]", "栈示例2");
    }

    private static void check(String actual, String expected, String name) {
        System.out.println((expected.equals(actual) ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (!expected.equals(actual)) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

三种解法时间均 O(n),空间:递归 O(h)(h 为树高),BFS O(w)(最宽层),栈 O(h)。


227. Basic Calculator II(基本计算器 II)

题目描述

给你一个字符串表达式 s,请你实现一个基本计算器来计算并返回它的值。

整数除法仅保留整数部分。注意:不允许使用任何将字符串作为数学表达式计算的内置函数,

比如 eval()。

  • 输入 :String s(1 <= s.length <= 3 * 10^5,s 由整数和算符 ('+','-','*','/') 组成,
    中间由一些空格隔开,不含括号)
  • 输出 :int(表达式的值,保证结果在 32 位整数范围内)

示例 1:

复制代码
s="3+2*2" -> 7

示例 2:

复制代码
s=" 3/2 " -> 1

示例 3:

复制代码
s=" 3+5 / 2 " -> 5

约束:

  • 1 <= s.length <= 3 * 10^5;s 由整数和算符 ('+','-','*','/') 组成;不含括号。

解题思路

栈 + 延迟运算(在处理运算符时触发计算)

乘除优先级高于加减,因此不能从左到右直接累加。经典做法:

扫描时维护两个信息:上一运算符 op 与上一操作数 num。遇到新运算符时结算:

  • 若 op 为 '+',num 入栈(正数);
  • 若 op 为 '-',-num 入栈;
  • 若 op 为 '*',栈顶 *= num(立即乘);
  • 若 op 为 '/',栈顶 /= num。
    遍历结束后栈内元素之和即结果(所有加减已被拆成 正/负 数,乘除已合并进栈顶)。
    为什么能这样:把表达式改写成"若干个带符号数字相加",乘除在遇到下一运算符时
    立刻作用于栈顶,天然满足优先级。

代码实现

java 复制代码
public class Solution {

    public int calculate(String s) {
        java.util.Deque<Integer> stack = new java.util.ArrayDeque<>();
        int num = 0;
        char op = '+'; // 前一个运算符,初始为 '+'
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (Character.isDigit(c)) {
                num = num * 10 + (c - '0');
            }
            // 当前是运算符或末尾:结算之前的 num
            if ((!Character.isDigit(c) && c != ' ') || i == s.length() - 1) {
                switch (op) {
                    case '+': stack.push(num); break;
                    case '-': stack.push(-num); break;
                    case '*': stack.push(stack.pop() * num); break;
                    case '/': stack.push(stack.pop() / num); break;
                }
                op = c; // 更新运算符
                num = 0;
            }
        }
        int res = 0;
        for (int v : stack) res += v; // 栈内正负数之和
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.calculate("3+2*2"), 7, "示例1");
        check(s.calculate(" 3/2 "), 1, "示例2");
        check(s.calculate(" 3+5 / 2 "), 5, "示例3");
        check(s.calculate("1-1+1"), 1, "加减连续");
        check(s.calculate("42"), 42, "单数");
        check(s.calculate(" 12 / 4 * 3 "), 9, "乘除连续");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

时间 O(n),空间 O(n)(栈中元素不超过操作数个数)。


228. Summary Ranges(汇总区间)

题目描述

给定一个无重复元素的有序整数数组 nums。

返回恰好覆盖数组中所有数字的最小有序区间范围列表。

也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个范围但不属于

nums 的数字 x。列表中的每个区间范围 `a`,`b` 应该按如下格式输出:

  • "a->b",如果 a != b;
  • "a",如果 a == b。
  • 输入 :int[] nums(0 <= nums.length <= 20,-2^31 <= nums[i] <= 2^31 - 1)
  • 输出 :List<String>(区间范围列表)

示例 1:

复制代码
nums=[0,1,2,4,5,7] -> ["0->2","4->5","7"]

示例 2:

复制代码
nums=[0,2,3,4,6,8,9] -> ["0","2->4","6","8->9"]

约束:

  • 0 <= nums.length <= 20;-2^31 <= nums[i] <= 2^31 - 1;nums 中的所有值都互不相同,且按递增顺序排列。

解题思路

线性扫描合并连续段

维护"当前区间起点 start",从下标 1 开始:若 numsi != numsi-1 + 1,

说明连续性断开,把 start, i-1 收尾成字符串,并更新 start = i。

  • 解法一(快照式循环):一次遍历,必要时使用 numsi - 1 == numsi-1 判断连续。
  • 解法二(内层 while 扩展区间):每次从 start 出发 while 连续推进 end,
    一段区间处理完立即输出;思路等价但结构更直观。
    为什么 O(n) 足够:数组有序无重复,连续性只由相邻元素决定,单趟扫描即可。

代码实现

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

public class Solution {

    public List<String> summaryRanges(int[] nums) {
        List<String> res = new ArrayList<>();
        if (nums.length == 0) return res;
        int start = nums[0];
        for (int i = 1; i <= nums.length; i++) {
            // 到达末尾 或 连续性断开
            if (i == nums.length || nums[i] != nums[i - 1] + 1) {
                res.add(start == nums[i - 1] ? String.valueOf(start)
                        : start + "->" + nums[i - 1]);
                if (i < nums.length) start = nums[i];
            }
        }
        return res;
    }

    public List<String> summaryRangesWhile(int[] nums) {
        List<String> res = new ArrayList<>();
        int i = 0;
        while (i < nums.length) {
            int start = nums[i];
            int j = i;
            while (j + 1 < nums.length && nums[j + 1] == nums[j] + 1) {
                j++;
            }
            res.add(start == nums[j] ? String.valueOf(start) : start + "->" + nums[j]);
            i = j + 1;
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.summaryRanges(new int[]{0,1,2,4,5,7}), new String[]{"0->2","4->5","7"}, "示例1");
        check(s.summaryRanges(new int[]{0,2,3,4,6,8,9}), new String[]{"0","2->4","6","8->9"}, "示例2");
        check(s.summaryRanges(new int[]{}), new String[]{}, "空数组");
        check(s.summaryRangesWhile(new int[]{0,1,2,4,5,7}), new String[]{"0->2","4->5","7"}, "while版示例1");
        check(s.summaryRanges(new int[]{-2147483648,-2147483647,2147483647}), new String[]{"-2147483648->-2147483647","2147483647"}, "边界值");
    }

    private static void check(List<String> actual, String[] expected, String name) {
        boolean ok = actual.size() == expected.length;
        if (ok) {
            for (int i = 0; i < actual.size(); i++) {
                if (!actual.get(i).equals(expected[i])) { ok = false; break; }
            }
        }
        System.out.println((ok ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + java.util.Arrays.toString(expected));
        if (!ok) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

时间 O(n),空间 O(1)(不计输出列表)。


229. Majority Element II(多数元素 II)

题目描述

给定一个大小为 n 的整数数组,找出其中所有出现超过 ⌊n/3⌋ 次的元素。

  • 输入 :int[] nums(1 <= nums.length <= 5 * 104,-109 <= nums[i] <= 10^9)
  • 输出 :List<Integer>(所有出现次数超过 n/3 的元素)

示例 1:

复制代码
nums=[3,2,3] -> [3]

示例 2:

复制代码
nums=[1] -> [1]

示例 3:

复制代码
nums=[1,2] -> [1,2]

约束:

  • 1 <= nums.length <= 5 * 104;-109 <= nums[i] <= 10^9。

解题思路

摩尔投票扩展(Boyer-Moore 多数投票 ≥ n/3 -> 至多两个候选)

出现超过 n/3 的元素至多 2 个。因此可以维护两个候选及其计数:

  • 遍历:若 value 等于候选一/二则对应计数加一;
    否则两个计数都大于 0 时都减一("抵消");
    若某候选计数为 0,则把当前值设为新候选。
  • 第一遍得到至多两个"候选项",第二遍统计真实出现次数,超过 n/3 才收录。
    为什么需要第二遍验证:抵消过程只能保证"多数元素必在候选里",不能保证候选真的
    超过 n/3,必须重新统计。
  • 附加解法(HashMap 计数):直接统计所有元素频次再过滤,O(n) 空间,代码直观。

代码实现

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

public class Solution {

    public List<Integer> majorityElement(int[] nums) {
        // 第一趟:选出至多两个候选
        int cand1 = 0, cand2 = 0, cnt1 = 0, cnt2 = 0;
        for (int v : nums) {
            if (cnt1 > 0 && v == cand1) {
                cnt1++;
            } else if (cnt2 > 0 && v == cand2) {
                cnt2++;
            } else if (cnt1 == 0) {
                cand1 = v; cnt1 = 1;
            } else if (cnt2 == 0) {
                cand2 = v; cnt2 = 1;
            } else {
                cnt1--; cnt2--; // 三个不同元素互相抵消
            }
        }
        // 第二趟:验证真实次数
        List<Integer> res = new ArrayList<>();
        int n = nums.length;
        int c1 = 0, c2 = 0;
        for (int v : nums) {
            if (cnt1 > 0 && v == cand1) c1++;
            else if (cnt2 > 0 && v == cand2) c2++;
        }
        if (cnt1 > 0 && c1 > n / 3) res.add(cand1);
        if (cnt2 > 0 && c2 > n / 3) res.add(cand2);
        return res;
    }

    public List<Integer> majorityElementMap(int[] nums) {
        java.util.Map<Integer, Integer> cnt = new java.util.HashMap<>();
        for (int v : nums) cnt.put(v, cnt.getOrDefault(v, 0) + 1);
        List<Integer> res = new ArrayList<>();
        for (java.util.Map.Entry<Integer, Integer> e : cnt.entrySet()) {
            if (e.getValue() > nums.length / 3) res.add(e.getKey());
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.majorityElement(new int[]{3,2,3}), new int[]{3}, "示例1");
        check(s.majorityElement(new int[]{1}), new int[]{1}, "示例2");
        check(s.majorityElement(new int[]{1,2}), new int[]{1,2}, "示例3");
        check(s.majorityElementMap(new int[]{3,2,3}), new int[]{3}, "哈希法示例1");
        check(s.majorityElement(new int[]{1,1,1,2,2,3,3,3}), new int[]{1,3}, "两个候选");
        check(s.majorityElement(new int[]{2,2,2,3,3,3,4}), new int[]{2,3}, "补充:双合格");
    }

    private static void check(List<Integer> actual, int[] expected, String name) {
        java.util.Collections.sort(actual);
        int[] e = expected.clone();
        java.util.Arrays.sort(e);
        boolean ok = actual.size() == e.length;
        if (ok) {
            for (int i = 0; i < e.length; i++) {
                if (actual.get(i) != e[i]) { ok = false; break; }
            }
        }
        System.out.println((ok ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + java.util.Arrays.toString(expected));
        if (!ok) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

摩尔投票:时间 O(n),空间 O(1)。哈希法:时间 O(n),空间 O(n)。


230. Kth Smallest Element in a BST(二叉搜索树中第K小的元素)

题目描述

给定一个二叉搜索树的根节点 root,和一个整数 k,

请你设计一个算法查找其中第 k 个最小元素(从 1 开始计数)。

  • 输入 :TreeNode root, int k(树的节点数 n,1 <= k <= n <= 10^4,0 <= Node.val <= 10^4)
  • 输出 :int(BST 中第 k 小的值)

示例 1:

复制代码
root=[3,1,4,null,2], k=1 -> 1

示例 2:

复制代码
root=[5,3,6,2,4,null,null,1], k=3 -> 3

约束:

  • 树的节点数 n;1 <= k <= n <= 10^4;0 <= Node.val <= 10^4。
    进阶:如果二叉搜索树经常被修改(插入/删除操作)并且你需要频繁地查找第 k 小的值,
    你将如何优化算法?

解题思路

中序遍历第 k 个 / 迭代栈 / 计数子树(进阶)

BST 的中序遍历结果是升序序列,第 k 小即中序第 k 个访问到的节点。

  • 解法一(递归中序 + 计数):left -> 自己 -> right 的顺序遍历,计数到 k 即返回。
  • 解法二(显式栈迭代):等价中序,空间 O(h),便于 early-stop。
  • 进阶优化(计数节点子树):给每个节点记录"左子树节点数",查找时比较 k 与左子树规模,
    O(log n) 定位,但需要维护字段,本文件以中序为主并简单说明。
    为什么中序即升序:BST 左子树所有值 < 根 < 右子树所有值,按"左-根-右"访问
    恰好按从小到大输出。

代码实现

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

public class Solution {

    private int count = 0;
    private int ans = 0;

    public int kthSmallest(TreeNode root, int k) {
        count = 0;
        ans = 0;
        inorder(root, k);
        return ans;
    }

    private void inorder(TreeNode node, int k) {
        if (node == null || count >= k) return;
        inorder(node.left, k);          // 左子树全部比根小
        if (++count == k) {             // 访问到第 k 个
            ans = node.val;
            return;
        }
        inorder(node.right, k);
    }

    public int kthSmallestStack(TreeNode root, int k) {
        java.util.Deque<TreeNode> stack = new java.util.ArrayDeque<>();
        TreeNode cur = root;
        while (cur != null || !stack.isEmpty()) {
            while (cur != null) {           // 一路向左压栈
                stack.push(cur);
                cur = cur.left;
            }
            cur = stack.pop();              // 弹栈即访问
            if (--k == 0) return cur.val;   // 第 k 个
            cur = cur.right;                // 转向右子树
        }
        return -1; // 不会到达
    }

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

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.kthSmallest(build(new Integer[]{3,1,4,null,2}), 1), 1, "示例1");
        check(s.kthSmallest(build(new Integer[]{5,3,6,2,4,null,null,1}), 3), 3, "示例2");
        check(s.kthSmallestStack(build(new Integer[]{3,1,4,null,2}), 1), 1, "栈法示例1");
        check(s.kthSmallestStack(build(new Integer[]{5,3,6,2,4,null,null,1}), 3), 3, "栈法示例2");
        check(s.kthSmallest(build(new Integer[]{1}), 1), 1, "单节点");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

解法一/二:时间 O(h + k)(h 为树高),空间 O(h)。


231. Power of Two(2 的幂)

题目描述

给你一个整数 n,请你判断该整数是否是 2 的幂次方。

如果是,返回 true;否则,返回 false。

如果存在一个整数 x 使得 n == 2^x,则认为 n 是 2 的幂次方。

  • 输入 :int n(-2^31 <= n <= 2^31 - 1)
  • 输出 :boolean(是否为 2 的幂)

示例 1:

复制代码
n=1 -> true

示例 2:

复制代码
n=16 -> true

示例 3:

复制代码
n=3 -> false

示例 4:

复制代码
n=4 -> true

示例 5:

复制代码
n=5 -> false

约束:

  • -2^31 <= n <= 2^31 - 1。

解题思路

n & (n-1) 清零低位 / 位运算

  • 解法一(n>0 且 n & (n-1) == 0):2 的幂的二进制只有一个 1;
    n-1 会把唯一的 1 及其后所有位变为 1,按位与为 0。
  • 解法二(n>0 且 n & (-n) == n):lowbit 等于自身(只保留最低位的 1)。
  • 解法三(循环除 2):一直除以 2,最后到 1 则是;O(log n),直观。
    为什么 n > 0 是前提:负数和 0 的二进制不含单个 1(负数补码有多个 1)。

代码实现

java 复制代码
public class Solution {

    public boolean isPowerOfTwo(int n) {
        return n > 0 && (n & (n - 1)) == 0;
    }

    public boolean isPowerOfTwoLowbit(int n) {
        return n > 0 && (n & -n) == n;
    }

    public boolean isPowerOfTwoLoop(int n) {
        if (n <= 0) return false;
        while (n % 2 == 0) {
            n /= 2;
        }
        return n == 1;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.isPowerOfTwo(1), true, "示例1");
        check(s.isPowerOfTwo(16), true, "示例2");
        check(s.isPowerOfTwo(3), false, "示例3");
        check(s.isPowerOfTwo(4), true, "示例4");
        check(s.isPowerOfTwo(5), false, "示例5");
        check(s.isPowerOfTwoLowbit(16), true, "lowbit法");
        check(s.isPowerOfTwoLoop(16), true, "循环法");
        check(s.isPowerOfTwo(0), false, "0");
        check(s.isPowerOfTwo(-16), false, "负数");
    }

    private static void check(boolean actual, boolean expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

解法一/二:时间 O(1),空间 O(1)。

解法三:时间 O(log n),空间 O(1)。


232. Implement Queue using Stacks(用栈实现队列)

题目描述

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作

(push、pop、peek、empty)。

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾;
  • int pop() 从队列的开头移除并返回元素;
  • int peek() 返回队列开头的元素;
  • boolean empty() 如果队列为空,返回 true;否则返回 false。
    说明:只能使用标准的栈操作(也就是 push to top、peek/pop from top、size 和 is empty)。
  • 输入 :方法调用(1 <= x <= 9,最多调用 100 次 push、pop、peek 和 empty)
  • 输出 :各方法返回值
    示例:MyQueue myQueue = new MyQueue();
    myQueue.push(1); myQueue.push(2);
    myQueue.peek(); // 1
    myQueue.pop(); // 1
    myQueue.empty(); // false
    约束:
  • 1 <= x <= 9;最多调用 100 次 push、pop、peek 和 empty;每次调用 pop 和 peek 都能保证队列非空。

解题思路

双栈:入队栈 + 出队栈(摊还 O(1))

  • 入队:直接压入 in 栈(元素按时间逆序叠放);
  • 出队/取队头:若 out 栈为空,把 in 栈全部弹出压入 out(此时 out 顶即最早入队元素),
    再从 out 弹出/查看。
    为什么摊还 O(1):每个元素恰好被"从 in 弹出、压入 out、从 out 弹出"一次,
    n 次操作总移动 O(n)。

代码实现

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

public class MyQueue {

    private Deque<Integer> in = new ArrayDeque<>();
    private Deque<Integer> out = new ArrayDeque<>();

    public void push(int x) {
        in.push(x);
    }

    private void transfer() {
        if (out.isEmpty()) {
            while (!in.isEmpty()) {
                out.push(in.pop()); // 反转顺序,栈顶即最早入队元素
            }
        }
    }

    public int pop() {
        transfer();
        return out.pop();
    }

    public int peek() {
        transfer();
        return out.peek();
    }

    public boolean empty() {
        return in.isEmpty() && out.isEmpty();
    }

    public static void main(String[] args) {
        MyQueue q = new MyQueue();
        q.push(1);
        q.push(2);
        check(q.peek(), 1, "peek()");
        check(q.pop(), 1, "pop()");
        check(q.empty(), false, "empty()");
        q.push(3);
        check(q.pop(), 2, "pop() 2");
        check(q.pop(), 3, "pop() 3");
        check(q.empty(), true, "empty() 空");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }

    private static void check(boolean actual, boolean expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

push O(1);pop/peek 摊还 O(1)(最坏 O(n) 发生在转移那一刻);空间 O(n)。


233. Number of Digit One(数字 1 的个数)

题目描述

给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。

  • 输入 :int n(0 <= n <= 10^9)
  • 输出 :int(1 出现的总次数)

示例 1:

复制代码
n=13 -> 6(1,10,11,12,13 中 1 出现 6 次)

示例 2:

复制代码
n=0 -> 0

约束:

  • 0 <= n <= 10^9。

解题思路

逐位统计(数学计数)

对十进制每一位(个、十、百...),统计"该位上是 1 的数字个数":

设当前位权重 w = 10^i,高位 high = n / (w*10),低位 low = n % w,当前位 cur = (n / w) % 10。

  • cur == 0:该位 1 出现 high * w 次;
  • cur == 1:high * w + (low + 1) 次;
  • cur >= 2:(high + 1) * w 次。
    推导依据:把 n 看成 high | cur | low,枚举 high 从 0 到 high-1 时该位固定为 1
    有 w 种低频组合;cur==1 时 low 可 0...low(low+1 种);cur>=2 时 low 全可(w 种)。
  • 附加解法(数位 DP):记忆化搜索,通用性强,适合 n 更大或统计其他数字,
    实现略重,但作为对比写出数位 DP 版本。
    为什么逐位累加不重不漏:每个数的每一位各被统计一次,按位独立互斥。

代码实现

java 复制代码
public class Solution {

    public int countDigitOne(int n) {
        int count = 0;
        for (long w = 1; w <= n; w *= 10) {
            long high = n / (w * 10);
            long cur = (n / w) % 10;
            long low = n % w;
            if (cur == 0) {
                count += high * w;
            } else if (cur == 1) {
                count += high * w + low + 1;
            } else {
                count += (high + 1) * w;
            }
        }
        return count;
    }

    private char[] digits;
    private int[][] memo;

    public int countDigitOneDP(int n) {
        digits = String.valueOf(n).toCharArray();
        memo = new int[digits.length][digits.length + 1];
        for (int[] row : memo) java.util.Arrays.fill(row, -1);
        return dfs(0, 0, true);
    }

    private int dfs(int pos, int cnt, boolean limited) {
        if (pos == digits.length) return cnt;
        if (!limited && memo[pos][cnt] != -1) return memo[pos][cnt];
        int bound = limited ? digits[pos] - '0' : 9;
        int sum = 0;
        for (int d = 0; d <= bound; d++) {
            sum += dfs(pos + 1, cnt + (d == 1 ? 1 : 0), limited && d == bound);
        }
        if (!limited) memo[pos][cnt] = sum;
        return sum;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.countDigitOne(13), 6, "示例1");
        check(s.countDigitOne(0), 0, "示例2");
        check(s.countDigitOneDP(13), 6, "数位DP示例1");
        check(s.countDigitOneDP(0), 0, "数位DP示例2");
        check(s.countDigitOne(99), 20, "补充:1..99共20个1");
        check(s.countDigitOne(100), 21, "补充:1..100");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

逐位法:时间 O(log10 n),空间 O(1)。

数位 DP:时间 O(位数 * 11),空间 O(位数)。


234. Palindrome Linked List(回文链表)

题目描述

给你一个单链表的头节点 head,请你判断该链表是否为回文链表。

如果是,返回 true;否则,返回 false。

  • 输入 :ListNode head(链表中节点数目在范围 1, 10\^5 内,0 <= Node.val <= 9)
  • 输出 :boolean(是否为回文)

示例 1:

复制代码
head=[1,2,2,1] -> true

示例 2:

复制代码
head=[1,2] -> false

约束:

  • 链表中节点数目在范围 1, 10\^5 内;0 <= Node.val <= 9。
    进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

解题思路

快慢指针找中点 + 反转后半段

  • 解法一(快慢指针 + 反转后半):快指针走两步、慢指针走一步,慢指针停在(左)中点;
    反转后半段链表,然后双指针从两端向中间比较,最后(可选)恢复链表。
    空间 O(1),满足进阶要求。
  • 解法二(复制到数组 + 双指针):先拷贝全部值到数组,从两端向中间比较。
    空间 O(n),实现最直观,也可作为基准验证。
    为什么反转后半不破坏前半:前半段起点 head 不动,后半段起点是反转后的新头;
    两段等长(奇数时中点归属前半,忽略)。

代码实现

java 复制代码
public class Solution {

    public boolean isPalindrome(ListNode head) {
        // 1) 快慢指针找中点
        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        // 2) 反转后半段(从 slow 开始的链表)
        ListNode second = reverse(slow);
        ListNode first = head;
        // 3) 逐节点比较
        while (second != null) {
            if (first.val != second.val) {
                return false;
            }
            first = first.next;
            second = second.next;
        }
        return true;
    }

    private ListNode reverse(ListNode node) {
        ListNode prev = null;
        while (node != null) {
            ListNode next = node.next;
            node.next = prev;
            prev = node;
            node = next;
        }
        return prev;
    }

    public boolean isPalindromeArray(ListNode head) {
        java.util.List<Integer> vals = new java.util.ArrayList<>();
        while (head != null) {
            vals.add(head.val);
            head = head.next;
        }
        int i = 0, j = vals.size() - 1;
        while (i < j) {
            if (!vals.get(i).equals(vals.get(j))) return false;
            i++;
            j--;
        }
        return true;
    }

    static class ListNode {
        int val;
        ListNode next;
        ListNode() {}
        ListNode(int val) { this.val = val; }
        ListNode(int val, ListNode next) { this.val = val; this.next = next; }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.isPalindrome(build(new int[]{1,2,2,1})), true, "示例1");
        check(s.isPalindrome(build(new int[]{1,2})), false, "示例2");
        check(s.isPalindrome(build(new int[]{1})), true, "单节点");
        check(s.isPalindromeArray(build(new int[]{1,2,2,1})), true, "数组法示例1");
        check(s.isPalindromeArray(build(new int[]{1,2})), false, "数组法示例2");
        check(s.isPalindrome(build(new int[]{1,2,3,2,1})), true, "奇数长回文");
    }

    private static void check(boolean actual, boolean expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

解法一:时间 O(n),空间 O(1)。

解法二:时间 O(n),空间 O(n)。


题目描述

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

百度百科中最近公共祖先的定义为:"对于有根树 T 的两个结点 p、q,

最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大

(一个节点也可以是它自己的祖先)。"

  • 输入 :TreeNode root, TreeNode p, TreeNode q(树中节点数目在范围 2, 10\^5 内,
    -10^9 <= Node.val <= 10^9,所有值互不相同,p != q)
  • 输出 :TreeNode(p 和 q 的最近公共祖先)

示例 1:

复制代码
root=[6,2,8,0,4,7,9,null,null,3,5], p=2, q=8 -> 6

示例 2:

复制代码
root=[6,2,8,0,4,7,9,null,null,3,5], p=2, q=4 -> 2

示例 3:

复制代码
root=[2,1], p=2, q=1 -> 2

约束:

  • 所有 Node.val 互不相同;p != q;p 和 q 均存在于给定的 BST 中。

解题思路

利用 BST 性质二分下沉

  • 解法一(迭代):从根开始,若 p、q 都小于当前节点则向左;都大于则向右;
    其他情况(p、q 分列两侧,或其一等于当前节点)当前节点即 LCA。
  • 解法二(递归):同样逻辑递归实现。
    为什么"分叉点即 LCA":若 p、q 一个在左子树一个在右子树,公共祖先只可能是
    当前节点(再往下不可能同时包含两侧的值);若某节点等于 p 或 q,则它自身即 LCA。

代码实现

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

public class Solution {

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        TreeNode cur = root;
        while (cur != null) {
            if (p.val < cur.val && q.val < cur.val) {
                cur = cur.left;       // 都在左子树
            } else if (p.val > cur.val && q.val > cur.val) {
                cur = cur.right;      // 都在右子树
            } else {
                return cur;           // 分叉点或等于 p/q 之一
            }
        }
        return null;
    }

    public TreeNode lowestCommonAncestorRec(TreeNode root, TreeNode p, TreeNode q) {
        if (p.val < root.val && q.val < root.val) {
            return lowestCommonAncestorRec(root.left, p, q);
        }
        if (p.val > root.val && q.val > root.val) {
            return lowestCommonAncestorRec(root.right, p, q);
        }
        return root;
    }

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

    public static void main(String[] args) {
        Solution s = new Solution();
        TreeNode r1 = build(new Integer[]{6,2,8,0,4,7,9,null,null,3,5});
        check(s.lowestCommonAncestor(r1, find(r1, 2), find(r1, 8)).val, 6, "示例1");
        check(s.lowestCommonAncestor(r1, find(r1, 2), find(r1, 4)).val, 2, "示例2");
        TreeNode r3 = build(new Integer[]{2,1});
        check(s.lowestCommonAncestor(r3, find(r3, 2), find(r3, 1)).val, 2, "示例3");
        check(s.lowestCommonAncestorRec(r1, find(r1, 2), find(r1, 4)).val, 2, "递归版");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

时间 O(h)(h 为树高,最坏 O(n) 退化链),空间 O(1)(迭代)/ O(h)(递归)。


236. Lowest Common Ancestor of a Binary Tree(二叉树的最近公共祖先)

题目描述

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

百度百科中最近公共祖先的定义为:"对于有根树 T 的两个节点 p、q,

最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大

(一个节点也可以是它自己的祖先)。"

  • 输入 :TreeNode root, TreeNode p, TreeNode q(树中节点数目在范围 2, 10\^5 内
    -10^9 <= Node.val <= 10^9,所有 Node.val 互不相同,p != q)
  • 输出 :TreeNode(p 和 q 的最近公共祖先)

示例 1:

复制代码
root=[3,5,1,6,2,0,8,null,null,7,4], p=5, q=1 -> 3

示例 2:

复制代码
root=[3,5,1,6,2,0,8,null,null,7,4], p=5, q=4 -> 5

示例 3:

复制代码
root=[1,2], p=1, q=2 -> 1

约束:

  • 树中节点数目在范围 2, 10\^5 内;-10^9 <= Node.val <= 10^9;
    所有 Node.val 互不相同;p != q;p 和 q 均存在于给定的二叉树中。

解题思路

递归后序"返回值"判定 / 记录父节点法

  • 解法一(递归后序):从子树中找 p、q:
    若左子树找到 p/q、右子树找到另一个,说明当前节点是 LCA;
    若只在一侧找到,则把"找到的那个节点"向上返回;
    若两边都是 null,返回 null。
    为什么正确:LCA 是"分叉点",即 p、q 首次分属左右子树的那个节点;
    若 p/q 是祖先关系,则返回先遇到的 p 或 q 本身。
  • 解法二(哈希表记录父节点 + 集合追溯):BFS/DFS 一遍记录每个节点的父节点,
    再从 p 出发沿父链把祖先全部加入 set,再沿 q 的父链找第一个出现在 set 中的节点。
  • 解法三(路径法):找到 root->p 与 root->q 的路径,比较最后一个公共节点。
    为什么解法一不需要额外数据结构:后序遍历天然"先处理子树再决定返回值",
    每个节点只被访问一次。

代码实现

java 复制代码
public class Solution {

    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == null || root == p || root == q) {
            return root; // 空或命中的节点(自身可作祖先)
        }
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        if (left != null && right != null) {
            return root; // 左右各找到一个 => 当前节点是 LCA
        }
        return left != null ? left : right; // 只在一侧找到
    }

    public TreeNode lowestCommonAncestorParent(TreeNode root, TreeNode p, TreeNode q) {
        // 第二遍 BFS 记录父节点
        java.util.Map<TreeNode, TreeNode> parent = new java.util.HashMap<>();
        java.util.Deque<TreeNode> stack = new java.util.ArrayDeque<>();
        parent.put(root, null);
        stack.push(root);
        while (!parent.containsKey(p) || !parent.containsKey(q)) {
            TreeNode cur = stack.pop();
            if (cur.left != null) {
                parent.put(cur.left, cur);
                stack.push(cur.left);
            }
            if (cur.right != null) {
                parent.put(cur.right, cur);
                stack.push(cur.right);
            }
        }
        java.util.Set<TreeNode> ancestors = new java.util.HashSet<>();
        while (p != null) {
            ancestors.add(p);   // p 的祖链(含自己)
            p = parent.get(p);
        }
        while (!ancestors.contains(q)) {
            q = parent.get(q);  // q 向上找第一个共同祖先
        }
        return q;
    }

    public TreeNode lowestCommonAncestorPath(TreeNode root, TreeNode p, TreeNode q) {
        java.util.List<TreeNode> pathP = new java.util.ArrayList<>();
        java.util.List<TreeNode> pathQ = new java.util.ArrayList<>();
        if (!findPath(root, p, pathP) || !findPath(root, q, pathQ)) {
            return null; // 题目保证存在,正常不会走到
        }
        TreeNode lca = root;
        int i = 0;
        while (i < pathP.size() && i < pathQ.size() && pathP.get(i) == pathQ.get(i)) {
            lca = pathP.get(i);
            i++;
        }
        return lca;
    }

    private boolean findPath(TreeNode root, TreeNode target, java.util.List<TreeNode> path) {
        if (root == null) return false;
        path.add(root);
        if (root == target) return true;
        if (findPath(root.left, target, path) || findPath(root.right, target, path)) {
            return true;
        }
        path.remove(path.size() - 1); // 回溯
        return false;
    }

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

    public static void main(String[] args) {
        Solution s = new Solution();
        TreeNode r1 = build(new Integer[]{3,5,1,6,2,0,8,null,null,7,4});
        check(s.lowestCommonAncestor(r1, find(r1, 5), find(r1, 1)).val, 3, "示例1");
        check(s.lowestCommonAncestor(r1, find(r1, 5), find(r1, 4)).val, 5, "示例2");
        TreeNode r3 = build(new Integer[]{1,2});
        check(s.lowestCommonAncestor(r3, find(r3, 1), find(r3, 2)).val, 1, "示例3");
        check(s.lowestCommonAncestorParent(r1, find(r1, 5), find(r1, 4)).val, 5, "父节点法示例2");
        check(s.lowestCommonAncestor(r1, find(r1, 6), find(r1, 4)).val, 5, "补充:同侧祖先");
        check(s.lowestCommonAncestorPath(r1, find(r1, 5), find(r1, 1)).val, 3, "路径法示例1");
        check(s.lowestCommonAncestorPath(r1, find(r1, 5), find(r1, 4)).val, 5, "路径法示例2");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

解法一:时间 O(n),空间 O(h)(递归栈深)。

解法二:时间 O(n),空间 O(n)。

解法三(路径法):时间 O(n),空间 O(h)。


237. Delete Node in a Linked List(删除链表中的节点)

题目描述

有一个单链表的 head,我们想删除其中某一个节点 node。

给你一个需要删除的节点 node。你将无法访问第一个节点 head。

链表所有值都是唯一的,并且给定的节点 node 不是链表中的最后一个节点。

删除给定的节点。注意,删除节点并不是指从内存中删除它,而是:

  • 给定节点的值不应该存在于链表中;
  • 链表节点数应该减少 1;
  • node 前面的所有值顺序相同;
  • node 后面的所有值顺序相同。
  • 输入 :ListNode node(链表节点数目范围 2, 1000)
  • 输出 :void(原地删除节点)

示例 1:

复制代码
head=[4,5,1,9], node=5 -> [4,1,9]

示例 2:

复制代码
head=[4,5,1,9], node=1 -> [4,5,9]

约束:

  • 链表中所有值互不相同;给定的节点 node 不是链表中的最后一个节点。

解题思路

"偷天换日":拿后继节点的值覆盖当前节点,再跳过后继

因为我们拿不到 head,无法知道前驱,也不能真正断开 node 的入边。

经典解法:

  1. 令 node.val = node.next.val(把后继的值复制过来);
  2. node.next = node.next.next(跳过真正的后继)。
    效果:node 承载了原先"后继"的值,等价于删除了原 node(后续节点整体前移一位)。
    为什么这是唯一可行解:无法访问前驱,只能"借尸还魂"。
    为什么题目保证 node 不是尾节点:若 node 是尾节点,node.next 为空,
    该技巧失效(需要前驱)。

代码实现

java 复制代码
public class Solution {

    public void deleteNode(ListNode node) {
        node.val = node.next.val;       // 用后继的值覆盖当前节点
        node.next = node.next.next;     // 删除后继
    }

    static class ListNode {
        int val;
        ListNode next;
        ListNode(int val) { this.val = val; }
        ListNode(int val, ListNode next) { this.val = val; this.next = next; }
    }

    private static ListNode find(ListNode head, int val) {
        while (head != null) {
            if (head.val == val) return head;
            head = head.next;
        }
        return null;
    }

    private static void check(int[] actual, int[] expected, String name) {
        boolean ok = java.util.Arrays.equals(actual, expected);
        System.out.println((ok ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + java.util.Arrays.toString(actual)
                + " expected=" + java.util.Arrays.toString(expected));
        if (!ok) System.out.println("  <<< 断言失败: " + name);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        ListNode l1 = build(new int[]{4,5,1,9});
        s.deleteNode(find(l1, 5));
        check(toArr(l1), new int[]{4,1,9}, "示例1");
        ListNode l2 = build(new int[]{4,5,1,9});
        s.deleteNode(find(l2, 1));
        check(toArr(l2), new int[]{4,5,9}, "示例2");
        ListNode l3 = build(new int[]{1,2,3});
        s.deleteNode(find(l3, 2));
        check(toArr(l3), new int[]{1,3}, "补充:中间节点");
    }
}

复杂度分析

时间 O(1),空间 O(1)。


238. Product of Array Except Self(除自身以外数组的乘积)

题目描述

给你一个整数数组 nums,返回数组 answer,其中 answeri 等于 nums 中

除 nums[i] 之外其余各元素的乘积。

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

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

  • 输入 :int[] nums(2 <= nums.length <= 10^5,-30 <= nums[i] <= 30)
  • 输出 :int[](answer 数组)

示例 1:

复制代码
nums=[1,2,3,4] -> [24,12,8,6]

示例 2:

复制代码
nums=[-1,1,0,-3,3] -> [0,0,9,0,0]

约束:

  • 2 <= nums.length <= 10^5;-30 <= nums[i] <= 30;保证任意前缀/后缀乘积在 32 位整数范围内。
    进阶:你可以在 O(1) 的额外空间复杂度内完成这个题目吗?(出于对空间复杂度分析的目的,
    输出数组不被视为额外空间。)

解题思路

前缀积 × 后缀积

  • 解法一(左右乘积数组):Li = nums0...i-1 的乘积,Ri = numsi+1...end 的乘积,
    answeri = Li * Ri。空间 O(n)。
  • 解法二(输出数组代替 L,滚动变量代替 R):第一趟从左往右把前缀积写入 answer;
    第二趟从右往左用一个变量 R 累计后缀积,answeri *= R 后更新 R *= numsi。
    空间 O(1)(除输出数组)。满足进阶要求。
    为什么不能直接用总积除以自身:题目禁止除法,且有 0 时会出现 0/0 特判,
    前后缀乘法天然规避。

代码实现

java 复制代码
public class Solution {

    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] ans = new int[n];
        // 第一趟:ans[i] = 前缀积(nums[0..i-1])
        ans[0] = 1;
        for (int i = 1; i < n; i++) {
            ans[i] = ans[i - 1] * nums[i - 1];
        }
        // 第二趟:R 从右往左累计后缀积
        int R = 1;
        for (int i = n - 1; i >= 0; i--) {
            ans[i] *= R;   // 前缀积 * 后缀积
            R *= nums[i];  // 更新后缀积
        }
        return ans;
    }

    public int[] productExceptSelfLR(int[] nums) {
        int n = nums.length;
        int[] L = new int[n], R = new int[n], ans = new int[n];
        L[0] = 1;
        for (int i = 1; i < n; i++) L[i] = L[i - 1] * nums[i - 1];
        R[n - 1] = 1;
        for (int i = n - 2; i >= 0; i--) R[i] = R[i + 1] * nums[i + 1];
        for (int i = 0; i < n; i++) ans[i] = L[i] * R[i];
        return ans;
    }

    private static void check(int[] actual, int[] expected, String name) {
        boolean ok = java.util.Arrays.equals(actual, expected);
        System.out.println((ok ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + java.util.Arrays.toString(actual)
                + " expected=" + java.util.Arrays.toString(expected));
        if (!ok) System.out.println("  <<< 断言失败: " + name);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.productExceptSelf(new int[]{1,2,3,4}), new int[]{24,12,8,6}, "示例1");
        check(s.productExceptSelf(new int[]{-1,1,0,-3,3}), new int[]{0,0,9,0,0}, "示例2");
        check(s.productExceptSelfLR(new int[]{1,2,3,4}), new int[]{24,12,8,6}, "左右数组法示例1");
        check(s.productExceptSelf(new int[]{5,6}), new int[]{6,5}, "双元素");
    }
}

复杂度分析

解法一:时间 O(n),空间 O(n)。解法二:时间 O(n),空间 O(1)(不计输出)。


239. Sliding Window Maximum(滑动窗口最大值)

题目描述

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。

你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回滑动窗口中的最大值。

  • 输入 :int[] nums, int k(1 <= nums.length <= 105,-104 <= nums[i] <= 10^4,1 <= k <= nums.length)
  • 输出 :int[](每个滑动窗口中的最大值)

示例 1:

复制代码
nums=[1,3,-1,-3,5,3,6,7], k=3 -> [3,3,5,5,6,7]

示例 2:

复制代码
nums=[1], k=1 -> [1]

约束:

  • 1 <= nums.length <= 105;-104 <= nums[i] <= 10^4;1 <= k <= nums.length。

解题思路

单调队列(双端队列)/ 优先队列 / 分块法

  • 解法一(单调递减双端队列):队首是窗口最大值。
    窗口滑动时:
  1. 移除队首过期元素(下标 < i-k+1);
  2. 从队尾弹出所有值 <= 当前值的元素(它们不可能再成为窗口最大值);
  3. 当前元素下标入队尾;
  4. 窗口满后才输出队首。
    单调性:队列中下标递增、值严格递减。
  • 解法二(优先队列/大顶堆 + 懒删除):堆按值排序,元素存 (值, 下标),
    取堆顶时把下标过期者弹出。时间 O(n log n)。
  • 解法三(前缀/后缀分解分块):按 k 分成若干块,预处理每块前后缀最大值,
    每个窗口 = 两块组合,O(n)。作为进阶参考。
    为什么弹出队尾的小值安全:小值在当前窗口内永远不会成为最大值;
    只要有大的新元素在后面,小值只可能被新元素"盖过",所以可以提前淘汰。

代码实现

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

public class Solution {

    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        int[] ans = new int[n - k + 1];
        Deque<Integer> dq = new ArrayDeque<>(); // 存下标,队首最大
        for (int i = 0; i < n; i++) {
            // 1) 移除窗口外的下标
            while (!dq.isEmpty() && dq.peekFirst() < i - k + 1) {
                dq.pollFirst();
            }
            // 2) 弹出所有值更小的队尾(它们不可能做最大值)
            while (!dq.isEmpty() && nums[dq.peekLast()] <= nums[i]) {
                dq.pollLast();
            }
            // 3) 当前下标入队
            dq.offerLast(i);
            // 4) 窗口满,取队首
            if (i >= k - 1) {
                ans[i - k + 1] = nums[dq.peekFirst()];
            }
        }
        return ans;
    }

    public int[] maxSlidingWindowHeap(int[] nums, int k) {
        int n = nums.length;
        int[] ans = new int[n - k + 1];
        java.util.PriorityQueue<int[]> heap = new java.util.PriorityQueue<>((a, b) -> b[0] - a[0]);
        for (int i = 0; i < n; i++) {
            heap.offer(new int[]{nums[i], i});
            if (i >= k - 1) {
                // 弹出过期元素
                while (heap.peek()[1] < i - k + 1) heap.poll();
                ans[i - k + 1] = heap.peek()[0];
            }
        }
        return ans;
    }

    public int[] maxSlidingWindowBlock(int[] nums, int k) {
        int n = nums.length;
        int[] ans = new int[n - k + 1];
        // left[i]:当前块内从块首到 i 的最大值
        int[] left = new int[n];
        // right[i]:当前块内从 i 到块尾的最大值
        int[] right = new int[n];
        for (int i = 0; i < n; i++) {
            if (i % k == 0) {
                left[i] = nums[i]; // 块首
            } else {
                left[i] = Math.max(left[i - 1], nums[i]);
            }
        }
        for (int i = n - 1; i >= 0; i--) {
            if (i == n - 1 || (i + 1) % k == 0) {
                right[i] = nums[i]; // 块尾
            } else {
                right[i] = Math.max(right[i + 1], nums[i]);
            }
        }
        for (int i = 0; i + k - 1 < n; i++) {
            // 窗口 [i, i+k-1] 的左半在 right,右半在 left
            ans[i] = Math.max(right[i], left[i + k - 1]);
        }
        return ans;
    }

    private static void check(int[] actual, int[] expected, String name) {
        boolean ok = java.util.Arrays.equals(actual, expected);
        System.out.println((ok ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + java.util.Arrays.toString(actual)
                + " expected=" + java.util.Arrays.toString(expected));
        if (!ok) System.out.println("  <<< 断言失败: " + name);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.maxSlidingWindow(new int[]{1,3,-1,-3,5,3,6,7}, 3), new int[]{3,3,5,5,6,7}, "示例1");
        check(s.maxSlidingWindow(new int[]{1}, 1), new int[]{1}, "示例2");
        check(s.maxSlidingWindowHeap(new int[]{1,3,-1,-3,5,3,6,7}, 3), new int[]{3,3,5,5,6,7}, "堆法示例1");
        check(s.maxSlidingWindowBlock(new int[]{1,3,-1,-3,5,3,6,7}, 3), new int[]{3,3,5,5,6,7}, "分块法示例1");
        check(s.maxSlidingWindowBlock(new int[]{1}, 1), new int[]{1}, "分块法示例2");
        check(s.maxSlidingWindow(new int[]{9,8,7,6,5}, 3), new int[]{9,8,7}, "降序窗口");
        check(s.maxSlidingWindow(new int[]{1,3,1,2,0,5}, 3), new int[]{3,3,2,5}, "补充");
    }
}

复杂度分析

解法一:每个元素进出队一次,总时间 O(n),空间 O(k)。

解法二:时间 O(n log n),空间 O(k)。

解法三(分块):时间 O(n),空间 O(n)。


题目描述

编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target。

该矩阵具有以下特性:

  • 每行的元素从左到右升序排列;
  • 每列的元素从上到下升序排列。
  • 输入 :int[][] matrix, int target(m == matrix.length, n == matrix[i].length,
    1 <= n, m <= 300,-10^9 <= matrix[i][j] <= 10^9)
  • 输出 :boolean(是否存在目标值)

示例 1:

复制代码
matrix=[[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10,13,14,17,24],[18,21,23,26,30]],

target=5 -> true

示例 2:

复制代码
target=20 -> false

约束:

  • m == matrix.length;n == matrix[i].length;1 <= n, m <= 300;
    -10^9 <= matrix[i][j] <= 10^9;每行的所有元素从左到右升序排列;每列的所有元素从上到下升序排列;
    -10^9 <= target <= 10^9。

解题思路

右上角"阶梯搜索" / 逐行二分

  • 解法一(右上角开始):从右上角 (0, n-1) 出发:
    若 cur > target,则整列都更大,左移一列;
    若 cur < target,则整行都更小,下移一行;
    相等则命中。每一步都排除一整行或一整列,最多 m+n 步。
    为什么这个点能用:右上角满足"同行左边都小、同列下边都大",两个方向唯一性。
  • 解法二(逐行二分):对每行做二分 O(m log n)。
  • 解法三(暴力):O(mn) 作为基准。
    为什么不从左上角开始:左上角右下都更大,无法唯一决定排除方向。

代码实现

java 复制代码
public 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) {
            int cur = matrix[i][j];
            if (cur == target) {
                return true;
            } else if (cur > target) {
                j--; // 当前列全部更大,左移
            } else {
                i++; // 当前行全部更小,下移
            }
        }
        return false;
    }

    public boolean searchMatrixBinary(int[][] matrix, int target) {
        for (int[] row : matrix) {
            if (row.length > 0 && java.util.Arrays.binarySearch(row, target) >= 0) {
                return true;
            }
        }
        return false;
    }

    public boolean searchMatrixBrute(int[][] matrix, int target) {
        for (int[] row : matrix) {
            for (int v : row) {
                if (v == target) return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[][] m1 = {{1,4,7,11,15},{2,5,8,12,19},{3,6,9,16,22},{10,13,14,17,24},{18,21,23,26,30}};
        check(s.searchMatrix(m1, 5), true, "示例1");
        check(s.searchMatrix(m1, 20), false, "示例2");
        check(s.searchMatrixBinary(m1, 5), true, "二分法示例1");
        check(s.searchMatrixBrute(m1, 5), true, "暴力示例1");
        check(s.searchMatrixBrute(m1, 20), false, "暴力示例2");
        check(s.searchMatrix(m1, 30), true, "右下角边界");
        check(s.searchMatrix(m1, 1), true, "左上角边界");
        check(s.searchMatrix(new int[][]{{-5}}, -5), true, "单格");
    }

    private static void check(boolean actual, boolean expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

解法一:时间 O(m + n),空间 O(1)。

解法二:时间 O(m log n),空间 O(1)。

解法三(暴力):时间 O(mn),空间 O(1)。


241. Different Ways to Add Parentheses(为运算表达式设计优先级)

题目描述

给你一个由数字和运算符组成的字符串 expression,按不同优先级组合数字和运算符,

计算并返回所有可能组合的结果。你可以按任意顺序返回答案。

生成的测试用例满足其对应输出结果符合 32 位整数范围,不同结果的数量不超过 10^4。

  • 输入 :String expression(1 <= expression.length <= 20)
  • 输出 :List<Integer>(所有可能的计算结果)

示例 1:

复制代码
expression="2-1-1" -> [0,2]

((2-1)-1 = 0;2-(1-1) = 2)

示例 2:

复制代码
expression="2*3-4*5" -> [-34,-14,-10,-10,10]

2*(3-(4*5)) = -34;2*((3-4)*5) = -10;(2*(3-4))*5 = -10;

((2*3)-4)*5 = 10;(2*3)-(4*5) = -14

约束:

  • 1 <= expression.length <= 20;expression 由数字和算符 ('+','-','*') 组成;
    expression 中的所有整数互不相同(题意按数字与算符交替,无括号)。

解题思路

分治:以每个运算符为"最后执行"切分左右

任意加括号方式等价于"某个运算符最后做"。对表达式 expressionl...r:

  • 枚举每个运算符位置 op:
    左半 expressionl...op-1 的所有结果 lefts;
    右半 expressionop+1...r 的所有结果 rights;
    两两按 op 运算得到本层的所有结果。
  • 若区间内没有运算符,则本身是一个数。
    记忆化可避免重复计算;expression 长度 <= 20,直接递归也足够。
    为什么按"最后运算符"分治不漏解:任何括号化方案最外层总有一个最后执行的
    运算符,它把表达式切分成两个独立子问题,枚举它即枚举全部方案。

代码实现

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

public class Solution {

    public List<Integer> diffWaysToCompute(String expression) {
        return helper(expression, 0, expression.length() - 1);
    }

    private List<Integer> helper(String s, int l, int r) {
        List<Integer> res = new ArrayList<>();
        int num = 0;
        boolean hasOp = false;
        // 枚举"最后执行"的运算符
        for (int i = l; i <= r; i++) {
            char c = s.charAt(i);
            if (c == '+' || c == '-' || c == '*') {
                hasOp = true;
                List<Integer> lefts = helper(s, l, i - 1);
                List<Integer> rights = helper(s, i + 1, r);
                for (int a : lefts) {
                    for (int b : rights) {
                        res.add(apply(c, a, b));
                    }
                }
            }
        }
        // 无运算符:纯数字区间
        if (!hasOp) {
            for (int i = l; i <= r; i++) num = num * 10 + (s.charAt(i) - '0');
            res.add(num);
        }
        return res;
    }

    private int apply(char op, int a, int b) {
        switch (op) {
            case '+': return a + b;
            case '-': return a - b;
            default:  return a * b;
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(equalSorted(s.diffWaysToCompute("2-1-1"), new ArrayList<>(java.util.Arrays.asList(0, 2))), true, "示例1");
        check(equalSorted(s.diffWaysToCompute("2*3-4*5"), new ArrayList<>(java.util.Arrays.asList(-34,-14,-10,-10,10))), true, "示例2");
        check(s.diffWaysToCompute("3").equals(java.util.Arrays.asList(3)), true, "单数字");
        check(equalSorted(s.diffWaysToCompute("1+2*3"), new ArrayList<>(java.util.Arrays.asList(7, 9))), true, "补充:1+(2*3)=7,(1+2)*3=9");
    }

    private static void check(boolean ok, boolean expected, String name) {
        System.out.println((expected == ok ? "PASS" : "FAIL") + " | " + name
                + " | ok=" + ok + " expected=" + expected);
        if (expected != ok) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

时间 O(C(n))(卡特兰数级别,n 为算符个数,可以用记忆化解)

空间 O(C(n));n <= 20 时结果数 <= 10^4,安全。


242. Valid Anagram(有效的字母异位词)

题目描述

给定两个字符串 s 和 t,编写一个函数来判断 t 是否是 s 的字母异位词。

注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

  • 输入 :String s, String t(1 <= s.length, t.length <= 5 * 10^4)
  • 输出 :boolean(是否为字母异位词)

示例 1:

复制代码
s="anagram", t="nagaram" -> true

示例 2:

复制代码
s="rat", t="car" -> false

约束:

  • s 和 t 仅包含小写字母。
    进阶:如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况?

解题思路

计数数组 / 排序 / 哈希表(unicode)

  • 解法一(计数数组):题目限制小写字母,用 int26。遍历 s 加计数,
    遍历 t 减计数,最后全为 0 则 true。时间 O(n),空间 O(1)。
  • 解法二(排序比较):排序后相等即异位词,时间 O(n log n)。
  • 进阶(HashMap 计数):字符是 unicode 时计数数组不再适用,改用
    Map<Character, Integer>,逻辑相同但空间随字符种类变化。
    为什么计数数组可行:小写字母只有 26 种,频率数组天然是"字符->次数"的映射,
    最长两个字符串相同长度的前提下,频率一致等价于异位词。

代码实现

java 复制代码
public class Solution {

    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) return false;
        int[] cnt = new int[26];
        for (char c : s.toCharArray()) cnt[c - 'a']++;
        for (char c : t.toCharArray()) {
            if (--cnt[c - 'a'] < 0) return false; // 频次不够
        }
        return true;
    }

    public boolean isAnagramSort(String s, String t) {
        char[] a = s.toCharArray();
        char[] b = t.toCharArray();
        java.util.Arrays.sort(a);
        java.util.Arrays.sort(b);
        return java.util.Arrays.equals(a, b);
    }

    public boolean isAnagramUnicode(String s, String t) {
        if (s.length() != t.length()) return false;
        java.util.Map<Character, Integer> map = new java.util.HashMap<>();
        for (char c : s.toCharArray()) map.put(c, map.getOrDefault(c, 0) + 1);
        for (char c : t.toCharArray()) {
            int v = map.getOrDefault(c, 0);
            if (v == 0) return false;
            map.put(c, v - 1);
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.isAnagram("anagram", "nagaram"), true, "示例1");
        check(s.isAnagram("rat", "car"), false, "示例2");
        check(s.isAnagramSort("anagram", "nagaram"), true, "排序法示例1");
        check(s.isAnagramSort("rat", "car"), false, "排序法示例2");
        check(s.isAnagramUnicode("anagram", "nagaram"), true, "unicode版示例1");
        check(s.isAnagram("a", "ab"), false, "长度不等");
    }

    private static void check(boolean actual, boolean expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

解法一:时间 O(n),空间 O(1)(固定 26)。

解法二:时间 O(n log n),空间 O(log n)(排序栈)。进阶:空间 O(|字符集|)。


243. Shortest Word Distance(最短单词距离)

题目描述

给定一个字符串数组 wordsDict 和两个字符串 word1、word2,

返回这两个单词在列表中出现的最短距离。

  • 输入 :String[] wordsDict, String word1, String word2(2 <= wordsDict.length <= 3 * 10^4)
  • 输出 :int(word1 与 word2 最近出现位置的距离)
    示例:words = ["practice", "makes", "perfect", "coding", "makes"]
    word1="coding", word2="practice" -> 3
    word1="makes", word2="coding" -> 1
    约束:
  • word1 不等于 word2;word1 与 word2 均出现在数组 wordsDict 中。

解题思路

一遍扫描维护两个最近下标

  • 解法一(双指针/最近下标):遍历数组,遇到 word1 更新 i1、遇到 word2 更新 i2;
    两个下标都有效时更新答案 = min(ans, |i1 - i2|)。
    为什么"最近出现位置"即可:任意两个出现位置之间必然隔着更接近的两个,
    用最近下标两两比较不会漏最优解。
  • 解法二(预存位置 + 双指针归并):先建 词->位置列表 的哈希表,再对两个列表
    归并求最小差。适合多次查询场景(即 244 的预处理思想),单次查询也可用。

代码实现

java 复制代码
public class Solution {

    public int shortestDistance(String[] wordsDict, String word1, String word2) {
        int i1 = -1, i2 = -1;
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i < wordsDict.length; i++) {
            if (wordsDict[i].equals(word1)) {
                i1 = i;
            } else if (wordsDict[i].equals(word2)) {
                i2 = i;
            }
            if (i1 != -1 && i2 != -1) {
                ans = Math.min(ans, Math.abs(i1 - i2));
            }
        }
        return ans;
    }

    public int shortestDistancePreindex(String[] wordsDict, String word1, String word2) {
        java.util.List<Integer> pos1 = new java.util.ArrayList<>();
        java.util.List<Integer> pos2 = new java.util.ArrayList<>();
        for (int i = 0; i < wordsDict.length; i++) {
            if (wordsDict[i].equals(word1)) pos1.add(i);
            else if (wordsDict[i].equals(word2)) pos2.add(i);
        }
        int ans = Integer.MAX_VALUE;
        int i = 0, j = 0;
        while (i < pos1.size() && j < pos2.size()) {
            int diff = Math.abs(pos1.get(i) - pos2.get(j));
            ans = Math.min(ans, diff);
            if (pos1.get(i) < pos2.get(j)) i++;
            else j++;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] words = {"practice", "makes", "perfect", "coding", "makes"};
        check(s.shortestDistance(words, "coding", "practice"), 3, "示例1");
        check(s.shortestDistance(words, "makes", "coding"), 1, "示例2");
        check(s.shortestDistancePreindex(words, "coding", "practice"), 3, "预存位置法示例1");
        check(s.shortestDistancePreindex(words, "makes", "coding"), 1, "预存位置法示例2");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

解法一:时间 O(n),空间 O(1)。解法二:时间 O(n)(建表+归并),空间 O(n)。


244. Shortest Word Distance II(最短单词距离 II)

题目描述

设计一个类 WordDistance,接收一个字符串数组 wordsDict 初始化,

并实现方法:给定两个单词 word1、word2(两者不同,且都出现在列表中),

返回它们之间的最短距离。该方法会被多次调用,因此需要尽可能高效。

  • 输入 :构造方法 String[] wordsDict;查询 String word1, String word2
  • 输出 :int(最短距离)
    示例:wordsDict = ["practice", "makes", "perfect", "coding", "makes"]
    new WordDistance(wordsDict);
    wordDistance.shortest("coding", "practice") -> 3
    wordDistance.shortest("makes", "coding") -> 1
    约束:
  • word1 不等于 word2;word1 和 word2 均存在于列表中。

解题思路

预处理:词 -> 全部出现下标列表;查询:双指针归并求最小差

  • 构造:遍历一次数组,用 Map<String, List> 记录每个词的所有下标(天然升序)。
  • 查询:对 word1、word2 的两个下标列表做双指针归并;
    每次取 |ai - bj| 更新答案,谁小谁前进(贪心逼近)。
    时间 O(m + n),m、n 为两词出现次数。因为列表升序,双指针不会漏最优解。
    为什么预处理能加速多次查询:每词下标列表长度远小于全数组,查询从 O(N)
    降到 O(出现次数和)。

代码实现

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

public class WordDistance {

    private Map<String, List<Integer>> map;

    public WordDistance(String[] wordsDict) {
        map = new HashMap<>();
        for (int i = 0; i < wordsDict.length; i++) {
            map.computeIfAbsent(wordsDict[i], k -> new ArrayList<>()).add(i);
        }
    }

    public int shortest(String word1, String word2) {
        List<Integer> l1 = map.get(word1);
        List<Integer> l2 = map.get(word2);
        int i = 0, j = 0;
        int ans = Integer.MAX_VALUE;
        while (i < l1.size() && j < l2.size()) {
            int a = l1.get(i), b = l2.get(j);
            ans = Math.min(ans, Math.abs(a - b));
            if (a < b) i++; else j++; // 让更小的下标前进,距离才可能缩小
        }
        return ans;
    }

    public static void main(String[] args) {
        WordDistance wd = new WordDistance(new String[]{"practice", "makes", "perfect", "coding", "makes"});
        check(wd.shortest("coding", "practice"), 3, "示例1");
        check(wd.shortest("makes", "coding"), 1, "示例2");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

构造:时间 O(N),空间 O(N)。查询:时间 O(m + n),空间 O(1)。


245. Shortest Word Distance III(最短单词距离 III)

题目描述

给定一个字符串数组 wordsDict 和两个字符串 word1、word2,

返回这两个单词在列表中出现的最短距离。

本题与 243 的唯一区别:word1 和 word2 可能相同,它们代表列表中两个单独的单词

(即 word1 == word2 时,需要找该单词两次不同出现位置之间的最短距离)。

  • 输入 :String[] wordsDict, String word1, String word2
  • 输出 :int(最短距离)
    示例:words = ["practice", "makes", "perfect", "coding", "makes"]
    word1="makes", word2="coding" -> 1
    word1="makes", word2="makes" -> 3
    约束:
  • word1 和 word2 均出现在数组中;数组中至少有两次不同位置可满足 word1==word2 的查询。

解题思路

单遍扫描 + 延迟赋值处理同词

相同词时不能"各自维护一个下标再求差"(那样差恒为 0),需要记录上一次出现位置:

  • 当 word1.equals(word2) 时:遍历过程遇到该词,用"上一次出现位置"算距离,
    再更新 prev = i。这等价于相邻两次出现之间的距离(相邻是最短的)。
  • 当 word1 != word2 时:用 243 的双下标法。
  • 统一写法:维护 last 表示"上一次匹配到 word1 或 word2 的位置",同时用 idx1/idx2
    记录当前各词最近位置;若两词相同则令 idx2 = idx1 再更新 idx1,即可复用同一循环。
    为什么相邻出现即最短:同词多次出现之间按位置排序,任意两次之间的区间内必有
    更近的相邻对,所以只需检查相邻出现。

代码实现

java 复制代码
public class Solution {

    public int shortestWordDistance(String[] wordsDict, String word1, String word2) {
        long idx1 = -1, idx2 = -1; // 用 long 防止 -1 与 0 的减法边界误判
        int ans = Integer.MAX_VALUE;
        boolean same = word1.equals(word2);
        for (int i = 0; i < wordsDict.length; i++) {
            if (wordsDict[i].equals(word1)) {
                if (same) {
                    idx2 = idx1; // 相同词:原偏移位给 word2,之后 word1 更新到当前
                }
                idx1 = i;
            } else if (!same && wordsDict[i].equals(word2)) {
                idx2 = i;
            }
            if (idx1 >= 0 && idx2 >= 0) {
                ans = (int) Math.min(ans, Math.abs(idx1 - idx2));
            }
        }
        return ans;
    }

    public int shortestWordDistance2(String[] wordsDict, String word1, String word2) {
        int prev = -1;   // 最近一次匹配位置(任意一个词)
        int idx1 = -1, idx2 = -1;
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i < wordsDict.length; i++) {
            int matchIdx = -1;
            if (wordsDict[i].equals(word1)) matchIdx = i;
            else if (wordsDict[i].equals(word2)) matchIdx = i;
            if (matchIdx == -1) continue;
            if (word1.equals(word2)) {
                // 同词:用上一次出现位置计算
                if (prev != -1) ans = Math.min(ans, i - prev);
                prev = i;
            } else {
                // 异词:更新对应下标
                if (wordsDict[i].equals(word1)) idx1 = i; else idx2 = i;
                if (idx1 != -1 && idx2 != -1) ans = Math.min(ans, Math.abs(idx1 - idx2));
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] words = {"practice", "makes", "perfect", "coding", "makes"};
        check(s.shortestWordDistance(words, "makes", "coding"), 1, "示例1-异词");
        check(s.shortestWordDistance(words, "makes", "makes"), 3, "示例2-同词");
        check(s.shortestWordDistance2(words, "makes", "coding"), 1, "解法二示例1");
        check(s.shortestWordDistance2(words, "makes", "makes"), 3, "解法二示例2");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

时间 O(n),空间 O(1)。


246. Strobogrammatic Number(中心对称数)

题目描述

中心对称数是指一个数字旋转 180 度之后看起来依旧相同的数字(或者说上下颠倒看)。

请写一个函数来判断一个数字是否为中心对称数。数字以字符串形式给出。

  • 输入 :String num(数字字符串)
  • 输出 :boolean(是否中心对称)

示例 1:

复制代码
num="69" -> true

示例 2:

复制代码
num="88" -> true

示例 3:

复制代码
num="962" -> false

约束:

  • num 由数字字符组成(确保输入合法)。

解题思路

旋转映射双指针

旋转 180° 后合法的数字映射:0->0,1->1,6->9,8->8,9->6;

其余数字(2/3/4/5/7)旋转后无意义,直接不合法。

  • 解法一(双指针 + 映射表):左指针依次取数,检查它的旋转值是否等于右指针的数;
    每步向内移动。中间单个字符必须是 0/1/8(自旋转映射)。
  • 解法二(构造反转串):对 num 逐字符替换为旋转值得到 rotated,
    再整体反转,比较是否等于原串;等价于"从右到左逐位检查映射"。
    为什么双指针正确:中心对称要求"位置 i 的字符旋转后恰好等于位置 n-1-i 的字符",
    这正是镜像对称,逐对检查即可。

代码实现

java 复制代码
import java.util.HashMap;
import java.util.Map;

public class Solution {

    private static final Map<Character, Character> ROT = new HashMap<>();
    static {
        ROT.put('0', '0');
        ROT.put('1', '1');
        ROT.put('6', '9');
        ROT.put('8', '8');
        ROT.put('9', '6');
    }

    public boolean isStrobogrammatic(String num) {
        int i = 0, j = num.length() - 1;
        while (i <= j) {
            char a = num.charAt(i), b = num.charAt(j);
            // a 必须可旋转,且旋转后等于 b
            if (!ROT.containsKey(a) || ROT.get(a) != b) {
                return false;
            }
            i++;
            j--;
        }
        return true;
    }

    public boolean isStrobogrammaticRotate(String num) {
        StringBuilder sb = new StringBuilder();
        for (char c : num.toCharArray()) {
            Character r = ROT.get(c);
            if (r == null) return false; // 不存在旋转映射
            sb.append(r);
        }
        return sb.reverse().toString().equals(num);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.isStrobogrammatic("69"), true, "示例1");
        check(s.isStrobogrammatic("88"), true, "示例2");
        check(s.isStrobogrammatic("962"), false, "示例3");
        check(s.isStrobogrammatic("818"), true, "补充:818");
        check(s.isStrobogrammatic("1"), true, "单字符1");
        check(s.isStrobogrammaticRotate("69"), true, "旋转串法示例1");
        check(s.isStrobogrammaticRotate("962"), false, "旋转串法示例3");
    }

    private static void check(boolean actual, boolean expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

时间 O(n),空间 O(1)(映射表固定大小)。


247. Strobogrammatic Number II(中心对称数 II)

题目描述

给定一个整数 n,返回所有长度为 n 的中心对称数。

中心对称数是指一个数字旋转 180 度之后看起来依旧相同的数字。

  • 输入 :int n(1 <= n <= 14)
  • 输出 :List<String>(所有长度为 n 的中心对称数)

示例 1:

复制代码
n=2 -> ["11","69","88","96"]

示例 2:

复制代码
n=1 -> ["0","1","8"]

约束:

  • 1 <= n <= 14。

解题思路

从中间向外构造(递归/迭代,长度增减 2)

旋转对称的对子:0-0、1-1、6-9、8-8、9-6。长度为 n 的数由长度为 n-2 的数

外面套一对旋转对子得到。可以参考 n=0("")与 n=1("0","1","8")为基底:

  • 解法一(递归):construct(m) 生成全部长度为 m 的中心对称数;
    if (m == 0) 返回 "";if (m == 1) 返回 "0","1","8";
    否则对 construct(m-2) 的每个串,在外面套上 5 种对子。
    调用入口:construct(n),但最外层不能以 '0' 开头,需要额外过滤。
  • 解法二(从左到右 DFS 填空):用字符数组从两端向中间填,两侧成对放置,
    中间位(奇数长度时)只能放 0/1/8;首位不能为 0。
    为什么长度每次减 2:最外一对字符占据两端,去掉后内部剩余位数为 m-2,
    因此以 n 或 n-1 之差递归(题目提示 n-2 而非 n-1)。

代码实现

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

public class Solution {

    public List<String> findStrobogrammatic(int n) {
        List<String> res = build(n);
        // 最外层不能以 0 开头(除非 n == 1)
        if (n > 1) res.removeIf(s -> s.startsWith("0"));
        return res;
    }

    private List<String> build(int m) {
        if (m == 0) return new ArrayList<>(Arrays.asList(""));
        if (m == 1) return new ArrayList<>(Arrays.asList("0", "1", "8"));
        List<String> inner = build(m - 2); // 内部所有中心对称数
        List<String> res = new ArrayList<>();
        String[][] pairs = {{"0","0"},{"1","1"},{"6","9"},{"8","8"},{"9","6"}};
        for (String s : inner) {
            for (String[] p : pairs) {
                res.add(p[0] + s + p[1]);
            }
        }
        return res;
    }

    public List<String> findStrobogrammaticDFS(int n) {
        List<String> res = new ArrayList<>();
        char[] buf = new char[n];
        dfs(buf, 0, n - 1, res);
        return res;
    }

    private static final char[][] PAIRS = {{'0','0'},{'1','1'},{'6','9'},{'8','8'},{'9','6'}};

    private void dfs(char[] buf, int l, int r, List<String> res) {
        if (l > r) {
            res.add(new String(buf));
            return;
        }
        if (l == r) {
            // 中间位只能放自旋转数字
            for (char c : new char[]{'0','1','8'}) {
                buf[l] = c;
                dfs(buf, l + 1, r - 1, res);
            }
            return;
        }
        for (char[] p : PAIRS) {
            // 最外层不能以 0 开头
            if (l == 0 && p[0] == '0') continue;
            buf[l] = p[0];
            buf[r] = p[1];
            dfs(buf, l + 1, r - 1, res);
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(equalList(s.findStrobogrammatic(2), new ArrayList<>(Arrays.asList("11","69","88","96"))), true, "示例1");
        check(equalList(s.findStrobogrammatic(1), new ArrayList<>(Arrays.asList("0","1","8"))), true, "示例2");
        check(equalList(s.findStrobogrammatic(3), new ArrayList<>(Arrays.asList("101","111","181","609","619","689","808","818","888","906","916","986"))), true, "n=3");
        check(equalList(s.findStrobogrammaticDFS(2), new ArrayList<>(Arrays.asList("11","69","88","96"))), true, "DFS法示例1");
    }

    private static void check(boolean actual, boolean expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

解法一:时间 O(5^(n/2)) 级别(结果数量级),空间 O(结果数 * n)。


248. Strobogrammatic Number III(中心对称数 III)

题目描述

给定两个字符串 low 和 high,表示两个整数 low, high,

请统计这个区间内中心对称数的个数。中心对称数是旋转 180 度后仍相同的数字。

  • 输入 :String low, String high(low 和 high 由数字组成,low <= high)
  • 输出 :int(区间内中心对称数的个数)

示例 1:

复制代码
low="50", high="100" -> 3(69、88、96)

示例 2:

复制代码
low="0", high="0" -> 1(0)

约束:

  • 由两个字符串给出的数字保证 low <= high。

解题思路

枚举所有长度在 len(low), len(high) 的中心对称数并过滤

中心对称数至多 5^7 个(长度 <= 14 时,每加 2 位乘 5,即 5 种对子),

因此可以先生成再按数值过滤:

  • 生成:复用 247 的"从中间向外构造",对每个长度 len 生成全部中心对称数;
  • 过滤:去除前导 0(长度 > 1 时);字符串按字典序比较即数值比较(等长时),
    跨长度时用长度判断即可覆盖区间。
  • 等长比较技巧:把 low 与 high 补齐到与目标长度相同再按字典序比较,或用 BigDecimal。
  • 进阶:可以按"最低长度基准"优化:长度恰好等于 len(low) 时要求 >= low,
    恰好等于 len(high) 时要求 <= high,中间长度全部计入。
    为什么枚举可行:n <= 14 因此最长 14 位,总生成量 = (5^1 + 5^2 + ... + 5^7 级别),
    在 10^4 量级以内,性能无忧。

代码实现

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

public class Solution {

    private static final char[][] PAIRS = {{'0','0'},{'1','1'},{'6','9'},{'8','8'},{'9','6'}};
    private int count = 0;

    public int strobogrammaticInRange(String low, String high) {
        count = 0;
        for (int len = low.length(); len <= high.length(); len++) {
            char[] buf = new char[len];
            dfs(buf, 0, len - 1, low, high);
        }
        return count;
    }

    private void dfs(char[] buf, int l, int r, String low, String high) {
        if (l > r) {
            String num = new String(buf);
            if (!isNoLeadingZero(num)) return;      // 长度 > 1 不允许前导 0
            if (inRange(num, low, high)) count++;
            return;
        }
        if (l == r) {
            for (char c : new char[]{'0','1','8'}) { // 中间位
                buf[l] = c;
                dfs(buf, l + 1, r - 1, low, high);
            }
            return;
        }
        for (char[] p : PAIRS) {
            buf[l] = p[0];
            buf[r] = p[1];
            dfs(buf, l + 1, r - 1, low, high);
        }
    }

    private boolean isNoLeadingZero(String s) {
        return s.length() == 1 || s.charAt(0) != '0';
    }

    private boolean inRange(String num, String low, String high) {
        if (num.length() < low.length() || num.length() > high.length()) return false;
        // 与 low 比较
        if (num.length() > low.length()) {
            // 长度更长必然 > low
        } else if (num.compareTo(low) < 0) {
            return false;
        }
        // 与 high 比较
        if (num.length() < high.length()) {
            // 长度更短必然 < high
        } else if (num.compareTo(high) > 0) {
            return false;
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.strobogrammaticInRange("50", "100"), 3, "示例1");
        check(s.strobogrammaticInRange("0", "0"), 1, "示例2");
        check(s.strobogrammaticInRange("0", "9"), 3, "补充:0,1,8");
        check(s.strobogrammaticInRange("1", "100"), 6, "补充:1,8,11,69,88,96");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

时间 O(5^(N/2))(N=high 长度),空间 O(结果数)。


249. Group Shifted Strings(移位字符串分组)

题目描述

给定一个字符串数组 strings,将其中的字符串按照"移位序列"分组。

移位序列是指:将字符串 s 的每个字母沿字母表向前/向后移动相同位数得到的字符串。

例如 "abc" -> "bcd" -> ... -> "xyz"。

注意:"az" 与 "ba" 属于同一移位序列(z 往后移到 a,即循环移位)。

  • 输入 :String[] strings(1 <= strings.length <= 200,1 <= stringsi.length <= 50)
  • 输出 :List<List<String>>(每个分组内的字符串按字典序排列)
    示例:strings = ["abc","bcd","acef","xyz","az","ba","a","z"]
    -> \["abc","bcd","xyz","az","ba","acef","`a`","z"]
    约束:
  • strings 由小写字母组成;1 <= strings.length <= 200;1 <= stringsi.length <= 50。

解题思路

标准化"差分键"哈希分组

两个串能互相移位得到,当且仅当它们的"相邻字符差的循环序列"完全相同:

  • 对每个字符串,相邻字符差 d = (si - si-1 + 26) % 26;
    把差分序列拼接成标准化键(如 "abc" -> "11","bcd" -> "11","xyz" -> "11";"az" -> "25",
    "ba" -> "25")。
  • 同一键的字符串必然同一组。
    为什么 % 26:字母表循环,'z' 移到 'a' 的差为 25(取模后仍用正整数表达)。
  • 分组后每组内部按字典序排序(题目要求)。
    为什么差分键充分必要:若 A 能移位得到 B,则对应字符差完全一致;反之亦然。

代码实现

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

public class Solution {

    public List<List<String>> groupStrings(String[] strings) {
        Map<String, List<String>> map = new HashMap<>();
        for (String s : strings) {
            String key = encode(s);
            map.computeIfAbsent(key, k -> new ArrayList<>()).add(s);
        }
        List<List<String>> res = new ArrayList<>();
        for (List<String> group : map.values()) {
            java.util.Collections.sort(group); // 组内按字典序
            res.add(group);
        }
        return res;
    }

    private String encode(String s) {
        StringBuilder sb = new StringBuilder();
        for (int i = 1; i < s.length(); i++) {
            int diff = (s.charAt(i) - s.charAt(i - 1) + 26) % 26; // 处理循环差
            sb.append(diff).append(',');
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        List<List<String>> r = s.groupStrings(new String[]{"abc","bcd","acef","xyz","az","ba","a","z"});
        // 校验分组数 = 4
        check(r.size(), 4, "示例:分组数");
        boolean hasAbcGroup = false, hasAzGroup = false;
        for (List<String> g : r) {
            if (g.toString().equals("[abc, bcd, xyz]")) hasAbcGroup = true;
            if (g.toString().equals("[az, ba]")) hasAzGroup = true;
        }
        check(hasAbcGroup, true, "示例:abc组");
        check(hasAzGroup, true, "示例:az组");
        check(s.groupStrings(new String[]{"a","a"}).size(), 1, "重复串");
        check(s.groupStrings(new String[]{"ab"}).size(), 1, "单串");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }

    private static void check(boolean actual, boolean expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

时间 O(∑len * avgLen)(标准化每个串 O(len)),空间 O(∑len)。


250. Count Univalue Subtrees(统计同值子树)

题目描述

给定一个二叉树的根节点 root,统计该二叉树中"同值子树"的数量。

同值子树是指子树中所有节点都有相同值的子树。

  • 输入 :TreeNode root(树的节点数范围 0, 1000)
  • 输出 :int(同值子树个数)
    示例:二叉树下(值):
    5
    /
    1 5
    / \
    5 5 5
    返回 4(三个叶节点 5 各自是同值子树,加上根节点的右子树 5-\>5 也是同值子树)
    约束:
  • 树的节点数范围 0, 1000;节点值范围为 -1000, 1000。

解题思路

后序遍历的"同值性"自底向上判定

一个节点为根的子树是同值子树,当且仅当:

  • 左右子树都(为空或)是同值子树;
  • 且左右子节点(若存在)的值都等于当前节点值。
  • 解法一(递归返回 boolean):后序遍历每个节点,子树的同值性作为返回值向上汇总,
    每次返回 true 时计数 +1。
  • 解法二(全局计数 + 递归判定):与解法一本质相同,只是把计数作为类变量累计。
    本代码实现同时给出"不带类变量"的变体(计数用数组/引用包装),供对比。
    为什么后序:判断父节点必须已知子树内部是否同值,只能自底向上。

代码实现

java 复制代码
public class Solution {

    private int count = 0;

    public int countUnivalSubtrees(TreeNode root) {
        count = 0;
        isUnival(root);
        return count;
    }

    private boolean isUnival(TreeNode node) {
        if (node == null) return true; // 空树视为同值
        boolean leftOk = isUnival(node.left);
        boolean rightOk = isUnival(node.right);
        // 若左子树不为空且值与当前节点不同,则非同值
        if (leftOk && rightOk
                && (node.left == null || node.left.val == node.val)
                && (node.right == null || node.right.val == node.val)) {
            count++;
            return true;
        }
        return false;
    }

    public int countUnivalSubtreesNoField(TreeNode root) {
        int[] cnt = new int[1]; // 用数组包装,避免类变量
        isUnivalNoField(root, cnt);
        return cnt[0];
    }

    private boolean isUnivalNoField(TreeNode node, int[] cnt) {
        if (node == null) return true;
        boolean leftOk = isUnivalNoField(node.left, cnt);
        boolean rightOk = isUnivalNoField(node.right, cnt);
        if (leftOk && rightOk
                && (node.left == null || node.left.val == node.val)
                && (node.right == null || node.right.val == node.val)) {
            cnt[0]++;
            return true;
        }
        return false;
    }

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

    public static void main(String[] args) {
        Solution s = new Solution();
        //          5
        //        /  \
        //       1    5
        //      / \    \
        //     5   5    5
        TreeNode r1 = build(new Integer[]{5,1,5,5,5,null,5});
        check(s.countUnivalSubtrees(r1), 4, "示例1");
        check(s.countUnivalSubtreesNoField(r1), 4, "无类变量版示例1");
        check(s.countUnivalSubtrees(build(new Integer[]{})), 0, "空树");
        check(s.countUnivalSubtrees(build(new Integer[]{5,5,5,5,5,null,5})), 6, "全同值");
        check(s.countUnivalSubtrees(build(new Integer[]{1,2,3})), 2, "根不同值:两叶子");
    }

    private static void check(int actual, int expected, String name) {
        System.out.println((expected == actual ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + actual + " expected=" + expected);
        if (expected != actual) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

两种解法时间均 O(n),空间 O(h)(递归栈深)。

相关推荐
小蒜学长2 小时前
基于Spring Boot+Vue3的在线游戏技术指导师匹配系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·在线游戏技术指导
Dovis(誓平步青云)2 小时前
逆变器报警却找不到板子,热斑图怎样定位到具体组串
java·开发语言·人工智能·机器学习·卷积神经网络
摇滚侠3 小时前
《Java并发编程之美》第 2 章 并发编程的其他基础知识 阅读笔记 3
android·java·笔记
墨子夕3 小时前
设计模式——Java × Python 双语言精讲 · 23 个经典模式 + 现代演进(第一篇)
java·python·设计模式
程序 代码狂人3 小时前
经典 SQL 面试题:连续登录问题的两种经典解法
java·开发语言·算法
weixin_BYSJ19873 小时前
【计算机毕设】房屋租赁系统02647
java·vue.js·spring boot·python·django·课程设计
VX_bysjlw9853 小时前
基于协同过滤的美食推荐系统的设计与实现32221-计算机毕设原创(免费领源码+带部署教程)
java·spring boot·redis·mysql·tomcat·maven·mybatis
Misnice3 小时前
Python 的数据类型
java·windows·python