LeetCode701~725题目解析

4 阅读29分钟

LeetCode 701-725. 题解合集


701. Insert into a Binary Search Tree(二叉搜索树中的插入操作)

题目描述

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value,将值插入二叉搜索树。
返回插入后二叉搜索树的根节点。输入数据保证新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。你可以返回
任意有效的结果。

示例 1

  • 输入:root = [4,2,7,1,3], val = 5
  • 输出:[4,2,7,1,3,5]
  • 解释:另一个满足题目要求的结果是 [5,2,7,1,3,null,null,null,null,null,4]。

示例 2

  • 输入:root = [40,20,60,10,30,50,70], val = 25
  • 输出:[40,20,60,10,30,50,70,null,null,25]

示例 3

  • 输入:root = [4,2,7,1,3,null,null,null,null,null,null], val = 5
  • 输出:[4,2,7,1,3,5]
    提示:
  • 树中的节点数在 [0, 10^4] 范围内
  • -10^8 <= Node.val <= 10^8
  • 所有值 Node.val 是独一无二的
  • -10^8 <= val <= 10^8
  • 保证 val 在原始 BST 中不存在

解题思路

解法一(递归,主提交):根据二叉搜索树性质,若 val < root.val 则插入左子树,
否则插入右子树;空节点处新建节点。返回新根。时间复杂度 O(h),空间复杂度 O(h)。
解法二(迭代):与递归等价,用指针沿树下降,遇到空位插入即可。时间复杂度 O(h),
空间复杂度 O(1)。

代码实现

class Solution {

    public 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 TreeNode insertIntoBST(TreeNode root, int val) {
        if (root == null) {
            return new TreeNode(val);
        }
        if (val < root.val) {
            root.left = insertIntoBST(root.left, val);
        } else {
            root.right = insertIntoBST(root.right, val);
        }
        return root;
    }

    // 解法二:迭代
    public TreeNode insertIntoBST2(TreeNode root, int val) {
        if (root == null) {
            return new TreeNode(val);
        }
        TreeNode cur = root;
        while (true) {
            if (val < cur.val) {
                if (cur.left == null) {
                    cur.left = new TreeNode(val);
                    break;
                }
                cur = cur.left;
            } else {
                if (cur.right == null) {
                    cur.right = new TreeNode(val);
                    break;
                }
                cur = cur.right;
            }
        }
        return root;
    }

    // 层序转数组(本地验证用,空节点用 -1 表示)
    private static java.util.List<Integer> toLevelList(TreeNode root) {
        java.util.List<Integer> res = new java.util.ArrayList<>();
        java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
        q.offer(root);
        while (!q.isEmpty()) {
            TreeNode node = q.poll();
            if (node == null) {
                res.add(-1);
                continue;
            }
            res.add(node.val);
            q.offer(node.left);
            q.offer(node.right);
        }
        while (!res.isEmpty() && res.get(res.size() - 1) == -1) {
            res.remove(res.size() - 1);
        }
        return res;
    }

    private static TreeNode build(Integer[] arr) {
        if (arr == null || arr.length == 0 || arr[0] == null) {
            return null;
        }
        TreeNode root = new TreeNode(arr[0]);
        java.util.Queue<TreeNode> q = new java.util.LinkedList<>();
        q.offer(root);
        int i = 1;
        while (!q.isEmpty() && i < arr.length) {
            TreeNode node = q.poll();
            if (arr[i] != null) {
                node.left = new TreeNode(arr[i]);
                q.offer(node.left);
            }
            ++i;
            if (i < arr.length && arr[i] != null) {
                node.right = new TreeNode(arr[i]);
                q.offer(node.right);
            }
            ++i;
        }
        return root;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        TreeNode r1 = build(new Integer[]{4, 2, 7, 1, 3});
        TreeNode res1 = sol.insertIntoBST(r1, 5);
        ++total; if (toLevelList(res1).equals(java.util.Arrays.asList(4, 2, 7, 1, 3, 5))) ++pass; else System.out.println("FAIL test1: " + toLevelList(res1));
        TreeNode r2 = build(new Integer[]{40, 20, 60, 10, 30, 50, 70});
        TreeNode res2 = sol.insertIntoBST(r2, 25);
        ++total; if (toLevelList(res2).equals(java.util.Arrays.asList(40, 20, 60, 10, 30, 50, 70, -1, -1, 25))) ++pass; else System.out.println("FAIL test2: " + toLevelList(res2));
        TreeNode r3 = build(new Integer[]{4, 2, 7, 1, 3});
        TreeNode res3 = sol.insertIntoBST2(r3, 5);
        ++total; if (toLevelList(res3).equals(java.util.Arrays.asList(4, 2, 7, 1, 3, 5))) ++pass; else System.out.println("FAIL test3: " + toLevelList(res3));
        TreeNode r4 = build(new Integer[]{});
        TreeNode res4 = sol.insertIntoBST2(null, 5);
        ++total; if (res4 != null && res4.val == 5) ++pass; else System.out.println("FAIL test4");
        if (pass == total) {
            System.out.println("== 0701 PASS ==");
        } else {
            System.out.println("== 0701 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(h)O(h)
解法二O(h)O(1)

702. Search in a Sorted Array of Unknown Size(搜索长度未知的有序数组)

题目描述

给定一个升序整数数组并写入内存。但你不知道数组的大小。与 ArrayReader 接口交互即可:
你可以调用 ArrayReader.get(index) 获取数组 index 下标对应的值。

  • 如果 0 <= index < arr.length,返回 arr[index]
  • 否则返回 2^31 - 1(即 2147483647)
    返回与 target 相等的下标;若数组中不存在 target,返回 -1。

示例 1

  • 输入:reader = [-1,0,3,5,9,12], target = 9
  • 输出:4
  • 解释:下标 4 处值为 9,返回 4。

示例 2

  • 输入:reader = [-1,0,3,5,9,12], target = 2
  • 输出:-1
    提示:
  • 1 <= arr.length <= 10^4
  • -10^4 <= arr[i] <= 10^4
  • arr 按升序排列
  • -10^4 <= target <= 10^4

解题思路

解法一(倍增右边界 + 二分,主提交):数组长度未知,先以 1,2,4,8,... 倍增找到第一个
get(r) >= target 的右边界(或越界返回 Integer.MAX_VALUE),再在 [l, r] 上标准二分。
时间复杂度 O(log n),空间复杂度 O(1)。
解法二(直接倍增二分右边界 + 左闭右闭二分):细节稍有不同,同样 O(log n)。

代码实现

class Solution {

    // 与 LeetCode 相同的 ArrayReader 接口(本地辅助)
    public interface ArrayReader {
        int get(int index);
    }

    // 解法一:倍增右边界 + 标准二分
    public int search(ArrayReader reader, int target) {
        int l = 0, r = 1;
        while (reader.get(r) < target) {
            l = r;
            r <<= 1;
        }
        while (l <= r) {
            int mid = (l + r) >>> 1;
            int v = reader.get(mid);
            if (v == target) {
                return mid;
            }
            if (v < target) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return -1;
    }

    // 解法二:仅倍增边界 + 线性逼近(等价二分)
    public int search2(ArrayReader reader, int target) {
        int hi = 1;
        while (reader.get(hi) < target) {
            hi <<= 1;
        }
        int lo = 0;
        while (lo <= hi) {
            int mid = (lo + hi) >>> 1;
            int v = reader.get(mid);
            if (v == target) {
                return mid;
            }
            if (v < target) {
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        final int[] arr = {-1, 0, 3, 5, 9, 12};
        ArrayReader reader = new ArrayReader() {
            public int get(int index) {
                if (index < 0 || index >= arr.length) {
                    return Integer.MAX_VALUE;
                }
                return arr[index];
            }
        };
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.search(reader, 9) == 4) ++pass; else System.out.println("FAIL test1: " + sol.search(reader, 9));
        ++total; if (sol.search(reader, 2) == -1) ++pass; else System.out.println("FAIL test2: " + sol.search(reader, 2));
        ++total; if (sol.search(reader, 12) == 5) ++pass; else System.out.println("FAIL test3: " + sol.search(reader, 12));
        ++total; if (sol.search(reader, -1) == 0) ++pass; else System.out.println("FAIL test4: " + sol.search(reader, -1));
        ++total; if (sol.search2(reader, 9) == 4) ++pass; else System.out.println("FAIL test5: " + sol.search2(reader, 9));
        ++total; if (sol.search2(reader, 2) == -1) ++pass; else System.out.println("FAIL test6: " + sol.search2(reader, 2));
        ++total; if (sol.search2(reader, 12) == 5) ++pass; else System.out.println("FAIL test7: " + sol.search2(reader, 12));
        ++total; if (sol.search2(reader, -1) == 0) ++pass; else System.out.println("FAIL test8: " + sol.search2(reader, -1));
        if (pass == total) {
            System.out.println("== 0702 PASS ==");
        } else {
            System.out.println("== 0702 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(log n)O(1)
解法二O(log n)O(1)

703. Kth Largest Element in a Stream(数据流中的第 K 大元素)

题目描述

设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k
个不同的元素。
请实现 KthLargest 类:

  • KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
  • int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。

示例

  • 输入:
    ["KthLargest", "add", "add", "add", "add", "add"]
    [[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
  • 输出:
    [null, 4, 5, 5, 8, 8]
  • 解释:
    KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
    kthLargest.add(3); // return 4
    kthLargest.add(5); // return 5
    kthLargest.add(10); // return 8
    kthLargest.add(9); // return 8
    kthLargest.add(4); // return 8
    提示:
  • 1 <= k <= 10^4
  • 0 <= nums.length <= 10^4
  • -10^4 <= nums[i] <= 10^4
  • -10^4 <= val <= 10^4
  • 最多调用 add 方法 10^4 次
  • 题目数据保证,在查找第 k 大元素时,数组中至少有 k 个元素

解题思路

解法一(小根堆保持 k 个元素,主提交):维护一个容量为 k 的小根堆,堆顶即第 k 大元素。
初始化时将 nums 全部加入堆,超过 k 就弹出最小;add(val) 先入堆,若堆大小超过 k
则弹出堆顶,返回堆顶。时间复杂度 O(log k)(add),空间复杂度 O(k)。
解法二(快选 / n 项排序思路):当数据总量不大时也可用有序数组维护,插入 O(k);仅作参考。

代码实现

import java.util.PriorityQueue;
import java.util.ArrayList;
import java.util.List;

class KthLargest {

    private final PriorityQueue<Integer> pq;
    private final int k;

    public KthLargest(int k, int[] nums) {
        this.k = k;
        this.pq = new PriorityQueue<>(k);
        for (int v : nums) {
            add(v);
        }
    }

    public int add(int val) {
        pq.offer(val);
        if (pq.size() > k) {
            pq.poll();
        }
        return pq.peek();
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        int pass = 0, total = 0;
        KthLargest kth = new KthLargest(3, new int[]{4, 5, 8, 2});
        ++total; if (kth.add(3) == 4) ++pass; else System.out.println("FAIL test1: " + kth.add(3));
        ++total; if (kth.add(5) == 5) ++pass; else System.out.println("FAIL test2");
        ++total; if (kth.add(10) == 5) ++pass; else System.out.println("FAIL test3");
        ++total; if (kth.add(9) == 8) ++pass; else System.out.println("FAIL test4");
        ++total; if (kth.add(4) == 8) ++pass; else System.out.println("FAIL test5");
        if (pass == total) {
            System.out.println("== 0703 PASS ==");
        } else {
            System.out.println("== 0703 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(log k)(add)O(k)
解法二O(k)(add)O(k)

704. Binary Search(二分查找)

题目描述

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target,写一个函数搜索
nums 中的 target,如果目标值存在返回下标,否则返回 -1。

示例 1

  • 输入:nums = [-1,0,3,5,9,12], target = 9
  • 输出: 4
  • 解释: 9 出现在 nums 中并且下标为 4

示例 2

  • 输入:nums = [-1,0,3,5,9,12], target = 2
  • 输出: -1
  • 解释: 2 不存在 nums 中因此返回 -1
    提示:
  • 你可以假设 nums 中的所有元素是不重复的
  • n 将在 [1, 10000] 之间
  • nums 的每个元素都将在 [-9999, 9999] 之间

解题思路

解法一(左闭右闭标准二分,主提交):l = 0, r = n - 1,mid 用无符号右移防溢出;
根据 nums[mid] 与 target 比较收缩区间。时间复杂度 O(log n),空间复杂度 O(1)。
解法二(左闭右开二分):l = 0, r = n,循环条件 l < r,收缩时 r = mid 或 l = mid + 1。
时间复杂度 O(log n),空间复杂度 O(1)。

代码实现

class Solution {

    // 解法一:左闭右闭
    public int search(int[] nums, int target) {
        int l = 0, r = nums.length - 1;
        while (l <= r) {
            int mid = (l + r) >>> 1;
            if (nums[mid] == target) {
                return mid;
            }
            if (nums[mid] < target) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return -1;
    }

    // 解法二:左闭右开
    public int search2(int[] nums, int target) {
        int l = 0, r = nums.length;
        while (l < r) {
            int mid = (l + r) >>> 1;
            if (nums[mid] == target) {
                return mid;
            }
            if (nums[mid] < target) {
                l = mid + 1;
            } else {
                r = mid;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[] nums = {-1, 0, 3, 5, 9, 12};
        ++total; if (sol.search(nums, 9) == 4) ++pass; else System.out.println("FAIL test1");
        ++total; if (sol.search(nums, 2) == -1) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.search(nums, -1) == 0) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.search(new int[]{5}, 5) == 0) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.search2(nums, 9) == 4) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.search2(nums, 2) == -1) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.search2(nums, 12) == 5) ++pass; else System.out.println("FAIL test7");
        ++total; if (sol.search2(new int[]{5}, 7) == -1) ++pass; else System.out.println("FAIL test8");
        if (pass == total) {
            System.out.println("== 0704 PASS ==");
        } else {
            System.out.println("== 0704 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(log n)O(1)
解法二O(log n)O(1)

705. Design HashSet(设计哈希集合)

题目描述

不使用任何内建的哈希表库设计一个哈希集合(HashSet)。
实现 MyHashSet 类:

  • void add(key) 向哈希集合中插入值 key。
  • void remove(key) 将给定值 key 从哈希集合中删除。如果哈希集合中没有这个值,什么也不做。
  • boolean contains(key) 返回哈希集合中是否存在这个值 key。

示例

  • 输入:
    ["MyHashSet", "add", "add", "contains", "contains", "add", "contains", "remove", "contains"]
    [[], [1], [2], [1], [3], [2], [2], [2], [2]]
  • 输出:
    [null, null, null, true, false, null, true, null, false]
  • 解释:
    MyHashSet myHashSet = new MyHashSet();
    myHashSet.add(1); // set = [1]
    myHashSet.add(2); // set = [1, 2]
    myHashSet.contains(1); // 返回 True
    myHashSet.contains(3); // 返回 False ,(未找到)
    myHashSet.add(2); // set = [1, 2]
    myHashSet.contains(2); // 返回 True
    myHashSet.remove(2); // set = [1]
    myHashSet.contains(2); // 返回 False ,(已移除)
    提示:
  • 0 <= key <= 10^6
  • 最多调用 10^4 次 add、remove 和 contains

解题思路

解法一(桶 + 链地址法,主提交):key 范围 10^6,可开 769 个桶(素数),每个桶内用
LinkedList/数组存储,取模定位桶。平均 O(1) 增删查。时间复杂度 O(1)(均摊),
空间复杂度 O(n)。
解法二(位图 BitSet):key <= 10^6,直接用 boolean[1000001] 或 BitSet 标记,
实现最简。时间复杂度 O(1),空间复杂度 O(10^6)。

代码实现

import java.util.Arrays;

class MyHashSet {

    // ===== 解法一:链地址法 =====
    private static final int SIZE = 769;
    private final java.util.LinkedList<Integer>[] buckets;

    public MyHashSet() {
        buckets = new java.util.LinkedList[SIZE];
        for (int i = 0; i < SIZE; ++i) {
            buckets[i] = new java.util.LinkedList<>();
        }
    }

    private int hash(int key) {
        return key % SIZE;
    }

    public void add(int key) {
        int idx = hash(key);
        java.util.LinkedList<Integer> b = buckets[idx];
        if (!b.contains(key)) {
            b.add(key);
        }
    }

    public void remove(int key) {
        int idx = hash(key);
        java.util.LinkedList<Integer> b = buckets[idx];
        b.remove((Integer) key);
    }

    public boolean contains(int key) {
        return buckets[hash(key)].contains(key);
    }

    // ===== 解法二:位图(BitSet) =====
    private static class MyHashSet2 {
        private boolean[] bits;

        MyHashSet2() {
            bits = new boolean[1000001];
        }

        void add(int key) {
            bits[key] = true;
        }

        void remove(int key) {
            bits[key] = false;
        }

        boolean contains(int key) {
            return bits[key];
        }
    }

    public static void main(String[] args) {
        int pass = 0, total = 0;
        MyHashSet s = new MyHashSet();
        s.add(1);
        s.add(2);
        ++total; if (s.contains(1)) ++pass; else System.out.println("FAIL test1");
        ++total; if (!s.contains(3)) ++pass; else System.out.println("FAIL test2");
        s.add(2);
        ++total; if (s.contains(2)) ++pass; else System.out.println("FAIL test3");
        s.remove(2);
        ++total; if (!s.contains(2)) ++pass; else System.out.println("FAIL test4");

        MyHashSet2 s2 = new MyHashSet2();
        s2.add(1000000);
        ++total; if (s2.contains(1000000)) ++pass; else System.out.println("FAIL test5");
        s2.remove(1000000);
        ++total; if (!s2.contains(1000000)) ++pass; else System.out.println("FAIL test6");
        s2.add(0);
        ++total; if (s2.contains(0)) ++pass; else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0705 PASS ==");
        } else {
            System.out.println("== 0705 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(1)(均摊)O(n)
解法二O(1)O(10^6)

706. Design HashMap(设计哈希映射)

题目描述

不使用任何内建的哈希表库设计一个哈希映射(HashMap)。
实现 MyHashMap 类:

  • MyHashMap() 用空映射初始化对象
  • void put(int key, int value) 向 HashMap 插入一个键值对 (key, value)。如果 key
    已经存在于映射中,则更新其对应的值 value。
  • int get(int key) 返回特定的 key 所映射的 value;如果映射中不包含 key 的映射,
    返回 -1。
  • void remove(key) 如果映射中存在 key 的映射,则移除 key 和它所映射的 value。

示例

  • 输入:
    ["MyHashMap", "put", "put", "get", "get", "put", "get", "remove", "get"]
    [[], [1, 1], [2, 2], [1], [3], [2, 1], [2], [2], [2]]
  • 输出:
    [null, null, null, 1, -1, null, 1, null, -1]
  • 解释:
    MyHashMap myHashMap = new MyHashMap();
    myHashMap.put(1, 1); // myHashMap 现在为 [[1,1]]
    myHashMap.put(2, 2); // myHashMap 现在为 [[1,1], [2,2]]
    myHashMap.get(1); // 返回 1 ,myHashMap 现在为 [[1,1], [2,2]]
    myHashMap.get(3); // 返回 -1(未找到),myHashMap 现在为 [[1,1], [2,2]]
    myHashMap.put(2, 1); // myHashMap 现在为 [[1,1], [2,1]](更新已有的值)
    myHashMap.get(2); // 返回 1 ,myHashMap 现在为 [[1,1], [2,1]]
    myHashMap.remove(2); // 删除键为 2 的数据,myHashMap 现在为 [[1,1]]
    myHashMap.get(2); // 返回 -1 (未找到),myHashMap 现在为 [[1,1]]
    提示:
  • 0 <= key, value <= 10^6
  • 最多调用 10^4 次 put、get 和 remove

解题思路

解法一(链地址法 + 桶内线性查找,主提交):开 769 个桶(素数),每桶存
int[]{key, value} 对。put 先查同 key 更新,否则尾插;get/remove 都在桶内线性找。
均摊 O(1)。时间复杂度 O(1)(均摊),空间复杂度 O(n)。
解法二(开放寻址法):key 范围 1e6 时可开 int[1e6+1][2] 直接数组映射,value 用
-1 表示不存在。最简直接。时间复杂度 O(1),空间复杂度 O(10^6)。

代码实现

import java.util.LinkedList;

class MyHashMap {

    // ===== 解法一:链地址法 =====
    private static final int SIZE = 769;
    private final LinkedList<int[]>[] buckets;

    public MyHashMap() {
        buckets = new LinkedList[SIZE];
        for (int i = 0; i < SIZE; ++i) {
            buckets[i] = new LinkedList<>();
        }
    }

    private int hash(int key) {
        return key % SIZE;
    }

    public void put(int key, int value) {
        LinkedList<int[]> b = buckets[hash(key)];
        for (int[] e : b) {
            if (e[0] == key) {
                e[1] = value;
                return;
            }
        }
        b.add(new int[]{key, value});
    }

    public int get(int key) {
        LinkedList<int[]> b = buckets[hash(key)];
        for (int[] e : b) {
            if (e[0] == key) {
                return e[1];
            }
        }
        return -1;
    }

    public void remove(int key) {
        LinkedList<int[]> b = buckets[hash(key)];
        b.removeIf(e -> e[0] == key);
    }

    // ===== 解法二:直接数组 =====
    private static class MyHashMap2 {
        private int[][] map;

        MyHashMap2() {
            map = new int[1000001][2];
            for (int[] e : map) {
                e[1] = -1;
            }
        }

        void put(int key, int value) {
            map[key][1] = value;
        }

        int get(int key) {
            return map[key][1];
        }

        void remove(int key) {
            map[key][1] = -1;
        }
    }

    public static void main(String[] args) {
        int pass = 0, total = 0;
        MyHashMap m = new MyHashMap();
        m.put(1, 1);
        m.put(2, 2);
        ++total; if (m.get(1) == 1) ++pass; else System.out.println("FAIL test1");
        ++total; if (m.get(3) == -1) ++pass; else System.out.println("FAIL test2");
        m.put(2, 1);
        ++total; if (m.get(2) == 1) ++pass; else System.out.println("FAIL test3");
        m.remove(2);
        ++total; if (m.get(2) == -1) ++pass; else System.out.println("FAIL test4");

        MyHashMap2 m2 = new MyHashMap2();
        m2.put(1000000, 7);
        ++total; if (m2.get(1000000) == 7) ++pass; else System.out.println("FAIL test5");
        m2.remove(1000000);
        ++total; if (m2.get(1000000) == -1) ++pass; else System.out.println("FAIL test6");
        m2.put(0, 0);
        ++total; if (m2.get(0) == 0) ++pass; else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0706 PASS ==");
        } else {
            System.out.println("== 0706 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(1)(均摊)O(n)
解法二O(1)O(10^6)

707. Design Linked List(设计链表)

题目描述

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:
val 和 next。val 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用
双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是
0-index 的。
在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回 -1。
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将
    成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index, val):在链表中的第 index 个节点之前添加值为 val 的节点。如果
    index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会
    插入节点。如果 index 小于 0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

示例

  • 输入:
    ["MyLinkedList", "addAtHead", "addAtTail", "addAtIndex", "get", "deleteAtIndex", "get"]
    [[], [1], [3], [1, 2], [1], [1], [1]]
  • 输出:
    [null, null, null, null, 2, null, 3]
  • 解释:
    MyLinkedList myLinkedList = new MyLinkedList();
    myLinkedList.addAtHead(1); // 链表变为 1
    myLinkedList.addAtTail(3); // 链表变为 1 -> 3
    myLinkedList.addAtIndex(1, 2); // 链表变为 1 -> 2 -> 3
    myLinkedList.get(1); // 返回 2
    myLinkedList.deleteAtIndex(1); // 现在链表是 1 -> 3
    myLinkedList.get(1); // 返回 3
    提示:
  • 所有 val 值都在 [1, 1000] 之内
  • 操作次数将在 [1, 1000] 之内
  • 请不要使用内置的 LinkedList 库

解题思路

解法一(双链表 + 哨兵节点,主提交):使用哑头 head 与哑尾 tail,prev/next 双向
链接,维护 size。插入删除都通过找到第 index 个节点或其前驱完成。时间复杂度
O(index)(最坏 O(n)),空间复杂度 O(n)。
解法二(带头节点的单链表实现):只维护哑头与 size,操作均 O(index)。

代码实现

class MyLinkedList {

    private static class Node {
        int val;
        Node prev, next;
        Node(int val) {
            this.val = val;
        }
    }

    private final Node head; // 哑头
    private final Node tail; // 哑尾
    private int size;

    public MyLinkedList() {
        head = new Node(-1);
        tail = new Node(-1);
        head.next = tail;
        tail.prev = head;
        size = 0;
    }

    public int get(int index) {
        if (index < 0 || index >= size) {
            return -1;
        }
        Node cur = head;
        for (int i = 0; i <= index; ++i) {
            cur = cur.next;
        }
        return cur.val;
    }

    public void addAtHead(int val) {
        addAtIndex(0, val);
    }

    public void addAtTail(int val) {
        addAtIndex(size, val);
    }

    public void addAtIndex(int index, int val) {
        if (index < 0) {
            index = 0;
        }
        if (index > size) {
            return;
        }
        Node prev = head;
        for (int i = 0; i < index; ++i) {
            prev = prev.next;
        }
        Node next = prev.next;
        Node node = new Node(val);
        node.prev = prev;
        node.next = next;
        prev.next = node;
        next.prev = node;
        ++size;
    }

    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            return;
        }
        Node cur = head;
        for (int i = 0; i <= index; ++i) {
            cur = cur.next;
        }
        cur.prev.next = cur.next;
        cur.next.prev = cur.prev;
        --size;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        int pass = 0, total = 0;
        MyLinkedList ml = new MyLinkedList();
        ml.addAtHead(1);   // 1
        ml.addAtTail(3);   // 1 -> 3
        ml.addAtIndex(1, 2); // 1 -> 2 -> 3
        ++total; if (ml.get(1) == 2) ++pass; else System.out.println("FAIL test1: " + ml.get(1));
        ml.deleteAtIndex(1);
        ++total; if (ml.get(1) == 3) ++pass; else System.out.println("FAIL test2: " + ml.get(1));

        MyLinkedList ml2 = new MyLinkedList();
        ml2.addAtIndex(0, 10);   // 10
        ml2.addAtIndex(0, 20);   // 20 -> 10
        ml2.addAtIndex(1, 30);   // 20 -> 30 -> 10
        ++total; if (ml2.get(0) == 20 && ml2.get(1) == 30 && ml2.get(2) == 10) ++pass; else System.out.println("FAIL test3");
        ++total; if (ml2.get(5) == -1) ++pass; else System.out.println("FAIL test4");
        ml2.deleteAtIndex(0);
        ++total; if (ml2.get(0) == 30) ++pass; else System.out.println("FAIL test5: " + ml2.get(0));
        if (pass == total) {
            System.out.println("== 0707 PASS ==");
        } else {
            System.out.println("== 0707 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(index)(最坏 O(n))O(n)
解法二O(index)(最坏 O(n))O(n)

708. Insert into a Sorted Circular Linked List(循环有序列表的插入)

题目描述

给定循环单调非递减列表中的一个点,写一个函数向这个列表中插入一个新元素 insertVal,
使这个列表仍然是循环升序的。
给定的可以是这个列表中任意一个顶点的指针,不一定是最小元素的那个指针。
如果一个列表有多个满足条件的插入位置,你可以选择任意一个位置插入新值后,整个列表仍然
有序。如果列表为空(给定的节点是 null),你需要创建一个循环有序列表并返回这个节点。
否则,请返回原先给定的节点。
下面的图可以帮您理解这个问题中的循环上升的元素:

示例 1

  • 输入:head = [3,4,1], insertVal = 2
  • 输出:[3,4,1,2]
  • 解释:在上图中,有一个包含三个元素的循环有序列表,你获得值为 3 的节点的指针,我们需要
    向表中插入元素 2。新插入的节点应该在 1 和 3 之间,插入之后,整个列表仍然是有序的。

示例 2

  • 输入:head = [], insertVal = 1
  • 输出:[1]
  • 解释:列表为空(给定的节点是 null),创建一个循环有序列表并返回这个节点。

示例 3

  • 输入:head = [1], insertVal = 0
  • 输出:[1,0]
    提示:
  • 0 <= Number of Nodes <= 5 * 10^4
  • -10^6 <= Node.val <= 10^6
  • -10^6 <= insertVal <= 10^6

解题思路

解法一(双指针绕环找插入点,主提交):head == null 时新建自环节点。否则用
prev = head, curr = head.next 绕环行走:若找到
prev.val <= insertVal <= curr.val 的区间直接插入;或者遇到环中“回折点”
(prev.val > curr.val,即最大值到最小值处),说明 insertVal 大于所有值或小于所有值,
也在此处插入。若整个环都相同(未找到 break 条件),绕完一圈后停在任一位置插入。
时间复杂度 O(n),空间复杂度 O(1)。
解法二(先找最小节点再单次扫描):从最小节点开始沿升序找第一个
insertVal <= curr.val 的位置插入。需先绕环找最小。时间复杂度 O(n),空间复杂度 O(1)。

代码实现

class Solution {

    // 与 LeetCode 相同的 Node 定义
    public static class Node {
        public int val;
        public Node next;
        public Node() {}
        public Node(int _val) {
            val = _val;
        }
        public Node(int _val, Node _next) {
            val = _val;
            next = _next;
        }
    }

    // 解法一:双指针绕环
    public Node insert(Node head, int insertVal) {
        Node node = new Node(insertVal);
        if (head == null) {
            node.next = node;
            return node;
        }
        Node prev = head, curr = head.next;
        while (curr != head) {
            if ((prev.val <= insertVal && insertVal <= curr.val)
                || (prev.val > curr.val && (insertVal >= prev.val || insertVal <= curr.val))) {
                break;
            }
            prev = curr;
            curr = curr.next;
        }
        prev.next = node;
        node.next = curr;
        return head;
    }

    // 解法二:先找最小再插入
    public Node insert2(Node head, int insertVal) {
        if (head == null) {
            Node node = new Node(insertVal);
            node.next = node;
            return node;
        }
        // 找最大节点(其后即最小节点)
        Node max = head;
        while (max.next != head && max.next.val >= max.val) {
            max = max.next;
        }
        Node start = max.next; // 最小节点
        if (insertVal <= start.val || insertVal >= max.val) {
            Node node = new Node(insertVal);
            node.next = start;
            max.next = node;
            return head;
        }
        Node prev = start, curr = start.next;
        while (curr.val < insertVal) {
            prev = curr;
            curr = curr.next;
        }
        Node node = new Node(insertVal);
        prev.next = node;
        node.next = curr;
        return head;
    }

    // 本地测试:数组转循环链表,返回给定 head 等价引用
    private static Node build(int[] arr) {
        if (arr.length == 0) {
            return null;
        }
        Node head = new Node(arr[0]);
        Node cur = head;
        for (int i = 1; i < arr.length; ++i) {
            cur.next = new Node(arr[i]);
            cur = cur.next;
        }
        cur.next = head;
        return head;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        Node h1 = build(new int[]{3, 4, 1});
        ++total; if (java.util.Arrays.equals(toArr(sol.insert(h1, 2)), new int[]{3, 4, 1, 2})) ++pass; else System.out.println("FAIL test1: " + java.util.Arrays.toString(toArr(sol.insert(h1, 2))));
        Node h2 = build(new int[]{});
        Node r2 = sol.insert(null, 1);
        ++total; if (r2.val == 1 && r2.next == r2) ++pass; else System.out.println("FAIL test2");
        Node h3 = build(new int[]{1});
        ++total; if (java.util.Arrays.equals(toArr(sol.insert(h3, 0)), new int[]{1, 0})) ++pass; else System.out.println("FAIL test3: " + java.util.Arrays.toString(toArr(sol.insert(h3, 0))));
        Node h4 = build(new int[]{3, 4, 1});
        ++total; if (java.util.Arrays.equals(toArr(sol.insert2(h4, 2)), new int[]{3, 4, 1, 2})) ++pass; else System.out.println("FAIL test4: " + java.util.Arrays.toString(toArr(sol.insert2(h4, 2))));
        Node h5 = build(new int[]{1, 3, 5});
        ++total; if (java.util.Arrays.equals(toArr(sol.insert2(h5, 4)), new int[]{1, 3, 4, 5})) ++pass; else System.out.println("FAIL test5: " + java.util.Arrays.toString(toArr(sol.insert2(h5, 4))));
        Node h6 = build(new int[]{1, 3, 5});
        ++total; if (java.util.Arrays.equals(toArr(sol.insert2(h6, 0)), new int[]{1, 3, 5, 0})) ++pass; else System.out.println("FAIL test6: " + java.util.Arrays.toString(toArr(sol.insert2(h6, 0))));
        if (pass == total) {
            System.out.println("== 0708 PASS ==");
        } else {
            System.out.println("== 0708 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)
解法二O(n)O(1)

709. To Lower Case(转换成小写字母)

题目描述

给你一个字符串 s,将该字符串中的大写字母转换成相同的小写字母,返回新的字符串。

示例 1

  • 输入:s = "Hello"
  • 输出:"hello"

示例 2

  • 输入:s = "here"
  • 输出:"here"

示例 3

  • 输入:s = "LOVELY"
  • 输出:"lovely"
    提示:
  • 1 <= s.length <= 100
  • s 由 ASCII 字符集中的可打印字符组成

解题思路

解法一(ASCII 位运算,主提交):大写字母 'A'~'Z' 的 ASCII 码区间 65~90,
加上 32 即对应小写。用 | 32 实现。时间复杂度 O(n),空间复杂度 O(1)。
解法二(String 内置方法):直接用 s.toLowerCase()。时间复杂度 O(n),空间复杂度 O(n)。

代码实现

class Solution {

    // 解法一:位运算
    public String toLowerCase(String s) {
        char[] cs = new char[s.length()];
        for (int i = 0; i < s.length(); ++i) {
            char c = s.charAt(i);
            if (c >= 'A' && c <= 'Z') {
                c |= 32;
            }
            cs[i] = c;
        }
        return new String(cs);
    }

    // 解法二:内置方法
    public String toLowerCase2(String s) {
        return s.toLowerCase();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.toLowerCase("Hello").equals("hello")) ++pass; else System.out.println("FAIL test1");
        ++total; if (sol.toLowerCase("here").equals("here")) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.toLowerCase("LOVELY").equals("lovely")) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.toLowerCase("aBcD123!@#").equals("abcd123!@#")) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.toLowerCase2("Hello").equals("hello")) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.toLowerCase2("LOVELY").equals("lovely")) ++pass; else System.out.println("FAIL test6");
        if (pass == total) {
            System.out.println("== 0709 PASS ==");
        } else {
            System.out.println("== 0709 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(n)(结果串)
解法二O(n)O(n)

710. Random Pick with Blacklist(黑名单中的随机数)

题目描述

给定一个整数 n 和一个无重复黑名单整数数组 blacklist。设计一种算法,从
[0, n - 1] 范围内的任意整数中选取一个未加入黑名单 blacklist 的整数。任何在上述范围内
且不在黑名单 blacklist 中的整数都应该有同等的可能性被返回。
优化你的算法,使它最小化调用语言内置随机函数的次数。
实现 Solution 类:

  • Solution(int n, int[] blacklist) 初始化整数 n 和被加入黑名单 blacklist 的整数
  • int pick() 返回一个范围为 [0, n - 1] 且不在黑名单 blacklist 中的随机整数

示例 1

输入
["Solution", "pick", "pick", "pick", "pick", "pick", "pick", "pick"]
[[7, [2, 3, 5]], [], [], [], [], [], [], []]
输出
[null, 0, 4, 1, 6, 1, 0, 4]
  • 解释:Solution solution = new Solution(7, [2, 3, 5]);
    solution.pick(); // 返回0,任何[0,1,4,6]的整数都可以。注意,对于每一个pick的调用,
    // 0、1、4和6的返回概率必须相等(即概率为1/4)。
    提示:
  • 1 <= n <= 10^9
  • 0 <= blacklist.length <= min(10^5, n - 1)
  • 0 <= blacklist[i] < n
  • blacklist 中所有值都不同
  • pick 最多被调用 2 * 10^4 次

解题思路

解法一(白名单重映射,主提交):白名单长度为 m = n - blacklist.length。把 [0, m) 区间
看成一个可随机索引的空间:随机取 x in [0, m),若 x 不在黑名单直接返回;若在黑名单,
则映射到从 [m, n) 挑出的“白名单尾部”元素(因为 [m, n) 中黑名单元素肯定也存在,
白名单尾元素数量恰等于 [0, m) 中黑名单元素数量)。用 Map 记录黑名单 -> 映射值。
初始化一次性建好映射;pick 只调用一次随机函数。时间复杂度 O(1)(pick),
初始化 O(b),空间复杂度 O(b)。
解法二(到白名单下标等价转换):含义相同,把黑名单元素重新映射,实现略有差异。

代码实现

import java.util.HashMap;
import java.util.Map;
import java.util.Random;

class Solution {

    private final Map<Integer, Integer> d = new HashMap<>();
    private final Random rand = new Random();
    private final int m;

    public Solution(int n, int[] blacklist) {
        java.util.Set<Integer> black = new java.util.HashSet<>();
        for (int b : blacklist) {
            black.add(b);
        }
        m = n - blacklist.length;
        // 尾部的白名单元素集
        int tail = n - 1;
        for (int b : blacklist) {
            if (b < m) {
                while (black.contains(tail)) {
                    --tail;
                }
                d.put(b, tail--);
            }
        }
    }

    public int pick() {
        int x = rand.nextInt(m);
        return d.getOrDefault(x, x);
    }

    public static void main(String[] args) {
        int pass = 0, total = 0;
        Solution sol = new Solution(7, new int[]{2, 3, 5});
        java.util.Set<Integer> white = new java.util.HashSet<>();
        for (int i = 0; i < 10000; ++i) {
            int v = sol.pick();
            white.add(v);
            if (v < 0 || v >= 7 || v == 2 || v == 3 || v == 5) {
                System.out.println("FAIL pick invalid: " + v);
                pass = -100;
                break;
            }
        }
        if (pass >= 0) {
            ++total;
            if (white.equals(new java.util.HashSet<>(java.util.Arrays.asList(0, 1, 4, 6)))) {
                ++pass;
            } else {
                System.out.println("FAIL coverage: " + white);
            }
        }
        Solution sol2 = new Solution(3, new int[]{1});
        ++total;
        boolean ok2 = true;
        for (int i = 0; i < 1000; ++i) {
            int v = sol2.pick();
            if (v != 0 && v != 2) {
                ok2 = false;
                break;
            }
        }
        if (ok2) ++pass; else System.out.println("FAIL test2");
        if (pass == total) {
            System.out.println("== 0710 PASS ==");
        } else {
            System.out.println("== 0710 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(1)(pick)O(b)
解法二O(1)(pick)O(b)

711. Number of Distinct Islands II(不同岛屿的数量 II)

题目描述

给定一个 m x n 二进制数组表示的网格 grid,一个岛屿由四连通(上、下、左、右四个方向)
的 1 组成(代表陆地)。你可以认为网格的四周被海水包围。
如果两个岛屿的形状相同,或者通过旋转(顺时针旋转 90°,180°,270°)、翻转(左右翻转、
上下翻转)后形状相同,那么就认为这两个岛屿是相同的。
返回这个网格中形状不同的岛屿的数量。

示例 1

  • 输入:grid = [[1,1,0,0,0],[1,0,0,0,0],[0,0,0,0,1],[0,0,0,1,1]]
  • 输出: 1
  • 解释: 这两个是相同的岛屿。因为我们通过 180° 旋转第一个岛屿,两个岛屿的形状相同。

示例 2

  • 输入:grid = [[1,1,0,0,0],[1,1,0,0,0],[0,0,0,1,1],[0,0,0,1,1]]
  • 输出: 1
    提示:
  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 50
  • grid[i][j] 不是 0 就是 1

解题思路

解法一(DFS 提取形状 + 8 种变换归一化,主提交):对每块岛屿 DFS 收集其所有格子坐标
(相对初始点归一化)。对每个形状,枚举 8 种等距变换:
(x,y)、(-x,y)、(x,-y)、(-x,-y)、(y,x)、(-y,x)、(y,-x)、(-y,-x),
每种变换后平移使最小 x、最小 y 为 0,再按字典序排序,取字典序最小的作为“规范化形状”,
用 Set<List<Integer>> 去重。时间复杂度 O(8·格数·排序),空间复杂度 O(格数)。
解法二(方向串 + 旋转矩阵):用方向字符串标记形状同时做旋转归一,逻辑与解法一本质相同。

代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

class Solution {

    private int[][] grid;
    private int m, n;

    public int numDistinctIslands2(int[][] grid) {
        this.grid = grid;
        m = grid.length;
        n = grid[0].length;
        Set<List<Integer>> shapes = new HashSet<>();
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1) {
                    List<int[]> cells = new ArrayList<>();
                    dfs(i, j, cells);
                    shapes.add(normalize(cells));
                }
            }
        }
        return shapes.size();
    }

    private void dfs(int i, int j, List<int[]> cells) {
        grid[i][j] = 0;
        cells.add(new int[]{i, j});
        int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
        for (int[] d : dirs) {
            int ni = i + d[0], nj = j + d[1];
            if (ni >= 0 && ni < m && nj >= 0 && nj < n && grid[ni][nj] == 1) {
                dfs(ni, nj, cells);
            }
        }
    }

    // 8 种等距变换后取字典序最小的规范化形状
    private List<Integer> normalize(List<int[]> cells) {
        List<List<int[]>> transforms = new ArrayList<>();
        for (int t = 0; t < 8; ++t) {
            List<int[]> cur = new ArrayList<>();
            for (int[] c : cells) {
                int x = c[0], y = c[1];
                switch (t) {
                    case 0: cur.add(new int[]{x, y}); break;
                    case 1: cur.add(new int[]{-x, y}); break;
                    case 2: cur.add(new int[]{x, -y}); break;
                    case 3: cur.add(new int[]{-x, -y}); break;
                    case 4: cur.add(new int[]{y, x}); break;
                    case 5: cur.add(new int[]{-y, x}); break;
                    case 6: cur.add(new int[]{y, -x}); break;
                    default: cur.add(new int[]{-y, -x}); break;
                }
            }
            // 平移
            int minX = Integer.MAX_VALUE, minY = Integer.MAX_VALUE;
            for (int[] c : cur) {
                minX = Math.min(minX, c[0]);
                minY = Math.min(minY, c[1]);
            }
            for (int[] c : cur) {
                c[0] -= minX;
                c[1] -= minY;
            }
            transforms.add(cur);
        }
        List<List<Integer>> list = new ArrayList<>();
        for (List<int[]> t : transforms) {
            t.sort((a, b) -> a[0] != b[0] ? a[0] - b[0] : a[1] - b[1]);
            List<Integer> flat = new ArrayList<>();
            for (int[] c : t) {
                flat.add(c[0]);
                flat.add(c[1]);
            }
            list.add(flat);
        }
        list.sort((a, b) -> {
            for (int i = 0; i < a.size(); ++i) {
                if (!a.get(i).equals(b.get(i))) {
                    return a.get(i) - b.get(i);
                }
            }
            return 0;
        });
        return list.get(0);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.numDistinctIslands2(new int[][]{{1, 1, 0, 0, 0}, {1, 0, 0, 0, 0}, {0, 0, 0, 0, 1}, {0, 0, 0, 1, 1}}) == 1) ++pass; else System.out.println("FAIL test1");
        ++total; if (sol.numDistinctIslands2(new int[][]{{1, 1, 0, 0, 0}, {1, 1, 0, 0, 0}, {0, 0, 0, 1, 1}, {0, 0, 0, 1, 1}}) == 1) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.numDistinctIslands2(new int[][]{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}) == 1) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.numDistinctIslands2(new int[][]{{1, 1, 0}, {0, 1, 0}, {0, 0, 1}}) == 2) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.numDistinctIslands2(new int[][]{{1}}) == 1) ++pass; else System.out.println("FAIL test5");
        if (pass == total) {
            System.out.println("== 0711 PASS ==");
        } else {
            System.out.println("== 0711 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(8·K·logK)(K 为最大岛屿格数)O(K)
解法二O(8·K·logK)O(K)

712. Minimum ASCII Delete Sum for Two Strings(两个字符串的最小ASCII删除和)

题目描述

给定两个字符串 s1 和 s2,返回使两个字符串相等所需删除字符的 ASCII 值的最小和。

示例 1

  • 输入:s1 = "sea", s2 = "eat"
  • 输出: 231
  • 解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。在 "eat" 中删除 "t" 并将 116 加入
    总和。结束时,两个字符串相等,我们可以得到 115 + 116 = 231 达到最小值。

示例 2

  • 输入:s1 = "delete", s2 = "leet"
  • 输出: 403
  • 解释: 在 "delete" 中删除 "dee" 字符串变成 "let",将 100[d]+101[e]+101[e] 加入总和。
    在 "leet" 中删除 "e" 将 101[e] 加入总和。结束时,两个字符串都等于 "let",结果即为
    100+101+101+101 = 403。如果改为将两个字符串转换为 "lee" 或 "eet",我们会得到 433 或 417,
    比答案更大。
    提示:
  • 0 <= s1.length, s2.length <= 1000
  • s1 和 s2 由小写英文字母组成

解题思路

解法一(二维 DP,主提交):dp[i][j] 表示 s1[0..i) 与 s2[0..j) 相等所需最小 ASCII
删除和。若 s1[i-1] == s2[j-1] 则 dp[i][j] = dp[i-1][j-1];否则删去其中一个字符取较小
——min(dp[i-1][j] + s1[i-1], dp[i][j-1] + s2[j-1])。边界:dp[i][0] 累加删空整个 s1,
dp[0][j] 同理。时间复杂度 O(m·n),空间复杂度 O(m·n)(可滚动至 O(n))。
解法二(滚动数组优化):只保留两行 dp,空间 O(n)。

代码实现

class Solution {

    // 解法一:二维 DP
    public int minimumDeleteSum(String s1, String s2) {
        int m = s1.length(), n = s2.length();
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 1; i <= m; ++i) {
            dp[i][0] = dp[i - 1][0] + s1.charAt(i - 1);
        }
        for (int j = 1; j <= n; ++j) {
            dp[0][j] = dp[0][j - 1] + s2.charAt(j - 1);
        }
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] = Math.min(dp[i - 1][j] + s1.charAt(i - 1),
                                        dp[i][j - 1] + s2.charAt(j - 1));
                }
            }
        }
        return dp[m][n];
    }

    // 解法二:滚动数组
    public int minimumDeleteSum2(String s1, String s2) {
        int m = s1.length(), n = s2.length();
        int[] dp = new int[n + 1];
        for (int j = 1; j <= n; ++j) {
            dp[j] = dp[j - 1] + s2.charAt(j - 1);
        }
        for (int i = 1; i <= m; ++i) {
            int pre = dp[0];
            dp[0] += s1.charAt(i - 1);
            for (int j = 1; j <= n; ++j) {
                int tmp = dp[j];
                if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                    dp[j] = pre;
                } else {
                    dp[j] = Math.min(dp[j] + s1.charAt(i - 1), dp[j - 1] + s2.charAt(j - 1));
                }
                pre = tmp;
            }
        }
        return dp[n];
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.minimumDeleteSum("sea", "eat") == 231) ++pass; else System.out.println("FAIL test1: " + sol.minimumDeleteSum("sea", "eat"));
        ++total; if (sol.minimumDeleteSum("delete", "leet") == 403) ++pass; else System.out.println("FAIL test2: " + sol.minimumDeleteSum("delete", "leet"));
        ++total; if (sol.minimumDeleteSum("a", "b") == 195) ++pass; else System.out.println("FAIL test3: " + sol.minimumDeleteSum("a", "b"));
        ++total; if (sol.minimumDeleteSum("", "") == 0) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.minimumDeleteSum2("sea", "eat") == 231) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.minimumDeleteSum2("delete", "leet") == 403) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.minimumDeleteSum2("a", "") == 97) ++pass; else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0712 PASS ==");
        } else {
            System.out.println("== 0712 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(m·n)O(m·n)
解法二O(m·n)O(n)

713. Subarray Product Less Than K(乘积小于 K 的子数组)

题目描述

给你一个整数数组 nums 和一个整数 k,请你返回子数组内所有元素的乘积严格小于 k 的 连续子数组的数目。

示例 1

  • 输入:nums = [10,5,2,6], k = 100
  • 输出:8
  • 解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2]、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。 需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。

示例 2

  • 输入:nums = [1,2,3], k = 0
  • 输出:0 提示:
  • 1 <= nums.length <= 3 * 10^4
  • 1 <= nums[i] <= 1000
  • 0 <= k <= 10^6

解题思路

解法一(滑动窗口,主提交):右指针 r 不断右移并累乘 prod;当 prod >= k 时左指针 l 右移、除以 nums[l],直到 prod < k 或 l > r。此时以 r 结尾且乘积小于 k 的子数组 个数为 r - l + 1(含单元素),累加即可。乘积用 long 防溢出。 时间复杂度 O(n),空间复杂度 O(1)。 解法二(二分 + 对数技巧思路):log 乘积前缀 + 二分可用,但复杂度 O(n log n),滑动窗口 更优。仅作对比参考。

代码实现

class Solution {

    // 解法一:滑动窗口
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if (k <= 1) {
            return 0;
        }
        int l = 0;
        long prod = 1;
        int ans = 0;
        for (int r = 0; r < nums.length; ++r) {
            prod *= nums[r];
            while (prod >= k) {
                prod /= nums[l++];
            }
            ans += r - l + 1;
        }
        return ans;
    }

    // 解法二:对数 + 前缀二分(参考)
    public int numSubarrayProductLessThanK2(int[] nums, int k) {
        if (k <= 1) {
            return 0;
        }
        int n = nums.length;
        double[] logPre = new double[n + 1];
        for (int i = 0; i < n; ++i) {
            logPre[i + 1] = logPre[i] + Math.log(nums[i]);
        }
        double limit = Math.log(k);
        int ans = 0;
        for (int l = 0; l < n; ++l) {
            // 找最大 r 使 logPre[r+1] - logPre[l] < limit
            int lo = l, hi = n;
            while (lo < hi) {
                int mid = (lo + hi + 1) >>> 1;
                if (logPre[mid] - logPre[l] < limit) {
                    lo = mid;
                } else {
                    hi = mid - 1;
                }
            }
            ans += lo - l;
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.numSubarrayProductLessThanK(new int[]{10, 5, 2, 6}, 100) == 8) ++pass; else System.out.println("FAIL test1: " + sol.numSubarrayProductLessThanK(new int[]{10, 5, 2, 6}, 100));
        ++total; if (sol.numSubarrayProductLessThanK(new int[]{1, 2, 3}, 0) == 0) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.numSubarrayProductLessThanK(new int[]{1, 1, 1}, 2) == 6) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.numSubarrayProductLessThanK(new int[]{10, 9, 10, 4, 3, 8, 3, 3, 6, 2, 10, 10, 9, 3}, 19) == 18) ++pass; else System.out.println("FAIL test4: " + sol.numSubarrayProductLessThanK(new int[]{10, 9, 10, 4, 3, 8, 3, 3, 6, 2, 10, 10, 9, 3}, 19));
        ++total; if (sol.numSubarrayProductLessThanK2(new int[]{10, 5, 2, 6}, 100) == 8) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.numSubarrayProductLessThanK2(new int[]{1, 2, 3}, 0) == 0) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.numSubarrayProductLessThanK2(new int[]{1, 1, 1}, 2) == 6) ++pass; else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0713 PASS ==");
        } else {
            System.out.println("== 0713 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)
解法二O(n log n)O(n)

714. Best Time to Buy and Sell Stock with Transaction Fee(买卖股票的最佳时机含手续费)

题目描述

给定一个整数数组 prices,其中 prices[i] 表示第 i 天的股票价格;整数 fee 代表了交易 股票的手续费用。 你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出 它之前你就不能再继续购买股票了。 返回获得利润的最大值。 注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1

  • 输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
  • 输出:8
  • 解释:能够达到的最大利润: 在此处买入 prices[0] = 1,在此处卖出 prices[3] = 8,在此处买入 prices[4] = 4,在此处卖出 prices[5] = 9,总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8。

示例 2

  • 输入:prices = [1,3,7,5,10,3], fee = 3
  • 输出:6 提示:
  • 1 <= prices.length <= 5 * 10^4
  • 1 <= prices[i] < 5 * 10^4
  • 0 <= fee < 5 * 10^4

解题思路

解法一(状态机 DP,主提交):用 hold 表示持有股票时的最大现金(可为负), cash 表示不持有时的最大现金。每天:cash = max(cash, hold + price - fee)(卖出), hold = max(hold, cash - price)(买入)。遍历一次即得答案 cash。 时间复杂度 O(n),空间复杂度 O(1)。 解法二(贪心):把手续费视为买入成本的一部分,minPrice 跟踪“有效买入价”;当卖出有利时 累加利润并抬高买入价(仍可继续持有合并交易)。时间复杂度 O(n),空间复杂度 O(1)。

代码实现

class Solution {

    // 解法一:状态机 DP
    public int maxProfit(int[] prices, int fee) {
        int cash = 0;
        int hold = -prices[0];
        for (int i = 1; i < prices.length; ++i) {
            cash = Math.max(cash, hold + prices[i] - fee);
            hold = Math.max(hold, cash - prices[i]);
        }
        return cash;
    }

    // 解法二:贪心(有效买入价)
    public int maxProfit2(int[] prices, int fee) {
        int n = prices.length;
        int buy = prices[0] + fee;
        int profit = 0;
        for (int i = 1; i < n; ++i) {
            if (prices[i] + fee < buy) {
                buy = prices[i] + fee;   // 更便宜的买入机会
            } else if (prices[i] > buy) {
                profit += prices[i] - buy;
                buy = prices[i];         // 抬高门槛,避免对同一段重复计费
            }
        }
        return profit;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.maxProfit(new int[]{1, 3, 2, 8, 4, 9}, 2) == 8) ++pass; else System.out.println("FAIL test1: " + sol.maxProfit(new int[]{1, 3, 2, 8, 4, 9}, 2));
        ++total; if (sol.maxProfit(new int[]{1, 3, 7, 5, 10, 3}, 3) == 6) ++pass; else System.out.println("FAIL test2: " + sol.maxProfit(new int[]{1, 3, 7, 5, 10, 3}, 3));
        ++total; if (sol.maxProfit(new int[]{1, 2, 3}, 1) == 1) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.maxProfit(new int[]{5}, 1) == 0) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.maxProfit2(new int[]{1, 3, 2, 8, 4, 9}, 2) == 8) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.maxProfit2(new int[]{1, 3, 7, 5, 10, 3}, 3) == 6) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.maxProfit2(new int[]{5}, 1) == 0) ++pass; else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0714 PASS ==");
        } else {
            System.out.println("== 0714 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)
解法二O(n)O(1)

715. Range Module(Range 模块)

题目描述

Range 模块是跟踪数字范围的模块。设计一个数据结构来跟踪表示为半开区间的范围并查询它们。 半开区间 [left, right) 表示所有 left <= x < right 的实数 x。 实现 RangeModule 类:

  • RangeModule() 初始化数据结构的对象。
  • void addRange(int left, int right) 添加半开区间 [left, right),跟踪该区间中的每个实数。 添加与当前跟踪的数字部分重叠的区间时,应当添加在区间 [left, right) 中尚未跟踪的任何 数字到该区间中。
  • boolean queryRange(int left, int right) 只有在当前正在跟踪区间 [left, right) 中的每一个 实数时,才返回 true,否则返回 false。
  • void removeRange(int left, int right) 停止跟踪半开区间 [left, right) 中当前正在跟踪的 每个实数。

示例

输入
["RangeModule", "addRange", "removeRange", "queryRange", "queryRange", "queryRange"]
[[], [10, 20], [14, 16], [10, 14], [13, 15], [16, 17]]
输出
[null, null, null, true, false, true]
  • 解释: RangeModule rangeModule = new RangeModule(); rangeModule.addRange(10, 20); rangeModule.removeRange(14, 16); rangeModule.queryRange(10, 14); 返回 true (区间 [10, 14) 中的每个数都正在被跟踪) rangeModule.queryRange(13, 15); 返回 false(未跟踪区间 [13, 15) 中像 14, 14.03, 14.17 这样的数字) rangeModule.queryRange(16, 17); 返回 true (尽管执行了删除操作,区间 [16, 17) 中的数字 16 仍然会被跟踪) 提示:
  • 1 <= left < right <= 10^9
  • 在单个测试用例中,对 addRange、queryRange 和 removeRange 的调用总数不超过 10^4 次

解题思路

解法一(有序集合维护不相交区间,主提交):用 TreeMap<Integer, Integer> 保存 起点 -> 终点,且任意两个区间互不重叠、不相邻(合并后)。每次操作先取 floorKey(left) 与 floorKey(right) 附近区间,区间合并/切分时利用 subMap 批量删除。

  • addRange:收集所有与 [left, right) 相交的区间合并;
  • queryRange:找 floorKey(left) 的区间是否覆盖 [left, right);
  • removeRange:删除被覆盖部分,并把新边界保留为剩余区间。 平均每次操作 O(log n + 区间数)。时间复杂度 O(log n + |S|),空间复杂度 O(|S|)。 解法二(动态开点线段树):维护区间是否被跟踪的布尔线段树。坐标 1e9 需动态开点。

代码实现

import java.util.Map;
import java.util.TreeMap;

class RangeModule {

    private final TreeMap<Integer, Integer> map = new TreeMap<>();

    public RangeModule() {
    }

    public void addRange(int left, int right) {
        // 找到可能与 [left, right) 相交的区间范围
        Integer lk = map.floorKey(left);
        if (lk != null && map.get(lk) >= left) {
            left = lk;
        }
        Integer rk = map.floorKey(right);
        if (rk != null && map.get(rk) > right) {
            right = map.get(rk);
        }
        // 删除 [left, right) 覆盖的所有区间
        map.subMap(left, true, right, true).clear();
        map.put(left, right);
    }

    public boolean queryRange(int left, int right) {
        Integer lk = map.floorKey(left);
        return lk != null && map.get(lk) >= right;
    }

    public void removeRange(int left, int right) {
        Integer lk = map.floorKey(left);
        if (lk != null && map.get(lk) > left) {
            int r = map.get(lk);
            if (r > right) {
                // [lk, r) 被 [left, right) 完全包含
                map.put(right, r);
            }
            map.put(lk, left);
        }
        map.subMap(left, true, right, false).clear();
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        int pass = 0, total = 0;
        RangeModule rm = new RangeModule();
        rm.addRange(10, 20);
        rm.removeRange(14, 16);
        ++total; if (rm.queryRange(10, 14)) ++pass; else System.out.println("FAIL test1");
        ++total; if (!rm.queryRange(13, 15)) ++pass; else System.out.println("FAIL test2");
        ++total; if (rm.queryRange(16, 17)) ++pass; else System.out.println("FAIL test3");

        RangeModule rm2 = new RangeModule();
        rm2.addRange(5, 8);
        rm2.addRange(10, 12);
        ++total; if (!rm2.queryRange(5, 12)) ++pass; else System.out.println("FAIL test4");
        rm2.addRange(8, 10);
        ++total; if (rm2.queryRange(5, 12)) ++pass; else System.out.println("FAIL test5");
        rm2.removeRange(6, 11);
        ++total; if (!rm2.queryRange(5, 12)) ++pass; else System.out.println("FAIL test6");
        ++total; if (rm2.queryRange(5, 6) && rm2.queryRange(11, 12)) ++pass; else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0715 PASS ==");
        } else {
            System.out.println("== 0715 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(log n + 相交区间数)O(S)
解法二O(log 坐标范围)O(n)(动态开点)

716. Max Stack(最大栈)

题目描述

设计一个最大栈数据结构,既支持栈操作,又支持查找栈中最大元素。 实现 MaxStack 类:

  • MaxStack() 初始化栈对象
  • void push(int x) 将元素 x 压入栈中。
  • int pop() 移除栈顶元素并返回这个元素。
  • int top() 返回栈顶元素,无需移除。
  • int peekMax() 检索并返回栈中最大元素,无需移除。
  • int popMax() 检索并返回栈中最大元素,并将其移除。如果有多个最大元素,只要移除 最靠近栈顶的那个。

示例

输入
["MaxStack", "push", "push", "push", "top", "popMax", "top", "peekMax", "pop", "top"]
[[], [5], [1], [5], [], [], [], [], [], []]
输出
[null, null, null, null, 5, 5, 1, 5, 1, 5]
  • 解释: MaxStack stk = new MaxStack(); stk.push(5); // [5] - 5 既是栈顶元素,也是最大元素 stk.push(1); // [5, 1] - 栈顶元素是 1,最大元素是 5 stk.push(5); // [5, 1, 5] - 5 既是栈顶元素,也是最大元素 stk.top(); // 返回 5,栈没有改变 stk.popMax(); // 返回 5,栈发生改变,栈顶元素不再是最大元素 stk.top(); // 返回 1 stk.peekMax(); // 返回 5 stk.pop(); // 返回 1 stk.top(); // 返回 5 提示:
  • -10^7 <= x <= 10^7
  • 最多调用 10^4 次 push、pop、top、peekMax 和 popMax
  • 调用 pop、top、peekMax 或 popMax 时,栈中至少存在一个元素

解题思路

解法一(双栈,主提交):stack 存元素、maxStack 同步存“当前位置的最大值”。 push 时 maxStack 压入 max(x, maxStack.peek());pop 时两栈同时出栈; peekMax 即 maxStack.peek();popMax 需要暂存弹出元素以恢复栈——用一个临时栈反复 pop 直到栈顶等于当前最大,弹出最大后把暂存元素压回。均摊 O(n)(popMax), 其他 O(1)。空间复杂度 O(n)。 解法二(双向链表 + TreeMap 值->节点集合):链表维护栈顺序、TreeMap 维护最大值到节点 集合,所有操作理论 O(log n)。实现较繁。

代码实现

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.TreeMap;

class MaxStack {

    private final Deque<Integer> stack = new ArrayDeque<>();
    private final Deque<Integer> maxStack = new ArrayDeque<>();

    public MaxStack() {
    }

    public void push(int x) {
        stack.push(x);
        maxStack.push(maxStack.isEmpty() ? x : Math.max(x, maxStack.peek()));
    }

    public int pop() {
        maxStack.pop();
        return stack.pop();
    }

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

    public int peekMax() {
        return maxStack.peek();
    }

    public int popMax() {
        int mx = peekMax();
        Deque<Integer> tmp = new ArrayDeque<>();
        while (top() != mx) {
            tmp.push(pop());
        }
        pop(); // 弹出最大值
        while (!tmp.isEmpty()) {
            push(tmp.pop());
        }
        return mx;
    }

    // ===== 本地测试 =====
    public static void main(String[] args) {
        int pass = 0, total = 0;
        MaxStack stk = new MaxStack();
        stk.push(5);
        stk.push(1);
        stk.push(5);
        ++total; if (stk.top() == 5) ++pass; else System.out.println("FAIL test1");
        ++total; if (stk.popMax() == 5) ++pass; else System.out.println("FAIL test2: " + stk.popMax());
        ++total; if (stk.top() == 1) ++pass; else System.out.println("FAIL test3: " + stk.top());
        ++total; if (stk.peekMax() == 5) ++pass; else System.out.println("FAIL test4: " + stk.peekMax());
        ++total; if (stk.pop() == 1) ++pass; else System.out.println("FAIL test5: " + stk.pop());
        ++total; if (stk.top() == 5) ++pass; else System.out.println("FAIL test6: " + stk.top());

        MaxStack stk2 = new MaxStack();
        stk2.push(2);
        stk2.push(3);
        stk2.push(1);
        ++total; if (stk2.popMax() == 3) ++pass; else System.out.println("FAIL test7: " + stk2.popMax());
        ++total; if (stk2.top() == 1) ++pass; else System.out.println("FAIL test8: " + stk2.top());
        if (pass == total) {
            System.out.println("== 0716 PASS ==");
        } else {
            System.out.println("== 0716 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(1);popMax 均摊 O(n)O(n)
解法二O(log n)O(n)

717. 1-bit and 2-bit Characters(1 比特与 2 比特字符)

题目描述

有两种特殊字符:

  • 第一种字符可以用一比特 0 表示
  • 第二种字符可以用两比特(10 或 11)表示 给你一个以 0 结尾的二进制数组 bits,如果最后一个字符必须是一个一比特字符,则返回 true。

示例 1

  • 输入:bits = [1, 0, 0]
  • 输出: true
  • 解释: 唯一的编码方式是一个两比特字符和一个一比特字符。 所以最后一个字符是一比特字符。

示例 2

  • 输入:bits = [1, 1, 1, 0]
  • 输出: false
  • 解释: 唯一的编码方式是两比特字符和两比特字符。 所以最后一个字符不是一比特字符。 提示:
  • 1 <= bits.length <= 1000
  • bits[i] 为 0 或 1
  • bits 以 0 结尾

解题思路

解法一(线性扫描,主提交):从头开始,若遇到 1 则它必是两比特字符的开头,跳过两位; 若遇到 0 则是一比特字符,跳一位。结束时若正好停在最后一个元素(即 i == n-1), 说明最后一个 0 是独立的一比特字符。时间复杂度 O(n),空间复杂度 O(1)。 解法二(从末尾倒数连续的 1 的个数):最后一个字符是一比特字符,当且仅当从倒数第二位 往前连续 1 的个数为偶数(因为每个两比特字符必须以 1 开头且占两位)。时间复杂度 O(n), 空间复杂度 O(1)。

代码实现

class Solution {

    // 解法一:线性扫描
    public boolean isOneBitCharacter(int[] bits) {
        int n = bits.length;
        int i = 0;
        while (i < n - 1) {
            if (bits[i] == 1) {
                i += 2;
            } else {
                i += 1;
            }
        }
        return i == n - 1;
    }

    // 解法二:末尾连续 1 的个数
    public boolean isOneBitCharacter2(int[] bits) {
        int n = bits.length;
        int cnt = 0;
        int i = n - 2;
        while (i >= 0 && bits[i] == 1) {
            ++cnt;
            --i;
        }
        return cnt % 2 == 0;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.isOneBitCharacter(new int[]{1, 0, 0})) ++pass; else System.out.println("FAIL test1");
        ++total; if (!sol.isOneBitCharacter(new int[]{1, 1, 1, 0})) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.isOneBitCharacter(new int[]{0, 1, 1, 0})) ++pass; else System.out.println("FAIL test3: " + sol.isOneBitCharacter(new int[]{0, 1, 1, 0}));
        ++total; if (sol.isOneBitCharacter(new int[]{0})) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.isOneBitCharacter2(new int[]{1, 0, 0})) ++pass; else System.out.println("FAIL test5");
        ++total; if (!sol.isOneBitCharacter2(new int[]{1, 1, 1, 0})) ++pass; else System.out.println("FAIL test6");
        ++total; if (sol.isOneBitCharacter2(new int[]{0})) ++pass; else System.out.println("FAIL test7");
        if (pass == total) {
            System.out.println("== 0717 PASS ==");
        } else {
            System.out.println("== 0717 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)
解法二O(n)O(1)

718. Maximum Length of Repeated Subarray(最长重复子数组)

题目描述

给两个整数数组 nums1 和 nums2,返回两个数组中公共的、长度最长的子数组的长度。

示例 1

  • 输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
  • 输出:3
  • 解释:长度最长的公共子数组是 [3,2,1] 。 提示:
  • 1 <= nums1.length, nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 100

解题思路

解法一(二维 DP,主提交):dp[i][j] 表示以 nums1[i]、nums2[j] 结尾的最长公共后缀长度。 若 nums1[i] == nums2[j] 则 dp[i][j] = dp[i-1][j-1] + 1,否则为 0;全程记录最大值。 时间复杂度 O(m·n),空间复杂度 O(m·n)(可滚动到 O(n))。 解法二(滚动数组):只保留上一行 dp,从后往前更新避免覆盖。时间复杂度 O(m·n), 空间复杂度 O(n)。

代码实现

class Solution {

    // 解法一:二维 DP
    public int findLength(int[] nums1, int[] nums2) {
        int m = nums1.length, n = nums2.length;
        int[][] dp = new int[m + 1][n + 1];
        int ans = 0;
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    ans = Math.max(ans, dp[i][j]);
                }
            }
        }
        return ans;
    }

    // 解法二:滚动数组
    public int findLength2(int[] nums1, int[] nums2) {
        int m = nums1.length, n = nums2.length;
        int[] dp = new int[n + 1];
        int ans = 0;
        for (int i = 1; i <= m; ++i) {
            for (int j = n; j >= 1; --j) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[j] = dp[j - 1] + 1;
                    ans = Math.max(ans, dp[j]);
                } else {
                    dp[j] = 0;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.findLength(new int[]{1, 2, 3, 2, 1}, new int[]{3, 2, 1, 4, 7}) == 3) ++pass; else System.out.println("FAIL test1: " + sol.findLength(new int[]{1, 2, 3, 2, 1}, new int[]{3, 2, 1, 4, 7}));
        ++total; if (sol.findLength(new int[]{0, 0, 0, 0, 0}, new int[]{0, 0, 0, 0, 0}) == 5) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.findLength(new int[]{1, 2, 3}, new int[]{4, 5, 6}) == 0) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.findLength2(new int[]{1, 2, 3, 2, 1}, new int[]{3, 2, 1, 4, 7}) == 3) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.findLength2(new int[]{0, 0, 0, 0, 0}, new int[]{0, 0, 0, 0, 0}) == 5) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.findLength2(new int[]{1, 2, 3}, new int[]{4, 5, 6}) == 0) ++pass; else System.out.println("FAIL test6");
        if (pass == total) {
            System.out.println("== 0718 PASS ==");
        } else {
            System.out.println("== 0718 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(m·n)O(m·n)
解法二O(m·n)O(n)

719. Find K-th Smallest Pair Distance(找出第 K 小的数对距离)

题目描述

数对 (a,b) 由整数 a 和 b 组成,其数对距离定义为 a 和 b 的绝对差值。 给你一个整数数组 nums 和一个整数 k,数对由 nums[i] 和 nums[j] 组成且满足 0 <= i < j < nums.length。返回所有数对距离中第 k 小的数对距离。

示例 1

  • 输入:nums = [1,3,1], k = 1
  • 输出:0
  • 解释:数对和对应的距离如下:(1,3) -> 2,(1,1) -> 0,(3,1) -> 2。距离第 1 小的数对是 (1,1) ,距离为 0 。

示例 2

  • 输入:nums = [1,1,1], k = 2
  • 输出:0

示例 3

  • 输入:nums = [1,6,1], k = 3
  • 输出:5 提示:
  • n == nums.length
  • 2 <= n <= 10^4
  • 0 <= nums[i] <= 10^6
  • 1 <= k <= n * (n - 1) / 2

解题思路

解法一(排序 + 二分答案 + 双指针计数,主提交):排序后所有数对距离在 [0, nums[n-1] - nums[0]] 之间。二分猜一个距离 mid,统计距离 <= mid 的数对个数 (双指针:对每个右端 i,左端 j 单调右移,ans += i - j);若个数 >= k 说明答案 在左半区,否则在右半区。时间复杂度 O(n log n + n log(maxDist)),空间复杂度 O(1)。 解法二(桶计数排序思路):距离最大值不超过 1e6,可开计数数组+前缀和统计,但受限于 nums 值域。附参考。

代码实现

import java.util.Arrays;

class Solution {

    // 解法一:二分答案 + 双指针计数
    public int smallestDistancePair(int[] nums, int k) {
        Arrays.sort(nums);
        int n = nums.length;
        int lo = 0, hi = nums[n - 1] - nums[0];
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            int cnt = 0;
            for (int i = 0, j = 0; i < n; ++i) {
                while (j < i && nums[i] - nums[j] > mid) {
                    ++j;
                }
                cnt += i - j;
            }
            if (cnt >= k) {
                hi = mid;
            } else {
                lo = mid + 1;
            }
        }
        return lo;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.smallestDistancePair(new int[]{1, 3, 1}, 1) == 0) ++pass; else System.out.println("FAIL test1: " + sol.smallestDistancePair(new int[]{1, 3, 1}, 1));
        ++total; if (sol.smallestDistancePair(new int[]{1, 1, 1}, 2) == 0) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.smallestDistancePair(new int[]{1, 6, 1}, 3) == 5) ++pass; else System.out.println("FAIL test3: " + sol.smallestDistancePair(new int[]{1, 6, 1}, 3));
        ++total; if (sol.smallestDistancePair(new int[]{1, 2, 3, 4}, 6) == 3) ++pass; else System.out.println("FAIL test4: " + sol.smallestDistancePair(new int[]{1, 2, 3, 4}, 6));
        ++total; if (sol.smallestDistancePair(new int[]{9, 10, 7, 10, 6, 1, 5, 4, 9, 8}, 18) == 2) ++pass; else System.out.println("FAIL test5: " + sol.smallestDistancePair(new int[]{9, 10, 7, 10, 6, 1, 5, 4, 9, 8}, 18));
        if (pass == total) {
            System.out.println("== 0719 PASS ==");
        } else {
            System.out.println("== 0719 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n log n + n log W)O(log n)(排序栈)
解法二O(n + W)O(W)

720. Longest Word in Dictionary(词典中最长的单词)

题目描述

给出一个字符串数组 words 组成的一本英语词典。返回 words 中最长的一个单词,该单词是 由 words 词典中其他单词逐步添加一个字母组成。 若其中有多个可行的答案,则返回答案中字典序最小的单词。 若无答案,则返回空字符串。

示例 1

  • 输入:words = ["w","wo","wor","worl", "world"]
  • 输出:"world"
  • 解释:单词"world"可由"w", "wo", "wor", 和 "worl"逐步添加一个字母组成。

示例 2

  • 输入:words = ["a", "banana", "app", "appl", "ap", "apply", "apple"]
  • 输出:"apple"
  • 解释:"apply" 和 "apple" 都能由词典中的单词组成。但是 "apple" 的字典序小于 "apply"。 提示:
  • 1 <= words.length <= 1000
  • 1 <= words[i].length <= 30
  • 所有输入的字符串 words[i] 都只包含小写字母

解题思路

解法一(排序 + 前缀集合校验,主提交):把单词按长度升序、同长按字典序排序;用 Set 存放所有可作为前缀的单词(空串可作起始)。遍历每个单词,若其去掉末字符的前缀在集合中, 则该单词可构成,加入集合并更新答案(保证字典序最小:同长只保留字典序小者)。 时间复杂度 O(n·L·log n),空间复杂度 O(n·L)。 解法二(Trie + DFS):把单词插入字典树,DFS 从根开始沿 isEnd 路径走,收集最长路径; 兼得前缀判断。时间复杂度 O(总字符数),空间复杂度 O(总字符数)。

代码实现

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

class Solution {

    // 解法一:排序 + 前缀集合
    public String longestWord(String[] words) {
        Arrays.sort(words, (a, b) -> a.length() != b.length() ? a.length() - b.length() : a.compareTo(b));
        Set<String> prefix = new HashSet<>();
        prefix.add("");
        String ans = "";
        for (String w : words) {
            if (prefix.contains(w.substring(0, w.length() - 1))) {
                prefix.add(w);
                if (w.length() > ans.length()) {
                    ans = w;
                }
            }
        }
        return ans;
    }

    // 解法二:Trie + DFS
    private static class TrieNode {
        TrieNode[] children = new TrieNode[26];
        boolean isEnd;
        String word;
    }

    private TrieNode root = new TrieNode();

    public String longestWord2(String[] words) {
        for (String w : words) {
            TrieNode node = root;
            for (char c : w.toCharArray()) {
                int idx = c - 'a';
                if (node.children[idx] == null) {
                    node.children[idx] = new TrieNode();
                }
                node = node.children[idx];
            }
            node.isEnd = true;
            node.word = w;
        }
        return dfs2(root);
    }

    private String dfs2(TrieNode node) {
        String best = node.word == null ? "" : node.word;
        for (TrieNode ch : node.children) {
            if (ch != null && ch.isEnd) {
                String cand = dfs2(ch);
                if (cand.length() > best.length()
                    || (cand.length() == best.length() && cand.compareTo(best) < 0)) {
                    best = cand;
                }
            }
        }
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.longestWord(new String[]{"w", "wo", "wor", "worl", "world"}).equals("world")) ++pass; else System.out.println("FAIL test1: " + sol.longestWord(new String[]{"w", "wo", "wor", "worl", "world"}));
        ++total; if (sol.longestWord(new String[]{"a", "banana", "app", "appl", "ap", "apply", "apple"}).equals("apple")) ++pass; else System.out.println("FAIL test2: " + sol.longestWord(new String[]{"a", "banana", "app", "appl", "ap", "apply", "apple"}));
        ++total; if (sol.longestWord(new String[]{"b", "br", "bre", "brea", "break", "brea"}).equals("break")) ++pass; else System.out.println("FAIL test3: " + sol.longestWord(new String[]{"b", "br", "bre", "brea", "break", "brea"}));
        ++total; if (sol.longestWord2(new String[]{"w", "wo", "wor", "worl", "world"}).equals("world")) ++pass; else System.out.println("FAIL test4");
        ++total; if (sol.longestWord2(new String[]{"a", "banana", "app", "appl", "ap", "apply", "apple"}).equals("apple")) ++pass; else System.out.println("FAIL test5");
        if (pass == total) {
            System.out.println("== 0720 PASS ==");
        } else {
            System.out.println("== 0720 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n·L·log n)O(n·L)
解法二O(总字符数)O(总字符数)

721. Accounts Merge(账户合并)

题目描述

给定一个列表 accounts,每个元素 accounts[i] 是一个字符串列表,其中第一个元素 accounts[i][0] 是名称(name),其余元素是 emails 表示该账户的邮箱地址。 现在,我们想合并这些账户。如果两个账户都有一些共同的邮箱地址,则两个账户必定属于同一个人。 请注意,即使两个账户具有相同的名称,它们也可能属于不同的人,因为人们可能具有相同的名称。 一个人最初可以拥有任意数量的账户,但其所有账户都具有相同的名称。 合并账户后,按以下格式返回账户:每个账户的第一个元素是名称,其余元素是按字符 ASCII 顺序 排序的邮箱地址。账户本身可以以任意顺序返回。

示例 1

解题思路

解法一(并查集合并邮箱,主提交):把每个邮箱映射到一个整数 id。遍历账户,把同一账户的 所有邮箱 union 到一起(用首个邮箱作为代表),并记录每个邮箱归属的人名。合并完成后,把 每个根 id 下的邮箱收集、排序、加上人名。时间复杂度 O(Σ|emails|·α + K·logK), 空间复杂度 O(邮箱数)。 解法二(DFS 构图):邮箱为节点,同一账户的邮箱彼此连边;对每个连通分量收集邮箱。

代码实现

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

class Solution {

    // 并查集
    private static class UnionFind {
        int[] parent;
        int[] rank;

        UnionFind(int n) {
            parent = new int[n];
            rank = new int[n];
            for (int i = 0; i < n; ++i) {
                parent[i] = i;
            }
        }

        int find(int x) {
            if (parent[x] != x) {
                parent[x] = find(parent[x]);
            }
            return parent[x];
        }

        void union(int a, int b) {
            int ra = find(a), rb = find(b);
            if (ra == rb) {
                return;
            }
            if (rank[ra] < rank[rb]) {
                parent[ra] = rb;
            } else if (rank[ra] > rank[rb]) {
                parent[rb] = ra;
            } else {
                parent[rb] = ra;
                ++rank[ra];
            }
        }
    }

    public List<List<String>> accountsMerge(List<List<String>> accounts) {
        Map<String, Integer> emailId = new HashMap<>();
        Map<String, String> emailName = new HashMap<>();
        UnionFind uf = new UnionFind(accounts.size() * 10 + 1);
        int id = 0;
        for (List<String> acc : accounts) {
            String name = acc.get(0);
            int firstId = -1;
            for (int i = 1; i < acc.size(); ++i) {
                String email = acc.get(i);
                if (!emailId.containsKey(email)) {
                    emailId.put(email, id);
                    emailName.put(email, name);
                    ++id;
                }
                if (firstId == -1) {
                    firstId = emailId.get(email);
                } else {
                    uf.union(firstId, emailId.get(email));
                }
            }
        }
        // 根 -> 邮箱列表
        Map<Integer, List<String>> groups = new TreeMap<>();
        for (Map.Entry<String, Integer> e : emailId.entrySet()) {
            int root = uf.find(e.getValue());
            groups.computeIfAbsent(root, k -> new ArrayList<>()).add(e.getKey());
        }
        List<List<String>> ans = new ArrayList<>();
        for (List<String> emails : groups.values()) {
            Collections.sort(emails);
            List<String> row = new ArrayList<>();
            row.add(emailName.get(emails.get(0)));
            row.addAll(emails);
            ans.add(row);
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        List<List<String>> acc1 = new ArrayList<>();
        acc1.add(Arrays.asList("John", "johnsmith@mail.com", "john00@mail.com"));
        acc1.add(Arrays.asList("John", "johnnybravo@mail.com"));
        acc1.add(Arrays.asList("John", "johnsmith@mail.com", "john_newyork@mail.com"));
        acc1.add(Arrays.asList("Mary", "mary@mail.com"));
        List<List<String>> r1 = sol.accountsMerge(acc1);
        ++total; if (r1.size() == 3) ++pass; else System.out.println("FAIL test1 size: " + r1.size());

        List<List<String>> acc2 = new ArrayList<>();
        acc2.add(Arrays.asList("a", "a@mail.com", "b@mail.com"));
        acc2.add(Arrays.asList("a", "b@mail.com", "c@mail.com"));
        List<List<String>> r2 = sol.accountsMerge(acc2);
        ++total; if (r2.size() == 1 && r2.get(0).size() == 4 && r2.get(0).get(0).equals("a")
            && r2.get(0).get(1).equals("a@mail.com") && r2.get(0).get(2).equals("b@mail.com")
            && r2.get(0).get(3).equals("c@mail.com")) ++pass; else System.out.println("FAIL test2: " + r2);

        List<List<String>> acc3 = new ArrayList<>();
        acc3.add(Arrays.asList("Gabe", "Gabe0@m.co", "Gabe3@m.co", "Gabe1@m.co"));
        acc3.add(Arrays.asList("Kevin", "Kevin3@m.co", "Kevin5@m.co", "Kevin0@m.co"));
        acc3.add(Arrays.asList("Ethan", "Ethan5@m.co", "Ethan4@m.co", "Ethan0@m.co"));
        acc3.add(Arrays.asList("Hanzo", "Hanzo3@m.co", "Hanzo1@m.co", "Hanzo0@m.co"));
        acc3.add(Arrays.asList("Fern", "Fern5@m.co", "Fern1@m.co", "Fern0@m.co"));
        List<List<String>> r3 = sol.accountsMerge(acc3);
        ++total; if (r3.size() == 5) ++pass; else System.out.println("FAIL test3 size: " + r3.size());
        if (pass == total) {
            System.out.println("== 0721 PASS ==");
        } else {
            System.out.println("== 0721 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(ΣE·α)O(E)
解法二O(ΣE)O(E)

722. Remove Comments(删除注释)

题目描述

给一个 C++ 程序,删除程序中的注释。这个程序 source 是一个数组,其中 source[i] 表示 源代码中的第 i 行。源代码表示的是由换行符分隔的多行文本。 注释有两种风格:

  • 行注释:// 和行尾之间。对于 // 之后的字符将不做处理。
  • 块注释:/* 和 *\/ 之间。块注释可以跨越多行。 你要写一个函数,输出解析后的代码。解析后的代码中,应该去掉注释、保留空白字符(空格、 制表符、换行符等)。比如 "int a; /* comment *\/" 解析后 "int a; "(注意空格被保留)。 输入的源码保证是合法的,即块注释总是闭合的,且 // 不会出现在块注释内部,/* 不会 出现在行注释或块注释内部。 提示:
  • 如果(在块注释中)出现 "*\/",它的作用只是结束块注释。块的层级结构不会嵌套。

示例 1

  • 输入:source = ["/*Test program /", "int main()", "{ ", " // variable declaration ", "int a, b, c;", "/ This is a test", " multiline ", " comment for ", " testing */", "a = b + c;", "}"]
  • 输出:["int main()", "{ ", " ", "int a, b, c;", "a = b + c;", "}"]
  • 解释:示例代码可以写成下面这样: /*Test program / // 删除注释 int main() // 保留 { // variable declaration // 行注释被删除 int a, b, c; // 保留 / This is a test // 块注释开始 multiline comment for testing */ // 块注释结束 a = b + c; // 保留 }

示例 2

  • 输入:source = ["a/comment", "line", "more_comment/b"]
  • 输出:["ab"]
  • 解释:原始的 source 字符串是 "a/*comment\nline`nmore_comment*\/b`",其中我们用粗体显示了 换行符。删除注释后,隐含的换行符被删除,留下字符串 "ab" 用换行符分隔成数组时就是 ["ab"]。 提示:
  • 1 <= source.length <= 100
  • 0 <= source[i].length <= 200
  • source[i] 中只包含 ASCII 码字符
  • 保证块注释总是正确地闭合,// 不会出现在块注释内部,/* 不会出现在行注释或块注释内部。

解题思路

解法一(状态机逐字符扫描,主提交):维护 inBlock 标志。逐行扫描:

  • 非块注释中遇到 //:跳过本行余下字符;
  • 遇到 /*:置 inBlock = true 并跳过这两个字符;
  • 块注释中遇到 *\/:置 inBlock = false 并跳过; 其余普通字符加入当前行缓冲。若一行缓冲非空,则将其加入结果(块注释产生的空行不出现在 输出中)。时间复杂度 O(总字符数),空间复杂度 O(总字符数)。 解法二(正则替换思路):把整体拼成一个字符串后用正则 (/\*.*?\*\/|//[^\n]*) 处理,再按 换行切分。.*? 非贪婪需注意,直接实现较容易出错,作为对比说明。

代码实现

import java.util.ArrayList;
import java.util.List;

class Solution {

    // 解法一:状态机
    public List<String> removeComments(String[] source) {
        List<String> ans = new ArrayList<>();
        boolean inBlock = false;
        StringBuilder cur = new StringBuilder();
        for (String line : source) {
            int i = 0;
            while (i < line.length()) {
                if (inBlock) {
                    if (i + 1 < line.length() && line.charAt(i) == '*' && line.charAt(i + 1) == '/') {
                        inBlock = false;
                        i += 2;
                    } else {
                        ++i;
                    }
                } else {
                    if (i + 1 < line.length() && line.charAt(i) == '/' && line.charAt(i + 1) == '/') {
                        i = line.length(); // 行注释,跳过余下
                    } else if (i + 1 < line.length() && line.charAt(i) == '/' && line.charAt(i + 1) == '*') {
                        inBlock = true;
                        i += 2;
                    } else {
                        cur.append(line.charAt(i));
                        ++i;
                    }
                }
            }
            if (!inBlock && cur.length() > 0) {
                ans.add(cur.toString());
                cur.setLength(0);
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        String[] src1 = {"/*Test program */", "int main()", "{ ", "  // variable declaration ",
                "int a, b, c;", "/* This is a test", "   multiline  ", "   comment for ",
                "   testing */", "a = b + c;", "}"};
        List<String> r1 = sol.removeComments(src1);
        List<String> e1 = new ArrayList<>();
        e1.add("int main()");
        e1.add("{ ");
        e1.add("  ");
        e1.add("int a, b, c;");
        e1.add("a = b + c;");
        e1.add("}");
        ++total; if (r1.equals(e1)) ++pass; else System.out.println("FAIL test1: " + r1);

        String[] src2 = {"a/*comment", "line", "more_comment*/b"};
        List<String> r2 = sol.removeComments(src2);
        ++total; if (r2.size() == 1 && r2.get(0).equals("ab")) ++pass; else System.out.println("FAIL test2: " + r2);

        String[] src3 = {"a//comment", "b"};
        List<String> r3 = sol.removeComments(src3);
        ++total; if (r3.equals(new ArrayList<>(java.util.Arrays.asList("a", "b")))) ++pass; else System.out.println("FAIL test3: " + r3);
        if (pass == total) {
            System.out.println("== 0722 PASS ==");
        } else {
            System.out.println("== 0722 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(Σsource[i])O(Σsource[i])

723. Candy Crush(粉碎糖果)

题目描述

这个问题是实现一个简单的消除算法。 给定一个 m x n 的二维整数数组 board 代表糖果所在的方格,不同的正整数 board[i][j] 代表不同种类的糖果,如果 board[i][j] == 0 代表 (i, j) 这个位置是空的。 给定的方格是玩家移动后的游戏状态,现在需要你根据以下规则粉碎糖果,使得整个方格处于稳定 状态并最终输出:

  1. 如果有三个及以上水平或者垂直相连的同种糖果,同一时间将它们粉碎,即将这些位置变成空的。
  2. 在同时粉碎掉这些糖果之后,如果有一个空的位置上方还有糖果,那么上方的糖果就会下落 直到碰到下方的糖果或者底部,这些糖果都是同时下落,也不会有新的糖果从顶部出现并落下来。
  3. 通过前两步的操作,可能又会出现可以粉碎的糖果,请继续重复前面的操作。
  4. 当不存在可以粉碎的糖果,也就是状态稳定之后,请输出最终的状态。

示例 1

  • 输入:board = [[110,5,112,113,114],[210,211,5,213,214],[310,311,3,313,314], [410,411,412,5,414],[5,1,512,3,3],[610,4,1,613,614], [710,1,2,713,714],[810,1,2,1,1],[1,1,2,2,2],[4,1,4,4,1014]]
  • 输出: [[0,0,0,0,0],[0,0,0,0,0],[0,0,0,0,0],[110,0,0,0,114],[210,0,0,0,214], [310,0,0,113,314],[410,0,0,213,414],[610,211,112,313,614],[710,311,412,613,714], [810,411,512,713,1014]]

示例 2

  • 输入:board = [[1,3,5,5,2],[3,4,3,3,1],[3,2,4,5,2],[2,4,4,5,5],[1,4,4,1,1]]
  • 输出: [[1,3,0,0,0],[3,4,0,5,2],[3,2,0,3,1],[2,4,0,5,2],[1,4,3,1,1]] 提示:
  • m == board.length
  • n == board[i].length
  • 3 <= m, n <= 50
  • 1 <= board[i][j] <= 2000

解题思路

解法一(标记 + 下落循环,主提交):循环执行两阶段直到没有可消除的糖果:

  1. 标记:对每个格子向右检查是否连续 3 个同值,向下检查是否连续 3 个同值;满足则把对应 位置取绝对值标记为负数(用绝对值保留原值,避免一列同时数行与列);
  2. 消除下落:把负值的格子置 0,然后逐列从底向上收集非 0 值,重新从底部填充,顶部补 0。 时间复杂度 O((m·n)²)(最坏需多轮),空间复杂度 O(1)(就地)。 解法二(每轮先整体标记到新矩阵再下落):思路一致,使用额外布尔矩阵标记待消除位,便于 理解;空间 O(m·n)。

代码实现

import java.util.Arrays;

class Solution {

    // 解法一:标记 + 就地下落
    public int[][] candyCrush(int[][] board) {
        int m = board.length, n = board[0].length;
        while (true) {
            boolean changed = false;
            // 1. 标记
            for (int i = 0; i < m; ++i) {
                for (int j = 0; j < n; ++j) {
                    if (board[i][j] == 0) {
                        continue;
                    }
                    int val = Math.abs(board[i][j]);
                    // 横向:右连续 >=3
                    if (j + 2 < n && Math.abs(board[i][j + 1]) == val && Math.abs(board[i][j + 2]) == val) {
                        for (int k = 0; j + k < n && Math.abs(board[i][j + k]) == val; ++k) {
                            board[i][j + k] = -val;
                        }
                        changed = true;
                    }
                    // 纵向:下连续 >=3
                    if (i + 2 < m && Math.abs(board[i + 1][j]) == val && Math.abs(board[i + 2][j]) == val) {
                        for (int k = 0; i + k < m && Math.abs(board[i + k][j]) == val; ++k) {
                            board[i + k][j] = -val;
                        }
                        changed = true;
                    }
                }
            }
            if (!changed) {
                break;
            }
            // 2. 消除与下落
            for (int j = 0; j < n; ++j) {
                int w = m - 1;
                for (int i = m - 1; i >= 0; --i) {
                    if (board[i][j] > 0) {
                        board[w--][j] = board[i][j];
                    }
                }
                while (w >= 0) {
                    board[w--][j] = 0;
                }
            }
        }
        return board;
    }

    // 解法二:布尔标记矩阵版
    public int[][] candyCrush2(int[][] board) {
        int m = board.length, n = board[0].length;
        while (true) {
            boolean[][] mark = new boolean[m][n];
            boolean changed = false;
            for (int i = 0; i < m; ++i) {
                for (int j = 0; j < n; ++j) {
                    if (board[i][j] == 0) {
                        continue;
                    }
                    int v = board[i][j];
                    if (j + 2 < n && board[i][j + 1] == v && board[i][j + 2] == v) {
                        mark[i][j] = mark[i][j + 1] = mark[i][j + 2] = true;
                        changed = true;
                    }
                    if (i + 2 < m && board[i + 1][j] == v && board[i + 2][j] == v) {
                        mark[i][j] = mark[i + 1][j] = mark[i + 2][j] = true;
                        changed = true;
                    }
                }
            }
            if (!changed) {
                break;
            }
            for (int j = 0; j < n; ++j) {
                int w = m - 1;
                for (int i = m - 1; i >= 0; --i) {
                    if (!mark[i][j]) {
                        board[w--][j] = board[i][j];
                    }
                }
                while (w >= 0) {
                    board[w--][j] = 0;
                }
            }
        }
        return board;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        int[][] b1 = {
            {110, 5, 112, 113, 114}, {210, 211, 5, 213, 214}, {310, 311, 3, 313, 314},
            {410, 411, 412, 5, 414}, {5, 1, 512, 3, 3}, {610, 4, 1, 613, 614},
            {710, 1, 2, 713, 714}, {810, 1, 2, 1, 1}, {1, 1, 2, 2, 2}, {4, 1, 4, 4, 1014}};
        int[][] e1 = {
            {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {0, 0, 0, 0, 0}, {110, 0, 0, 0, 114},
            {210, 0, 0, 0, 214}, {310, 0, 0, 113, 314}, {410, 0, 0, 213, 414},
            {610, 211, 112, 313, 614}, {710, 311, 412, 613, 714}, {810, 411, 512, 713, 1014}};
        int[][] r1 = sol.candyCrush(b1);
        ++total; if (Arrays.deepEquals(r1, e1)) ++pass; else System.out.println("FAIL test1: " + Arrays.deepToString(r1));

        int[][] b2 = {{1, 3, 5, 5, 2}, {3, 4, 3, 3, 1}, {3, 2, 4, 5, 2}, {2, 4, 4, 5, 5}, {1, 4, 4, 1, 1}};
        int[][] e2 = {{1, 3, 0, 0, 0}, {3, 4, 0, 5, 2}, {3, 2, 0, 3, 1}, {2, 4, 0, 5, 2}, {1, 4, 3, 1, 1}};
        int[][] r2 = sol.candyCrush2(b2);
        ++total; if (Arrays.deepEquals(r2, e2)) ++pass; else System.out.println("FAIL test2: " + Arrays.deepToString(r2));

        int[][] b3 = {{1, 1, 1}, {2, 2, 2}, {3, 3, 3}};
        int[][] r3 = sol.candyCrush(b3);
        ++total; if (Arrays.deepEquals(r3, new int[][]{{0, 0, 0}, {0, 0, 0}, {0, 0, 0}})) ++pass; else System.out.println("FAIL test3: " + Arrays.deepToString(r3));
        if (pass == total) {
            System.out.println("== 0723 PASS ==");
        } else {
            System.out.println("== 0723 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O((m·n)²)(最坏)O(1)
解法二O((m·n)²)(最坏)O(m·n)

724. Find Pivot Index(寻找数组的中心下标)

题目描述

给你一个整数数组 nums,请计算数组的中心下标。 数组中心下标是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。 如果中心下标位于数组最左端,那么左侧数之和视为 0,因为在下标的左侧不存在元素。这一点 对于中心下标位于数组最右端同样适用。 如果数组有多个中心下标,应该返回最靠近左边的那一个。如果数组不存在中心下标,返回 -1。

示例 1

  • 输入:nums = [1, 7, 3, 6, 5, 6]
  • 输出:3
  • 解释:中心下标是 3 。左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11, 右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11,二者相等。

示例 2

  • 输入:nums = [1, 2, 3]
  • 输出:-1
  • 解释:数组中不存在满足此条件的中心下标。

示例 3

  • 输入:nums = [2, 1, -1]
  • 输出:0
  • 解释:中心下标是 0 。左侧数之和 sum = 0 ,(下标 0 左侧不存在元素),右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 0 提示:
  • 1 <= nums.length <= 10^4
  • -1000 <= nums[i] <= 1000

解题思路

解法一(前缀和,主提交):计算出数组总和 total;从左到右维护左侧和 lt,当 2 * lt == total - nums[i](即 lt == 右侧和)时返回 i。时间复杂度 O(n), 空间复杂度 O(1)。 解法二(前缀和数组):先建前缀和数组再枚举 i。O(n) 时间、O(n) 空间。

代码实现

class Solution {

    // 解法一:总和 + 单变量
    public int pivotIndex(int[] nums) {
        int total = 0;
        for (int v : nums) {
            total += v;
        }
        int lt = 0;
        for (int i = 0; i < nums.length; ++i) {
            if (2 * lt == total - nums[i]) {
                return i;
            }
            lt += nums[i];
        }
        return -1;
    }

    // 解法二:前缀和数组
    public int pivotIndex2(int[] nums) {
        int n = nums.length;
        int[] pre = new int[n + 1];
        for (int i = 0; i < n; ++i) {
            pre[i + 1] = pre[i] + nums[i];
        }
        for (int i = 0; i < n; ++i) {
            if (pre[i] == pre[n] - pre[i + 1]) {
                return i;
            }
        }
        return -1;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ++total; if (sol.pivotIndex(new int[]{1, 7, 3, 6, 5, 6}) == 3) ++pass; else System.out.println("FAIL test1: " + sol.pivotIndex(new int[]{1, 7, 3, 6, 5, 6}));
        ++total; if (sol.pivotIndex(new int[]{1, 2, 3}) == -1) ++pass; else System.out.println("FAIL test2");
        ++total; if (sol.pivotIndex(new int[]{2, 1, -1}) == 0) ++pass; else System.out.println("FAIL test3");
        ++total; if (sol.pivotIndex(new int[]{-1, -1, -1, -1, -1, 0}) == 2) ++pass; else System.out.println("FAIL test4: " + sol.pivotIndex(new int[]{-1, -1, -1, -1, -1, 0}));
        ++total; if (sol.pivotIndex2(new int[]{1, 7, 3, 6, 5, 6}) == 3) ++pass; else System.out.println("FAIL test5");
        ++total; if (sol.pivotIndex2(new int[]{1, 2, 3}) == -1) ++pass; else System.out.println("FAIL test6");
        if (pass == total) {
            System.out.println("== 0724 PASS ==");
        } else {
            System.out.println("== 0724 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(1)
解法二O(n)O(n)

725. Split Linked List in Parts(分隔链表)

题目描述

给你一个头结点为 head 的单链表和一个整数 k,请你设计一个算法将链表分隔为 k 个 连续的部分。 每部分的长度应该尽可能的相等:任意两部分的长度差距不能超过 1,这可能导致有些部分为 null。 这 k 个部分应该按照在链表中出现的顺序排列,并且排在前面的部分的长度应该大于或等于排在 后面的长度。 返回一个由上述 k 部分组成的数组。

示例 1

  • 输入:head = [1,2,3], k = 5
  • 输出:[[1],[2],[3],[],[]]
  • 解释:第一个元素输出 [1] ,第二个元素输出 [2] ,第三个元素输出 [3] ,第四个和第五个为 空 []。

示例 2

  • 输入:head = [1,2,3,4,5,6,7,8,9,10], k = 3
  • 输出:[[1,2,3,4],[5,6,7],[8,9,10]]
  • 解释:输入被分成了几个连续的部分,并且每部分的长度相差不超过 1。前面部分的长度大于等于 后面部分的长度。 提示:
  • 链表中节点的数目在范围 [0, 1000]
  • 0 <= Node.val <= 1000
  • 1 <= k <= 50

解题思路

解法一(先求长再均分,主提交):先遍历统计链表长度 n。前 n % k 段每段长 n/k + 1, 其余每段长 n/k。第二次遍历按段长切分:每段先记录段头,再前进 len - 1 步后断开。 时间复杂度 O(n),空间复杂度 O(k)(结果数组)。 解法二(长度数组预分配):先求长并预分配 parts[k] 长度,再逐个截取。思路一致, 只是把长度算在前。

代码实现

import java.util.Arrays;

class Solution {

    public 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 ListNode[] splitListToParts(ListNode head, int k) {
        int n = 0;
        ListNode cur = head;
        while (cur != null) {
            ++n;
            cur = cur.next;
        }
        int base = n / k;
        int extra = n % k;
        ListNode[] ans = new ListNode[k];
        cur = head;
        for (int i = 0; i < k; ++i) {
            int len = base + (i < extra ? 1 : 0);
            if (len == 0) {
                ans[i] = null;
                continue;
            }
            ans[i] = cur;
            for (int j = 1; j < len; ++j) {
                cur = cur.next;
            }
            ListNode next = cur.next;
            cur.next = null;
            cur = next;
        }
        return ans;
    }

    // 解法二:预分配长度
    public ListNode[] splitListToParts2(ListNode head, int k) {
        int n = 0;
        ListNode cur = head;
        while (cur != null) {
            ++n;
            cur = cur.next;
        }
        int base = n / k, extra = n % k;
        int[] lens = new int[k];
        for (int i = 0; i < k; ++i) {
            lens[i] = base + (i < extra ? 1 : 0);
        }
        ListNode[] ans = new ListNode[k];
        cur = head;
        for (int i = 0; i < k; ++i) {
            if (lens[i] == 0) {
                ans[i] = null;
                continue;
            }
            ans[i] = cur;
            for (int j = 1; j < lens[i]; ++j) {
                cur = cur.next;
            }
            ListNode next = cur.next;
            cur.next = null;
            cur = next;
        }
        return ans;
    }

    private static ListNode build(int[] arr) {
        ListNode dummy = new ListNode(0);
        ListNode cur = dummy;
        for (int v : arr) {
            cur.next = new ListNode(v);
            cur = cur.next;
        }
        return dummy.next;
    }

    private static String fmt(ListNode[] parts) {
        StringBuilder sb = new StringBuilder("[");
        for (ListNode p : parts) {
            sb.append('[');
            while (p != null) {
                sb.append(p.val);
                if (p.next != null) {
                    sb.append(',');
                }
                p = p.next;
            }
            sb.append(']');
        }
        return sb.append(']').toString();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int pass = 0, total = 0;
        ListNode[] r1 = sol.splitListToParts(build(new int[]{1, 2, 3}), 5);
        ++total; if (fmt(r1).equals("[[1][2][3][][]]")) ++pass; else System.out.println("FAIL test1: " + fmt(r1));
        ListNode[] r2 = sol.splitListToParts(build(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}), 3);
        ++total; if (fmt(r2).equals("[[1,2,3,4][5,6,7][8,9,10]]")) ++pass; else System.out.println("FAIL test2: " + fmt(r2));
        ListNode[] r3 = sol.splitListToParts2(build(new int[]{1, 2, 3, 4, 5}), 2);
        ++total; if (fmt(r3).equals("[[1,2,3][4,5]]")) ++pass; else System.out.println("FAIL test3: " + fmt(r3));
        ListNode[] r4 = sol.splitListToParts2(null, 3);
        ++total; if (fmt(r4).equals("[[][][]]")) ++pass; else System.out.println("FAIL test4: " + fmt(r4));
        if (pass == total) {
            System.out.println("== 0725 PASS ==");
        } else {
            System.out.println("== 0725 FAIL ==");
            System.exit(1);
        }
    }
}

复杂度分析

解法时间复杂度空间复杂度
解法一O(n)O(k)
解法二O(n)O(k)