LeetCode1161~1180题目解析

LeetCode 1161-1180. 题解合集

文章目录


1161. Maximum Level Sum of a Binary Tree(最大层内元素和)

题目描述

给你一个二叉树的根节点 root。设根节点位于二叉树的第 1 层,而根节点的子节点位于第 2 层,依此类推。

返回总和最大的那一层的层号 x。如果有多层的总和一样大,返回其中最小的层号 x。

示例 1

复制代码
root = [1,7,0,7,-8,null,null] → 2(第 1 层 1,第 2 层 7+0=7,第 3 层 7-8=-1)

示例 2

复制代码
root = [989,null,10250,98693,-89388,null,null,null,-32127] → 2

约束:

  • 树中节点数在 1, 10\^4 范围内;-10^5 <= Node.val <= 10^5。

解题思路

解法一:BFS 层序遍历

  1. 用队列逐层遍历,统计每层和;记录和最大的层号(同和取更小层号);
  2. 返回层号。
    复杂度:时间 O(n),空间 O(宽)。
    解法二:DFS 逐层累加
  3. 递归遍历每个节点,把 val 累加到其所在层的总和数组 levelSuml;
  4. 遍历结束后取和最大的最小层号。
    复杂度:时间 O(n),空间 O(h + 层数)。

代码实现

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

public class Solution {

    public int maxLevelSum(TreeNode root) {
        Queue<TreeNode> q = new ArrayDeque<>();
        q.offer(root);
        int maxSum = Integer.MIN_VALUE;
        int ansLevel = 1;
        int level = 1;
        while (!q.isEmpty()) {
            int size = q.size();
            int sum = 0;
            for (int i = 0; i < size; i++) {
                TreeNode cur = q.poll();
                sum += cur.val;
                if (cur.left != null) q.offer(cur.left);
                if (cur.right != null) q.offer(cur.right);
            }
            if (sum > maxSum) {
                maxSum = sum;
                ansLevel = level;
            }
            level++;
        }
        return ansLevel;
    }

    public int maxLevelSumDfs(TreeNode root) {
        List<Integer> levelSum = new ArrayList<>();
        dfs(root, 0, levelSum);
        int maxSum = Integer.MIN_VALUE, ans = 1;
        for (int i = 0; i < levelSum.size(); i++) {
            if (levelSum.get(i) > maxSum) {
                maxSum = levelSum.get(i);
                ans = i + 1;
            }
        }
        return ans;
    }

    private void dfs(TreeNode node, int depth, List<Integer> levelSum) {
        if (node == null) return;
        if (levelSum.size() == depth) levelSum.add(0);
        levelSum.set(depth, levelSum.get(depth) + node.val);
        dfs(node.left, depth + 1, levelSum);
        dfs(node.right, depth + 1, levelSum);
    }

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

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

    public static void main(String[] args) {
        Solution s = new Solution();

        TreeNode r1 = build(new Integer[]{1,7,0,7,-8});
        System.out.println("示例1 解法一: " + s.maxLevelSum(r1) + "(期望 2)");
        System.out.println("示例1 解法二: " + s.maxLevelSumDfs(r1) + "(期望 2)");

        TreeNode r2 = build(new Integer[]{989,-1,10250,98693,-89388,-1,-1,-1,-32127});
        System.out.println("示例2 解法一: " + s.maxLevelSum(r2) + "(期望 2)");
        System.out.println("示例2 解法二: " + s.maxLevelSumDfs(r2) + "(期望 2)");
    }
}

复杂度分析

复杂度:时间 O(n),空间 O(宽)。

复杂度:时间 O(n),空间 O(h + 层数)。


1162. As Far from Land as Possible(地图分析)

题目描述

你现在手里有一份大小为 n × n 的网格 grid,上面的每个单元格都用 0 和 1 标记好了。其中 0 代表海洋,1 代表陆地。

请你找出一个海洋单元格,这个海洋单元格到离它最近的陆地单元格的距离是最大的,并返回该距离。

如果网格上只有陆地或者海洋,请返回 -1。距离是曼哈顿距离:|x0 - x1| + |y0 - y1|。

示例 1

复制代码
grid = [[1,0,1],[0,0,0],[1,0,1]] → 2

示例 2

复制代码
grid = [[1,0,0],[0,0,0],[0,0,0]] → 4

约束:

  • n == grid.length;n == grid[i].length;1 <= n <= 100;grid[i][j] 不是 0 就是 1。

解题思路

解法一:多源 BFS

  1. 所有陆地单元格作为初始源点同时入队(dist = 0);
  2. BFS 向四个方向扩散,每个海洋单元格第一次被访问时的距离即其到最近陆地的距离;
  3. 取全部海洋单元格距离的最大值;若没有陆地或没有海洋返回 -1。
    复杂度:时间 O(n^2),空间 O(n^2)。
    解法二:四方向两次扫描动态规划
  4. 先初始化 dist:陆地 0,海洋为无穷大;
  5. 从上到下、从左到右扫描一遍:从左上两个方向更新 distij = min(dist, dist上+1, dist左+1);
  6. 再从下到上、从右到左扫描一遍:从右下两个方向更新;
  7. 最终 dist 即每个海洋到最近陆地的曼哈顿距离,取最大。
    复杂度:时间 O(n^2),空间 O(n^2)。

代码实现

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

public class Solution {

    public int maxDistance(int[][] grid) {
        int n = grid.length;
        int[][] dist = new int[n][n];
        for (int[] row : dist) Arrays.fill(row, -1);
        Queue<int[]> q = new ArrayDeque<>();
        int land = 0, sea = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    q.offer(new int[]{i, j});
                    dist[i][j] = 0;
                    land++;
                } else sea++;
            }
        }
        if (land == 0 || sea == 0) return -1;
        int[][] dirs = {{1,0},{-1,0},{0,1},{0,-1}};
        int ans = 0;
        while (!q.isEmpty()) {
            int[] cur = q.poll();
            for (int[] d : dirs) {
                int nr = cur[0] + d[0], nc = cur[1] + d[1];
                if (nr < 0 || nr >= n || nc < 0 || nc >= n) continue;
                if (dist[nr][nc] == -1) {
                    dist[nr][nc] = dist[cur[0]][cur[1]] + 1;
                    ans = Math.max(ans, dist[nr][nc]);
                    q.offer(new int[]{nr, nc});
                }
            }
        }
        return ans;
    }

    public int maxDistanceDp(int[][] grid) {
        int n = grid.length;
        int[][] dist = new int[n][n];
        int INF = 1000000;
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                dist[i][j] = grid[i][j] == 1 ? 0 : INF;

        // 上左两方向
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (i > 0) dist[i][j] = Math.min(dist[i][j], dist[i - 1][j] + 1);
                if (j > 0) dist[i][j] = Math.min(dist[i][j], dist[i][j - 1] + 1);
            }
        }
        // 下右两方向
        for (int i = n - 1; i >= 0; i--) {
            for (int j = n - 1; j >= 0; j--) {
                if (i + 1 < n) dist[i][j] = Math.min(dist[i][j], dist[i + 1][j] + 1);
                if (j + 1 < n) dist[i][j] = Math.min(dist[i][j], dist[i][j + 1] + 1);
            }
        }
        int ans = -1;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 0) ans = Math.max(ans, dist[i][j]);
            }
        }
        return ans >= INF ? -1 : ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        int[][] g1 = {{1,0,1},{0,0,0},{1,0,1}};
        System.out.println("示例1 解法一: " + s.maxDistance(g1) + "(期望 2)");
        System.out.println("示例1 解法二: " + s.maxDistanceDp(g1) + "(期望 2)");

        int[][] g2 = {{1,0,0},{0,0,0},{0,0,0}};
        System.out.println("示例2 解法一: " + s.maxDistance(g2) + "(期望 4)");
        System.out.println("示例2 解法二: " + s.maxDistanceDp(g2) + "(期望 4)");

        int[][] g3 = {{1,1},{1,1}};
        System.out.println("全陆地 解法一: " + s.maxDistance(g3) + "(期望 -1)");
        System.out.println("全陆地 解法二: " + s.maxDistanceDp(g3) + "(期望 -1)");
    }
}

复杂度分析

复杂度:时间 O(n^2),空间 O(n^2)。


1163. Last Substring in Lexicographical Order(按字典序排在最后的子串)

题目描述

给你一个字符串 s,找出它的所有子串并按字典序排列,返回排在最后的那个子串。

示例 1

复制代码
s = "abab" → "bab"

示例 2

复制代码
s = "leetcode" → "tcode"

约束:

  • 1 <= s.length <= 4 * 10^5;s 仅含有小写英文字符。

解题思路

解法一:双指针 + 后缀比较(最大后缀)

  1. 关键观察:字典序最大的子串必然是某个后缀(从某个位置开始到末尾),
    因为若子串不是后缀,加上后面字符只会让它字典序更大;
  2. 因此问题转化为求字典序最大的后缀;
  3. 双指针算法:i 为当前最优后缀起点,j 为候选起点,k = 已比较的长度。
  • 若 si+k == sj+k:k++;
  • 若 si+k < sj+k:i = max(i+k+1, j),j = i+1,k = 0;
  • 若 si+k > sj+k:j += k+1,k = 0;
  1. 当 j+k 到达末尾时结束,返回 s.substring(i)。
    复杂度:时间 O(n),空间 O(1)(substring 返回新串但只需一次)。
    解法二:后缀排序/暴力枚举
  2. 暴力:枚举每个起点 i,比较所有后缀取最大;比较总代价 O(n^2) 会 TLE;
  3. 优化:只比较以「最大字符出现位置」开头的后缀------因为最大后缀的第一个字符必然是
    字符串中最大的字符,且后续字符决定进一步筛选;实现为对候选起点列表逐位收缩。
    复杂度:时间 O(n * 候选数)(平均佳),最坏 O(n^2);适合小数据 or 演示。

代码实现

java 复制代码
public class Solution {

    public String lastSubstring(String s) {
        int n = s.length();
        int i = 0, j = 1, k = 0;
        while (j + k < n) {
            char ci = s.charAt(i + k);
            char cj = s.charAt(j + k);
            if (ci == cj) {
                k++;
            } else if (ci < cj) {
                // j 所在后缀更大,跳转到 j+1(跳过已证明劣于 j 的起点)
                i = Math.max(i + k + 1, j);
                j = i + 1;
                k = 0;
            } else {
                j = j + k + 1;
                k = 0;
            }
        }
        return s.substring(i);
    }

    public String lastSubstringCandidates(String s) {
        int n = s.length();
        char maxC = 0;
        for (int i = 0; i < n; i++) maxC = (char) Math.max(maxC, s.charAt(i));
        // 候选起点:字符为最大字符的位置
        java.util.List<Integer> cand = new java.util.ArrayList<>();
        for (int i = 0; i < n; i++) if (s.charAt(i) == maxC) cand.add(i);
        // 逐位扩展收缩
        int offset = 0;
        java.util.Set<Integer> remove = new java.util.HashSet<>();
        while (cand.size() > 1) {
            if (offset + 1 >= n) break;
            char best = 0;
            // 找下一位列最大的候选
            for (int idx : cand) {
                int p = idx + offset + 1;
                if (p >= n) continue;
                best = (char) Math.max(best, s.charAt(p));
            }
            remove.clear();
            for (int idx : cand) {
                int p = idx + offset + 1;
                if (p >= n) remove.add(idx);
                else if (s.charAt(p) != best) remove.add(idx);
            }
            cand.removeAll(remove);
            offset++;
        }
        return s.substring(cand.get(0));
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        System.out.println("示例1 解法一: " + s.lastSubstring("abab") + "(期望 bab)");
        System.out.println("示例1 解法二: " + s.lastSubstringCandidates("abab") + "(期望 bab)");

        System.out.println("示例2 解法一: " + s.lastSubstring("leetcode") + "(期望 tcode)");
        System.out.println("示例2 解法二: " + s.lastSubstringCandidates("leetcode") + "(期望 tcode)");

        System.out.println("全同 解法一: " + s.lastSubstring("aaaa") + "(期望 aaaa)");
        System.out.println("全同 解法二: " + s.lastSubstringCandidates("aaaa") + "(期望 aaaa)");

        System.out.println("单字符 解法一: " + s.lastSubstring("z") + "(期望 z)");
        System.out.println("单字符 解法二: " + s.lastSubstringCandidates("z") + "(期望 z)");
    }
}

复杂度分析

复杂度:时间 O(n),空间 O(1)(substring 返回新串但只需一次)。

复杂度:时间 O(n * 候选数)(平均佳),最坏 O(n^2);适合小数据 or 演示。


1164. Product Price at a Given Date(指定日期的产品价格)

题目类型:数据库(SQL)

题目描述

产品数据表: Products

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| product_id    | int     |
| new_price     | int     |
| change_date   | date    |
+---------------+---------+
(product_id, change_date) 是此表的主键(具有唯一值的列组合)。
这张表的每一行分别记录了 某产品 在某个日期 更改后 的新价格。

一开始,所有产品价格都为 10。

编写一个解决方案,找出在 2019-08-16** **所有产品的价格。

以 **任意顺序 **返回结果表。

结果格式如下例所示。

示例 1:

复制代码
**输入:**
Products 表:
+------------+-----------+-------------+
| product_id | new_price | change_date |
+------------+-----------+-------------+
| 1          | 20        | 2019-08-14  |
| 2          | 50        | 2019-08-14  |
| 1          | 30        | 2019-08-15  |
| 1          | 35        | 2019-08-16  |
| 2          | 65        | 2019-08-17  |
| 3          | 20        | 2019-08-18  |
+------------+-----------+-------------+
**输出:**
+------------+-------+
| product_id | price |
+------------+-------+
| 2          | 50    |
| 1          | 35    |
| 3          | 10    |
+------------+-------+

解题思路

思考

每个产品取 ≤ 2019 \le 2019 ≤2019- 08 08 08- 16 16 16 的最后一次改价,若无改价则默认为 10 10 10。子查询按产品聚合该日期前的最大 change_date,再连回得到价格;全部产品的去重表左连该结果,空价格补 10 10 10。

代码实现

sql 复制代码
# Write your MySQL query statement below
WITH
    T AS (SELECT DISTINCT product_id FROM Products),
    P AS (
        SELECT product_id, new_price AS price
        FROM Products
        WHERE
            (product_id, change_date) IN (
                SELECT product_id, MAX(change_date) AS change_date
                FROM Products
                WHERE change_date <= '2019-08-16'
                GROUP BY 1
            )
    )
SELECT product_id, IFNULL(price, 10) AS price
FROM
    T
    LEFT JOIN P USING (product_id);

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1165. Single-Row Keyboard(单行键盘 🔒)

题目描述

我们定制了一款特殊的键盘,所有的键都排列在一行上。

给定一个长度为 26 的字符串 keyboard,来表示键盘的布局(索引从 0 到 25)。

一开始,你的手指在索引 0 处。要输入一个字符,你必须把你的手指移动到所需字符的索引处。

手指从索引 i 移动到索引 j 所需要的时间是 |i - j|。

您需要输入一个字符串 word。写一个函数来计算用一个手指输入需要多少时间。

示例 1

复制代码
keyboard = "abcdefghijklmnopqrstuvwxyz", word = "cba" → 4

示例 2

复制代码
keyboard = "pqrstuvwxyzabcdefghijklmno", word = "leetcode" → 73

约束:

  • keyboard.length == 26(含每个小写字母一次);1 <= word.length <= 10^4;word[i] 为小写字母。

解题思路

解法一:HashMap 位置映射

  1. 遍历 keyboard,用 Map<Character, Integer> 记录每个字母的索引;
  2. 初始手指在索引 0,遍历 word 的每个字符,累加 |当前索引 - 目标索引|;
  3. 更新当前索引并继续。每次查询都是 O(1),总时间 O(26 + len(word))。
    复杂度:时间 O(n),空间 O(26)。
    解法二:int26 数组位置表
  4. 与解法一思路相同,但用 int26 数组存储每个字母的位置(char - 'a' 直接映射),
    避免装箱开销,常数更小,是工程上更推荐的做法;
  5. 逐字符累加移动距离。
    复杂度:时间 O(n),空间 O(1)(固定 26 大小的数组)。

代码实现

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

public class Solution {

    public int calculateTime(String keyboard, String word) {
        Map<Character, Integer> pos = new HashMap<>();
        for (int i = 0; i < keyboard.length(); i++) {
            pos.put(keyboard.charAt(i), i);
        }
        int cur = 0;
        int total = 0;
        for (int i = 0; i < word.length(); i++) {
            int next = pos.get(word.charAt(i));
            total += Math.abs(cur - next);
            cur = next;
        }
        return total;
    }

    public int calculateTimeArray(String keyboard, String word) {
        int[] pos = new int[26];
        for (int i = 0; i < keyboard.length(); i++) {
            pos[keyboard.charAt(i) - 'a'] = i;
        }
        int cur = 0;
        int total = 0;
        for (int i = 0; i < word.length(); i++) {
            int next = pos[word.charAt(i) - 'a'];
            total += Math.abs(cur - next);
            cur = next;
        }
        return total;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String k1 = "abcdefghijklmnopqrstuvwxyz";
        String k2 = "pqrstuvwxyzabcdefghijklmno";

        System.out.println("示例1 解法一: " + s.calculateTime(k1, "cba") + "(期望 4)");
        System.out.println("示例1 解法二: " + s.calculateTimeArray(k1, "cba") + "(期望 4)");

        System.out.println("示例2 解法一: " + s.calculateTime(k2, "leetcode") + "(期望 73)");
        System.out.println("示例2 解法二: " + s.calculateTimeArray(k2, "leetcode") + "(期望 73)");

        System.out.println("单字符 解法一: " + s.calculateTime(k1, "a") + "(期望 0)");
        System.out.println("单字符 解法二: " + s.calculateTimeArray(k1, "a") + "(期望 0)");
    }
}

复杂度分析

  1. 更新当前索引并继续。每次查询都是 O(1),总时间 O(26 + len(word))。
    复杂度:时间 O(n),空间 O(26)。
    复杂度:时间 O(n),空间 O(1)(固定 26 大小的数组)。

1166. Design File System(设计文件系统 🔒)

题目描述

你需要设计一个文件系统,你可以创建新的路径并将它们与不同的值关联。

路径的格式是一个或多个连接在一起的字符串,形式为:/ 后面跟着一个或多个小写英文字母。

例如 "/leetcode" 和 "/leetcode/problems" 是有效路径,而空字符串 "" 和 "/" 不是。

实现 FileSystem 类:

  • boolean createPath(String path, int value):创建一个新的 path 并在可能的情况下关联 value,
    返回 true;如果路径已经存在或其父路径不存在,则返回 false;
  • int get(String path):返回与 path 关联的值,如果路径不存在则返回 -1。

示例

复制代码
create("/a", 1) → true, get("/a") → 1;create("/leet", 1) → true,
create("/leet/code", 2) → true, get("/leet/code") → 2, create("/c/d", 1) → false(父目录 /c 不存在)。

约束:

  • 两个函数调用总次数 <= 10^4;2 <= path.length <= 100;1 <= value <= 10^9。

解题思路

解法一:HashMap 存储 + 父路径截断检查

  1. 用 Map<String, Integer> pathValue 保存每个已创建路径的值;
  2. createPath 时先检查路径是否已存在,再通过 lastIndexOf('/') 截取父路径,
    若父路径非空且不在 map 中则返回 false,否则创建成功;
  3. get 直接查 map,不存在返回 -1。
    复杂度:时间 O(path.length) 分摊,空间 O(路径数 * path.length)。
    解法二:逐层前缀查询
  4. createPath 时按 '/' 逐层拆分父路径并逐步拼接前缀串,检查每一层父路径是否已创建,
    只有全部存在(或父路径为空)才允许创建,逻辑更显式、易读;
  5. get 与解法一相同。
    复杂度:时间 O(层数 * path.length),空间 O(路径数 * path.length)。

代码实现

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

public class Solution {

    public static class FileSystem {
        private final Map<String, Integer> pathValue = new HashMap<>();

        public FileSystem() {
        }

        public boolean createPath(String path, int value) {
            if (pathValue.containsKey(path)) {
                return false;
            }
            int idx = path.lastIndexOf('/');
            if (idx > 0) {
                String parent = path.substring(0, idx);
                if (!pathValue.containsKey(parent)) {
                    return false;
                }
            }
            pathValue.put(path, value);
            return true;
        }

        public int get(String path) {
            Integer v = pathValue.get(path);
            return v == null ? -1 : v;
        }
    }

    public static class FileSystemTrie {
        private final Map<String, Integer> pathValue = new HashMap<>();

        public FileSystemTrie() {
        }

        public boolean createPath(String path, int value) {
            if (pathValue.containsKey(path)) {
                return false;
            }
            // 逐层检查父路径:/a/b/c 的父路径为 /a/b、/a,必须全部存在
            String prefix = "";
            String[] parts = path.substring(1).split("/");
            for (int i = 0; i < parts.length - 1; i++) {
                prefix += "/" + parts[i];
                if (!pathValue.containsKey(prefix)) {
                    return false;
                }
            }
            pathValue.put(path, value);
            return true;
        }

        public int get(String path) {
            Integer v = pathValue.get(path);
            return v == null ? -1 : v;
        }
    }

    public static void main(String[] args) {
        FileSystem fs1 = new FileSystem();
        System.out.print("示例2 解法一: ");
        System.out.print(fs1.createPath("/leet", 1) + " ");   // true
        System.out.print(fs1.createPath("/leet/code", 2) + " "); // true
        System.out.print(fs1.get("/leet/code") + " ");        // 2
        System.out.print(fs1.createPath("/c/d", 1) + " ");    // false
        System.out.println(fs1.get("/c"));                     // -1

        FileSystemTrie fs2 = new FileSystemTrie();
        System.out.print("示例2 解法二: ");
        System.out.print(fs2.createPath("/leet", 1) + " ");
        System.out.print(fs2.createPath("/leet/code", 2) + " ");
        System.out.print(fs2.get("/leet/code") + " ");
        System.out.print(fs2.createPath("/c/d", 1) + " ");
        System.out.println(fs2.get("/c"));

        FileSystem fs3 = new FileSystem();
        System.out.print("重复创建 解法一: ");
        System.out.print(fs3.createPath("/a", 1) + " ");
        System.out.println(fs3.createPath("/a", 2) + "(期望 true false)");

        System.out.print("父路径为根 解法二: ");
        FileSystemTrie fs4 = new FileSystemTrie();
        System.out.print(fs4.createPath("/x", 10) + " ");
        System.out.println(fs4.get("/x") + "(期望 true 10)");
    }
}

复杂度分析

复杂度:时间 O(path.length) 分摊,空间 O(路径数 * path.length)。

复杂度:时间 O(层数 * path.length),空间 O(路径数 * path.length)。


1167. Minimum Cost to Connect Sticks(连接木棍的最低费用 🔒)

题目描述

你有一些长度为正整数的木棍。这些长度以数组 sticks 的形式给出,sticks`i` 是第 i 个木棍的长度。

你可以通过支付 x + y 的成本将任意两个长度为 x 和 y 的木棍连接成一个木棍。

你必须连接所有的木棍,直到剩下一个木棍。返回最小成本。

示例 1

复制代码
sticks = [2,4,3] → 14(先连 2+3=5,再连 5+4=9,总 14)

示例 2

复制代码
sticks = [1,8,3,5] → 30

示例 3

复制代码
sticks = [5] → 0

约束:

  • 1 <= sticks.length <= 10^4;1 <= sticks`i` <= 10^4。

解题思路

解法一:优先队列(小顶堆),贪心每次取最短两根

  1. 每次连接时,选择当前最短的两根木棍合并,成本最小------这是经典的「合并果子 / Huffman」贪心;
  2. 把所有长度加入小顶堆,循环取出最小的两根 x、y,累加 x + y,再把 x + y 放回堆中,
    直到堆中只剩一根;
  3. 贪心正确性:先合并短棍会使它们在后续合并中被多次累加的最小化(短的在前面被合并,
    对总成本的贡献次数最少)。
    复杂度:时间 O(n log n),空间 O(n)。
    解法二:排序 + 双队列
  4. 对初始木棍排序得到队列 q1(递增),维护合并结果队列 q2(递增)。
    每轮从两个队列头部取两个最小值(即四者中的最小两个),合并后追加到 q2 尾部;
  5. 由于合并结果不递减,q2 天然有序,无需排序;每轮取最小两个只需比较队列头部,
    时间复杂度降到 O(n log n)(排序主导)但常数更小;
  6. 直到两队列总元素为 1 时结束。
    复杂度:时间 O(n log n),空间 O(n)。

代码实现

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

public class Solution {

    public int connectSticks(int[] sticks) {
        if (sticks.length <= 1) {
            return 0;
        }
        PriorityQueue<Integer> pq = new PriorityQueue<>();
        for (int s : sticks) {
            pq.offer(s);
        }
        int cost = 0;
        while (pq.size() > 1) {
            int x = pq.poll();
            int y = pq.poll();
            int sum = x + y;
            cost += sum;
            pq.offer(sum);
        }
        return cost;
    }

    public int connectSticksTwoQueue(int[] sticks) {
        if (sticks.length <= 1) {
            return 0;
        }
        Arrays.sort(sticks);
        List<Integer> q1 = new ArrayList<>(); // 原始有序队列
        List<Integer> q2 = new ArrayList<>(); // 合并结果队列(天然递增)
        for (int s : sticks) {
            q1.add(s);
        }
        int i = 0, j = 0;
        int cost = 0;
        for (int round = 0; round < sticks.length - 1; round++) {
            // 从 q1[i] 与 q2[j] 中取最小(尚未被取走的两个最小值)
            int x;
            if (j >= q2.size() || (i < q1.size() && q1.get(i) <= q2.get(j))) {
                x = q1.get(i++);
            } else {
                x = q2.get(j++);
            }
            int y;
            if (j >= q2.size() || (i < q1.size() && q1.get(i) <= q2.get(j))) {
                y = q1.get(i++);
            } else {
                y = q2.get(j++);
            }
            int sum = x + y;
            cost += sum;
            q2.add(sum);
        }
        return cost;
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        System.out.println("示例1 解法一: " + s.connectSticks(new int[]{2,4,3}) + "(期望 14)");
        System.out.println("示例1 解法二: " + s.connectSticksTwoQueue(new int[]{2,4,3}) + "(期望 14)");

        System.out.println("示例2 解法一: " + s.connectSticks(new int[]{1,8,3,5}) + "(期望 30)");
        System.out.println("示例2 解法二: " + s.connectSticksTwoQueue(new int[]{1,8,3,5}) + "(期望 30)");

        System.out.println("示例3 解法一: " + s.connectSticks(new int[]{5}) + "(期望 0)");
        System.out.println("示例3 解法二: " + s.connectSticksTwoQueue(new int[]{5}) + "(期望 0)");

        System.out.println("两棍 解法一: " + s.connectSticks(new int[]{3,4}) + "(期望 7)");
        System.out.println("两棍 解法二: " + s.connectSticksTwoQueue(new int[]{3,4}) + "(期望 7)");
    }
}

复杂度分析

复杂度:时间 O(n log n),空间 O(n)。

时间复杂度降到 O(n log n)(排序主导)但常数更小;


1168. Optimize Water Distribution in a Village(水资源分配优化 🔒)

题目描述

村里面一共有 n 栋房子。我们希望通过建造水井和铺设管道来为所有房子供水。

对于每个房子 i,有两种可选的供水方案:一种是直接在房子内建造水井,成本为 wells`i`-1;

另一种是从另一口井铺设管道引水,数组 pipes 给出了在房子间铺设管道的成本,

其中每个 pipes`j` = `house1_j`, `house2_j`, `cost_j` 代表用管道将两个房子连接在一起的成本。

请返回为所有房子都供水的最低总成本。

示例 1

复制代码
n = 3, wells = [1,2,2], pipes = [[1,2,1],[2,3,1]] → 3(房子1建井成本1,管道连 2/3 成本 1+1)

示例 2

复制代码
n = 2, wells = [1,1], pipes = [[1,2,1]] → 2

约束:

  • 2 <= n <= 10^4;wells.length == n;pipes.length <= 10^4;0 <= wells`i`, cost_j <= 10^5。

解题思路

解法一:Kruskal 最小生成树 + 虚拟水源节点

  1. 建立虚拟节点 0 表示「水源」,把每口井当成 0 → i+1 的边,边权 = wellsi;
    原管道则按原样加入边集(两端点各 -1 转为 0 基);
  2. 问题转化为 n+1 个节点、求连接所有节点(含虚拟水源)的最小生成树,
    因为选中「井」边即代表在该房子建井,选中「管道」边即代表铺设管道;
  3. Kruskal:所有边按权重排序,并查集依次合并,统计加入的边权和。
    复杂度:时间 O(E log E)(E = n + pipes.length),空间 O(n)。
    解法二:Prim 优先队列
  4. 同样引入虚拟水源节点 0,构建邻接表(0 与每个房子 i 存在权 wellsi-1 的边);
  5. 从节点 0 出发跑 Prim,用优先队列取当前集合外最近节点,加入生成树并松弛;
  6. 直到所有 n+1 个节点都加入。
    复杂度:时间 O(E log V),空间 O(V + E)。

代码实现

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

public class Solution {

    public int minCostToSupplyWater(int n, int[] wells, int[][] pipes) {
        List<int[]> edges = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            edges.add(new int[]{0, i + 1, wells[i]}); // 井:虚拟水源 0 → 房子 i+1
        }
        for (int[] p : pipes) {
            edges.add(new int[]{p[0] - 1, p[1] - 1, p[2]}); // 管道:转为 0 基
        }
        edges.sort((a, b) -> Integer.compare(a[2], b[2]));

        int[] parent = new int[n + 1];
        for (int i = 0; i <= n; i++) {
            parent[i] = i;
        }
        int total = 0;
        int used = 0;
        for (int[] e : edges) {
            int ru = find(parent, e[0]);
            int rv = find(parent, e[1]);
            if (ru != rv) {
                parent[ru] = rv;
                total += e[2];
                used++;
                if (used == n) {
                    break; // n+1 个节点需要 n 条边
                }
            }
        }
        return total;
    }

    private int find(int[] parent, int x) {
        while (parent[x] != x) {
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }

    public int minCostToSupplyWaterPrim(int n, int[] wells, int[][] pipes) {
        List<List<int[]>> graph = new ArrayList<>();
        for (int i = 0; i <= n; i++) {
            graph.add(new ArrayList<int[]>());
        }
        for (int i = 0; i < n; i++) {
            graph.get(0).add(new int[]{i + 1, wells[i]});
            graph.get(i + 1).add(new int[]{0, wells[i]});
        }
        for (int[] p : pipes) {
            graph.get(p[0] - 1).add(new int[]{p[1] - 1, p[2]});
            graph.get(p[1] - 1).add(new int[]{p[0] - 1, p[2]});
        }
        boolean[] visited = new boolean[n + 1];
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Integer.compare(a[1], b[1]));
        pq.offer(new int[]{0, 0});
        int total = 0;
        int count = 0;
        while (!pq.isEmpty() && count <= n) {
            int[] cur = pq.poll();
            int u = cur[0];
            if (visited[u]) {
                continue;
            }
            visited[u] = true;
            total += cur[1];
            count++;
            for (int[] e : graph.get(u)) {
                if (!visited[e[0]]) {
                    pq.offer(e);
                }
            }
        }
        return total;
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        System.out.println("示例1 解法一: " + s.minCostToSupplyWater(3, new int[]{1,2,2}, new int[][]{{1,2,1},{2,3,1}}) + "(期望 3)");
        System.out.println("示例1 解法二: " + s.minCostToSupplyWaterPrim(3, new int[]{1,2,2}, new int[][]{{1,2,1},{2,3,1}}) + "(期望 3)");

        System.out.println("示例2 解法一: " + s.minCostToSupplyWater(2, new int[]{1,1}, new int[][]{{1,2,1}}) + "(期望 2)");
        System.out.println("示例2 解法二: " + s.minCostToSupplyWaterPrim(2, new int[]{1,1}, new int[][]{{1,2,1}}) + "(期望 2)");

        System.out.println("井下矿不铺管 解法一: " + s.minCostToSupplyWater(3, new int[]{1,1,1}, new int[][]{}) + "(期望 3)");
        System.out.println("井下矿不铺管 解法二: " + s.minCostToSupplyWaterPrim(3, new int[]{1,1,1}, new int[][]{}) + "(期望 3)");

        // 直接打印一次 find 调用验证(无报错即可)
        int res = s.minCostToSupplyWater(4, new int[]{5,3,5,4}, new int[][]{{1,2,2},{1,3,4},{2,4,3}});
        int res2 = s.minCostToSupplyWaterPrim(4, new int[]{5,3,5,4}, new int[][]{{1,2,2},{1,3,4},{2,4,3}});
        System.out.println("自建用例 解法一/二: " + res + " / " + res2 + "(期望 9 / 9)");
    }
}

复杂度分析

复杂度:时间 O(E log E)(E = n + pipes.length),空间 O(n)。

复杂度:时间 O(E log V),空间 O(V + E)。


1169. Invalid Transactions(查询无效交易)

题目描述

如果出现下述两种情况,交易可能无效:

  1. 交易金额超过 $1000;
  2. 或者,它和另一个城市中同名的一笔交易相隔不超过 60 分钟(包含 60 分钟整)。
    给定字符串数组交易清单 transactions,每个交易字符串由一些用逗号分隔的值组成,
    这些值分别表示交易的名称、时间(以分钟计)、金额以及城市。
    返回可能无效的交易列表,可以按任何顺序返回答案。

示例 1

复制代码
["alice,20,800,mtv","alice,50,100,beijing"] → 两笔都无效(同名、隔 30 分钟、不同城市)

示例 2

复制代码
["alice,20,800,mtv","alice,50,1200,mtv"] → 仅第二笔无效(金额 1200 > 1000)

示例 3

复制代码
["alice,20,800,mtv","bob,50,1200,mtv"] → 仅第二笔无效

约束:

  • transactions.length <= 1000;时间 0~1000;金额 0~2000。

解题思路

解法一:暴力双重循环

  1. 先把每个交易串解析成 Transaction{name, time, amount, city};
  2. 对每个交易 i,先检查 amount > 1000 标记无效;再遍历所有其他交易 j,
    若 name 相同、城市不同、|time_i - time_j| <= 60 则两条都标记无效;
  3. 收集所有标记为无效的原字符串返回。
    复杂度:时间 O(n^2),空间 O(n)。n <= 1000 时完全可行。
    解法二:按姓名分组 + 排序滑动窗口
  4. 按 name 分组,组内按 time 排序;
  5. 对同一人的交易,用滑动窗口维护「时间差 <= 60」的窗口,窗口内若城市不同则互相标记无效;
  6. 金额 > 1000 单独标记。组间互不影响,避免 O(n^2) 全量比较(组内仍可能 O(k^2),但均摊更优)。
    复杂度:时间 O(n log n)(排序主导),空间 O(n)。

代码实现

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

public class Solution {

    static class Transaction {
        String raw;
        String name;
        int time;
        int amount;
        String city;
        boolean invalid;

        Transaction(String raw) {
            this.raw = raw;
            String[] p = raw.split(",");
            name = p[0];
            time = Integer.parseInt(p[1]);
            amount = Integer.parseInt(p[2]);
            city = p[3];
        }
    }

    public List<String> invalidTransactions(String[] transactions) {
        int n = transactions.length;
        Transaction[] tx = new Transaction[n];
        for (int i = 0; i < n; i++) {
            tx[i] = new Transaction(transactions[i]);
        }
        for (int i = 0; i < n; i++) {
            if (tx[i].amount > 1000) {
                tx[i].invalid = true;
            }
            for (int j = i + 1; j < n; j++) {
                if (tx[i].name.equals(tx[j].name)
                        && !tx[i].city.equals(tx[j].city)
                        && Math.abs(tx[i].time - tx[j].time) <= 60) {
                    tx[i].invalid = true;
                    tx[j].invalid = true;
                }
            }
        }
        List<String> res = new ArrayList<>();
        for (Transaction t : tx) {
            if (t.invalid) {
                res.add(t.raw);
            }
        }
        return res;
    }

    public List<String> invalidTransactionsGroup(String[] transactions) {
        int n = transactions.length;
        Transaction[] tx = new Transaction[n];
        Map<String, List<Integer>> byName = new HashMap<>();
        for (int i = 0; i < n; i++) {
            tx[i] = new Transaction(transactions[i]);
            if (tx[i].amount > 1000) {
                tx[i].invalid = true;
            }
            byName.computeIfAbsent(tx[i].name, k -> new ArrayList<>()).add(i);
        }
        for (List<Integer> list : byName.values()) {
            // 按时间排序
            Integer[] arr = list.toArray(new Integer[0]);
            Arrays.sort(arr, (a, b) -> Integer.compare(tx[a].time, tx[b].time));
            // 滑动窗口:维护时间差 <= 60 的一组,检查城市是否不同
            int left = 0;
            for (int right = 0; right < arr.length; right++) {
                while (tx[arr[right]].time - tx[arr[left]].time > 60) {
                    left++;
                }
                // [left, right] 时间段内都可能与 right 同人不同城市
                boolean dup = false;
                for (int k = left; k < right; k++) {
                    if (!tx[arr[k]].city.equals(tx[arr[right]].city)) {
                        dup = true;
                        break;
                    }
                }
                if (dup) {
                    tx[arr[right]].invalid = true;
                    // 窗口内所有与 right 不同城市的也都要标记
                    for (int k = left; k < right; k++) {
                        if (!tx[arr[k]].city.equals(tx[arr[right]].city)) {
                            tx[arr[k]].invalid = true;
                        }
                    }
                }
            }
        }
        List<String> res = new ArrayList<>();
        for (Transaction t : tx) {
            if (t.invalid) {
                res.add(t.raw);
            }
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        List<String> r1 = s.invalidTransactions(new String[]{"alice,20,800,mtv", "alice,50,100,beijing"});
        java.util.Collections.sort(r1);
        System.out.println("示例1 解法一: " + r1 + "(期望 [alice,20,800,mtv, alice,50,100,beijing])");
        List<String> r1b = s.invalidTransactionsGroup(new String[]{"alice,20,800,mtv", "alice,50,100,beijing"});
        java.util.Collections.sort(r1b);
        System.out.println("示例1 解法二: " + r1b + "(期望同上)");

        List<String> r2 = s.invalidTransactions(new String[]{"alice,20,800,mtv", "alice,50,1200,mtv"});
        System.out.println("示例2 解法一: " + r2 + "(期望 [alice,50,1200,mtv])");
        List<String> r2b = s.invalidTransactionsGroup(new String[]{"alice,20,800,mtv", "alice,50,1200,mtv"});
        System.out.println("示例2 解法二: " + r2b + "(期望同上)");

        List<String> r3 = s.invalidTransactions(new String[]{"alice,20,800,mtv", "bob,50,1200,mtv"});
        System.out.println("示例3 解法一: " + r3 + "(期望 [bob,50,1200,mtv])");
        List<String> r3b = s.invalidTransactionsGroup(new String[]{"alice,20,800,mtv", "bob,50,1200,mtv"});
        System.out.println("示例3 解法二: " + r3b + "(期望同上)");
    }
}

复杂度分析

复杂度:时间 O(n^2),空间 O(n)。n <= 1000 时完全可行。

复杂度:时间 O(n log n)(排序主导),空间 O(n)。


1170. Compare Strings by Frequency of the Smallest Character(比较字符串最小字母出现频次)

题目描述

定义一个函数 f(s),统计 s 中(按字典序比较)最小字母的出现频次,其中 s 是一个非空字符串。

例如,若 s = "dcce",那么 f(s) = 2,因为字典序最小字母是 "c",它出现了 2 次。

现在,给你两个字符串数组待查表 queries 和词汇表 words。对于每次查询 queries`i`,

需统计 words 中满足 f(queries`i`) < f(W) 的词的数目。

请你返回一个整数数组 answer 作为答案。

示例 1

复制代码
queries = ["cbd"], words = ["zaaaz"] → [1](f("cbd")=1 < f("zaaaz")=3)

示例 2

复制代码
queries = ["bbb","cc"], words = ["a","aa","aaa","aaaa"] → [1,2]

约束:

  • 1 <= queries.length, words.length <= 2000;1 <= 字符串长度 <= 10。

解题思路

解法一:频次桶 + 后缀累计

  1. f(s) 的取值范围为 1...10(字符串最长 10),统计 f(s) 的频次桶 freq11;
  2. 对桶做后缀累计 greateri = 有多少个词满足 f(W) > i(即 freqi+1...10 之和);
  3. 每个 query 计算 f(q) 后直接 O(1) 取 greaterf(q)。
    复杂度:时间 O(n * L + m * L),空间 O(1)。
    解法二:排序 + 二分查找
  4. 先计算所有 words 的 f 值并升序排序;
  5. 对每个 query 计算 f(q),用二分查找第一个 > f(q) 的位置(upperBound),
    其后元素个数即为答案;
  6. 逻辑直观,时间复杂度 O((n + m) log n)。
    复杂度:时间 O(n log n + m log n),空间 O(n)。

代码实现

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

public class Solution {

    private int f(String s) {
        char minC = s.charAt(0);
        int cnt = 1;
        for (int i = 1; i < s.length(); i++) {
            char c = s.charAt(i);
            if (c < minC) {
                minC = c;
                cnt = 1;
            } else if (c == minC) {
                cnt++;
            }
        }
        return cnt;
    }

    public int[] numSmallerByFrequency(String[] queries, String[] words) {
        int[] freq = new int[11]; // f(W) 最大为 10
        for (String w : words) {
            freq[f(w)]++;
        }
        int[] greater = new int[11];
        int acc = 0;
        for (int i = 10; i >= 0; i--) {
            greater[i] = acc;
            acc += freq[i];
        }
        int[] res = new int[queries.length];
        for (int i = 0; i < queries.length; i++) {
            res[i] = greater[f(queries[i])];
        }
        return res;
    }

    public int[] numSmallerByFrequencySort(String[] queries, String[] words) {
        int n = words.length;
        int[] vals = new int[n];
        for (int i = 0; i < n; i++) {
            vals[i] = f(words[i]);
        }
        Arrays.sort(vals);
        int[] res = new int[queries.length];
        for (int i = 0; i < queries.length; i++) {
            int q = f(queries[i]);
            // 二分查找第一个 > q 的位置
            int lo = 0, hi = n;
            while (lo < hi) {
                int mid = (lo + hi) >>> 1;
                if (vals[mid] > q) {
                    hi = mid;
                } else {
                    lo = mid + 1;
                }
            }
            res[i] = n - lo;
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        System.out.println("示例1 解法一: " + Arrays.toString(s.numSmallerByFrequency(new String[]{"cbd"}, new String[]{"zaaaz"})) + "(期望 [1])");
        System.out.println("示例1 解法二: " + Arrays.toString(s.numSmallerByFrequencySort(new String[]{"cbd"}, new String[]{"zaaaz"})) + "(期望 [1])");

        System.out.println("示例2 解法一: " + Arrays.toString(s.numSmallerByFrequency(new String[]{"bbb","cc"}, new String[]{"a","aa","aaa","aaaa"})) + "(期望 [1,2])");
        System.out.println("示例2 解法二: " + Arrays.toString(s.numSmallerByFrequencySort(new String[]{"bbb","cc"}, new String[]{"a","aa","aaa","aaaa"})) + "(期望 [1,2])");

        System.out.println("全大 解法一: " + Arrays.toString(s.numSmallerByFrequency(new String[]{"a"}, new String[]{"zzzzzzzzzz"})) + "(期望 [1])");
        System.out.println("全大 解法二: " + Arrays.toString(s.numSmallerByFrequencySort(new String[]{"a"}, new String[]{"zzzzzzzzzz"})) + "(期望 [1])");
    }
}

复杂度分析

复杂度:时间 O(n * L + m * L),空间 O(1)。

  1. 逻辑直观,时间复杂度 O((n + m) log n)。

复杂度:时间 O(n log n + m log n),空间 O(n)。


1171. Remove Zero Sum Consecutive Nodes from Linked List(从链表中删去总和值为零的连续节点)

题目描述

给你一个链表的头节点 head,请你编写代码,反复删去链表中由总和值为 0 的连续节点组成的序列,

直到不存在这样的序列为止。删除完毕后,请你返回最终结果链表的头节点。

示例 1

复制代码
head = [1,2,-3,3,1] → [3,1](或 [1,2,1])

示例 2

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

示例 3

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

约束:

  • 1 到 1000 个节点;-1000 <= node.val <= 1000。

解题思路

解法一:前缀和 + 哈希表 <前缀和, 节点>

  1. 遍历链表累计前缀和 s。若 s 之前出现过,说明「上次出现位置的下一个节点」到「当前节点」
    这一段子链的和为 0,将其整段删除------即把 map 中该前缀和对应节点的 next 直接指向
    当前节点的 next,并在 map 中更新为当前节点;
  2. 若 s 第一次出现,将 s -> 当前节点 记入 map;
  3. 经典贪心:同一前缀和只需保留「最后一次出现的节点」,因为中间所有和为 0 的段都会被裁掉。
    复杂度:时间 O(n),空间 O(n)。
    解法二:辅助数组暴力删除
  4. 先把链表转成数组并同时计算前缀和,找到所有「首尾前缀和相等的区间」(即区间和为 0);
  5. 用布尔数组标记这些区间内所有节点为删除,最后保留未标记节点重建链表;
  6. 注意可能删除后有新的 0 和段出现,因此外层循环迭代执行,直到某轮不再发生删除。
    复杂度:时间 O(n^2)(最坏反复迭代),空间 O(n);更直观但慢,适合演示。

代码实现

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

public class Solution {

    static class ListNode {
        int val;
        ListNode next;

        ListNode(int val) {
            this.val = val;
        }
    }

    public ListNode removeZeroSumSublists(ListNode head) {
        ListNode dummy = new ListNode(0);
        dummy.next = head;
        Map<Integer, ListNode> seen = new HashMap<>();
        seen.put(0, dummy);
        int prefix = 0;
        ListNode cur = head;
        while (cur != null) {
            prefix += cur.val;
            if (seen.containsKey(prefix)) {
                // 删除 (seen.get(prefix), cur] 这段:将 map 内节点 next 指向 cur.next
                // 注意:还要把中间节点的「多余前缀和记录」从 map 中清掉,
                // 避免后续复用这些已经随片段删除的节点------重新遍历该段清理即可
                ListNode start = seen.get(prefix);
                ListNode runner = start.next;
                int tmpPrefix = prefix;
                while (runner != cur) {
                    tmpPrefix += runner.val;
                    seen.remove(Integer.valueOf(tmpPrefix));
                    runner = runner.next;
                }
                start.next = cur.next;
                // 重新登记当前前缀和指向当前节点(覆盖旧位置)
                seen.put(prefix, cur);
            } else {
                seen.put(prefix, cur);
            }
            cur = cur.next;
        }
        return dummy.next;
    }

    public ListNode removeZeroSumSublistsBrute(ListNode head) {
        // 转数组
        int[] vals = new int[1000];
        int len = 0;
        ListNode p = head;
        while (p != null) {
            vals[len++] = p.val;
            p = p.next;
        }
        while (true) {
            // 前缀和(前缀和长度 len+1:pre[0]=0,pre[i+1]=前 i+1 个元素和)
            int[] pre = new int[len + 1];
            for (int i = 0; i < len; i++) {
                pre[i + 1] = pre[i] + vals[i];
            }
            // 找首尾前缀和相等的最小区间并标记删除
            boolean[] del = new boolean[len];
            boolean found = false;
            // 优先处理最早出现、且区间最短的 0 和段:先按左端点扫描
            for (int i = 0; i < len; i++) {
                for (int j = i; j < len; j++) {
                    if (pre[j + 1] == pre[i]) {
                        // 区间 [i, j] 和为 0
                        for (int k = i; k <= j; k++) {
                            del[k] = true;
                        }
                        found = true;
                        i = j; // 跳过
                        break;
                    }
                }
            }
            if (!found) {
                break;
            }
            // 重建值数组
            int newLen = 0;
            for (int i = 0; i < len; i++) {
                if (!del[i]) {
                    vals[newLen++] = vals[i];
                }
            }
            len = newLen;
        }
        // 重建链表
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;
        for (int i = 0; i < len; i++) {
            tail.next = new ListNode(vals[i]);
            tail = tail.next;
        }
        return dummy.next;
    }

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

    private static String toStr(ListNode h) {
        StringBuilder sb = new StringBuilder();
        while (h != null) {
            sb.append(h.val).append(" ");
            h = h.next;
        }
        return sb.toString().trim();
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        System.out.println("示例1 解法一: [" + toStr(s.removeZeroSumSublists(build(new int[]{1,2,-3,3,1}))) + "](期望 3 1 或 1 2 1)");
        System.out.println("示例1 解法二: [" + toStr(s.removeZeroSumSublistsBrute(build(new int[]{1,2,-3,3,1}))) + "](期望 3 1 或 1 2 1)");

        System.out.println("示例2 解法一: [" + toStr(s.removeZeroSumSublists(build(new int[]{1,2,3,-3,4}))) + "](期望 1 2 4)");
        System.out.println("示例2 解法二: [" + toStr(s.removeZeroSumSublistsBrute(build(new int[]{1,2,3,-3,4}))) + "](期望 1 2 4)");

        System.out.println("示例3 解法一: [" + toStr(s.removeZeroSumSublists(build(new int[]{1,2,3,-3,-2}))) + "](期望 1)");
        System.out.println("示例3 解法二: [" + toStr(s.removeZeroSumSublistsBrute(build(new int[]{1,2,3,-3,-2}))) + "](期望 1)");

        System.out.println("零和整链 解法一: [" + toStr(s.removeZeroSumSublists(build(new int[]{1,-1}))) + "](期望 空)");
        System.out.println("零和整链 解法二: [" + toStr(s.removeZeroSumSublistsBrute(build(new int[]{1,-1}))) + "](期望 空)");
    }
}

复杂度分析

复杂度:时间 O(n),空间 O(n)。

复杂度:时间 O(n^2)(最坏反复迭代),空间 O(n);更直观但慢,适合演示。


1172. Dinner Plate Stacks(餐盘栈)

题目描述

我们把无限数量的栈排成一行,按从左到右的次序从 0 开始编号。每个栈的最大容量 capacity 都相同。

实现一个叫「餐盘」的类 DinnerPlates:

  • DinnerPlates(int capacity):给出栈的最大容量 capacity;
  • void push(int val):将给出的正整数 val 推入从左往右第一个没有满的栈;
  • int pop():返回从右往左第一个非空栈顶部的值,并将其从栈中删除;如果所有的栈都是空的,返回 -1;
  • int popAtStack(int index):返回编号 index 的栈顶部的值,并将其从栈中删除;如果该栈是空的,返回 -1。

示例

复制代码
capacity=2 时依次 push 1,2,3,4,5,popAtStack(0)=2, push 20, push 21, popAtStack(0)=20,
popAtStack(2)=21,随后 pop 依次得 5,4,3,1,-1。

约束:

  • 1 <= capacity <= 20000;最多 200000 次调用;index <= 100000。

解题思路

解法一:ArrayList + 两个 TreeSet 记录不满/非空栈

  1. 维护 stacks 列表保存每个栈;left 记录「未满栈」的编号(push 时取最小),
    right 记录「非空栈」的编号(pop 时取最大);
  2. push:如果 left 为空说明要新建栈;取 left.first 入栈,若该栈因此变满则从 left 移除,
    同时加入 right;
  3. pop:取 right.last,若为空则先清理;出栈后若栈变空则从 right 移除并加入 left(空栈可复用);
  4. popAtStack:直接对指定栈操作,同样维护 left/right。
    复杂度:均摊 O(log S)(S 为栈数),空间 O(总元素)。
    解法二:无 TreeSet,直接线性扫描 + 惰性清理
  5. push 时维护一个指针 leftIdx 从左向右找第一个未满栈(空位缓存);
  6. pop 时维护一个指针 rightIdx 从右向左跳过空栈;
  7. popAtStack 直接操作并清理空栈。实现简单,但最坏每一步需要 O(S) 扫描。
    复杂度:最坏时间 O(S),空间 O(总元素)。

代码实现

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

public class Solution {

    public static class DinnerPlates {
        private final int capacity;
        private final List<java.util.ArrayDeque<Integer>> stacks = new ArrayList<>();
        private final TreeSet<Integer> notFull = new TreeSet<>(); // 未满栈编号(小到大)
        private final TreeSet<Integer> nonEmpty = new TreeSet<>(); // 非空栈编号(大到小可用 last)

        public DinnerPlates(int capacity) {
            this.capacity = capacity;
        }

        public void push(int val) {
            if (notFull.isEmpty()) {
                // 新建一个栈
                stacks.add(new java.util.ArrayDeque<Integer>());
                notFull.add(stacks.size() - 1);
            }
            int idx = notFull.first();
            java.util.ArrayDeque<Integer> st = stacks.get(idx);
            st.push(val);
            nonEmpty.add(idx);
            if (st.size() == capacity) {
                notFull.remove(idx);
            }
        }

        public int pop() {
            while (!nonEmpty.isEmpty() && stacks.get(nonEmpty.last()).isEmpty()) {
                nonEmpty.remove(nonEmpty.last());
            }
            if (nonEmpty.isEmpty()) {
                return -1;
            }
            int idx = nonEmpty.last();
            java.util.ArrayDeque<Integer> st = stacks.get(idx);
            int val = st.pop();
            if (st.isEmpty()) {
                nonEmpty.remove(idx);
                notFull.add(idx);
            }
            return val;
        }

        public int popAtStack(int index) {
            if (index >= stacks.size()) {
                return -1;
            }
            java.util.ArrayDeque<Integer> st = stacks.get(index);
            if (st.isEmpty()) {
                return -1;
            }
            int val = st.pop();
            nonEmpty.add(index);
            notFull.add(index);
            if (st.isEmpty()) {
                nonEmpty.remove(index);
            }
            return val;
        }
    }

    public static class DinnerPlatesLinear {
        private final int capacity;
        private final List<java.util.ArrayDeque<Integer>> stacks = new ArrayList<>();

        public DinnerPlatesLinear(int capacity) {
            this.capacity = capacity;
        }

        public void push(int val) {
            for (int i = 0; i < stacks.size(); i++) {
                if (stacks.get(i).size() < capacity) {
                    stacks.get(i).push(val);
                    return;
                }
            }
            java.util.ArrayDeque<Integer> st = new java.util.ArrayDeque<>();
            st.push(val);
            stacks.add(st);
        }

        public int pop() {
            for (int i = stacks.size() - 1; i >= 0; i--) {
                if (!stacks.get(i).isEmpty()) {
                    int val = stacks.get(i).pop();
                    return val;
                }
            }
            return -1;
        }

        public int popAtStack(int index) {
            if (index >= stacks.size()) {
                return -1;
            }
            java.util.ArrayDeque<Integer> st = stacks.get(index);
            if (st.isEmpty()) {
                return -1;
            }
            return st.pop();
        }
    }

    public static void main(String[] args) {
        // 官方示例流程
        DinnerPlates d = new DinnerPlates(2);
        StringBuilder sb = new StringBuilder();
        d.push(1);
        d.push(2);
        d.push(3);
        d.push(4);
        d.push(5);
        sb.append("popAt(0)=").append(d.popAtStack(0)).append(" ");   // 2
        d.push(20);
        d.push(21);
        sb.append("popAt(0)=").append(d.popAtStack(0)).append(" ");   // 20
        sb.append("popAt(2)=").append(d.popAtStack(2)).append(" ");   // 21
        sb.append("pop=").append(d.pop()).append(" ");           // 5
        sb.append("pop=").append(d.pop()).append(" ");           // 4
        sb.append("pop=").append(d.pop()).append(" ");           // 3
        sb.append("pop=").append(d.pop()).append(" ");           // 1
        sb.append("pop=").append(d.pop());                        // -1
        System.out.println("解法一 示例流程: " + sb);
        System.out.println("期望: popAt(0)=2 popAt(0)=20 popAt(2)=21 pop=5 pop=4 pop=3 pop=1 pop=-1");

        DinnerPlatesLinear dl = new DinnerPlatesLinear(2);
        StringBuilder sb2 = new StringBuilder();
        dl.push(1);
        dl.push(2);
        dl.push(3);
        dl.push(4);
        dl.push(5);
        sb2.append("popAt(0)=").append(dl.popAtStack(0)).append(" "); // 2
        dl.push(20);
        dl.push(21);
        sb2.append("popAt(0)=").append(dl.popAtStack(0)).append(" "); // 20
        sb2.append("popAt(2)=").append(dl.popAtStack(2)).append(" "); // 21
        sb2.append("pop=").append(dl.pop()).append(" ");         // 5
        sb2.append("pop=").append(dl.pop()).append(" ");         // 4
        sb2.append("pop=").append(dl.pop()).append(" ");         // 3
        sb2.append("pop=").append(dl.pop()).append(" ");         // 1
        sb2.append("pop=").append(dl.pop());                      // -1
        System.out.println("解法二 示例流程: " + sb2);
        System.out.println("期望: popAt(0)=2 popAt(0)=20 popAt(2)=21 pop=5 pop=4 pop=3 pop=1 pop=-1");

        // 空栈 pop
        DinnerPlates empty = new DinnerPlates(1);
        System.out.println("空栈 pop 解法一: " + empty.pop() + "(期望 -1)");
        DinnerPlatesLinear empty2 = new DinnerPlatesLinear(1);
        System.out.println("空栈 pop 解法二: " + empty2.pop() + "(期望 -1)");
    }
}

复杂度分析

复杂度:均摊 O(log S)(S 为栈数),空间 O(总元素)。

复杂度:最坏时间 O(S),空间 O(总元素)。


1173. Immediate Food Delivery I(即时食物配送 I)

题目类型:数据库(SQL)

题目描述

配送表: Delivery

复制代码
+-----------------------------+---------+
| Column Name                 | Type    |
+-----------------------------+---------+
| delivery_id                 | int     |
| customer_id                 | int     |
| order_date                  | date    |
| customer_pref_delivery_date | date    |
+-----------------------------+---------+
delivery_id 是表的主键(具有唯一值的列)。
该表保存着顾客的食物配送信息,顾客在某个日期下了订单,并指定了一个期望的配送日期(和下单日期相同或者在那之后)。

如果顾客期望的配送日期和下单日期相同,则该订单称为 「即时订单」,否则称为「计划订单」。

编写解决方案统计即时订单所占的百分比, 保留两位小数。

返回结果如下所示。

示例 1:

复制代码
**输入:**
Delivery 表:
+-------------+-------------+------------+-----------------------------+
| delivery_id | customer_id | order_date | customer_pref_delivery_date |
+-------------+-------------+------------+-----------------------------+
| 1           | 1           | 2019-08-01 | 2019-08-02                  |
| 2           | 5           | 2019-08-02 | 2019-08-02                  |
| 3           | 1           | 2019-08-11 | 2019-08-11                  |
| 4           | 3           | 2019-08-24 | 2019-08-26                  |
| 5           | 4           | 2019-08-21 | 2019-08-22                  |
| 6           | 2           | 2019-08-11 | 2019-08-13                  |
+-------------+-------------+------------+-----------------------------+
**输出:**
+----------------------+
| immediate_percentage |
+----------------------+
| 33.33                |
+----------------------+
**解释:**2 和 3 号订单为即时订单,其他的为计划订单。

解题思路

思考

即时订单满足 order_date = customer_pref_delivery_date。布尔表达式在求和中视为 0 / 1 0/1 0/1,除以行数再乘 100 100 100 并四舍五入,即得百分比,无需先过滤再计数。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT
    ROUND(SUM(order_date = customer_pref_delivery_date) / COUNT(1) * 100, 2) AS immediate_percentage
FROM Delivery;

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1174. Immediate Food Delivery II(即时食物配送 II)

题目类型:数据库(SQL)

题目描述

配送表: Delivery

复制代码
+-----------------------------+---------+
| Column Name                 | Type    |
+-----------------------------+---------+
| delivery_id                 | int     |
| customer_id                 | int     |
| order_date                  | date    |
| customer_pref_delivery_date | date    |
+-----------------------------+---------+
delivery_id 是该表中具有唯一值的列。
该表保存着顾客的食物配送信息,顾客在某个日期下了订单,并指定了一个期望的配送日期(和下单日期相同或者在那之后)。

如果顾客期望的配送日期和下单日期相同,则该订单称为 「即时订单 」,否则称为「计划订单」。

「首次订单」是顾客最早创建的订单。我们保证一个顾客只会有一个「首次订单」。

编写解决方案以获取即时订单在所有用户的首次订单中的比例。保留两位小数。

结果示例如下所示:

示例 1:

复制代码
**输入:**
Delivery 表:
+-------------+-------------+------------+-----------------------------+
| delivery_id | customer_id | order_date | customer_pref_delivery_date |
+-------------+-------------+------------+-----------------------------+
| 1           | 1           | 2019-08-01 | 2019-08-02                  |
| 2           | 2           | 2019-08-02 | 2019-08-02                  |
| 3           | 1           | 2019-08-11 | 2019-08-12                  |
| 4           | 3           | 2019-08-24 | 2019-08-24                  |
| 5           | 3           | 2019-08-21 | 2019-08-22                  |
| 6           | 2           | 2019-08-11 | 2019-08-13                  |
| 7           | 4           | 2019-08-09 | 2019-08-09                  |
+-------------+-------------+------------+-----------------------------+
**输出:**
+----------------------+
| immediate_percentage |
+----------------------+
| 50.00                |
+----------------------+
**解释:**
1 号顾客的 1 号订单是首次订单,并且是计划订单。
2 号顾客的 2 号订单是首次订单,并且是即时订单。
3 号顾客的 5 号订单是首次订单,并且是计划订单。
4 号顾客的 7 号订单是首次订单,并且是即时订单。
因此,一半顾客的首次订单是即时的。

解题思路

思考

只统计每名顾客的第一笔订单是否即时。子查询按 customer_id 取 MIN(order_date),外层留下这些行后对「日期相等」取平均再乘 100 100 100。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT
    ROUND(AVG(order_date = customer_pref_delivery_date) * 100, 2) AS immediate_percentage
FROM Delivery
WHERE
    (customer_id, order_date) IN (
        SELECT customer_id, MIN(order_date)
        FROM Delivery
        GROUP BY 1
    );

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1175. Prime Arrangements(质数排列)

题目描述

请你帮忙给从 1 到 n 的数设计排列方案,使得所有的「质数」都应该被放在「质数索引」(索引从 1 开始)上;

返回可能的方案总数,结果对 10^9 + 7 取模。

示例 1

复制代码
n = 5 → 12([2,3,5] 三个质数放 [2,3,5] 三个质数索引,[1,4] 两个合数放两个合数索引,
3! * 2! = 12)

示例 2

复制代码
n = 100 → 682289015

约束:

  • 1 <= n <= 100。

解题思路

解法一:埃氏筛 + 阶乘取模

  1. 用埃拉托斯特尼筛法统计 1...n 中的质数个数 primeCnt,则合数个数 compositeCnt = n - primeCnt;
  2. 质数必须放在质数索引上(共 primeCnt 个位置),方案数为 primeCnt!;
    合数放在其余索引上,方案数为 compositeCnt!;答案 = primeCnt! * compositeCnt! mod 1e9+7;
  3. 阶乘计算时边乘边取模,避免溢出。
    复杂度:时间 O(n log log n),空间 O(n)。
    解法二:朴素质数判定 + 阶乘
  4. 对每个数 2...n 用「试除到 sqrt」判断是否为质数,统计个数;
  5. 同样计算阶乘乘积取模。
    复杂度:时间 O(n sqrt(n)),空间 O(1)。

代码实现

java 复制代码
public class Solution {

    private static final int MOD = 1_000_000_007;

    private long fact(int count) {
        long res = 1;
        for (int i = 2; i <= count; i++) {
            res = res * i % MOD;
        }
        return res;
    }

    public int numPrimeArrangements(int n) {
        boolean[] isPrime = new boolean[n + 1];
        for (int i = 2; i <= n; i++) {
            isPrime[i] = true;
        }
        for (int i = 2; i * i <= n; i++) {
            if (isPrime[i]) {
                for (int j = i * i; j <= n; j += i) {
                    isPrime[j] = false;
                }
            }
        }
        int primeCnt = 0;
        for (int i = 2; i <= n; i++) {
            if (isPrime[i]) {
                primeCnt++;
            }
        }
        return (int) (fact(primeCnt) * fact(n - primeCnt) % MOD);
    }

    private boolean isPrimeRaw(int x) {
        if (x < 2) {
            return false;
        }
        for (int i = 2; i * i <= x; i++) {
            if (x % i == 0) {
                return false;
            }
        }
        return true;
    }

    public int numPrimeArrangementsTrial(int n) {
        int primeCnt = 0;
        for (int i = 2; i <= n; i++) {
            if (isPrimeRaw(i)) {
                primeCnt++;
            }
        }
        return (int) (fact(primeCnt) * fact(n - primeCnt) % MOD);
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        System.out.println("示例1 解法一: " + s.numPrimeArrangements(5) + "(期望 12)");
        System.out.println("示例1 解法二: " + s.numPrimeArrangementsTrial(5) + "(期望 12)");

        System.out.println("示例2 解法一: " + s.numPrimeArrangements(100) + "(期望 682289015)");
        System.out.println("示例2 解法二: " + s.numPrimeArrangementsTrial(100) + "(期望 682289015)");

        System.out.println("n=1 解法一: " + s.numPrimeArrangements(1) + "(期望 1)");
        System.out.println("n=1 解法二: " + s.numPrimeArrangementsTrial(1) + "(期望 1)");

        System.out.println("n=2 解法一: " + s.numPrimeArrangements(2) + "(期望 1)");
        System.out.println("n=2 解法二: " + s.numPrimeArrangementsTrial(2) + "(期望 1)");
    }
}

复杂度分析

复杂度:时间 O(n log log n),空间 O(n)。

复杂度:时间 O(n sqrt(n)),空间 O(1)。


1176. Diet Plan Performance(健身计划评估 🔒)

题目描述

一个节食者在第 i 天消耗 calories`i` 卡路里。

给定一个整数 k,对于每个连续的 k 天序列,他们想要知道 T,即在这 k 天序列期间消耗的总卡路里:

  • 如果 T < lower,那么这份计划相对糟糕,并失去 1 分;
  • 如果 T > upper,那么这份计划相对优秀,并获得 1 分;
  • 否则,这份计划普普通通,分值不做变动。
    请返回统计完所有 days 后得到的总分作为评估结果。总分可能是负数。

示例 1

复制代码
calories = [1,2,3,4,5], k = 1, lower = 3, upper = 3 → 0

示例 2

复制代码
calories = [3,2], k = 2, lower = 0, upper = 1 → 1

示例 3

复制代码
calories = [6,5,0,0], k = 2, lower = 1, upper = 5 → 0

约束:

  • 1 <= k <= calories.length <= 10^5;0 <= calories`i` <= 20000;0 <= lower <= upper。

解题思路

解法一:固定窗口滑动(前缀维护)

  1. 先计算第一个窗口 [0, k) 的和,判断加分/扣分;
  2. 窗口每右移一格:sum += caloriesi - caloriesi - k,O(1) 更新;
  3. 每步与 lower / upper 比较。
    复杂度:时间 O(n),空间 O(1)。
    解法二:前缀和数组
  4. 预处理前缀和 prei+1 = prei + caloriesi;
  5. 每个窗口 i, i+k) 的和 = pre\[i+k - prei,直接 O(1) 取窗口和,再判断得分;
  6. 相比解法一思路更直白,同样时间 O(n),空间 O(n)。
    复杂度:时间 O(n),空间 O(n)。

代码实现

java 复制代码
public class Solution {

    public int dietPlanPerformance(int[] calories, int k, int lower, int upper) {
        int n = calories.length;
        int sum = 0;
        for (int i = 0; i < k; i++) {
            sum += calories[i];
        }
        int score = score(sum, lower, upper);
        for (int i = k; i < n; i++) {
            sum += calories[i] - calories[i - k];
            score += score(sum, lower, upper);
        }
        return score;
    }

    private int score(int t, int lower, int upper) {
        if (t < lower) {
            return -1;
        }
        if (t > upper) {
            return 1;
        }
        return 0;
    }

    public int dietPlanPerformancePrefix(int[] calories, int k, int lower, int upper) {
        int n = calories.length;
        int[] pre = new int[n + 1];
        for (int i = 0; i < n; i++) {
            pre[i + 1] = pre[i] + calories[i];
        }
        int total = 0;
        for (int i = 0; i + k <= n; i++) {
            int t = pre[i + k] - pre[i];
            total += score(t, lower, upper);
        }
        return total;
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        System.out.println("示例1 解法一: " + s.dietPlanPerformance(new int[]{1,2,3,4,5}, 1, 3, 3) + "(期望 0)");
        System.out.println("示例1 解法二: " + s.dietPlanPerformancePrefix(new int[]{1,2,3,4,5}, 1, 3, 3) + "(期望 0)");

        System.out.println("示例2 解法一: " + s.dietPlanPerformance(new int[]{3,2}, 2, 0, 1) + "(期望 1)");
        System.out.println("示例2 解法二: " + s.dietPlanPerformancePrefix(new int[]{3,2}, 2, 0, 1) + "(期望 1)");

        System.out.println("示例3 解法一: " + s.dietPlanPerformance(new int[]{6,5,0,0}, 2, 1, 5) + "(期望 0)");
        System.out.println("示例3 解法二: " + s.dietPlanPerformancePrefix(new int[]{6,5,0,0}, 2, 1, 5) + "(期望 0)");

        System.out.println("全低于 解法一: " + s.dietPlanPerformance(new int[]{1,1,1}, 1, 2, 3) + "(期望 -3)");
        System.out.println("全低于 解法二: " + s.dietPlanPerformancePrefix(new int[]{1,1,1}, 1, 2, 3) + "(期望 -3)");
    }
}

复杂度分析

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

  1. 相比解法一思路更直白,同样时间 O(n),空间 O(n)。

复杂度:时间 O(n),空间 O(n)。


1177. Can Make Palindrome from Substring(构建回文串检测)

题目描述

给你一个字符串 s,请你对 s 的子串进行检测。

每次检测,待检子串都可以表示为 queries`i` = `left`, `right`, `k`。

我们可以重新排列子串 s[left], ..., s[right],并从中选择最多 k 项替换成任何小写英文字母。

如果在上述检测过程中,子串可以变成回文形式的字符串,那么检测结果为 true,否则为 false。

示例

复制代码
s = "abcda", queries = [[3,3,0],[1,2,0],[0,3,1],[0,3,2],[0,4,1]]
→ 输出 [true,false,false,true,true]

约束:

  • 1 <= s.length, queries.length <= 10^5;只含小写字母。

解题思路

解法一:26 个前缀异或/前缀计数 + 奇偶性判断

  1. 前缀计数:preic 表示前 i 个字符中字母 c 出现的次数;也可用 26 个前缀「异或」记录奇偶性,
    异或前缀按下标 0...n:xori 为前 i 个字符中每个字母出现奇偶位(1 为奇);
  2. 对查询 left, right:区间奇偶掩码 = xorright+1 ^ xorleft,统计其中 1 的个数 odd;
    替换最少次数 = odd / 2(每替换一个字母可同时修好两个奇数位),
    故 odd / 2 <= k 即可变回文;
  3. 利用 int 位掩码(26 位)记录奇偶性,每个字符翻转一个位,O(1) 异或。
    复杂度:时间 O(n * 26 + m),空间 O(n)。
    解法二:前缀计数表 + 逐字母统计
  4. 保存 int26 前缀计数:cntic 为前 i 个字符中字母 c 的出现次数;
  5. 区间内字母 c 数量 = cntright+1c - cntleftc,全部 26 个字母逐个统计奇数个数;
  6. 判断 odd / 2 <= k。
    复杂度:时间 O(n * 26 + m * 26),空间 O(n * 26)。

代码实现

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

public class Solution {

    public List<Boolean> canMakePaliQueries(String s, int[][] queries) {
        int n = s.length();
        int[] xor = new int[n + 1];
        for (int i = 0; i < n; i++) {
            xor[i + 1] = xor[i] ^ (1 << (s.charAt(i) - 'a'));
        }
        List<Boolean> res = new ArrayList<>();
        for (int[] q : queries) {
            int mask = xor[q[1] + 1] ^ xor[q[0]];
            int odd = Integer.bitCount(mask);
            res.add(odd / 2 <= q[2]);
        }
        return res;
    }

    public List<Boolean> canMakePaliQueriesTable(String s, int[][] queries) {
        int n = s.length();
        int[][] cnt = new int[n + 1][26];
        for (int i = 0; i < n; i++) {
            System.arraycopy(cnt[i], 0, cnt[i + 1], 0, 26);
            cnt[i + 1][s.charAt(i) - 'a']++;
        }
        List<Boolean> res = new ArrayList<>();
        for (int[] q : queries) {
            int odd = 0;
            for (int c = 0; c < 26; c++) {
                int diff = cnt[q[1] + 1][c] - cnt[q[0]][c];
                if ((diff & 1) == 1) {
                    odd++;
                }
            }
            res.add(odd / 2 <= q[2]);
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        int[][] q1 = {{3,3,0},{1,2,0},{0,3,1},{0,3,2},{0,4,1}};
        System.out.println("示例 解法一: " + s.canMakePaliQueries("abcda", q1) + "(期望 [true,false,false,true,true])");
        System.out.println("示例 解法二: " + s.canMakePaliQueriesTable("abcda", q1) + "(期望 [true,false,false,true,true])");

        int[][] q2 = {{0,0,0}};
        System.out.println("单字符 解法一: " + s.canMakePaliQueries("ab", q2) + "(期望 [true])");
        System.out.println("单字符 解法二: " + s.canMakePaliQueriesTable("ab", q2) + "(期望 [true])");
    }
}

复杂度分析

复杂度:时间 O(n * 26 + m),空间 O(n)。

复杂度:时间 O(n * 26 + m * 26),空间 O(n * 26)。


1178. Number of Valid Words for Each Puzzle(猜字谜)

题目描述

外国友人仿照中国字谜设计了一个英文版猜字谜小游戏。

字谜的迷面 puzzle 按字符串形式给出,如果一个单词 word 符合下面两个条件,那么它就可以算作谜底:

  1. 单词 word 中包含谜面 puzzle 的第一个字母;
  2. 单词 word 中的每一个字母都可以在谜面 puzzle 中找到。
    返回一个答案数组 answer,answer`i` 是在给出的单词列表 words 中可以作为谜面 puzzles`i` 的谜底的单词数目。

示例

复制代码
words = ["aaaa","asas","able","ability","actt","actor","access"],
puzzles = ["aboveyz","abrodyz","abslute","absoryz","actresz","gaswxyz"] → [1,1,3,2,4,0]

约束:

  • 1 <= words.length <= 10^5;1 <= puzzles.length <= 10^4;puzzles`i`.length == 7;
  • 每个 puzzle 包含的字符不重复。

解题思路

解法一:word 掩码计数 + 枚举 puzzle 的子集

  1. 把每个 word 压缩成 26 位掩码(去重:只用「出现过哪些字母」),
    用 Map<Integer, Integer> 统计每个掩码的词数(同一掩码多个词合并计数);
  2. 对每个 puzzle,首字母对应位必须在 word 掩码中出现,其余字母随意(是 puzzle 子集);
    枚举 puzzle 包含首字母的 2^6 = 64 个子集掩码(不含首字母的子集不用枚举),
    直接查表累加这些子集掩码的词数;
  3. 因为 word 只需是 puzzle 的「子集 + 含首字母」,枚举所有可行子集即可精确统计。
    复杂度:时间 O(words * L + puzzles * 2^6),空间 O(2^26 数量级,实际即不同掩码数)。
    解法二:直接对每个 word 与每个 puzzle 匹配,朴素
  4. 对每个 word 生成掩码,对每个 puzzle 生成掩码;
  5. 双重循环检查 (wordMask & puzzleMask) == wordMask 且含 puzzle 首字母位;
  6. 复杂度 O(words * puzzles * 7),10^5 * 10^4 会 TLE,仅用于小数据演示。
    复杂度:时间 O(W * P),空间 O(W + P)。

代码实现

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

public class Solution {

    public List<Integer> findNumOfValidWords(String[] words, String[] puzzles) {
        Map<Integer, Integer> wordCnt = new HashMap<>();
        for (String w : words) {
            int mask = 0;
            for (int i = 0; i < w.length(); i++) {
                mask |= 1 << (w.charAt(i) - 'a');
            }
            wordCnt.put(mask, wordCnt.getOrDefault(mask, 0) + 1);
        }
        List<Integer> res = new ArrayList<>();
        for (String p : puzzles) {
            int pMask = 0;
            for (int i = 0; i < p.length(); i++) {
                pMask |= 1 << (p.charAt(i) - 'a');
            }
            int first = 1 << (p.charAt(0) - 'a');
            int total = 0;
            // 枚举所有包含首字母的子集:sub 从 first 开始,依次加入 puzzle 中其他字符
            int sub = first;
            // 遍历 puzzle 除首字母外的 6 个字符做组合
            List<Integer> others = new ArrayList<>();
            for (int i = 1; i < p.length(); i++) {
                others.add(1 << (p.charAt(i) - 'a'));
            }
            // 用迭代法枚所有「含 first 的子集」
            total += dfs(wordCnt, others, 0, first);
            res.add(total);
        }
        return res;
    }

    private int dfs(Map<Integer, Integer> wordCnt, List<Integer> others, int idx, int cur) {
        if (idx == others.size()) {
            return wordCnt.getOrDefault(cur, 0);
        }
        // 不选
        int sum = dfs(wordCnt, others, idx + 1, cur);
        // 选
        sum += dfs(wordCnt, others, idx + 1, cur | others.get(idx));
        return sum;
    }

    public List<Integer> findNumOfValidWordsBrute(String[] words, String[] puzzles) {
        int[] wm = new int[words.length];
        for (int i = 0; i < words.length; i++) {
            int m = 0;
            for (int j = 0; j < words[i].length(); j++) {
                m |= 1 << (words[i].charAt(j) - 'a');
            }
            wm[i] = m;
        }
        List<Integer> res = new ArrayList<>();
        for (String p : puzzles) {
            int pm = 0;
            for (int i = 0; i < p.length(); i++) {
                pm |= 1 << (p.charAt(i) - 'a');
            }
            int first = 1 << (p.charAt(0) - 'a');
            int cnt = 0;
            for (int m : wm) {
                if ((m & first) != 0 && (m & pm) == m) {
                    cnt++;
                }
            }
            res.add(cnt);
        }
        return res;
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        String[] words = {"aaaa", "asas", "able", "ability", "actt", "actor", "access"};
        String[] puzzles = {"aboveyz", "abrodyz", "abslute", "absoryz", "actresz", "gaswxyz"};

        System.out.println("示例 解法一: " + s.findNumOfValidWords(words, puzzles) + "(期望 [1,1,3,2,4,0])");
        System.out.println("示例 解法二: " + s.findNumOfValidWordsBrute(words, puzzles) + "(期望 [1,1,3,2,4,0])");
    }
}

复杂度分析

复杂度:时间 O(words * L + puzzles * 2^6),空间 O(2^26 数量级,实际即不同掩码数)。

复杂度:时间 O(W * P),空间 O(W + P)。


1179. Reformat Department Table(重新格式化部门表)

题目类型:数据库(SQL)

题目描述

表 Department:

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| id            | int     |
| revenue       | int     |
| month         | varchar |
+---------------+---------+
在 SQL 中,(id, month) 是表的联合主键。
这个表格有关于每个部门每月收入的信息。
月份(month)可以取下列值 ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]。

重新格式化表格,使得 **每个月 **都有一个部门 id 列和一个收入列。

以 任意顺序 返回结果表。

结果格式如以下示例所示。

示例 1:

复制代码
<b>输入:</b>
Department table:
+------+---------+-------+
| id   | revenue | month |
+------+---------+-------+
| 1    | 8000    | Jan   |
| 2    | 9000    | Jan   |
| 3    | 10000   | Feb   |
| 1    | 7000    | Feb   |
| 1    | 6000    | Mar   |
+------+---------+-------+
<b>输出:</b>
+------+-------------+-------------+-------------+-----+-------------+
| id   | Jan_Revenue | Feb_Revenue | Mar_Revenue | ... | Dec_Revenue |
+------+-------------+-------------+-------------+-----+-------------+
| 1    | 8000        | 7000        | 6000        | ... | null        |
| 2    | 9000        | null        | null        | ... | null        |
| 3    | null        | 10000       | null        | ... | null        |
+------+-------------+-------------+-------------+-----+-------------+
<b>解释:</b>四月到十二月的收入为空。 
请注意,结果表共有 13 列(1 列用于部门 ID,其余 12 列用于各个月份)。

解题思路

思考

行式的「部门---月份---收入」要展成每个部门一行、十二月各一列。按 id 分组,对每个月用 CASE 取出对应 revenue 再 SUM:每月至多一行,SUM 即该月收入,无记录则为空。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT
    id,
    SUM(
        CASE month
            WHEN 'Jan' THEN revenue
        END
    ) AS Jan_Revenue,
    SUM(
        CASE month
            WHEN 'Feb' THEN revenue
        END
    ) AS Feb_Revenue,
    SUM(
        CASE month
            WHEN 'Mar' THEN revenue
        END
    ) AS Mar_Revenue,
    SUM(
        CASE month
            WHEN 'Apr' THEN revenue
        END
    ) AS Apr_Revenue,
    SUM(
        CASE month
            WHEN 'May' THEN revenue
        END
    ) AS May_Revenue,
    SUM(
        CASE month
            WHEN 'Jun' THEN revenue
        END
    ) AS Jun_Revenue,
    SUM(
        CASE month
            WHEN 'Jul' THEN revenue
        END
    ) AS Jul_Revenue,
    SUM(
        CASE month
            WHEN 'Aug' THEN revenue
        END
    ) AS Aug_Revenue,
    SUM(
        CASE month
            WHEN 'Sep' THEN revenue
        END
    ) AS Sep_Revenue,
    SUM(
        CASE month
            WHEN 'Oct' THEN revenue
        END
    ) AS Oct_Revenue,
    SUM(
        CASE month
            WHEN 'Nov' THEN revenue
        END
    ) AS Nov_Revenue,
    SUM(
        CASE month
            WHEN 'Dec' THEN revenue
        END
    ) AS Dec_Revenue
FROM Department
GROUP BY 1;

复杂度分析

本题为数据库查询题,不涉及传统算法的时间/空间复杂度,执行效率由数据库优化器决定;建议对查询涉及的过滤列与连接列建立索引以加速。


1180. Count Substrings with Only One Distinct Letter(统计只含单一字母的子串 🔒)

题目描述

给你一个字符串 s,返回只含单一字母的子串个数。

示例 1

复制代码
s = "aaaba" → 8("aaa" 1 次 + "aa" 2 次 + "a" 4 次 + "b" 1 次)

示例 2

复制代码
s = "aaaaaaaaaa" → 55(10 * 11 / 2)

约束:

  • 1 <= s.length <= 1000;s[i] 仅由小写英文字母组成。

解题思路

解法一:连续段分组计数

  1. 扫描字符串,把连续相同的字母分成一段,段长为 len;
  2. 该段内所有「纯单一字母」子串个数 = len * (len + 1) / 2(起点 × 终点组合数);
  3. 各段累加即答案。
    复杂度:时间 O(n),空间 O(1)。
    解法二:逐位扩展(暴力)
  4. 对每个起点 i,向右扩展 j 直到 sj != si,每扩展一步就是一个合法子串;
  5. 统计总数。三重思路直白,但最坏 O(n^2),适合 N 小的情况。
    复杂度:时间 O(n^2),空间 O(1)。

代码实现

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

public class Solution {

    public int countLetters(String s) {
        int ans = 0;
        int i = 0;
        int n = s.length();
        while (i < n) {
            int j = i;
            while (j < n && s.charAt(j) == s.charAt(i)) {
                j++;
            }
            int len = j - i;
            ans += len * (len + 1) / 2;
            i = j;
        }
        return ans;
    }

    public int countLettersBrute(String s) {
        int n = s.length();
        int ans = 0;
        for (int i = 0; i < n; i++) {
            for (int j = i; j < n && s.charAt(j) == s.charAt(i); j++) {
                ans++;
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution s = new Solution();

        System.out.println("示例1 解法一: " + s.countLetters("aaaba") + "(期望 8)");
        System.out.println("示例1 解法二: " + s.countLettersBrute("aaaba") + "(期望 8)");

        System.out.println("示例2 解法一: " + s.countLetters("aaaaaaaaaa") + "(期望 55)");
        System.out.println("示例2 解法二: " + s.countLettersBrute("aaaaaaaaaa") + "(期望 55)");

        System.out.println("交错 解法一: " + s.countLetters("abab") + "(期望 4)");
        System.out.println("交错 解法二: " + s.countLettersBrute("abab") + "(期望 4)");
    }
}

复杂度分析

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

复杂度:时间 O(n^2),空间 O(1)。

相关推荐
古法老登瞎折腾9 小时前
大数据智能运维实战(十一):运维 MCP 的工程化——权限、限流、审计与错误处理
java·大数据·运维·spring ai·mcp
智能运维指南10 小时前
研制过程“两张皮“怎么破:研制一体化支撑平台的定位、能力边界与落地前提
java·前端·人工智能·嘉为蓝鲸
yz_aiks11 小时前
Spring AI 2.1 ChatMemory 已过时用什么替代:迁移到 Session Memory 实战(JDBC 存储与会话压缩)
java·spring boot·大模型·ai agent·spring ai
金銀銅鐵11 小时前
[Java] 用 GUI 展示 EnumMap 增删键值对时的内部变化
java·后端·ai编程
学长毕设11 小时前
基于SpringBoot的上门洗车服务系统设计与实现
java·spring boot·后端
jason成都11 小时前
RTKLIB-Java 多基线联合RTK:FIX率从0%到91.5%的三层根因修复
java·gnss·rtklib·rtklib_java
鱼宵11 小时前
Stream 终端操作一次讲透:collect、count、reduce 怎么选
java·stream·终端操作
一去不复返的通信er11 小时前
贪心算法-合并区间(LeetCode 56)
算法·leetcode·贪心算法
白山编程大哥11 小时前
Java 后端如何优雅地把 List 集合返回给前端
java·前端·list