LeetCode201~225题目解析

LeetCode 201-225 题解合集

文章目录


201. Bitwise AND of Numbers Range(数字范围按位与)

题目描述

给你两个整数 left 和 right,表示区间 `left`, `right`,返回此区间内所有数字按位与的结果

(包含 left、right 两端点)。

  • 输入 :int left, int right(0 <= left <= right <= 2^31 - 1)
  • 输出 :int(区间内所有数字按位与的结果)

示例 1:

复制代码
left=5, right=7 -> 4

示例 2:

复制代码
left=0, right=0 -> 0

示例 3:

复制代码
left=1, right=2147483647 -> 0

约束:

  • 0 <= left <= right <= 2^31 - 1。

解题思路

公共前缀法

观察按位与的性质:区间 left, right 连续计数时,只要某一位在计数过程中发生过 0->1 翻转,

这一位相与结果必为 0。因此最终结果只可能保留 left 与 right 的"公共二进制前缀",

其余低位全部清零。

  • 解法一(右移找公共前缀):同时右移 left 和 right 直到两者相等,记录右移次数 shift,
    把相等的值左移 shift 位即为答案。
  • 解法二(Brian Kernighan 式清最低位):反复把 right 最右侧的 1 清零,
    直到 right <= left,此时 right 即为公共前缀(含 left 本身)。
    为什么正确:每清掉一个最低位的 1,等价于把前缀中较长的那一侧截短到最近的公共前缀分界,
    最终 right 会停在公共前缀的位置。

代码实现

java 复制代码
public class Solution {

    public int rangeBitwiseAnd(int left, int right) {
        int shift = 0;
        // 同时右移直到两数相等,得到公共前缀
        while (left < right) {
            left >>= 1;
            right >>= 1;
            shift++;
        }
        // 公共前缀左移回原位,低位补 0
        return left << shift;
    }

    public int rangeBitwiseAnd2(int left, int right) {
        while (left < right) {
            // 清除 right 最低位的 1(Brian Kernighan 技巧变体)
            right &= (right - 1);
        }
        return right;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.rangeBitwiseAnd(5, 7), 4, "示例1");
        check(s.rangeBitwiseAnd(0, 0), 0, "示例2");
        check(s.rangeBitwiseAnd(1, 2147483647), 0, "示例3");
        check(s.rangeBitwiseAnd2(5, 7), 4, "解法二示例1");
        check(s.rangeBitwiseAnd2(0, 0), 0, "解法二示例2");
        check(s.rangeBitwiseAnd2(3, 3), 3, "单数区间");
        check(s.rangeBitwiseAnd(12, 15), 12, "补充:12&13&14&15=12");
    }

    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(log n):最多右移/清零 32 位;

空间复杂度 O(1)。


202. Happy Number(快乐数)

题目描述

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,

然后重复这个过程直到这个数变为 1,也可能是无限循环但始终变不到 1。

如果可以变为 1,那么这个数就是快乐数;否则不是快乐数。

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

示例 1:

复制代码
n=19 -> true (19 -> 82 -> 68 -> 100 -> 1)

示例 2:

复制代码
n=2 -> false

约束:

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

解题思路

快慢指针/集合判环

关键观察:非快乐数会进入循环(不是无限发散),因为平方和的最大值是有限的

(如 999...9 的平方和 < 3*位数),最终必然回到某个已出现的数或 1。

  • 解法一(哈希集合):不断计算平方和,若出现 1 则快乐;若某个数再次出现(成环)则不快乐。
  • 解法二(快慢指针/Floyd 判环):slow 每次算一次平方和,fast 每次算两次;
    若 slow 与 fast 相遇(成环)且不等于 1 则不快乐,等于 1 则快乐。
    相比集合法省去额外空间。
    为什么 92 位整数平方和收敛:n 最多 10 位数,各位平方和最大为 9^210=810,
    一旦落到 1000 以内,其平方和最多 9^2
    3 + 1^2 = 244 内,变化范围有限必成环。

代码实现

java 复制代码
import java.util.HashSet;
import java.util.Set;

public class Solution {

    public boolean isHappy(int n) {
        Set<Integer> seen = new HashSet<>();
        // 不断迭代;1 是快乐数出口,出现重复数表明成环(非快乐数)
        while (n != 1 && seen.add(n)) {
            n = sumOfSquares(n);
        }
        return n == 1;
    }

    public boolean isHappy2(int n) {
        int slow = n, fast = sumOfSquares(n);
        // 慢指针走一步,快指针走两步;相遇即成环
        while (fast != 1 && slow != fast) {
            slow = sumOfSquares(slow);
            fast = sumOfSquares(sumOfSquares(fast));
        }
        return fast == 1;
    }

    private int sumOfSquares(int n) {
        int sum = 0;
        while (n > 0) {
            int d = n % 10;
            sum += d * d;
            n /= 10;
        }
        return sum;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.isHappy(19), true, "示例1");
        check(s.isHappy(2), false, "示例2");
        check(s.isHappy2(19), true, "解法二示例1");
        check(s.isHappy2(2), false, "解法二示例2");
        check(s.isHappy(1), true, "边界1");
        check(s.isHappy(7), true, "补充:7是快乐数");
    }

    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(log n)(进入循环前迭代次数有限),空间 O(log n)(集合大小)。

解法二:时间复杂度 O(log n),空间 O(1)。


203. Remove Linked List Elements(移除链表元素)

题目描述

给你一个链表的头节点 head 和一个整数 val,请你删除链表中所有满足 Node.val == val 的节点,

并返回新的头节点。

  • 输入 :ListNode head, int val(链表中节点数目范围 0, 10\^4,1 <= Node.val <= 50,0 <= val <= 50)
  • 输出 :ListNode(删除后链表的头节点)

示例 1:

复制代码
head=[1,2,6,3,4,5,6], val=6 -> [1,2,3,4,5]

示例 2:

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

示例 3:

复制代码
head=[7,7,7,7], val=7 -> []

约束:

  • 链表中节点数目范围 0, 10\^4;1 <= Node.val <= 50;0 <= val <= 50。

解题思路

哨兵节点 + 单指针遍历

头节点本身也可能是待删除节点,引入哑节点 dummy 统一逻辑:

  • 解法一(哑节点 + 迭代):cur 从 dummy 出发,若 cur.next.val == val 则跳过
    (cur.next = cur.next.next),否则 cur 前进;最后返回 dummy.next。
  • 解法二(递归):对子问题 removeElements(head.next, val) 递归求解,若头节点值等于 val
    直接返回子问题的解,否则让 head.next 指向子问题的解并返回 head。
    递归本质是"从尾部向头部逐个过滤"。

代码实现

java 复制代码
public class Solution {

    public ListNode removeElements(ListNode head, int val) {
        ListNode dummy = new ListNode(0, head);
        ListNode cur = dummy;
        // 只要后继存在就检查:值相等则删除后继,否则前移
        while (cur.next != null) {
            if (cur.next.val == val) {
                cur.next = cur.next.next; // 跳过待删节点
            } else {
                cur = cur.next;
            }
        }
        return dummy.next;
    }

    public ListNode removeElements2(ListNode head, int val) {
        if (head == null) {
            return null;
        }
        head.next = removeElements2(head.next, val); // 先处理后续链表
        return head.val == val ? head.next : head;   // 再决定当前节点去留
    }

    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; }
    }

    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(toArr(s.removeElements(build(new int[]{1,2,6,3,4,5,6}), 6)),
                new int[]{1,2,3,4,5}, "示例1");
        check(toArr(s.removeElements(build(new int[]{}), 1)),
                new int[]{}, "示例2-空链表");
        check(toArr(s.removeElements(build(new int[]{7,7,7,7}), 7)),
                new int[]{}, "示例3-全删");
        check(toArr(s.removeElements2(build(new int[]{1,2,6,3,4,5,6}), 6)),
                new int[]{1,2,3,4,5}, "解法二示例1");
        check(toArr(s.removeElements(build(new int[]{1,2,2,1}), 2)),
                new int[]{1,1}, "补充:中间连续");
    }
}

复杂度分析

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

解法二:时间复杂度 O(n),空间 O(n)(递归栈深度)。


204. Count Primes(计数质数)

题目描述

给定整数 n,返回所有小于非负整数 n 的质数的数量。

  • 输入 :int n(0 <= n <= 5 * 10^6)
  • 输出 :int([0, n) 区间内质数个数)

示例 1:

复制代码
n=10 -> 4 (2, 3, 5, 7)

示例 2:

复制代码
n=0 -> 0

示例 3:

复制代码
n=1 -> 0

约束:

  • 0 <= n <= 5 * 10^6。

解题思路

埃拉托色尼筛法

从 2 开始,若 i 未被标记为合数则它是质数,并把它的倍数全部标记为合数。

  • 优化 1(只筛到 sqrt(n)):i 只需要枚举到 sqrt(n),因为任何合数都有不超过 sqrt(n) 的质因子;
  • 优化 2(从 ii 开始标记):i 的小于 ii 的倍数一定已被更小的质因子标记过。
  • 优化 3(只对奇数或按 2 的倍数跳过):本实现以标准筛为主体。
    为什么需要"小于 n"注意边界:n<=2 时答案为 0。
    暴力逐个 isPrime 对 5*10^6 规模会超时,必须用筛法。

代码实现

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

public class Solution {

    public int countPrimes(int n) {
        if (n < 2) {
            return 0;
        }
        boolean[] notPrime = new boolean[n]; // 默认 false:假设全为质数
        int count = 0;
        for (int i = 2; i < n; i++) {
            if (!notPrime[i]) {
                count++;
                // 避免 int 溢出:long 判断
                if ((long) i * i < n) {
                    // 从 i*i 开始标记(更小的倍数已被更小质因子覆盖)
                    for (long j = (long) i * i; j < n; j += i) {
                        notPrime[(int) j] = true;
                    }
                }
            }
        }
        return count;
    }

    public int countPrimesOdd(int n) {
        if (n < 3) {
            return 0;
        }
        boolean[] notPrime = new boolean[n];
        int count = 1; // 质数 2
        for (int i = 3; i < n; i += 2) {
            if (!notPrime[i]) {
                count++;
                if ((long) i * i < n) {
                    // i 是奇数,i*i 也是奇数;步长 2i 保证只标记奇数倍
                    for (long j = (long) i * i; j < n; j += 2L * i) {
                        notPrime[(int) j] = true;
                    }
                }
            }
        }
        return count;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.countPrimes(10), 4, "示例1");
        check(s.countPrimes(0), 0, "示例2");
        check(s.countPrimes(1), 0, "示例3");
        check(s.countPrimes(2), 0, "边界:n=2");
        check(s.countPrimes(3), 1, "边界:n=3");
        check(s.countPrimesOdd(10), 4, "优化版示例1");
        check(s.countPrimesOdd(2), 0, "优化版边界");
        // 大数据规模对比
        System.out.println("n=5000000 -> " + s.countPrimesOdd(5000000));
    }

    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 log log n),空间 O(n)。


205. Isomorphic Strings(同构字符串)

题目描述

给定两个字符串 s 和 t,判断它们是否是同构的。

如果 s 中的字符可以按某种映射关系替换得到 t,那么这两个字符串是同构的。

每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。

不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。

  • 输入 :String s, String t(1 <= s.length <= 5 * 10^4,s.length == t.length)
  • 输出 :boolean(是否同构)

示例 1:

复制代码
s="egg", t="add" -> true

示例 2:

复制代码
s="foo", t="bar" -> false

示例 3:

复制代码
s="paper", t="title" -> true

约束:

  • s 和 t 由任意有效的 ASCII 字符组成;1 <= s.length <= 5 * 10^4。

解题思路

双向映射校验

同构要求两个方向的映射都保持一对一(单射):

  • 解法一(两张哈希表):sToT 记录 s 的每个字符 -> t 对应位置的字符;tToS 反向。
    若发现 si 已映射为别的 t 字符,或 ti 已被别的 s 字符映射,则返回 false。
  • 解法二(首次出现位置索引对照):字符首次出现的位置必须一致------
    把每个字符替换成它"第一次出现的下标",两个字符串变换后必须完全相同。
    等价考察:s.indexOf(s.charAt(i)) == t.indexOf(t.charAt(i)) 对所有 i 成立。
    为什么需要双向:若只做单向(如 foo->bar),s 的 'o' 都映射到 t 的 'a' 看似一致,
    但反向检查发现 t 的 'b' 与 'a' 都被 s 的 'f','o' 映射,是非法的。

代码实现

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

public class Solution {

    public boolean isIsomorphic(String s, String t) {
        Map<Character, Character> sToT = new HashMap<>();
        Map<Character, Character> tToS = new HashMap<>();
        for (int i = 0; i < s.length(); i++) {
            char a = s.charAt(i), b = t.charAt(i);
            // 检查 s->t 方向是否冲突:a 已映射到别的字符
            if (sToT.containsKey(a) && sToT.get(a) != b) {
                return false;
            }
            // 检查 t->s 方向是否冲突:b 已被别的字符映射
            if (tToS.containsKey(b) && tToS.get(b) != a) {
                return false;
            }
            sToT.put(a, b);
            tToS.put(b, a);
        }
        return true;
    }

    public boolean isIsomorphic2(String s, String t) {
        for (int i = 0; i < s.length(); i++) {
            // 两串同一位置的字符首次出现下标必须相同
            if (s.indexOf(s.charAt(i)) != t.indexOf(t.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.isIsomorphic("egg", "add"), true, "示例1");
        check(s.isIsomorphic("foo", "bar"), false, "示例2");
        check(s.isIsomorphic("paper", "title"), true, "示例3");
        check(s.isIsomorphic2("egg", "add"), true, "解法二示例1");
        check(s.isIsomorphic2("foo", "bar"), false, "解法二示例2");
        check(s.isIsomorphic("ab", "aa"), false, "补充:不同字符映射到同一字符");
        check(s.isIsomorphic("13", "42"), 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(|字符集|)。

解法二:时间 O(n * k)(indexOf 扫描,k 为字符集大小,最坏 O(n^2)),空间 O(1)。


206. Reverse Linked List(反转链表)

题目描述

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

  • 输入 :ListNode head(链表中节点的数目范围是 0, 5000,-5000 <= Node.val <= 5000)
  • 输出 :ListNode(反转后的链表头节点)

示例 1:

复制代码
head=[1,2,3,4,5] -> [5,4,3,2,1]

示例 2:

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

示例 3:

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

约束:

  • 链表中节点的数目范围是 0, 5000;-5000 <= Node.val <= 5000。

解题思路

迭代头插 / 递归

  • 解法一(迭代·双指针):prev 指向已反转部分的头,cur 指向待处理节点;
    每次把 cur 从原链表摘下插到 prev 前面(反向),直到 cur 为空,prev 即新头。
  • 解法二(迭代·原地反指):维护 prev 与 cur,令 cur.next 指回 prev 再整体前移,
    三个变量原地反转每一条边的方向,最后 prev 即为新头。
  • 解法三(递归):假设 head 之后的链表已反转好,只要把 head.next.next 指向 head、
    head.next 置空即可把 head 挂到已反转链表的尾部;基准情形为 head 或 head.next 为空。
    (解一与解二思路相同细节不同:解一通过"插入头部"、解二通过"边方向反转"理解)
    为什么递归成立:reverseList(head.next) 返回的"新头"是原链表尾节点,
    原 head.next 成为反转后链表的尾节点,正好把 head 接到它后面。

代码实现

java 复制代码
public class Solution {

    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode cur = head;
        // cur 为空说明走到了原链表尾部,prev 即新链表头
        while (cur != null) {
            ListNode next = cur.next; // 先保存后继,避免断链
            cur.next = prev;          // 反向指向前驱
            prev = cur;               // prev 前进
            cur = next;               // cur 前进
        }
        return prev;
    }

    public ListNode reverseList2(ListNode head) {
        if (head == null || head.next == null) {
            return head; // 空表或单节点:本身就是反转后的结果
        }
        ListNode newHead = reverseList2(head.next); // 反转头节点之后的链表
        head.next.next = head; // 让原后继指向自己(回链)
        head.next = null;      // 断开原后继边
        return newHead;
    }

    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; }
    }

    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(toArr(s.reverseList(build(new int[]{1,2,3,4,5}))), new int[]{5,4,3,2,1}, "示例1");
        check(toArr(s.reverseList(build(new int[]{1,2}))), new int[]{2,1}, "示例2");
        check(toArr(s.reverseList(build(new int[]{}))), new int[]{}, "示例3-空表");
        check(toArr(s.reverseList2(build(new int[]{1,2,3,4,5}))), new int[]{5,4,3,2,1}, "递归示例1");
        check(toArr(s.reverseList2(build(new int[]{1}))), new int[]{1}, "单节点");
    }
}

复杂度分析

迭代:时间 O(n),空间 O(1)。

递归:时间 O(n),空间 O(n)(递归栈深度)。


207. Course Schedule(课程表)

题目描述

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1。

在选修某些课程之前需要一些先修课程。先修课程按数组 prerequisites 给出,

其中 prerequisitesi = `a_i`, `b_i`,表示如果要学习课程 a_i 则必须先学习课程 b_i。

请你判断是否可能完成所有课程的学习?如果可以,返回 true;否则返回 false。

  • 输入 :int numCourses, int[][] prerequisites(1 <= numCourses <= 2000,0 <= prerequisites.length <= 5000)
  • 输出 :boolean(能否学完所有课程)

示例 1:

复制代码
numCourses=2, prerequisites=[[1,0]] -> true

示例 2:

复制代码
numCourses=2, prerequisites=[[1,0],[0,1]] -> false

约束:

  • 1 <= numCourses <= 2000;0 <= prerequisites.length <= 5000;
    prerequisitesi.length == 2;0 <= a_i, b_i < numCourses;所有先修关系对互不相同。

解题思路

拓扑排序判环(Kahn BFS / DFS 三色法)

把课程看作点、先修关系看作"b -> a"的有向边,问题等价于判断图是否无环(DAG)。

  • 解法一(Kahn 算法 / BFS + 入度表):统计每门课的入度(直接先修数量),
    把入度为 0 的课程入队;弹出时把 its 指向的课程入度减 1,减到 0 再入队。
    若最终出队课程数 == numCourses 则无环可完成。
  • 解法二(DFS 三色标记):0=未访问,1=访问中(当前递归栈),2=已完成。
    对每个节点 DFS,若遇到"访问中"的节点说明有环,返回 false。
    为什么判环等价于可行性:有环意味着某些课程互为先修,永远无法满足学习顺序。

代码实现

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

public class Solution {

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 邻接表:pre -> 依赖它的课程
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) graph.add(new ArrayList<>());
        int[] indegree = new int[numCourses];
        for (int[] p : prerequisites) {
            graph.get(p[1]).add(p[0]);
            indegree[p[0]]++;
        }
        Deque<Integer> queue = new ArrayDeque<>();
        for (int i = 0; i < numCourses; i++) {
            if (indegree[i] == 0) queue.offer(i); // 无先修的课先学
        }
        int learned = 0;
        while (!queue.isEmpty()) {
            int cur = queue.poll();
            learned++;
            for (int next : graph.get(cur)) {
                if (--indegree[next] == 0) queue.offer(next); // 先修齐了,可以学
            }
        }
        return learned == numCourses;
    }

    private int[] state;
    private List<List<Integer>> g;

    public boolean canFinishDFS(int numCourses, int[][] prerequisites) {
        g = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) g.add(new ArrayList<>());
        for (int[] p : prerequisites) g.get(p[1]).add(p[0]);
        state = new int[numCourses];
        for (int i = 0; i < numCourses; i++) {
            if (state[i] == 0 && !dfs(i)) {
                return false; // 发现环
            }
        }
        return true;
    }

    private boolean dfs(int v) {
        state[v] = 1; // 标记访问中
        for (int next : g.get(v)) {
            if (state[next] == 1) return false;       // 回边:成环
            if (state[next] == 0 && !dfs(next)) return false;
        }
        state[v] = 2; // 完成
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.canFinish(2, new int[][]{{1,0}}), true, "示例1");
        check(s.canFinish(2, new int[][]{{1,0},{0,1}}), false, "示例2");
        check(s.canFinishDFS(2, new int[][]{{1,0}}), true, "DFS示例1");
        check(s.canFinishDFS(2, new int[][]{{1,0},{0,1}}), false, "DFS示例2");
        check(s.canFinish(20, new int[][]{{0,10},{3,18},{5,5},{6,11},{11,14},{13,1},{15,1},{17,4}}), false, "自环:5->5");
        check(s.canFinish(1, new int[][]{}), 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(V+E),BFS O(V+E),总时间 O(V+E),空间 O(V+E)。

解法二:时间 O(V+E),空间 O(V+E)。


208. Implement Trie (Prefix Tree)(实现 Trie(前缀树))

题目描述

Trie(发音类似 "try")或者说前缀树是一种树形数据结构,用于高效地存储和检索

字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补完和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象;
  • void insert(String word) 向前缀树中插入字符串 word;
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即检索之前已经插入);
    否则返回 false;
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix,
    返回 true;否则返回 false。
  • 输入 :方法调用(word、prefix 由小写英文字母组成,0 <= 调用次数 <= 3 * 10^4)
  • 输出 :各方法的返回值
    示例:Trie trie = new Trie();
    trie.insert("apple"); trie.search("apple"); // true
    trie.search("app"); // false
    trie.startsWith("app"); // true
    trie.insert("app"); trie.search("app"); // true
    约束:
  • 1 <= word.length, prefix.length <= 2000;word 和 prefix 仅由小写英文字母组成;
    insert、search 和 startsWith 调用次数总计不超过 3 * 10^4 次。

解题思路

数组型 Trie / 哈希型 Trie

前缀树每个节点维护一个 children 数组(或哈希表)+ isEnd 标志。

  • 解法一(数组子节点):因为字符集固定为 26 个小写字母,用 Trie26 更高效,
    insert 从根开始逐字符下降,缺失则新建节点,末尾置 isEnd;
    search 同样下降,缺失或非末尾返回 false;startsWith 只查存在性。
  • 解法二(哈希表子节点):子节点用 Map<Character, TrieNode>,适合字符集不确定的场景,
    逻辑完全一致,只是查找由数组索引变为哈希。
    为什么搜索必须区分 search/startsWith:search 要求单词"完整存在"(以 isEnd 结尾),
    而 startsWith 只要求前缀路径存在。

代码实现

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

public class Trie {

    static class TrieNode {
        TrieNode[] children = new TrieNode[26];
        boolean isEnd;
    }

    private TrieNode root;

    public Trie() {
        root = new TrieNode();
    }

    public void insert(String word) {
        TrieNode cur = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (cur.children[idx] == null) {
                cur.children[idx] = new TrieNode(); // 路径缺失则新建
            }
            cur = cur.children[idx];
        }
        cur.isEnd = true; // 单词结尾标记
    }

    public boolean search(String word) {
        TrieNode node = findNode(word);
        return node != null && node.isEnd;
    }

    public boolean startsWith(String prefix) {
        return findNode(prefix) != null;
    }

    private TrieNode findNode(String word) {
        TrieNode cur = root;
        for (char c : word.toCharArray()) {
            TrieNode child = cur.children[c - 'a'];
            if (child == null) return null;
            cur = child;
        }
        return cur;
    }

    public static void main(String[] args) {
        Trie trie = new Trie();
        trie.insert("apple");
        check(trie.search("apple"), true, "search(apple)");
        check(trie.search("app"), false, "search(app)");
        check(trie.startsWith("app"), true, "startsWith(app)");
        trie.insert("app");
        check(trie.search("app"), true, "search(app)-插入后");
        check(trie.startsWith("ap"), true, "startsWith(ap)");
        check(trie.startsWith("b"), false, "startsWith(b)");
    }

    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(单词长度) 每次操作;空间 O(所有插入单词的字符总数 * 字符集大小)。


209. Minimum Size Subarray Sum(长度最小的子数组)

题目描述

给定一个含有 n 个正整数的数组和一个正整数 target。

找出该数组中满足其总和大于等于 target 的长度最小的连续子数组

`nums_l`, `nums_l`+1, ..., `nums_r`-1, `nums_r`,并返回其长度。

如果不存在符合条件的子数组,返回 0。

  • 输入 :int target, int[] nums(1 <= target <= 10^9,1 <= nums.length <= 10^5,1 <= nums[i] <= 10^4)
  • 输出 :int(最小长度,不存在则 0)

示例 1:

复制代码
target=7, nums=[2,3,1,2,4,3] -> 2(子数组 [4,3])

示例 2:

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

示例 3:

复制代码
target=11, nums=[1,1,1,1,1,1,1,1] -> 0

约束:

  • 1 <= target <= 10^9;1 <= nums.length <= 10^5;1 <= nums[i] <= 10^4。

解题思路

滑动窗口 / 前缀和 + 二分

  • 解法一(滑动窗口):右指针扩展窗口直到和 >= target,然后收缩左指针
    尝试缩短长度,期间更新最小长度。因为元素全为正,"窗口和 >= target 时右移
    左指针一定可以找到更短或等价窗口"是单调可行的。
  • 解法二(前缀和 + 二分):构造前缀和数组(单调递增),枚举左端点 i,
    二分找第一个 j 使 prefixj - prefixi >= target,即找 prefixj >= prefixi+target;
    距离 j-i 即该左端点下的最短长度。需要 O(n log n)。
    为什么滑动窗口必须"正数"前提:正数保证右移左指针时窗口和单调下降,
    可以在不回溯右指针的前提下穷尽所有满足条件的窗口。

代码实现

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

public class Solution {

    public int minSubArrayLen(int target, int[] nums) {
        int n = nums.length;
        int left = 0, sum = 0, ans = Integer.MAX_VALUE;
        for (int right = 0; right < n; right++) {
            sum += nums[right];                       // 纳入右端点
            while (sum >= target) {                   // 满足条件时收缩
                ans = Math.min(ans, right - left + 1);
                sum -= nums[left++];                  // 左端点右移尝试更短
            }
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

    public int minSubArrayLenBinary(int target, int[] nums) {
        int n = nums.length;
        int[] prefix = new int[n + 1]; // prefix[i] = nums[0..i-1] 之和
        for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + nums[i];
        int ans = Integer.MAX_VALUE;
        for (int i = 0; i < n; i++) {
            // 需要 prefix[j] >= prefix[i] + target 的最小 j
            int lo = i + 1, hi = n;
            while (lo < hi) {
                int mid = (lo + hi) >>> 1;
                if (prefix[mid] >= prefix[i] + target) hi = mid;
                else lo = mid + 1;
            }
            if (lo <= n && prefix[lo] >= prefix[i] + target) {
                ans = Math.min(ans, lo - i);
            }
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.minSubArrayLen(7, new int[]{2,3,1,2,4,3}), 2, "示例1");
        check(s.minSubArrayLen(4, new int[]{1,4,4}), 1, "示例2");
        check(s.minSubArrayLen(11, new int[]{1,1,1,1,1,1,1,1}), 0, "示例3");
        check(s.minSubArrayLenBinary(7, new int[]{2,3,1,2,4,3}), 2, "二分解法示例1");
        check(s.minSubArrayLenBinary(4, new int[]{1,4,4}), 1, "二分解法示例2");
        check(s.minSubArrayLen(15, new int[]{1,2,3,4,5}), 5, "补充:整个数组");
    }

    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 log n),空间 O(n)。


210. Course Schedule II(课程表 II)

题目描述

现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。

给你一个数组 prerequisites,其中 prerequisitesi = `a_i`, `b_i`,

表示在选修课程 a_i 前必须先修 b_i。

例如,想要学习课程 0,你需要先完成课程 1,我们用一个匹配来表示:0,1。

返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,

你只要返回任意一种就可以了。如果不可能完成所有课程,返回一个空数组。

  • 输入 :int numCourses, int[][] prerequisites(1 <= numCourses <= 2000,0 <= prerequisites.length <= numCourses * (numCourses - 1))
  • 输出 :int[](任意一种拓扑顺序,或有环返回空数组)

示例 1:

复制代码
numCourses=2, prerequisites=[[1,0]] -> [0,1]

示例 2:

复制代码
numCourses=4, prerequisites=[[1,0],[2,0],[3,1],[3,2]] -> [0,2,1,3] 或 [0,1,2,3]

示例 3:

复制代码
numCourses=1, prerequisites=[] -> [0]

约束:

  • 1 <= numCourses <= 2000;0 <= prerequisites.length <= numCourses * (numCourses - 1)。

解题思路

拓扑排序(Kahn BFS)--- 在 207 的基础上多记录出队顺序

  • 解法一(Kahn BFS + 入度表):入度为 0 的课程入队,逐个出队并加入结果数组,
    同步把后继课程入度减 1;最后若结果长度 == numCourses 则返回,否则有环返回空。
  • 解法二(DFS 后序 + 反序):三色标记 DFS,完成后把节点按"完成时间"倒序收集,
    DFS 完成的逆序即拓扑序;检测到"访问中"节点即环,返回空。
    为什么出队顺序即合法学习顺序:Kahn 每次选择"当前所有先修都已完成"的课程,
    满足了"先修在后修之前"的全部约束。

代码实现

java 复制代码
public class Solution {

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        java.util.List<java.util.List<Integer>> graph = new java.util.ArrayList<>();
        for (int i = 0; i < numCourses; i++) graph.add(new java.util.ArrayList<>());
        int[] indegree = new int[numCourses];
        for (int[] p : prerequisites) {
            graph.get(p[1]).add(p[0]);
            indegree[p[0]]++;
        }
        java.util.Deque<Integer> queue = new java.util.ArrayDeque<>();
        for (int i = 0; i < numCourses; i++) {
            if (indegree[i] == 0) queue.offer(i);
        }
        int[] order = new int[numCourses];
        int idx = 0;
        while (!queue.isEmpty()) {
            int cur = queue.poll();
            order[idx++] = cur; // 出队顺序即学习顺序
            for (int next : graph.get(cur)) {
                if (--indegree[next] == 0) queue.offer(next);
            }
        }
        return idx == numCourses ? order : new int[0]; // 有环则空
    }

    public int[] findOrderDFS(int numCourses, int[][] prerequisites) {
        java.util.List<java.util.List<Integer>> graph = new java.util.ArrayList<>();
        for (int i = 0; i < numCourses; i++) graph.add(new java.util.ArrayList<>());
        for (int[] p : prerequisites) graph.get(p[1]).add(p[0]);
        int[] state = new int[numCourses]; // 0未访问 1访问中 2完成
        java.util.List<Integer> post = new java.util.ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            if (state[i] == 0) {
                if (!dfs(i, graph, state, post)) return new int[0]; // 有环
            }
        }
        // 完成后收集的顺序逆序即拓扑序
        int[] ans = new int[numCourses];
        for (int i = 0; i < numCourses; i++) ans[i] = post.get(numCourses - 1 - i);
        return ans;
    }

    private boolean dfs(int v, java.util.List<java.util.List<Integer>> graph, int[] state, java.util.List<Integer> post) {
        state[v] = 1;
        for (int next : graph.get(v)) {
            if (state[next] == 1) return false;
            if (state[next] == 0 && !dfs(next, graph, state, post)) return false;
        }
        state[v] = 2;
        post.add(v);
        return true;
    }

    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.findOrder(2, new int[][]{{1,0}}), new int[]{0,1}, "示例1");
        // 示例2拓扑序不唯一,校验合法性而非相等
        int[] r2 = s.findOrder(4, new int[][]{{1,0},{2,0},{3,1},{3,2}});
        System.out.println((isValidTopo(4, r2, new int[][]{{1,0},{2,0},{3,1},{3,2}}) ? "PASS" : "FAIL")
                + " | 示例2 | order=" + java.util.Arrays.toString(r2));
        check(s.findOrder(1, new int[][]{}), new int[]{0}, "示例3");
        check(s.findOrder(2, new int[][]{{1,0},{0,1}}), new int[]{}, "有环返回空");
    }

}

复杂度分析

解法一:时间 O(V+E),空间 O(V+E)。

解法二:时间 O(V+E),空间 O(V+E)。


题目描述

请你设计一个数据结构,支持添加新单词和查找字符串是否与任何先前添加的字符串匹配。

实现词典类 WordDictionary:

  • WordDictionary() 初始化词典对象;
  • void addWord(word) 将 word 添加到数据结构中,之后可以对它进行匹配;
  • boolean search(word) 如果数据结构中存在字符串与 word 匹配,则返回 true;
    word 中可能包含一些 '.',每个 '.' 都可以表示任何一个字母。
  • 输入 :方法调用(word 仅由小写英文字母或 '.' 组成,1 <= word.length <= 25,最多 10^4 次调用)
  • 输出 :各方法返回值
    示例:
    WordDictionary wordDictionary = new WordDictionary();
    wordDictionary.addWord("bad"); wordDictionary.addWord("dad"); wordDictionary.addWord("mad");
    wordDictionary.search("pad"); // false
    wordDictionary.search("bad"); // true
    wordDictionary.search(".ad"); // true
    wordDictionary.search("b.."); // true
    约束:
  • addWord 中的 word 由小写英文字母构成;search 中的 word 由 '.' 或小写英文字母组成;
    每次调用 addWord 需要 O(|word|) 时间。

解题思路

Trie + 回溯(通配符 '.' 匹配)

  • 解法一(Trie + DFS):沿用 208 的 Trie 结构;addWord 与普通插入一致;
    search 支持 '.',当遇到 '.' 时遍历当前节点的所有子节点递归匹配。
  • 解法二(哈希表按长度分组 + 逐位匹配):以 word 长度为 key 存到 Set,
    搜索时只在等长的单词里比对,'.' 位直接跳过;实现简单但最坏 O(n*len)。
    为什么不能只用 HashMap<word, true>:'.' 通配符使查询无法精确命中键,
    需要结构化的逐字符匹配能力,Trie 天然支持按前缀/模式下降。

代码实现

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

public class WordDictionary {

    static class TrieNode {
        TrieNode[] children = new TrieNode[26];
        boolean isEnd;
    }

    private TrieNode root;

    public WordDictionary() {
        root = new TrieNode();
    }

    public void addWord(String word) {
        TrieNode cur = root;
        for (char c : word.toCharArray()) {
            int idx = c - 'a';
            if (cur.children[idx] == null) cur.children[idx] = new TrieNode();
            cur = cur.children[idx];
        }
        cur.isEnd = true;
    }

    public boolean search(String word) {
        return dfs(root, word, 0);
    }

    private boolean dfs(TrieNode node, String word, int pos) {
        if (pos == word.length()) {
            return node.isEnd; // 必须抵达真实单词结尾
        }
        char c = word.charAt(pos);
        if (c != '.') {
            TrieNode child = node.children[c - 'a'];
            return child != null && dfs(child, word, pos + 1);
        }
        // '.' 通配:尝试所有子节点,任一分支成功即成功
        for (TrieNode child : node.children) {
            if (child != null && dfs(child, word, pos + 1)) {
                return true;
            }
        }
        return false;
    }

    public static void main(String[] args) {
        WordDictionary dict = new WordDictionary();
        dict.addWord("bad"); dict.addWord("dad"); dict.addWord("mad");
        check(dict.search("pad"), false, "search(pad)");
        check(dict.search("bad"), true, "search(bad)");
        check(dict.search(".ad"), true, "search(.ad)");
        check(dict.search("b.."), true, "search(b..)");
        dict.addWord("b");
        check(dict.search("."), true, "search(.)");
        check(dict.search(".."), false, "search(..)");
        check(dict.search("c.."), false, "search(c..)");
    }

    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);
    }
}

复杂度分析

解法一:addWord O(len),search 最坏 O(26^len)(全为 '.'),一般 O(len)。

空间 O(所有单词字符总数*26)。

解法二:空间 O(单词总长),时间 search 最坏 O(等长单词数 * len)。


题目描述

给定一个 m x n 二维字符网格 board 和一个单词(字符串)列表 words,

返回所有二维网格上的单词。单词必须按照字母顺序,通过相邻的单元格内的字母构成,

其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。

同一个单元格内的字母在一个单词中不允许被重复使用。

  • 输入 :char[][] board, String[] words(m == board.length, n == board[i].length,
    1 <= m, n <= 12,1 <= words.length <= 3 * 10^4,1 <= words[i].length <= 10)
  • 输出 :List<String>(能在网格中找到的全部单词,顺序不限)

示例 1:

复制代码
board=[["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]],

words="oath","pea","eat","rain" -> "eat","oath"

示例 2:

复制代码
board=[["a","b"],["c","d"]], words=["abcb"] -> []

约束:

  • words 中的所有字符串互不相同;board 与 words 中的字符均为小写英文字母。

解题思路

Trie + 回溯(DFS)+ 剪枝

  • 解法一(Trie 前缀树剪枝):把所有 words 插进 Trie。DFS 网格时只沿"Trie 中存在
    的路径"前进,命中完整单词(isEnd)即收集并把该节点 isEnd 置 false(去重,避免同词重复输出)。
    相比"每词独立 DFS"(words*board 量级)复杂度大幅下降,是本题的正解。
  • 解法二(朴素 DFS):对每个单词分别用 79 的 DFS 在网格中查找,最坏 O(words * m * n * 4^len),
    代码结构简单但仅适合作对比理解。
    为什么收集后要清 isEnd:同一单词可能在网格中出现多次,置 false 保证每个词只输出一次。

代码实现

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

public class Solution {

    static class TrieNode {
        TrieNode[] children = new TrieNode[26];
        String word; // 用 word 非空标记"到这是完整单词",省去 isEnd 查找
    }

    private TrieNode root = new TrieNode();

    private void buildTrie(String[] words) {
        for (String w : words) {
            TrieNode cur = root;
            for (char c : w.toCharArray()) {
                int idx = c - 'a';
                if (cur.children[idx] == null) cur.children[idx] = new TrieNode();
                cur = cur.children[idx];
            }
            cur.word = w;
        }
    }

    public List<String> findWords(char[][] board, String[] words) {
        buildTrie(words);
        List<String> res = new ArrayList<>();
        int m = board.length, n = board[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                dfs(board, i, j, root, res);
            }
        }
        return res;
    }

    private void dfs(char[][] board, int i, int j, TrieNode node, List<String> res) {
        if (i < 0 || i >= board.length || j < 0 || j >= board[0].length) return;
        char c = board[i][j];
        if (c == '#') return; // 已访问标记
        TrieNode next = node.children[c - 'a'];
        if (next == null) return; // Trie 中无此前缀,剪枝
        if (next.word != null) {  // 找到一个完整单词
            res.add(next.word);
            next.word = null;     // 去重:避免重复加入
        }
        board[i][j] = '#';        // 标记占用,防止单词内重复使用
        dfs(board, i + 1, j, next, res);
        dfs(board, i - 1, j, next, res);
        dfs(board, i, j + 1, next, res);
        dfs(board, i, j - 1, next, res);
        board[i][j] = c;          // 回溯恢复
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        char[][] b1 = {{'o','a','a','n'},{'e','t','a','e'},{'i','h','k','r'},{'i','f','l','v'}};
        java.util.List<String> r1 = s.findWords(b1, new String[]{"oath","pea","eat","rain"});
        java.util.Collections.sort(r1);
        check(r1, java.util.Arrays.asList("eat","oath"), "示例1");
        char[][] b2 = {{'a','b'},{'c','d'}};
        java.util.List<String> r2 = s.findWords(b2, new String[]{"abcb"});
        check(r2, java.util.Arrays.asList(), "示例2");
    }

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

复杂度分析

解法一:建 Trie O(∑words长度);网格 DFS 每格至多沿 Trie 深度 len 前进,

总时间 O(m * n * 4 * 3^(len-1))(四向中减去回溯方向),通常远小于朴素法;空间 O(Trie)。

解法二:时间 O(words * m * n * 4^len),空间 O(len)。


213. House Robber II(打家劫舍 II)

题目描述

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。

这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。

同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,

系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组 nums,

请你计算不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。

  • 输入 :int[] nums(1 <= nums.length <= 100,0 <= nums[i] <= 1000)
  • 输出 :int(最高金额)

示例 1:

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

示例 2:

复制代码
nums=[1,2,3,1] -> 4(偷 1+3)

示例 3:

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

约束:

  • 1 <= nums.length <= 100;0 <= nums[i] <= 1000。

解题思路

环形拆解:首尾不能同偷 => 两个线性 DP 取最大

首尾相邻带来的唯一约束是"第 0 间与最后一间不能同时被偷"。于是可以:

  • 不偷最后一间:只考虑 nums0...n-2(线性打家劫舍);
  • 不偷第一间:只考虑 nums1...n-1;
    答案 = 两者的较大值(n == 1 时单独返回 nums0,因为上述两个区间会退化为空)。
    线性打家劫舍的 DP:dpi = max(dpi-1, dpi-2 + numsi),
    当前房间要么不偷(继承 dpi-1),要么偷(跳过前一间,加 dpi-2 + numsi)。
  • 解法一(两个区间分别滚动变量 DP):逐间递推只需两个变量 prev2/prev1。
  • 解法二(复用 198 的线性函数):写一个 robRange(nums, l, r) 函数,
    分别调用 0, n-2 与 1, n-1 取 max。
    为什么不能直接套 198 的答案:若首尾都偷,环形报警,必须排除。

代码实现

java 复制代码
public class Solution {

    public int rob(int[] nums) {
        int n = nums.length;
        if (n == 1) return nums[0];
        // 情况 A:不偷最后一间(0..n-2);情况 B:不偷第一间(1..n-1)
        return Math.max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
    }

    private int robRange(int[] nums, int l, int r) {
        int prev2 = 0, prev1 = 0;
        for (int i = l; i <= r; i++) {
            int cur = Math.max(prev1, prev2 + nums[i]); // 不偷 vs 偷
            prev2 = prev1;
            prev1 = cur;
        }
        return prev1;
    }

    public int robDp(int[] nums) {
        int n = nums.length;
        if (n == 1) return nums[0];
        int[] a = new int[n], b = new int[n];
        // 区间 [0, n-2]
        for (int i = 0; i <= n - 2; i++) {
            a[i] = Math.max((i >= 1 ? a[i - 1] : 0), (i >= 2 ? a[i - 2] : 0) + nums[i]);
        }
        // 区间 [1, n-1]
        for (int i = 1; i <= n - 1; i++) {
            b[i] = Math.max((i >= 2 ? b[i - 1] : 0), (i >= 3 ? b[i - 2] : 0) + nums[i]);
        }
        return Math.max(a[n - 2], b[n - 1]);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.rob(new int[]{2,3,2}), 3, "示例1");
        check(s.rob(new int[]{1,2,3,1}), 4, "示例2");
        check(s.rob(new int[]{1,2,3}), 3, "示例3");
        check(s.rob(new int[]{5}), 5, "单房");
        check(s.robDp(new int[]{2,3,2}), 3, "DP版示例1");
        check(s.robDp(new int[]{1,2,3,1}), 4, "DP版示例2");
        check(s.rob(new int[]{2,7,9,3,1}), 11, "补充:2+9+1=12? 环形取max(0..3:11, 1..4:11) -> 11");
    }

    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)(滚动变量)。


214. Shortest Palindrome(最短回文串)

题目描述

给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。

找到并返回可以用这种方式转换的最短回文串。

  • 输入 :String s(0 <= s.length <= 5 * 10^4)
  • 输出 :String(在 s 前面添加若干字符后得到的最短回文串)

示例 1:

复制代码
s="aacecaaa" -> "aaacecaaa"

示例 2:

复制代码
s="abcd" -> "dcbabcd"

约束:

  • s 仅由小写英文字母组成;0 <= s.length <= 5 * 10^4。

解题思路

求 s 的"最长回文前缀"(KMP 思路)

目标:找到 s 的最长前缀 s0...k,它是回文。那么答案 = reverse(sk+1...) + s。

关键是高效求最长回文前缀。经典方法:

  • 解法一(KMP/双串匹配):构造 t = s + "#" + reverse(s)。
    观察:s 的回文前缀 s0...k 恰等于 reverse(s) 的对应后缀。
    所以求 t 自身的最长相等前后缀(KMP 的 next/π 数组),其值即最长回文前缀长度。
    因为 '#' 分隔,前缀不会跨过 s 与 reverse(s) 的边界,正确性有保证。
  • 解法二(Rabin-Karp 滚动哈希):从左往右同时维护 正向哈希 与 反向哈希;
    若正向哈希 == 反向哈希,则当前前缀是回文;记录最长的回文前缀下标,结束。
    存在极小的哈希碰撞概率(可用双哈希/取较大模降低)。
  • 解法三(暴力/中心扩展):对每个中心做回文检测,取能覆盖下标 0 的最长回文,
    最坏 O(n^2),仅作理解。
    为什么 KMP 可行:π 数组定义"最长相等真前后缀",t 的前缀含 s、后缀含 reverse(s);
    相等长度 L 意味着 s0...L-1 == reverse(s)end-L+1...end,恰为回文条件。

代码实现

java 复制代码
public class Solution {

    public String shortestPalindrome(String s) {
        String rev = new StringBuilder(s).reverse().toString();
        String t = s + "#" + rev;
        int[] pi = new int[t.length()];
        // KMP 前缀函数:pi[i] = t[0..i] 的最长相等真前后缀长度
        for (int i = 1; i < t.length(); i++) {
            int j = pi[i - 1];
            while (j > 0 && t.charAt(i) != t.charAt(j)) j = pi[j - 1];
            if (t.charAt(i) == t.charAt(j)) j++;
            pi[i] = j;
        }
        // pi[末尾] 就是 s 的最长回文前缀长度
        int len = pi[t.length() - 1];
        return rev.substring(0, s.length() - len) + s;
    }

    public String shortestPalindromeHash(String s) {
        int n = s.length();
        long base = 29, mod = (long) 1e9 + 7;
        long left = 0, right = 0, pow = 1;
        int best = -1; // 最长回文前缀的末尾下标
        for (int i = 0; i < n; i++) {
            int c = s.charAt(i) - 'a' + 1;
            left = (left * base + c) % mod;            // s[0..i] 的正向哈希
            right = (right + (long) c * pow) % mod;    // s[i..0] 的反向(头一尾二)哈希
            if (left == right) best = i;               // 正向哈希=反向哈希 => s[0..i] 是回文
            pow = pow * base % mod;
        }
        // 需要补的是 s[best+1..] 的反转
        String tail = s.substring(best + 1);
        return new StringBuilder(tail).reverse().toString() + s;
    }

    public String shortestPalindromeBrute(String s) {
        int n = s.length();
        int best = 0; // 最长回文前缀的末尾下标
        // 从长到短枚举前缀长度,找到的第一个回文前缀即最长
        for (int end = n - 1; end >= 0; end--) {
            if (isPal(s, 0, end)) {
                best = end;
                break;
            }
        }
        String tail = s.substring(best + 1);
        return new StringBuilder(tail).reverse().toString() + s;
    }

    private boolean isPal(String s, int l, int r) {
        while (l < r) {
            if (s.charAt(l++) != s.charAt(r--)) return false;
        }
        return true;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.shortestPalindrome("aacecaaa"), "aaacecaaa", "示例1");
        check(s.shortestPalindrome("abcd"), "dcbabcd", "示例2");
        check(s.shortestPalindromeHash("aacecaaa"), "aaacecaaa", "哈希法示例1");
        check(s.shortestPalindromeHash("abcd"), "dcbabcd", "哈希法示例2");
        check(s.shortestPalindromeBrute("aacecaaa"), "aaacecaaa", "暴力法示例1");
        check(s.shortestPalindromeBrute("abcd"), "dcbabcd", "暴力法示例2");
        check(s.shortestPalindrome("a"), "a", "单字符");
        check(s.shortestPalindrome(""), "", "空串");
    }

    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);
    }
}

复杂度分析

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

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

解法三(暴力):时间 O(n^2),空间 O(1),仅作理解。


215. Kth Largest Element in an Array(数组中的第K个最大元素)

题目描述

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

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

  • 输入 :int[] nums, int k(1 <= k <= nums.length <= 105,-104 <= nums[i] <= 10^4)
  • 输出 :int(第 k 大元素)

示例 1:

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

示例 2:

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

约束:

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

解题思路

快速选择(Quickselect)/ 堆

  • 解法一(快速选择 + 三路划分优化):标准快排分区,只递归含目标的一侧。
    目标下标 = n - k(升序下标)。为对抗最坏 O(n^2),可随机化 pivot。
    三路划分(小于/等于/大于 pivot)对大量重复元素特别高效。
  • 解法二(小顶堆):维护大小为 k 的小顶堆,堆顶即第 k 大;遍历时堆满且
    元素大于堆顶则替换。时间 O(n log k)。
  • 解法三(排序):Arrays.sort 后取 numsn-k,O(n log n),作为最朴素参考。
    为什么快速选择期望 O(n):每轮分区后只需处理一侧,规模约减半/常数倍,
    T(n) = T(n/2) + O(n) => O(n)。

代码实现

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

public class Solution {

    public int findKthLargest(int[] nums, int k) {
        int target = nums.length - k; // 升序排序后的目标下标
        return quickSelect(nums, 0, nums.length - 1, target);
    }

    private int quickSelect(int[] a, int lo, int hi, int target) {
        if (lo == hi) return a[lo];
        // 随机选 pivot 并交换到末尾
        int pIdx = lo + (int) (Math.random() * (hi - lo + 1));
        swap(a, pIdx, hi);
        int pivot = a[hi];
        // 单指针分区:小于 pivot 的放左边
        int lt = lo;
        for (int i = lo; i < hi; i++) {
            if (a[i] < pivot) {
                swap(a, lt++, i);
            }
        }
        swap(a, lt, hi); // pivot 归位;a[lo..lt-1]<pivot, a[lt]==pivot, a[lt+1..hi]>=pivot
        if (target == lt) return pivot;
        if (target < lt) return quickSelect(a, lo, lt - 1, target);
        return quickSelect(a, lt + 1, hi, target);
    }

    private void swap(int[] a, int i, int j) {
        int t = a[i]; a[i] = a[j]; a[j] = t;
    }

    public int findKthLargestHeap(int[] nums, int k) {
        java.util.PriorityQueue<Integer> heap = new java.util.PriorityQueue<>();
        for (int v : nums) {
            heap.offer(v);
            if (heap.size() > k) heap.poll(); // 保持堆内为当前最大的 k 个
        }
        return heap.peek();
    }

    public int findKthLargestSort(int[] nums, int k) {
        Arrays.sort(nums);
        return nums[nums.length - k];
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.findKthLargest(new int[]{3,2,1,5,6,4}, 2), 5, "示例1");
        check(s.findKthLargest(new int[]{3,2,3,1,2,4,5,5,6}, 4), 4, "示例2");
        check(s.findKthLargestHeap(new int[]{3,2,1,5,6,4}, 2), 5, "堆解法示例1");
        check(s.findKthLargestHeap(new int[]{3,2,3,1,2,4,5,5,6}, 4), 4, "堆解法示例2");
        check(s.findKthLargestSort(new int[]{3,2,1,5,6,4}, 2), 5, "排序法示例1");
        check(s.findKthLargest(new int[]{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(n),最坏 O(n^2)(随机化 pivot 避免),空间 O(log n) 递归栈。

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


216. Combination Sum III(组合总和 III)

题目描述

找出所有相加之和为 n 的 k 个数的组合,且满足下列条件:

  • 只使用数字 1 到 9;
  • 每个数字最多使用一次。
    返回所有可能的有效组合的列表。该列表不能包含相同的组合两次,组合可以以任何顺序返回。
  • 输入 :int k, int n(2 <= k <= 9,1 <= n <= 60)
  • 输出 :List<List<Integer>>(所有和为 n 的 k 个数组合)

示例 1:

复制代码
k=3, n=7 -> [[1,2,4]]

示例 2:

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

示例 3:

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

约束:

  • 2 <= k <= 9;1 <= n <= 60。

解题思路

回溯(组合枚举,不重复)

数字 1...9 每个可选可不选,且组合不强调顺序------按升序选择天然去重:

  • 解法一(回溯 + 剪枝):从 start 开始选(start 递增,保证组合单调递增无重复),
    选够 k 个数且和为 n 即记录;提前剪枝:剩余数字不足/剩余和已超/当前和已超。
  • 解法二(可选 1...9 的 2^9 位掩码枚举):只有 9 个数字,共有 512 种选择子集,
    枚举掩码统计置位数与和,满足条件即记录。规模小,暴力可过,适合对比。
    为什么升序搜索不重复:每次固定"当前层从 start 之后取",不会回头取更小的数,
    因此不会出现 {1,2} 与 {2,1} 同时入选。

代码实现

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

public class Solution {

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

    public List<List<Integer>> combinationSum3(int k, int n) {
        res.clear();
        backtrack(k, n, 1, 0, new ArrayList<>());
        return res;
    }

    private void backtrack(int k, int n, int start, int sum, List<Integer> cur) {
        if (cur.size() == k) {
            if (sum == n) res.add(new ArrayList<>(cur));
            return;
        }
        // 剪枝:从 start 到 9 中最多还需要 need 个数,且和不能超过 n
        int need = k - cur.size();
        for (int i = start; i <= 9; i++) {
            if (10 - i < need) break;           // 剩余数字不足,直接结束
            if (sum + i > n) break;             // 超过目标和,后续更大,直接结束
            cur.add(i);
            backtrack(k, n, i + 1, sum + i, cur);
            cur.remove(cur.size() - 1);
        }
    }

    public List<List<Integer>> combinationSum3Mask(int k, int n) {
        List<List<Integer>> ans = new ArrayList<>();
        for (int mask = 0; mask < (1 << 9); mask++) {
            if (Integer.bitCount(mask) != k) continue;
            int sum = 0;
            List<Integer> cur = new ArrayList<>();
            for (int i = 0; i < 9; i++) {
                if ((mask & (1 << i)) != 0) {
                    cur.add(i + 1);
                    sum += i + 1;
                }
            }
            if (sum == n) ans.add(cur);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.combinationSum3(3, 7), new int[][]{{1,2,4}}, "示例1");
        check(s.combinationSum3(3, 9), new int[][]{{1,2,6},{1,3,5},{2,3,4}}, "示例2");
        check(s.combinationSum3(4, 1), new int[][]{}, "示例3");
        check(s.combinationSum3Mask(3, 7), new int[][]{{1,2,4}}, "掩码法示例1");
        check(s.combinationSum3Mask(3, 9), new int[][]{{1,2,6},{1,3,5},{2,3,4}}, "掩码法示例2");
    }

    private static void check(List<List<Integer>> actual, int[][] expected, String name) {
        List<int[]> act = new ArrayList<>();
        for (List<Integer> l : actual) {
            int[] a = new int[l.size()];
            for (int i = 0; i < a.length; i++) a[i] = l.get(i);
            act.add(a);
        }
        act.sort((x, y) -> {
            for (int i = 0; i < Math.min(x.length, y.length); i++) {
                if (x[i] != y[i]) return x[i] - y[i];
            }
            return x.length - y.length;
        });
        boolean ok = act.size() == expected.length;
        if (ok) {
            for (int i = 0; i < act.size(); i++) {
                if (!java.util.Arrays.equals(act.get(i), expected[i])) { ok = false; break; }
            }
        }
        System.out.println((ok ? "PASS" : "FAIL") + " | " + name
                + " | actual=" + act + " expected=" + java.util.Arrays.deepToString(expected));
        if (!ok) System.out.println("  <<< 断言失败: " + name);
    }
}

复杂度分析

解法一:组合数 C(9, k) 为最坏节点数,时间 O(C(9,k) * k),空间 O(k)(栈深+结果开销)。

解法二:O(2^9 * 9),空间 O(k)。


217. Contains Duplicate(存在重复元素)

题目描述

给你一个整数数组 nums。如果任一值在数组中出现至少两次,返回 true;

如果数组中每个元素互不相同,返回 false。

  • 输入 :int[] nums(1 <= nums.length <= 105,-109 <= nums[i] <= 10^9)
  • 输出 :boolean(是否存在重复元素)

示例 1:

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

示例 2:

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

示例 3:

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

约束:

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

解题思路

哈希集合 / 排序

  • 解法一(哈希集合):遍历时若元素已在集合中则重复;否则加入。时间 O(n),空间 O(n)。
  • 解法二(排序):排序后相邻元素相等即重复。时间 O(n log n),空间 O(1)(原地)。
  • 解法三(数据范围技巧,可选):若数值有界可标记数组;本题值域大不适用,仅一提。
    为什么集合判重是线性时间最优读法:哈希集合的插入与查找均摊 O(1)。

代码实现

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

public class Solution {

    public boolean containsDuplicate(int[] nums) {
        java.util.Set<Integer> seen = new java.util.HashSet<>();
        for (int v : nums) {
            if (!seen.add(v)) return true; // add 返回 false 表示已存在
        }
        return false;
    }

    public boolean containsDuplicateSort(int[] nums) {
        java.util.Arrays.sort(nums);
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] == nums[i - 1]) return true;
        }
        return false;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.containsDuplicate(new int[]{1,2,3,1}), true, "示例1");
        check(s.containsDuplicate(new int[]{1,2,3,4}), false, "示例2");
        check(s.containsDuplicate(new int[]{1,1,1,3,3,4,3,2,4,2}), true, "示例3");
        check(s.containsDuplicateSort(new int[]{1,2,3,1}), true, "排序解示例1");
        check(s.containsDuplicateSort(new int[]{1,2,3,4}), false, "排序解示例2");
    }

    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(n)。解法二:时间 O(n log n),空间 O(1)。


218. The Skyline Problem(天际线问题)

题目描述

城市的天际线是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。

给你所有建筑物的位置和高度,请返回由这些建筑物形成的天际线。

每个建筑物的几何信息由数组 buildings 表示,其中三元组 buildingsi = `left_i`, `right_i`, `height_i`。

left_i 是第 i 座建筑物左边缘的 x 坐标,right_i 是右边缘的 x 坐标,

height_i 是建筑物高度。天际线应该表示为由"关键点"组成的列表,

其形式为 \[x1, y1, x2, y2, ...],并按 x 坐标进行排序。

关键点是水平线段的左端点。请注意,最右侧的建筑物可能恰好有一个关键点,

当该点是天际线的终点时,只需返回它即可。

  • 输入 :int[][] buildings(1 <= buildings.length <= 10^4,0 <= left_i < right_i <= 2^31 - 1,
    1 <= height_i <= 2^31 - 1)
  • 输出 :List<List<Integer>>(天际线的关键点列表)

示例 1:

复制代码
buildings=[[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]

-> \[2,10,3,15,7,12,12,0,15,10,20,8,24,0]

示例 2:

复制代码
buildings=[[0,2,3],[2,5,3]] -> [[0,3],[5,0]]

约束:

  • 1 <= buildings.length <= 10^4;0 <= left_i < right_i <= 2^31 - 1;1 <= height_i <= 2^31 - 1。

解题思路

扫描线 + 最大堆(multiset 语义)

建筑只影响"仍活跃的最高高度"。关键点只出现在建筑的左右边缘。

  • 解法一(扫描线 + 优先队列/堆):
    算法步骤:
  1. 对每个建筑生成两个"事件":左边缘 (left, -height)(入事件)、右边缘 (right, height)(出事件)。
    (用正负区分:负=开始,正=结束)
  2. 按 x 排序所有事件;x 相同时约定:入事件排在出事件前(先加后减)。
  3. 用最大堆维护"当前活跃建筑高度"。处理事件:入事件即加入 height;
    出事件即把对应 height 标记为待删(懒删除:堆中仍存在,只是用另一个表记次数)。
  4. 处理完一批同 x 事件后,取堆顶有效高度 cur;
    若 cur != 上一个关键点的 y,则新增关键点 (x, cur)。
  • 解法二(分治法/线段树):经典但实现复杂,本文件以扫描线为主并给出分治思想说明。
    为什么右边缘是"出事件":建筑在 right 处恰好消失,且题目示例显示当最高建筑结束,
    在右边缘处输出 y=次高或 0 的关键点。

代码实现

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

public class Solution {

    public List<List<Integer>> getSkyline(int[][] buildings) {
        List<int[]> events = new ArrayList<>();
        for (int[] b : buildings) {
            events.add(new int[]{b[0], -b[2]}); // 左边缘:入事件,高度取负
            events.add(new int[]{b[1], b[2]});  // 右边缘:出事件,高度取正
        }
        events.sort((a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
        // 注意:x 相同按 高度索引排序后,负数(入)在正数(出)之前?数组下标1存的是 ±height,需比较数值本身:
        java.util.PriorityQueue<Integer> heap = new java.util.PriorityQueue<>((a, b) -> b - a);
        java.util.Map<Integer, Integer> pending = new java.util.HashMap<>(); // 懒删除统计
        heap.offer(0); // 地面高度
        List<List<Integer>> res = new ArrayList<>();
        int prev = 0;
        int i = 0;
        while (i < events.size()) {
            int x = events.get(i)[0];
            // 一次性处理同 x 的所有事件
            while (i < events.size() && events.get(i)[0] == x) {
                int h = events.get(i)[1];
                if (h < 0) heap.offer(-h);        // 入事件:加入高度
                else {                             // 出事件:懒删除计数+1
                    pending.put(h, pending.getOrDefault(h, 0) + 1);
                }
                i++;
            }
            // 清除堆顶"已过期"的高度(懒删除)
            while (!heap.isEmpty() && pending.getOrDefault(heap.peek(), 0) > 0) {
                pending.put(heap.peek(), pending.get(heap.peek()) - 1);
                heap.poll();
            }
            int cur = heap.isEmpty() ? 0 : heap.peek();
            if (cur != prev) {
                res.add(Arrays.asList(x, cur)); // 轮廓变化才记录关键点
                prev = cur;
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        List<List<Integer>> r1 = s.getSkyline(new int[][]{{2,9,10},{3,7,15},{5,12,12},{15,20,10},{19,24,8}});
        checkList(r1, new int[][]{{2,10},{3,15},{7,12},{12,0},{15,10},{20,8},{24,0}}, "示例1");
        List<List<Integer>> r2 = s.getSkyline(new int[][]{{0,2,3},{2,5,3}});
        checkList(r2, new int[][]{{0,3},{5,0}}, "示例2");
    }

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

复杂度分析

解法一:事件排序 O(n log n),堆操作 O(n log n),总时间 O(n log n),空间 O(n)。


219. Contains Duplicate II(存在重复元素 II)

题目描述

给你一个整数数组 nums 和一个整数 k,判断数组中是否存在两个不同的索引 i 和 j,

满足 nums[i] == nums[j] 且 |i - j| <= k。

如果存在,返回 true;否则返回 false。

  • 输入 :int[] nums, int k(1 <= nums.length <= 105,-109 <= nums[i] <= 10^9,0 <= k <= 10^5)
  • 输出 :boolean(是否存在距离不超过 k 的重复元素对)

示例 1:

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

示例 2:

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

示例 3:

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

约束:

  • 1 <= nums.length <= 105;-109 <= nums[i] <= 10^9;0 <= k <= 10^5。

解题思路

哈希表存最近下标 / 滑动窗口定长集合

  • 解法一(哈希表记录最近一次下标):遍历数组,若当前值已存在且 i - lastIndex <= k
    则返回 true,否则更新下标。时间 O(n),空间 O(n)。
  • 解法二(固定大小 k+1 的滑动窗口集合):维护窗口内元素集合;
    窗口大小超过 k+1 时就移除最早元素,保证集合内任意两元素下标差 <= k。
    若加入时集合中已有同值,则返回 true。空间 O(k)。
    为什么只记录"最近一次"足够:若更早的下标 j' 满足距离,则最近的下标 j 必然更近,
    校验最近一次即可覆盖所有可能配对。

代码实现

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

public class Solution {

    public boolean containsNearbyDuplicate(int[] nums, int k) {
        Map<Integer, Integer> last = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            Integer prev = last.get(nums[i]);
            if (prev != null && i - prev <= k) {
                return true;
            }
            last.put(nums[i], i); // 更新为最近下标
        }
        return false;
    }

    public boolean containsNearbyDuplicateWindow(int[] nums, int k) {
        java.util.Set<Integer> window = new java.util.HashSet<>();
        for (int i = 0; i < nums.length; i++) {
            if (!window.add(nums[i])) {
                return true; // 窗口内已有同值,下标差必然 <= k
            }
            if (window.size() > k) {
                window.remove(nums[i - k]); // 窗口滑出
            }
        }
        return false;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.containsNearbyDuplicate(new int[]{1,2,3,1}, 3), true, "示例1");
        check(s.containsNearbyDuplicate(new int[]{1,0,1,1}, 1), true, "示例2");
        check(s.containsNearbyDuplicate(new int[]{1,2,3,1,2,3}, 2), false, "示例3");
        check(s.containsNearbyDuplicateWindow(new int[]{1,2,3,1}, 3), true, "窗口法示例1");
        check(s.containsNearbyDuplicateWindow(new int[]{1,2,3,1,2,3}, 2), false, "窗口法示例3");
        check(s.containsNearbyDuplicate(new int[]{1}, 1), 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(n)。解法二:时间 O(n),空间 O(k)。


220. Contains Duplicate III(存在重复元素 III)

题目描述

给你一个整数数组 nums 和两个整数 indexDiff 和 valueDiff。

找出是否存在下标 i 和 j(i != j)满足:

  • |i - j| <= indexDiff;
  • |nums[i] - nums[j]| <= valueDiff。
    如果存在,返回 true;否则返回 false。
  • 输入 :int[] nums, int indexDiff, int valueDiff
    (2 <= nums.length <= 10^5,-10^9 <= nums[i] <= 10^9,1 <= indexDiff <= nums.length,
    0 <= valueDiff <= 10^9)
  • 输出 :boolean(是否存在满足条件的下标对)

示例 1:

复制代码
nums=[1,2,3,1], indexDiff=3, valueDiff=0 -> true

示例 2:

复制代码
nums=[1,5,9,1,5,9], indexDiff=2, valueDiff=3 -> false

示例 3:

复制代码
nums=[1,5,9,1,5,9], indexDiff=2, valueDiff=3 -> false

约束:

  • 2 <= nums.length <= 105;-109 <= nums[i] <= 10^9;1 <= indexDiff <= nums.length;
    0 <= valueDiff <= 10^9

解题思路

有序集合(TreeSet)滑动窗口 / 桶

  • 解法一(TreeSet + 滑动窗口):维护窗口 i-indexDiff, i-1 内的值的有序集合;
    对当前 x = numsi,用 ceiling(x - valueDiff) 查最小 >= x-valueDiff 的值,
    若存在且 <= x + valueDiff,则 ceiling, x 差值满足 <= valueDiff。
    Java 的 ceiling 是 O(log k)。
    用 long 避免 int 溢出(valueDiff 可达 1e9,差值可达 2e9)。
  • 解法二(桶):把数值按桶大小 valueDiff+1 分桶,同一桶内必然满足;
    只查当前桶与相邻桶(桶大小 > valueDiff,跨桶也可能满足)。
    需处理负数下取整问题。时间期望 O(n)。
    为什么需要有序集合:除了"等于"还要查"范围最近",裸哈希无法回答
    "是否存在值在 x-d, x+d 内"的查询,有序结构支持 O(log n) 邻域查找。

代码实现

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

public class Solution {

    public boolean containsNearbyAlmostDuplicate(int[] nums, int indexDiff, int valueDiff) {
        if (valueDiff < 0) return false;
        TreeSet<Long> window = new TreeSet<>();
        for (int i = 0; i < nums.length; i++) {
            long x = nums[i];
            // 找 >= x - valueDiff 的最小值
            Long ceil = window.ceiling(x - valueDiff);
            if (ceil != null && ceil <= x + valueDiff) {
                return true; // ceil 与 x 的差满足 <= valueDiff
            }
            window.add(x);
            if (window.size() > indexDiff) {
                window.remove((long) nums[i - indexDiff]); // 窗口滑动移除最旧
            }
        }
        return false;
    }

    public boolean containsNearbyAlmostDuplicateBucket(int[] nums, int indexDiff, int valueDiff) {
        if (valueDiff < 0) return false;
        long bucketSize = (long) valueDiff + 1;
        java.util.Map<Long, Long> bucket = new java.util.HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            long x = nums[i];
            long id = getBucketId(x, bucketSize);
            if (bucket.containsKey(id)) return true;                  // 同桶必满足
            // 相邻桶:与左侧桶比较
            Long left = bucket.get(id - 1);
            if (left != null && x - left <= valueDiff) return true;
            Long right = bucket.get(id + 1);
            if (right != null && right - x <= valueDiff) return true;
            bucket.put(id, x);
            if (i >= indexDiff) {
                bucket.remove(getBucketId(nums[i - indexDiff], bucketSize)); // 移除窗口最旧
            }
        }
        return false;
    }

    private long getBucketId(long x, long bucketSize) {
        return Math.floorDiv(x, bucketSize);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.containsNearbyAlmostDuplicate(new int[]{1,2,3,1}, 3, 0), true, "示例1");
        check(s.containsNearbyAlmostDuplicate(new int[]{1,5,9,1,5,9}, 2, 3), false, "示例2");
        check(s.containsNearbyAlmostDuplicateBucket(new int[]{1,2,3,1}, 3, 0), true, "桶法示例1");
        check(s.containsNearbyAlmostDuplicateBucket(new int[]{1,5,9,1,5,9}, 2, 3), false, "桶法示例2");
        check(s.containsNearbyAlmostDuplicate(new int[]{1,2,2,3}, 1, 0), true, "补充:相邻重复");
        check(s.containsNearbyAlmostDuplicate(new int[]{8,7,15,1,6,1,9,15}, 1, 3), 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 log n),空间 O(n)(最坏窗口为全集)。

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


221. Maximal Square(最大正方形)

题目描述

在一个由 '0' 和 '1' 组成的二维矩阵内,找到只包含 '1' 的最大正方形,

并返回其面积。

  • 输入 :char[][] matrix(m == matrix.length, n == matrix[i].length,1 <= m, n <= 300)
  • 输出 :int(最大正方形面积)

示例 1:

复制代码
matrix = [["1","0","1","0","0"],["1","0","1","1","1"],["1","1","1","1","1"],["1","0","0","1","0"]]

-> 4

示例 2:

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

示例 3:

复制代码
matrix = [["0"]] -> 0

约束:

  • m == matrix.length;n == matrix[i].length;1 <= m, n <= 300;
    matrix[i][j] 为 '0' 或 '1'。

解题思路

动态规划(右下角扩展)

  • 解法一(二维 DP):dpij 表示以 (i,j) 为右下角的最大正方形边长。
    若 matrixij == '1',则 dpij = min(dpi-1j, dpij-1, dpi-1j-1) + 1;
    因为要形成边长为 L 的正方形,上、左、左上三个邻居都必须支持边长为 L-1 的正方形。
    答案 = max(dp) 的平方。
  • 解法二(滚动数组一维优化):dp 每行只依赖上一行,用一维数组 + 左上角临时变量即可,
    空间 O(n)。
  • 解法三(暴力枚举左上角 + 边长):最坏 O(m^2 n^2),仅作对比。
    为什么 min 取三者:以 (i,j) 为右下角能扩多大,取决于三个邻居能撑起的最小边长;
    (直观:左上、上方、左方三者中最短的"短板"决定了扩张上限)

代码实现

java 复制代码
public class Solution {

    public int maximalSquare(char[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int[][] dp = new int[m][n];
        int maxSide = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '1') {
                    if (i == 0 || j == 0) {
                        dp[i][j] = 1; // 边界只能边长 1
                    } else {
                        dp[i][j] = Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
                    }
                    maxSide = Math.max(maxSide, dp[i][j]);
                }
            }
        }
        return maxSide * maxSide;
    }

    public int maximalSquare1D(char[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int[] dp = new int[n];
        int maxSide = 0;
        for (int i = 0; i < m; i++) {
            int topLeft = 0; // dp[i-1][j-1]
            for (int j = 0; j < n; j++) {
                int tmp = dp[j]; // 保存本行的 dp[j] 作为下一列的 topLeft
                if (matrix[i][j] == '1') {
                    if (i == 0 || j == 0) {
                        dp[j] = 1;
                    } else {
                        dp[j] = Math.min(Math.min(dp[j], dp[j - 1]), topLeft) + 1;
                    }
                    maxSide = Math.max(maxSide, dp[j]);
                } else {
                    dp[j] = 0;
                }
                topLeft = tmp;
            }
        }
        return maxSide * maxSide;
    }

    public int maximalSquareBrute(char[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int maxSide = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i][j] == '0') continue;
                // 以 (i,j) 为左上角,尝试边长不断增大
                int len = 1;
                boolean ok = true;
                while (i + len <= m && j + len <= n && ok) {
                    for (int r = i; r < i + len; r++) {
                        for (int c = j; c < j + len; c++) {
                            if (matrix[r][c] == '0') { ok = false; break; }
                        }
                    }
                    if (ok) { maxSide = Math.max(maxSide, len); len++; }
                }
            }
        }
        return maxSide * maxSide;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        char[][] m1 = {{'1','0','1','0','0'},{'1','0','1','1','1'},{'1','1','1','1','1'},{'1','0','0','1','0'}};
        check(s.maximalSquare(m1), 4, "示例1");
        char[][] m2 = {{'0','1'},{'1','0'}};
        check(s.maximalSquare(m2), 1, "示例2");
        char[][] m3 = {{'0'}};
        check(s.maximalSquare(m3), 0, "示例3");
        check(s.maximalSquare1D(m1), 4, "滚动数组版示例1");
        check(s.maximalSquare1D(m2), 1, "滚动数组版示例2");
        check(s.maximalSquareBrute(m1), 4, "暴力版示例1");
        check(s.maximalSquareBrute(m2), 1, "暴力版示例2");
        char[][] m4 = {{'1'}};
        check(s.maximalSquare(m4), 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(mn),空间 O(m n)。解法二:时间 O(m*n),空间 O(n)。

解法三(暴力):时间 O(m^2 n^2),空间 O(1),仅作对比。


222. Count Complete Tree Nodes(完全二叉树的节点个数)

题目描述

给你一棵完全二叉树的根节点 root,求出该树的节点个数。

完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,

其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。

若最底层为第 h 层,则该层包含 1 ~ 2^h 个节点。

  • 输入 :TreeNode root(树中节点的数目范围是 0, 5 \* 10\^4,0 <= Node.val <= 5 * 10^4)
  • 输出 :int(节点总数)

示例 1:

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

示例 2:

复制代码
root=[] -> 0

示例 3:

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

约束:

  • 树中节点的数目范围是 0, 5 \* 10\^4;0 <= Node.val <= 5 * 10^4;题目数据保证输入的树是完全二叉树。

解题思路

利用完全二叉树性质:满子树直接公式

对任意节点,若左子树高 == 右子树高,则左子树是满二叉树(节点数 = 2^leftH - 1),

只需递归右子树并加上 2^leftH;否则右子树是满二叉树,递归左子树。

  • 解法一(高度对比递归):每次下沉一层只走一侧,复杂度 O(log^2 n)。
  • 解法二(朴素遍历):BFS/DFS 数全部节点,O(n),作为通用对照。
    为什么 O(log^2 n):每层递归要算左右高度 O(log n),总共 O(log n) 层。
    (严谨:每次递归规模减半为"完全树一侧",最多 log n 次,每次求高度 O(log n))

代码实现

java 复制代码
public class Solution {

    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        int lh = height(root.left);
        int rh = height(root.right);
        if (lh == rh) {
            // 左子树是满二叉树:节点数 = 2^lh - 1 + 根节点1 + 右子树节点数
            return (1 << lh) + countNodes(root.right);
        } else {
            // 右子树是满二叉树(高度 lh-1):2^(lh-1) - 1 + 根 + 左子树
            return (1 << rh) + countNodes(root.left);
        }
    }

    private int height(TreeNode node) {
        int h = 0;
        while (node != null) {
            h++;
            node = node.left; // 完全二叉树最左侧即高度
        }
        return h;
    }

    public int countNodesNaive(TreeNode root) {
        return root == null ? 0 : 1 + countNodesNaive(root.left) + countNodesNaive(root.right);
    }

    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.countNodes(build(new Integer[]{1,2,3,4,5,6})), 6, "示例1");
        check(s.countNodes(build(new Integer[]{})), 0, "示例2");
        check(s.countNodes(build(new Integer[]{1})), 1, "示例3");
        check(s.countNodesNaive(build(new Integer[]{1,2,3,4,5,6})), 6, "朴素版示例1");
        check(s.countNodes(build(new Integer[]{1,2,3,4,5,6,7,8,9,10,11,12})), 12, "补充");
    }

    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(log^2 n),空间 O(log n)(递归栈)。

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


223. Rectangle Area(矩形面积)

题目描述

给你二维平面上两个由直线构成且边与坐标轴平行/垂直的矩形,

请你计算并返回两个矩形覆盖的总面积。

每个矩形由其左下顶点和右上顶点坐标表示:

第一个矩形由其左下角 (ax1, ay1) 和右上角 (ax2, ay2) 定义;

第二个矩形由其左下角 (bx1, by1) 和右上角 (bx2, by2) 定义。

  • 输入 :int ax1, int ay1, int ax2, int ay2, int bx1, int by1, int bx2, int by2
    (-10^4 <= ax1 < ax2 <= 10^4,其余类似)
  • 输出 :int(两矩形覆盖总面积)

示例 1:

复制代码
ax1=-3, ay1=0, ax2=3, ay2=4, bx1=0, by1=-1, bx2=9, by2=2 -> 45

示例 2:

复制代码
ax1=-2, ay1=-2, ax2=2, ay2=2, bx1=-2, by1=-2, bx2=2, by2=2 -> 16

约束:

  • -10^4 <= ax1 < ax2 <= 10^4(各坐标同理)。

解题思路

面积 = 两矩形面积之和 - 重叠面积

重叠矩形的左下角 = (max(ax1,bx1), max(ay1,by1)),右上角 = (min(ax2,bx2), min(ay2,by2))。

若 overlapW <= 0 或 overlapH <= 0 则无重叠,重叠面积为 0。

为什么会有 int32 溢出风险:坐标最大 10^4,矩形面积最大约 (2104)2 = 4 10^8,

两面积之和约 8*10^8,在 int 范围内;但为稳健可用 long。

代码实现

java 复制代码
public class Solution {

    public int computeArea(int ax1, int ay1, int ax2, int ay2,
                           int bx1, int by1, int bx2, int by2) {
        long areaA = (long) (ax2 - ax1) * (ay2 - ay1);
        long areaB = (long) (bx2 - bx1) * (by2 - by1);
        // 重叠部分
        long overW = Math.min(ax2, bx2) - Math.max(ax1, bx1);
        long overH = Math.min(ay2, by2) - Math.max(ay1, by1);
        long overlap = 0;
        if (overW > 0 && overH > 0) {
            overlap = overW * overH;
        }
        return (int) (areaA + areaB - overlap);
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.computeArea(-3, 0, 3, 4, 0, -1, 9, 2), 45, "示例1");
        check(s.computeArea(-2, -2, 2, 2, -2, -2, 2, 2), 16, "示例2");
        check(s.computeArea(0, 0, 0, 0, -1, -1, 1, 1), 4, "空矩形A");
        check(s.computeArea(-2, -2, 2, 2, 1, 1, 3, 3), 19, "部分重叠");
        check(s.computeArea(-2, -2, 2, 2, 3, 3, 4, 4), 17, "不重叠:16+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(1),空间 O(1)。


224. Basic Calculator(基本计算器)

题目描述

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

注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval()。

s 由数字、'+'、'-'、'('、')' 和空格组成。'+' 不能用作一元运算(例如 "+1"),

但 '-'(减号)可以作为一元运算(例如 "-1" 和 "-(2+3)")。

  • 输入 :String s(1 <= s.length <= 3 * 10^5,s 由数字、'+'、'-'、'('、')' 和空格组成)
  • 输出 :int(表达式结果,保证结果在 32 位整数范围内)

示例 1:

复制代码
s="1 + 1" -> 2

示例 2:

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

示例 3:

复制代码
s="(1+(4+5+2)-3)+(6+8)" -> 23

约束:

  • 1 <= s.length <= 3 * 10^5;s 由数字、'+'、'-'、'('、')' 和空格组成;'+' 和 '-' 不能作为"+"的一元运算。

解题思路

栈 + 符号栈(单变量扫描)

只有 + - 与括号,没有乘除,因此可以在扫描时直接累加:

  • sign 表示当前数字前的符号(+1/-1),num 累计当前数字;
  • 遇到 '+'/'-' 时把 num*sign 计入结果,并更新 sign;
  • 遇到 '(' 时,把 (当前结果, 当前符号) 压栈,重置 result=0、sign=1;
  • 遇到 ')' 时,先结算括号内最后一个数字,再与栈顶符号相乘,加上栈顶结果。
    为什么只维护一个栈:括号嵌套时只需记住"进入括号前的符号与结果",出括号恢复即可,
    不需要对加减法构造复杂表达式树。

代码实现

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

public class Solution {

    public int calculate(String s) {
        Deque<Integer> stack = new ArrayDeque<>(); // 存括号前的 result 和 sign
        int result = 0;
        int num = 0;
        int sign = 1; // 当前符号
        for (char c : s.toCharArray()) {
            if (Character.isDigit(c)) {
                num = num * 10 + (c - '0');
            } else if (c == '+') {
                result += sign * num; // 结算当前数
                num = 0;
                sign = 1;
            } else if (c == '-') {
                result += sign * num;
                num = 0;
                sign = -1;
            } else if (c == '(') {
                stack.push(result); // 存括号前结果
                stack.push(sign);   // 存括号前符号
                result = 0;         // 重置为计算括号内
                sign = 1;
            } else if (c == ')') {
                result += sign * num; // 结算括号内最后一个数
                num = 0;
                result *= stack.pop(); // 乘上括号前符号
                result += stack.pop(); // 加上括号前结果
            }
        }
        // 末尾可能还有未结算的数字(无最后运算符)
        return result + sign * num;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        check(s.calculate("1 + 1"), 2, "示例1");
        check(s.calculate(" 2-1 + 2 "), 3, "示例2");
        check(s.calculate("(1+(4+5+2)-3)+(6+8)"), 23, "示例3");
        check(s.calculate("-2"), -2, "一元负号");
        check(s.calculate("-(1+2)"), -3, "括号前负号");
        check(s.calculate("2147483647"), 2147483647, "大数");
    }

    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)(嵌套最深的括号层数)。


225. Implement Stack using Queues(用队列实现栈)

题目描述

请你仅使用两个队列实现一个后入先出(LIFO)的栈,

并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶;
  • int pop() 移除并返回栈顶元素;
  • int top() 返回栈顶元素;
  • boolean empty() 如果栈是空的,返回 true;否则返回 false。
    注意:你只能使用队列的基本操作(也就是 push to back、peek/pop from front、size 和 is empty)
    这些操作。
  • 输入 :方法调用(1 <= x <= 9,最多调用 100 次 push、pop、top 和 empty)
  • 输出 :各方法返回值
    示例:MyStack myStack = new MyStack();
    myStack.push(1); myStack.push(2);
    myStack.top(); // 2
    myStack.pop(); // 2
    myStack.empty(); // false
    约束:
  • 1 <= x <= 9;最多调用 100 次 push、pop、top 和 empty;每次调用 pop 和 top 都能保证栈非空。

解题思路

两个队列模拟栈

  • 解法一(入栈 O(n)):新元素先进 queue2,再把 queue1 的全部元素按序搬到 queue2 后面
    (使新元素位于队首),交换两队列引用。这样队首永远是栈顶。
  • 解法二(出栈 O(n)):入栈直接进 queue1(队尾即栈顶?否------栈顶应是最后入的),
    出栈时把前 n-1 个搬到 queue2,剩下的队首即栈顶,之后交换。本实现以解法一为主,
    两种思路任一即可。
    为什么解法一更直观:始终保证队首 == 栈顶,top 直接 peek,pop 直接 poll。

代码实现

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

public class MyStack {

    private Deque<Integer> q1 = new ArrayDeque<>();
    private Deque<Integer> q2 = new ArrayDeque<>();

    public void push(int x) {
        q2.offer(x);        // 新元素进辅助队列
        while (!q1.isEmpty()) {
            q2.offer(q1.poll()); // 把原元素全部搬到新元素后面
        }
        Deque<Integer> tmp = q1;
        q1 = q2;
        q2 = tmp;           // 交换:q1 队首即栈顶
    }

    public int pop() {
        return q1.poll();
    }

    public int top() {
        return q1.peek();
    }

    public boolean empty() {
        return q1.isEmpty();
    }

    public static void main(String[] args) {
        MyStack st = new MyStack();
        st.push(1);
        st.push(2);
        check(st.top(), 2, "top()");
        check(st.pop(), 2, "pop()");
        check(st.empty(), false, "empty()");
        st.push(3);
        check(st.pop(), 3, "pop() 后入");
        check(st.pop(), 1, "pop() 最底");
        check(st.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(n),pop/top/empty O(1);空间 O(n)。

相关推荐
SL_staff3 小时前
4G设备掉线三层归因指南:SIM卡、模组、JVS-IOT网关配置精准锁定
java·物联网·产品
Wang's Blog4 小时前
Java 项目实战: 交友项目 - 黑名单:取消黑名单
java·windows·交友
杨杨杨大侠4 小时前
DeepSeek Harness:连 Agent Loop 都能替换的插件架构
java·人工智能·agent
SL_staff4 小时前
私有化低代码运维三重雷区:JVS开源栈中的Kafka/ES/RabbitMQ治理实践
java·低代码·全栈
yuniko-n4 小时前
【算法】约瑟夫环问题
java·算法
郝亚军4 小时前
manifest 中 `sha256` 与 `signValue` 的作用
java·开发语言·数据结构
譕痕5 小时前
stripe 实例
java·开发语言·stripe·checksession
Nil2085 小时前
LeetCode 283移动零
数据结构·算法·leetcode
墨子夕5 小时前
设计模式——Java × Python 双语言精讲 · 23 个经典模式 + 现代演进(前言篇)
java·开发语言·设计模式