LeetCode1581~1600题目解析

LeetCode 1581-1600. 题解合集

文章目录


1581. Customer Who Visited but Did Not Make Any Transactions(进店却未进行过交易的顾客)

题目类型:数据库(SQL)

题目描述

表:Visits

复制代码
+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| visit_id    | int     |
| customer_id | int     |
+-------------+---------+
visit_id 是该表中具有唯一值的列。
该表包含有关光临过购物中心的顾客的信息。

表:Transactions

复制代码
+----------------+---------+
| Column Name    | Type    |
+----------------+---------+
| transaction_id | int     |
| visit_id       | int     |
| amount         | int     |
+----------------+---------+
transaction_id 是该表中具有唯一值的列。
此表包含 visit_id 期间进行的交易的信息。

有一些顾客可能光顾了购物中心但没有进行交易。请你编写一个解决方案,来查找这些顾客的 ID ,以及他们只光顾不交易的次数。

返回以 任何顺序 排序的结果表。

返回结果格式如下例所示。

示例 1:

复制代码
`**输入:**
Visits`
+----------+-------------+
| visit_id | customer_id |
+----------+-------------+
| 1        | 23          |
| 2        | 9           |
| 4        | 30          |
| 5        | 54          |
| 6        | 96          |
| 7        | 54          |
| 8        | 54          |
+----------+-------------+
`Transactions`
+----------------+----------+--------+
| transaction_id | visit_id | amount |
+----------------+----------+--------+
| 2              | 5        | 310    |
| 3              | 5        | 300    |
| 9              | 5        | 200    |
| 12             | 1        | 910    |
| 13             | 2        | 970    |
+----------------+----------+--------+
**输出:**
+-------------+----------------+
| customer_id | count_no_trans |
+-------------+----------------+
| 54          | 2              |
| 30          | 1              |
| 96          | 1              |
+-------------+----------------+
**解释:**
ID = 23 的顾客曾经逛过一次购物中心,并在 ID = 12 的访问期间进行了一笔交易。
ID = 9 的顾客曾经逛过一次购物中心,并在 ID = 13 的访问期间进行了一笔交易。
ID = 30 的顾客曾经去过购物中心,并且没有进行任何交易。
ID = 54 的顾客三度造访了购物中心。在 2 次访问中,他们没有进行任何交易,在 1 次访问中,他们进行了 3 次交易。
ID = 96 的顾客曾经去过购物中心,并且没有进行任何交易。
如我们所见,ID 为 30 和 96 的顾客一次没有进行任何交易就去了购物中心。顾客 54 也两次访问了购物中心并且没有进行任何交易。

解题思路

思考

统计每位顾客「到访但未消费」的次数。消费记录在另一张表,需要找出没有对应交易的访问。

子查询列出所有出现在 T r a n s a c t i o n s Transactions Transactions 中的 v i s i t _ i d visit\_id visit_id,外层保留不在该集合中的访问,再按顾客计数。

考察LEFT JOIN + 分组计数:访问表左连接交易表,按客户分组统计无交易访问次数(CONDITIONAL COUNT 或 SUM(CASE...)),排除有交易的客户。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT customer_id, COUNT(1) AS count_no_trans
FROM Visits
WHERE visit_id NOT IN (SELECT visit_id FROM Transactions)
GROUP BY 1;

复杂度分析

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


1582. Special Positions in a Binary Matrix(二进制矩阵中的特殊位置)

题目描述

给定一个 m x n 的二进制矩阵 mat,返回矩阵中特殊位置的数量。

位置 (i, j) 是特殊的当 mat[i][j] == 1 且第 i 行与第 j 列中的所有其他元素都是 0。

示例 1

复制代码
mat=[[1,0,0],[0,0,1],[1,0,0]],输出 1。

示例 2

复制代码
mat=[[1,0,0],[0,1,0],[0,0,1]],输出 3。

约束:

  • 1 <= m, n <= 100。

解题思路

解法一:行和列计数(主提交版本)

  • 统计每行 1 的个数 rowCnt、每列 1 的个数 colCnt;
  • 位置 (i,j) 特殊当 matij==1 且 rowCnti==1 且 colCntj==1。
    复杂度:时间复杂度 O(mn),空间复杂度 O(m + n)。
    解法二:暴力逐个检查(对照)
  • 对每个 1 位置检查该行该列是否还有其它 1。
    复杂度:时间复杂度 O(mn * (m+n)),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    public int numSpecialCount(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        int[] row = new int[m];
        int[] col = new int[n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                row[i] += mat[i][j];
                col[j] += mat[i][j];
            }
        }
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (mat[i][j] == 1 && row[i] == 1 && col[j] == 1) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public int numSpecialBrute(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        int ans = 0;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (mat[i][j] != 1) {
                    continue;
                }
                boolean ok = true;
                for (int c = 0; c < n; c++) {
                    if (c != j && mat[i][c] == 1) {
                        ok = false;
                        break;
                    }
                }
                if (!ok) {
                    continue;
                }
                for (int r = 0; r < m; r++) {
                    if (r != i && mat[r][j] == 1) {
                        ok = false;
                        break;
                    }
                }
                if (ok) {
                    ans++;
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.numSpecialCount(new int[][]{{1, 0, 0}, {0, 0, 1}, {1, 0, 0}})); // 1
        System.out.println(sol.numSpecialCount(new int[][]{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}})); // 3
        System.out.println(sol.numSpecialBrute(new int[][]{{1, 0, 0}, {0, 0, 1}, {1, 0, 0}})); // 1
        System.out.println(sol.numSpecialBrute(new int[][]{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}})); // 3
    }
}

复杂度分析

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

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


1583. Count Unhappy Friends(统计不开心的朋友)

题目描述

给你一份 n 位朋友的亲近程度列表(n 总为偶数)。对每位朋友 i,preferences[i] 包含一份

按亲近程度从高到低排列的朋友列表。朋友被分成几对,配对情况以 pairs 给出。

在 x 与 y 配对且 u 与 v 配对的情况下,如果同时满足下述两个条件,x 就会不开心:

  1. x 与 u 的亲近程度胜过 x 与 y;
  2. u 与 x 的亲近程度胜过 u 与 v。
    返回不开心的朋友的数目。

示例 1

复制代码
n=4, preferences=[[1,2,3],[3,2,0],[3,1,0],[1,2,0]], pairs=[[0,1],[2,3]],输出 2。

约束:

  • 2 <= n <= 500;n 是偶数;每个 preferences[i] 包含除 i 外的 n-1 个朋友。

解题思路

解法一:亲近度排名表 + 枚举(主提交版本)

  • 用 rankij 表示朋友 j 在 i 的偏好列表中的排名(越小越亲近);
  • 对每对 (x,y) 和另一对 (u,v) ★:判断 x 是否不开心:
    rankxu < rankxy 且 rankux < rankuv;
    同样 u 也可能不开心(对称判断);
  • 用 boolean 数组标记不开心,最后统计。
    复杂度:时间复杂度 O(n^2)(对数固定 n/2),空间复杂度 O(n^2)。
    解法二:按配对压缩关系(对照)
  • 对每个 x,找到其配对 y,检查 x 的偏好列表中排在 y 前面的所有 u 是否也"更想和 x 在一起"。
    复杂度:时间复杂度 O(n^2),空间复杂度 O(n^2)。

代码实现

java 复制代码
public class Solution {

    public int unhappyFriendsRank(int n, int[][] preferences, int[][] pairs) {
        int[][] rank = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n - 1; j++) {
                rank[i][preferences[i][j]] = j;
            }
        }
        // match[x] = x 的配对对象
        int[] match = new int[n];
        for (int[] p : pairs) {
            match[p[0]] = p[1];
            match[p[1]] = p[0];
        }
        boolean[] unhappy = new boolean[n];
        int m = pairs.length;
        for (int i = 0; i < m; i++) {
            for (int j = i + 1; j < m; j++) {
                int x = pairs[i][0], y = pairs[i][1];
                int u = pairs[j][0], v = pairs[j][1];
                // 判断 x 是否不开心
                if (rank[x][u] < rank[x][y] && rank[u][x] < rank[u][v]) {
                    unhappy[x] = true;
                }
                if (rank[x][v] < rank[x][y] && rank[v][x] < rank[v][u]) {
                    unhappy[x] = true;
                }
                if (rank[y][u] < rank[y][x] && rank[u][y] < rank[u][v]) {
                    unhappy[y] = true;
                }
                if (rank[y][v] < rank[y][x] && rank[v][y] < rank[v][u]) {
                    unhappy[y] = true;
                }
                // 对称判断 u、v
                if (rank[u][x] < rank[u][v] && rank[x][u] < rank[x][y]) {
                    unhappy[u] = true;
                }
                if (rank[u][y] < rank[u][v] && rank[y][u] < rank[y][x]) {
                    unhappy[u] = true;
                }
                if (rank[v][x] < rank[v][u] && rank[x][v] < rank[x][y]) {
                    unhappy[v] = true;
                }
                if (rank[v][y] < rank[v][u] && rank[y][v] < rank[y][x]) {
                    unhappy[v] = true;
                }
            }
        }
        int cnt = 0;
        for (boolean b : unhappy) {
            if (b) {
                cnt++;
            }
        }
        return cnt;
    }

    public int unhappyFriendsSimple(int n, int[][] preferences, int[][] pairs) {
        int[][] rank = new int[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n - 1; j++) {
                rank[i][preferences[i][j]] = j;
            }
        }
        int[] match = new int[n];
        for (int[] p : pairs) {
            match[p[0]] = p[1];
            match[p[1]] = p[0];
        }
        int ans = 0;
        for (int x = 0; x < n; x++) {
            int y = match[x];
            // x 偏好列表中排在 y 前面的朋友
            OUTER:
            for (int idx = 0; idx < n - 1; idx++) {
                int u = preferences[x][idx];
                if (u == y) {
                    break; // 之后都不如 y,无更亲的
                }
                // u 的配对对象
                int v = match[u];
                if (rank[u][x] < rank[u][v]) {
                    ans++;
                    break; // x 已不开心
                }
            }
        }
        return ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        int[][] p1 = {{1, 2, 3}, {3, 2, 0}, {3, 1, 0}, {1, 2, 0}};
        int[][] pairs1 = {{0, 1}, {2, 3}};
        System.out.println(sol.unhappyFriendsRank(4, p1, pairs1)); // 2
        System.out.println(sol.unhappyFriendsSimple(4, p1, pairs1)); // 2
        int[][] p2 = {{1}, {0}};
        System.out.println(sol.unhappyFriendsRank(2, p2, new int[][]{{0, 1}})); // 0
        System.out.println(sol.unhappyFriendsSimple(2, p2, new int[][]{{0, 1}})); // 0
    }
}

复杂度分析

复杂度:时间复杂度 O(n^2)(对数固定 n/2),空间复杂度 O(n^2)。

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


1584. Min Cost to Connect All Points(连接所有点的最小费用)

题目描述

给你一个 points 数组表示 2D 平面上的点(points[i] = `xi`, `yi`)。

连接点 `xi`, `yi` 和 `xj`, `yj` 的费用为曼哈顿距离 |xi - xj| + |yi - yj|。

返回将所有点连接的最小总费用(即最小生成树权值和)。

示例 1

复制代码
points=[[0,0],[2,2],[3,10],[5,2],[7,0]],输出 20。

示例 2

复制代码
points=[[3,12],[-2,5],[-4,1]],输出 18。

约束:

  • 1 <= points.length <= 1000;坐标范围 -10\^6, 10\^6。

解题思路

解法一:Prim 算法(主提交版本)

  • 稠密图(n <= 1000,全边 n^2 条)用朴素 Prim O(n^2) 最合适:
    维护 disti 表示 i 到当前生成树的最小距离,每轮选最小加入并更新。
    复杂度:时间复杂度 O(n^2),空间复杂度 O(n)。
    解法二:Kruskal + 并查集(对照)
  • 枚举全部边排序后用并查集合并。
    复杂度:时间复杂度 O(n^2 log n),空间复杂度 O(n^2)。

代码实现

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

public class Solution {

    public int minCostConnectPointsPrim(int[][] points) {
        int n = points.length;
        boolean[] inTree = new boolean[n];
        int[] dist = new int[n];
        java.util.Arrays.fill(dist, Integer.MAX_VALUE);
        dist[0] = 0;
        int ans = 0;
        for (int t = 0; t < n; t++) {
            int u = -1;
            int best = Integer.MAX_VALUE;
            for (int i = 0; i < n; i++) {
                if (!inTree[i] && dist[i] < best) {
                    best = dist[i];
                    u = i;
                }
            }
            inTree[u] = true;
            ans += best;
            for (int v = 0; v < n; v++) {
                if (!inTree[v]) {
                    int cost = Math.abs(points[u][0] - points[v][0])
                            + Math.abs(points[u][1] - points[v][1]);
                    dist[v] = Math.min(dist[v], cost);
                }
            }
        }
        return ans;
    }

    public int minCostConnectPointsKruskal(int[][] points) {
        int n = points.length;
        List<int[]> edges = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                edges.add(new int[]{
                        Math.abs(points[i][0] - points[j][0]) + Math.abs(points[i][1] - points[j][1]),
                        i, j});
            }
        }
        edges.sort((a, b) -> a[0] - b[0]);
        int[] parent = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
        int ans = 0;
        int cnt = 0;
        for (int[] e : edges) {
            int ra = find(parent, e[1]), rb = find(parent, e[2]);
            if (ra != rb) {
                parent[ra] = rb;
                ans += e[0];
                cnt++;
                if (cnt == n - 1) {
                    break;
                }
            }
        }
        return ans;
    }

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

    public static void main(String[] args) {
        Solution sol = new Solution();
        System.out.println(sol.minCostConnectPointsPrim(new int[][]{{0, 0}, {2, 2}, {3, 10}, {5, 2}, {7, 0}})); // 20
        System.out.println(sol.minCostConnectPointsPrim(new int[][]{{3, 12}, {-2, 5}, {-4, 1}})); // 18
        System.out.println(sol.minCostConnectPointsKruskal(new int[][]{{0, 0}, {2, 2}, {3, 10}, {5, 2}, {7, 0}})); // 20
        System.out.println(sol.minCostConnectPointsKruskal(new int[][]{{3, 12}, {-2, 5}, {-4, 1}})); // 18
        System.out.println(sol.minCostConnectPointsPrim(new int[][]{{0, 0}})); // 0
    }
}

复杂度分析

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

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


1585. Check If String Is Transformable With Substring Sort Operations(检查字符串是否可以通过排序子字符串得到另一个字符串)

题目描述

给你两个字符串 s 和 t,请你通过若干次以下操作将字符串 s 转化成字符串 t:

  • 选择 s 中一个非空子字符串并将它包含的字符就地升序排序。
    如果可以将字符串 s 变成 t,返回 true;否则,返回 false。

示例 1

复制代码
s = "84532", t = "34852",输出 true。

示例 2

复制代码
s = "34521", t = "23415",输出 true。

示例 3

复制代码
s = "12345", t = "12435",输出 false。

示例 4

复制代码
s = "1", t = "2",输出 false。

约束:

  • s.length == t.length;1 <= n <= 10^5;s 和 t 都只包含数字字符 '0' 到 '9'。

解题思路

解法一:贪心 + 队列(主提交版本)

  • 升序排序只会把"较小的字符向左移动",相对顺序上:若 t 要求某个较大字符 c 出现在
    某个尚未匹配的较小字符 d(d < c)的前面,而 s 中 d 位于 c 的左侧且尚未被"取走",
    由于排序无法把 c 挪到 d 的左侧(小字符只会更靠左),则该排列不可达。
  • 为每个数字维护它在 s 中出现下标的队列;从左到右匹配 t 的每个字符 c:
    取出 s 中尚未使用的最靠左的 c 的位置 p,检查所有 d < c 的队列中是否存在
    未使用的下标 < p(说明有一个更小的字符挡在 p 左侧还没有被匹配),存在则返回 false。
    复杂度:时间复杂度 O(10 * n),空间复杂度 O(n)。
    解法二:10 棵树状数组计数(对照)
  • 思路同解法一,但"位置 p 左侧是否还有未匹配的更小数字"用 10 棵 BIT 查询,
    每棵 BITi 维护数字 i 在 s 中尚未被消费的下标集合。
    复杂度:时间复杂度 O(10 * n log n),空间复杂度 O(n)。

代码实现

java 复制代码
import java.util.LinkedList;
import java.util.Queue;

public class Solution {

    public boolean isTransformable(String s, String t) {
        int n = s.length();
        Queue<Integer>[] pos = new Queue[10];
        for (int d = 0; d < 10; d++) {
            pos[d] = new LinkedList<>();
        }
        for (int i = 0; i < n; i++) {
            pos[s.charAt(i) - '0'].offer(i);
        }
        for (int i = 0; i < n; i++) {
            int c = t.charAt(i) - '0';
            if (pos[c].isEmpty()) {
                return false; // s 中没有足够的字符 c
            }
            int p = pos[c].poll(); // s 中尚未使用的最靠左的 c
            // 若 p 左侧存在尚未使用的更小数字,则 c 无法越过它被移动到左侧
            for (int d = 0; d < c; d++) {
                if (!pos[d].isEmpty() && pos[d].peek() < p) {
                    return false;
                }
            }
        }
        return true;
    }

    public boolean isTransformableBIT(String s, String t) {
        int n = s.length();
        Queue<Integer>[] pos = new Queue[10];
        Fenwick[] fw = new Fenwick[10];
        for (int d = 0; d < 10; d++) {
            pos[d] = new LinkedList<>();
            fw[d] = new Fenwick(n);
        }
        for (int i = 0; i < n; i++) {
            int d = s.charAt(i) - '0';
            pos[d].offer(i);
            fw[d].add(i, 1);
        }
        for (int i = 0; i < n; i++) {
            int c = t.charAt(i) - '0';
            if (pos[c].isEmpty()) {
                return false;
            }
            int p = pos[c].poll();
            fw[c].add(p, -1); // 消费位置 p 的字符 c
            int left = 0;
            for (int d = 0; d < c; d++) {
                left += fw[d].sum(p - 1); // [0, p-1] 中未消费的 d
            }
            if (left > 0) {
                return false;
            }
        }
        return true;
    }

    static class Fenwick {
        private final int[] tree;
        private final int size;

        Fenwick(int size) {
            this.size = size;
            this.tree = new int[size + 2];
        }

        void add(int idx, int delta) {
            for (int i = idx + 1; i <= size; i += i & (-i)) {
                tree[i] += delta;
            }
        }

        int sum(int idx) {
            if (idx < 0) {
                return 0;
            }
            int s = 0;
            for (int i = idx + 1; i > 0; i -= i & (-i)) {
                s += tree[i];
            }
            return s;
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:true
        System.out.println(sol.isTransformable("84532", "34852") + " " + sol.isTransformableBIT("84532", "34852"));
        // 示例 2:true
        System.out.println(sol.isTransformable("34521", "23415") + " " + sol.isTransformableBIT("34521", "23415"));
        // 示例 3:false
        System.out.println(sol.isTransformable("12345", "12435") + " " + sol.isTransformableBIT("12345", "12435"));
        // 示例 4:false
        System.out.println(sol.isTransformable("1", "2") + " " + sol.isTransformableBIT("1", "2"));
        // 附加:相同串 true
        System.out.println(sol.isTransformable("13579", "13579") + " " + sol.isTransformableBIT("13579", "13579"));
        // 附加:s="123" t="321" -> false(3 无法越过 1、2 到最左)
        System.out.println(sol.isTransformable("123", "321") + " " + sol.isTransformableBIT("123", "321"));
        // 附加:s="544", t="454" -> true
        System.out.println(sol.isTransformable("544", "454") + " " + sol.isTransformableBIT("544", "454"));
    }
}

复杂度分析

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

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


题目描述

实现一个二叉搜索树(BST)的中序遍历迭代器 BSTIterator 类:

  • BSTIterator(TreeNode root):用根节点 root 初始化,内部指针指向一个不存在于树中且小于树中任意值的数值。
  • boolean hasNext():如果当前指针在中序遍历序列中,存在右侧数值,返回 true。
  • int next():将指针向右移动,并返回移动后指针所指数值。
  • boolean hasPrev():如果当前指针在中序遍历序列中,存在左侧数值,返回 true。
  • int prev():将指针向左移动,并返回移动后指针所指数值。
    注意:第一次调用 next() 需返回 BST 中最小的元素;可以假设 next()/prev() 总有效。
    进阶:不提前遍历整棵树的值来解决问题。

示例

复制代码
BSTIterator([7,3,15,null,null,9,20]);
next()->3, next()->7, prev()->3, next()->7, hasNext()->true,
next()->9, next()->15, next()->20, hasNext()->false, hasPrev()->true,
prev()->15, prev()->9。

约束:

  • 树节点 1, 10\^5;值 0 <= val <= 10^6;最多调用 10^5 次 hasNext/next/hasPrev/prev。

解题思路

解法一:栈模拟中序推进(主提交版本)

  • 维护 leftStack 保存"待展开的最左路径",维护一个 list 保存已经 next() 出来的节点。
  • next() 若右侧还有节点(index + 1 < list.size())直接取;否则继续沿左路径压栈展开,
    取栈顶并将右子树的最左路径压栈,得到的节点追加进 list。
  • prev() 直接从 list 中向左取(list 中保存的正是已遍历过的中序前缀)。hasNext/hasPrev 同理。
    复杂度:均摊 O(1) 每次操作,空间 O(h)(栈深)+ O(n)(已遍历列表)。
    解法二:全量中序 + 游标(对照)
  • 预先中序遍历整棵树存进数组,用游标 i 模拟指针,next() = 先 ++i 再取值,prev() = 先 --i 再取值。
    复杂度:初始化 O(n),每次 O(1),空间 O(n)。

代码实现

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

public class Solution {

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

    static class BSTIterator {
        private final Deque<TreeNode> stack;   // 左路径栈
        private final List<TreeNode> list;     // 已输出的中序前缀
        private int index;                     // 当前指针位置(list 中下标)

        public BSTIterator(TreeNode root) {
            stack = new ArrayDeque<>();
            list = new ArrayList<>();
            index = -1; // 初始指针位于"哨兵"(小于所有值),未指向任何真实节点
            pushLeft(root);
        }

        private void pushLeft(TreeNode node) {
            while (node != null) {
                stack.push(node);
                node = node.left;
            }
        }

        public boolean hasNext() {
            // 右指针位置之后还有已展开的元素,或者栈中还有未展开的节点
            return index + 1 < list.size() || !stack.isEmpty();
        }

        public int next() {
            // 如果当前指针右侧还没有展开的元素,则继续展开中序
            if (index + 1 >= list.size()) {
                TreeNode node = stack.pop();
                list.add(node);
                pushLeft(node.right);
            }
            index++;
            return list.get(index).val;
        }

        public boolean hasPrev() {
            return index > 0;
        }

        public int prev() {
            index--;
            return list.get(index).val;
        }
    }

    static class BSTIteratorPreload {
        private final List<Integer> seq = new ArrayList<>();
        private int index;

        public BSTIteratorPreload(TreeNode root) {
            inorder(root, seq);
            index = -1;
        }

        private void inorder(TreeNode node, List<Integer> out) {
            if (node == null) {
                return;
            }
            inorder(node.left, out);
            out.add(node.val);
            inorder(node.right, out);
        }

        public boolean hasNext() {
            return index + 1 < seq.size();
        }

        public int next() {
            return seq.get(++index);
        }

        public boolean hasPrev() {
            return index > 0;
        }

        public int prev() {
            return seq.get(--index);
        }
    }

    public static void main(String[] args) {
        // 构造示例树:[7,3,15,null,null,9,20]
        TreeNode n9 = new TreeNode(9);
        TreeNode n20 = new TreeNode(20);
        TreeNode n15 = new TreeNode(15, n9, n20);
        TreeNode n3 = new TreeNode(3);
        TreeNode root = new TreeNode(7, n3, n15);

        BSTIterator it = new BSTIterator(root);
        StringBuilder sb = new StringBuilder();
        sb.append(it.next()).append(' ');   // 3
        sb.append(it.next()).append(' ');   // 7
        sb.append(it.prev()).append(' ');   // 3
        sb.append(it.next()).append(' ');   // 7
        sb.append(it.hasNext()).append(' ');// true
        sb.append(it.next()).append(' ');   // 9
        sb.append(it.next()).append(' ');   // 15
        sb.append(it.next()).append(' ');   // 20
        sb.append(it.hasNext()).append(' ');// false
        sb.append(it.hasPrev()).append(' ');// true
        sb.append(it.prev()).append(' ');   // 15
        sb.append(it.prev()).append(' ');   // 9
        System.out.println(sb.toString().trim());

        // 对照解法,同样走一遍
        BSTIteratorPreload it2 = new BSTIteratorPreload(root);
        StringBuilder sb2 = new StringBuilder();
        sb2.append(it2.next()).append(' ');
        sb2.append(it2.next()).append(' ');
        sb2.append(it2.prev()).append(' ');
        sb2.append(it2.next()).append(' ');
        sb2.append(it2.hasNext()).append(' ');
        sb2.append(it2.next()).append(' ');
        sb2.append(it2.next()).append(' ');
        sb2.append(it2.next()).append(' ');
        sb2.append(it2.hasNext()).append(' ');
        sb2.append(it2.hasPrev()).append(' ');
        sb2.append(it2.prev()).append(' ');
        sb2.append(it2.prev()).append(' ');
        System.out.println(sb2.toString().trim());
    }
}

复杂度分析

复杂度:均摊 O(1) 每次操作,空间 O(h)(栈深)+ O(n)(已遍历列表)。

复杂度:初始化 O(n),每次 O(1),空间 O(n)。


1587. Bank Account Summary II(银行账户概要 II)

题目类型:数据库(SQL)

题目描述

表: Users

复制代码
+--------------+---------+
| Column Name  | Type    |
+--------------+---------+
| account      | int     |
| name         | varchar |
+--------------+---------+
account 是该表的主键(具有唯一值的列)。
该表的每一行都包含银行中每个用户的帐号。
表中不会有两个用户具有相同的名称。

表: Transactions

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| trans_id      | int     |
| account       | int     |
| amount        | int     |
| transacted_on | date    |
+---------------+---------+
trans_id 是该表主键(具有唯一值的列)。
该表的每一行包含了所有账户的交易改变情况。
如果用户收到了钱, 那么金额是正的; 如果用户转了钱, 那么金额是负的。
所有账户的起始余额为 0。

编写解决方案, 报告余额高于 10000 的所有用户的名字和余额. 账户的余额等于包含该账户的所有交易的总和。

返回结果表单 无顺序要求 。

查询结果格式如下例所示。

示例 1:

复制代码
`**输入:**
Users` table:
+------------+--------------+
| account    | name         |
+------------+--------------+
| 900001     | Alice        |
| 900002     | Bob          |
| 900003     | Charlie      |
+------------+--------------+

`Transactions` table:
+------------+------------+------------+---------------+
| trans_id   | account    | amount     | transacted_on |
+------------+------------+------------+---------------+
| 1          | 900001     | 7000       |  2020-08-01   |
| 2          | 900001     | 7000       |  2020-09-01   |
| 3          | 900001     | -3000      |  2020-09-02   |
| 4          | 900002     | 1000       |  2020-09-12   |
| 5          | 900003     | 6000       |  2020-08-07   |
| 6          | 900003     | 6000       |  2020-09-07   |
| 7          | 900003     | -4000      |  2020-09-11   |
+------------+------------+------------+---------------+
**输出:**
+------------+------------+
| `name    `   | `balance  `  |
+------------+------------+
| Alice      | 11000      |
+------------+------------+
**解释:**
Alice 的余额为(7000 + 7000 - 3000) = 11000.
Bob 的余额为1000.
Charlie 的余额为(6000 + 6000 - 4000) = 8000.

解题思路

思考

列出余额超过 10000 10000 10000 的用户。姓名在用户表,流水在交易表,必须先按账户对齐再汇总。

等值连接后按账户对 a m o u n t amount amount 求和,用 H A V I N G HAVING HAVING 留下余额大于 10000 10000 10000 的组,同时选出姓名。

考察JOIN + GROUP BY + HAVING:用户表连接交易表,按账户分组累加金额,用 HAVING 过滤余额 > 10000 的账户。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT
    name,
    SUM(amount) AS balance
FROM
    Users
    JOIN Transactions USING (account)
GROUP BY account
HAVING balance > 10000;

复杂度分析

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


1588. Sum of All Odd Length Subarrays(所有奇数长度子数组的和)

题目描述

给你一个正整数数组 arr,请你计算所有可能的奇数长度子数组的和。

子数组定义为原数组中的一个连续子序列。返回 arr 中所有奇数长度子数组的和。

示例 1

复制代码
arr = [1,4,2,5,3],输出 58。

示例 2

复制代码
arr = [1,2],输出 3。

示例 3

复制代码
arr = [10,11,12],输出 66。

约束:

  • 1 <= arr.length <= 100;1 <= arr[i] <= 1000。
  • 进阶:设计一个 O(n) 时间复杂度的算法。

解题思路

解法一:前缀和枚举(主提交版本)

  • 枚举所有奇数长度 l = 1, 3, 5, ...,滑动窗口求和累加。
    复杂度:时间复杂度 O(n^2),空间复杂度 O(1)。
    解法二:数学贡献法 O(n)(对照,满足进阶)
  • 统计每个元素 arri 出现在多少个奇数长度子数组中,贡献为 arri * count。
  • count = 包含 i 的左端点个数 * 右端点个数(再取奇偶):对奇数长度子数组,
    记 i 左边可选位置数 L = i + 1、右边可选位置数 R = n - i。
    起点与终点下标奇偶性相同的区间长度为奇数,所以:
    oddL = (L + 1) / 2(奇数起点数),oddR = (R + 1) / 2;
    贡献次数 = oddL * oddR + evenL * evenR = ((L + 1) / 2) * ((R + 1) / 2) + (L / 2) * (R / 2)。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    public int sumOddLengthSubarrays(int[] arr) {
        int n = arr.length;
        int[] prefix = new int[n + 1];
        for (int i = 0; i < n; i++) {
            prefix[i + 1] = prefix[i] + arr[i];
        }
        int sum = 0;
        for (int len = 1; len <= n; len += 2) {
            for (int i = 0; i + len <= n; i++) {
                sum += prefix[i + len] - prefix[i];
            }
        }
        return sum;
    }

    public int sumOddLengthSubarraysMath(int[] arr) {
        int n = arr.length;
        int sum = 0;
        for (int i = 0; i < n; i++) {
            int L = i + 1;      // 左边可选位置数
            int R = n - i;      // 右边可选位置数
            long oddL = (L + 1) / 2;  // 起点下标为奇数的个数
            long oddR = (R + 1) / 2;  // 终点下标为奇数的个数
            long evenL = L / 2;
            long evenR = R / 2;
            sum += arr[i] * (oddL * oddR + evenL * evenR);
        }
        return sum;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:58
        System.out.println(sol.sumOddLengthSubarrays(new int[]{1,4,2,5,3}) + " " + sol.sumOddLengthSubarraysMath(new int[]{1,4,2,5,3}));
        // 示例 2:3
        System.out.println(sol.sumOddLengthSubarrays(new int[]{1,2}) + " " + sol.sumOddLengthSubarraysMath(new int[]{1,2}));
        // 示例 3:66
        System.out.println(sol.sumOddLengthSubarrays(new int[]{10,11,12}) + " " + sol.sumOddLengthSubarraysMath(new int[]{10,11,12}));
        // 附加:单个元素
        System.out.println(sol.sumOddLengthSubarrays(new int[]{7}) + " " + sol.sumOddLengthSubarraysMath(new int[]{7}));
        // 附加:5 个 1 -> 所有奇数长度子数组共有 1+3+5+3+1 = 13 个,和为 13
        System.out.println(sol.sumOddLengthSubarrays(new int[]{1,1,1,1,1}) + " " + sol.sumOddLengthSubarraysMath(new int[]{1,1,1,1,1}));
    }
}

复杂度分析

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

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


1589. Maximum Sum Obtained of Any Permutation(所有排列中的最大和)

题目描述

有一个整数数组 nums 和一个查询数组 requests,其中 requests[i] = `starti`, `endi`。

第 i 个查询求 nums[starti] + nums[starti+1] + ... + nums[endi] 的结果,下标从 0 开始。

你可以任意排列 nums 中的数字,请你返回所有查询结果之和的最大值。

由于答案可能很大,请对它取模 10^9 + 7 后返回。

示例 1

复制代码
nums = [1,2,3,4,5], requests = [[1,3],[0,1]],输出 19。

示例 2

复制代码
nums = [1,2,3,4,5,6], requests = [[0,1]],输出 11。

示例 3

复制代码
nums = [1,2,3,4,5,10], requests = [[0,2],[1,3],[1,1]],输出 47。

约束:

  • n 与 requests 长度 <= 10^5;nums[i] 与 requests[i][j] <= 10^5。

解题思路

解法一:差分数组 + 贪心排序(主提交版本)

  • 总查询和 = Σ numsi * counti,其中 counti 为下标 i 被多少个查询区间覆盖。
    要最大化,就要把最大的数放在覆盖次数最多的位置------两边各自升序排序后同下标相乘累加。
  • counti 用差分数组 O(n + m) 求出。
    复杂度:时间复杂度 O(n log n + m),空间复杂度 O(n)。
    解法二:朴素区间累加(对照)
  • 直接对每个 request 的区间累加 count,再排序相乘。
    复杂度:时间复杂度 O(m * n),空间复杂度 O(n)。

代码实现

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

public class Solution {

    private static final int MOD = 1_000_000_007;

    public int maxSumRangeQuery(int[] nums, int[][] requests) {
        int n = nums.length;
        int[] diff = new int[n + 1];
        for (int[] req : requests) {
            diff[req[0]]++;
            diff[req[1] + 1]--;
        }
        int[] count = new int[n];
        long cur = 0;
        for (int i = 0; i < n; i++) {
            cur += diff[i];
            count[i] = (int) cur;
        }
        Arrays.sort(nums);
        Arrays.sort(count);
        long ans = 0;
        for (int i = 0; i < n; i++) {
            ans = (ans + (long) nums[i] * count[i]) % MOD;
        }
        return (int) ans;
    }

    public int maxSumRangeQueryBrute(int[] nums, int[][] requests) {
        int n = nums.length;
        int[] count = new int[n];
        for (int[] req : requests) {
            for (int i = req[0]; i <= req[1]; i++) {
                count[i]++;
            }
        }
        Arrays.sort(nums);
        Arrays.sort(count);
        long ans = 0;
        for (int i = 0; i < n; i++) {
            ans = (ans + (long) nums[i] * count[i]) % MOD;
        }
        return (int) ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:19
        System.out.println(sol.maxSumRangeQuery(new int[]{1,2,3,4,5}, new int[][]{{1,3},{0,1}})
                + " " + sol.maxSumRangeQueryBrute(new int[]{1,2,3,4,5}, new int[][]{{1,3},{0,1}}));
        // 示例 2:11
        System.out.println(sol.maxSumRangeQuery(new int[]{1,2,3,4,5,6}, new int[][]{{0,1}})
                + " " + sol.maxSumRangeQueryBrute(new int[]{1,2,3,4,5,6}, new int[][]{{0,1}}));
        // 示例 3:47
        System.out.println(sol.maxSumRangeQuery(new int[]{1,2,3,4,5,10}, new int[][]{{0,2},{1,3},{1,1}})
                + " " + sol.maxSumRangeQueryBrute(new int[]{1,2,3,4,5,10}, new int[][]{{0,2},{1,3},{1,1}}));
        // 附加:单点查询
        System.out.println(sol.maxSumRangeQuery(new int[]{9,5}, new int[][]{{1,1}})
                + " " + sol.maxSumRangeQueryBrute(new int[]{9,5}, new int[][]{{1,1}}));
    }
}

复杂度分析

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

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


1590. Make Sum Divisible by P(使数组和能被 P 整除)

题目描述

给你一个正整数数组 nums,请你移除最短子数组(可以为空),使得剩余元素的和能被 p 整除。

不允许将整个数组都移除。返回需要移除的最短子数组的长度;如果无法满足,返回 -1。

子数组定义为原数组中连续的一组元素。

示例 1

复制代码
nums = [3,1,4,2], p = 6,输出 1。

示例 2

复制代码
nums = [6,3,5,2], p = 9,输出 2。

示例 3

复制代码
nums = [1,2,3], p = 3,输出 0。

示例 4

复制代码
nums = [1,2,3], p = 7,输出 -1。

示例 5

复制代码
nums = [1000000000,1000000000,1000000000], p = 3,输出 0。

约束:

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

解题思路

解法一:前缀和哈希(主提交版本)

  • 设 total = sum(nums)。若 total % p == 0,直接返回 0。
  • 需要移除一个和为 sub 的连续子数组,使得 (total - sub) % p == 0,即 sub % p == total % p。
  • 记 rem = total % p,用前缀和+哈希表:遍历时保留"最早的出现位置",
    对当前前缀和 cur = prefix % p,期待前缀 pre 满足 (cur - pre) % p == rem,即 pre = (cur - rem + p) % p,
    找到最早的该前缀位置,更新最短长度。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。
    解法二:两重循环暴力(对照)
  • 枚举全部子数组检查,复杂度 O(n^2)。
    复杂度:时间复杂度 O(n^2),空间复杂度 O(1)。

代码实现

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

public class Solution {

    public int minSubarray(int[] nums, int p) {
        long total = 0;
        for (int x : nums) {
            total += x;
        }
        int rem = (int) (total % p);
        if (rem == 0) {
            return 0;
        }
        int n = nums.length;
        Map<Integer, Integer> first = new HashMap<>();
        first.put(0, -1); // 前缀 0 出现在下标 -1 处
        int cur = 0;
        int ans = n;
        for (int i = 0; i < n; i++) {
            cur = (int) (((long) cur + nums[i]) % p);
            int need = (cur - rem + p) % p;
            if (first.containsKey(need)) {
                ans = Math.min(ans, i - first.get(need));
            }
            // 记录当前前缀余数最近一次出现的位置(越靠近当前位置,移除的子数组越短)
            first.put(cur, i);
        }
        return ans == n ? -1 : ans;
    }

    public int minSubarrayBrute(int[] nums, int p) {
        long total = 0;
        int n = nums.length;
        for (int x : nums) {
            total += x;
        }
        if (total % p == 0) {
            return 0;
        }
        int ans = n;
        for (int i = 0; i < n; i++) {
            long sub = 0;
            for (int j = i; j < n; j++) {
                sub += nums[j];
                if ((total - sub) % p == 0) {
                    ans = Math.min(ans, j - i + 1);
                }
            }
        }
        return ans == n ? -1 : ans;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:1
        System.out.println(sol.minSubarray(new int[]{3,1,4,2}, 6) + " " + sol.minSubarrayBrute(new int[]{3,1,4,2}, 6));
        // 示例 2:2
        System.out.println(sol.minSubarray(new int[]{6,3,5,2}, 9) + " " + sol.minSubarrayBrute(new int[]{6,3,5,2}, 9));
        // 示例 3:0
        System.out.println(sol.minSubarray(new int[]{1,2,3}, 3) + " " + sol.minSubarrayBrute(new int[]{1,2,3}, 3));
        // 示例 4:-1
        System.out.println(sol.minSubarray(new int[]{1,2,3}, 7) + " " + sol.minSubarrayBrute(new int[]{1,2,3}, 7));
        // 示例 5:0
        System.out.println(sol.minSubarray(new int[]{1000000000,1000000000,1000000000}, 3)
                + " " + sol.minSubarrayBrute(new int[]{1000000000,1000000000,1000000000}, 3));
    }
}

复杂度分析

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

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


1591. Strange Printer II(奇怪的打印机 II)

题目描述

奇怪打印机有两个特殊规则:

  • 每一次操作时,打印机会用同一种颜色打印一个矩形的形状,每次打印会覆盖矩形对应格子里原本的颜色。
  • 一旦矩形根据上面的规则使用了一种颜色,那么相同的颜色不能再被使用。
    给你一个初始没有颜色的 m x n 矩形 targetGrid,其中 targetGrid[row][col] 是位置 (row, col) 的颜色。
    如果能够按照上述规则打印出矩形,返回 true;否则返回 false。

示例 1

复制代码
targetGrid = [[1,1,1,1],[1,2,2,1],[1,2,2,1],[1,1,1,1]],输出 true。

示例 2

复制代码
[[1,1,1,1],[1,1,3,3],[1,1,3,4],[5,5,1,4]],输出 true。

示例 3

复制代码
[[1,2,1],[2,1,2],[1,2,1]],输出 false。

示例 4

复制代码
[[1,1,1],[3,1,3]],输出 false。

约束:

  • m, n <= 60;1 <= 颜色 <= 60。

解题思路

解法一:覆写回退 + 拓扑排序(主提交版本)

  • 逆推:最后一次打印的颜色 c 必须覆盖"当前网格中所有 c 的最小外接矩形",
    且该矩形内不得出现除 c 外的其他颜色(否则那些颜色会被 c 覆盖)。
  • 每次找到一个这样的颜色 c,把网格中所有 c 的格子"清空"(抹掉该次打印),
    记录 cnt 加一;重复直到没有可清空的颜色。
  • 全部颜色都被抹掉 => 网格可由这些打印顺序构造出来,返回 true;否则 false。
    复杂度:时间复杂度 O(k * m * n)(k 为颜色数,<= 60),空间复杂度 O(m * n)。
    解法二:拓扑排序判定(对照)
  • 对每种颜色求最小外接矩形;若颜色 a 的矩形内出现了颜色 b(b != a),
    则 b 必须先于 a 打印(b 在 a 之前),建立有向边 b -> a。
  • 若图有环则无法给出打印顺序,返回 false;否则 true。
    复杂度:时间复杂度 O(k^2 + k * m * n),空间复杂度 O(k^2)。

代码实现

java 复制代码
public class Solution {

    private static final int[][] DIRS = {{0,1},{0,-1},{1,0},{-1,0}};

    public boolean isPrintable(int[][] targetGrid) {
        int m = targetGrid.length;
        int n = targetGrid[0].length;
        boolean[] removed = new boolean[61];
        int colorCount = 0;
        boolean[] seen = new boolean[61];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (!seen[targetGrid[i][j]]) {
                    seen[targetGrid[i][j]] = true;
                    colorCount++;
                }
            }
        }
        int cleared = 0;
        while (cleared < colorCount) {
            boolean any = false;
            for (int c = 1; c <= 60; c++) {
                if (!seen[c] || removed[c]) {
                    continue;
                }
                if (canRemove(targetGrid, c, removed, m, n)) {
                    removed[c] = true;
                    cleared++;
                    any = true;
                }
            }
            // 一轮内没有任何颜色可移除 => 死锁(存在循环覆盖)
            if (!any) {
                return false;
            }
        }
        return true;
    }

    private boolean canRemove(int[][] grid, int color, boolean[] removed, int m, int n) {
        int minR = m, maxR = -1, minC = n, maxC = -1;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == color) {
                    minR = Math.min(minR, i);
                    maxR = Math.max(maxR, i);
                    minC = Math.min(minC, j);
                    maxC = Math.max(maxC, j);
                }
            }
        }
        if (minR > maxR) {
            return true; // 该颜色不存在(已被抹掉)
        }
        // 外接矩形内不能有其他未被移除的颜色
        for (int i = minR; i <= maxR; i++) {
            for (int j = minC; j <= maxC; j++) {
                if (grid[i][j] != color && !removed[grid[i][j]]) {
                    return false;
                }
            }
        }
        // 抹掉该颜色的所有格子
        for (int i = minR; i <= maxR; i++) {
            for (int j = minC; j <= maxC; j++) {
                if (grid[i][j] == color) {
                    grid[i][j] = 0;
                }
            }
        }
        return true;
    }

    public boolean isPrintableTopo(int[][] targetGrid) {
        int m = targetGrid.length;
        int n = targetGrid[0].length;
        int[] minR = new int[61], maxR = new int[61], minC = new int[61], maxC = new int[61];
        java.util.Arrays.fill(minR, m);
        java.util.Arrays.fill(maxR, -1);
        java.util.Arrays.fill(minC, n);
        java.util.Arrays.fill(maxC, -1);
        boolean[] seen = new boolean[61];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int c = targetGrid[i][j];
                seen[c] = true;
                minR[c] = Math.min(minR[c], i);
                maxR[c] = Math.max(maxR[c], i);
                minC[c] = Math.min(minC[c], j);
                maxC[c] = Math.max(maxC[c], j);
            }
        }
        // 建图:颜色 b 出现在颜色 a 的外接矩形内 => b 必须先于 a 印,边 b -> a
        boolean[][] adj = new boolean[61][61];
        int[] indeg = new int[61];
        for (int a = 1; a <= 60; a++) {
            if (!seen[a]) {
                continue;
            }
            for (int i = minR[a]; i <= maxR[a]; i++) {
                for (int j = minC[a]; j <= maxC[a]; j++) {
                    int b = targetGrid[i][j];
                    if (b != a && !adj[b][a]) {
                        adj[b][a] = true;
                        indeg[a]++;
                    }
                }
            }
        }
        int[] q = new int[61];
        int head = 0, tail = 0;
        int total = 0;
        for (int c = 1; c <= 60; c++) {
            if (seen[c]) {
                total++;
                if (indeg[c] == 0) {
                    q[tail++] = c;
                }
            }
        }
        int visited = 0;
        while (head < tail) {
            int u = q[head++];
            visited++;
            for (int v = 1; v <= 60; v++) {
                if (adj[u][v] && --indeg[v] == 0) {
                    q[tail++] = v;
                }
            }
        }
        return visited == total;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:true
        System.out.println(sol.isPrintable(new int[][]{{1,1,1,1},{1,2,2,1},{1,2,2,1},{1,1,1,1}})
                + " " + sol.isPrintableTopo(new int[][]{{1,1,1,1},{1,2,2,1},{1,2,2,1},{1,1,1,1}}));
        // 示例 2:true
        System.out.println(sol.isPrintable(new int[][]{{1,1,1,1},{1,1,3,3},{1,1,3,4},{5,5,1,4}})
                + " " + sol.isPrintableTopo(new int[][]{{1,1,1,1},{1,1,3,3},{1,1,3,4},{5,5,1,4}}));
        // 示例 3:false
        System.out.println(sol.isPrintable(new int[][]{{1,2,1},{2,1,2},{1,2,1}})
                + " " + sol.isPrintableTopo(new int[][]{{1,2,1},{2,1,2},{1,2,1}}));
        // 示例 4:false
        System.out.println(sol.isPrintable(new int[][]{{1,1,1},{3,1,3}})
                + " " + sol.isPrintableTopo(new int[][]{{1,1,1},{3,1,3}}));
        // 附加:单一颜色
        System.out.println(sol.isPrintable(new int[][]{{2,2},{2,2}})
                + " " + sol.isPrintableTopo(new int[][]{{2,2},{2,2}}));
    }
}

复杂度分析

复杂度:时间复杂度 O(k * m * n)(k 为颜色数,<= 60),空间复杂度 O(m * n)。

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


1592. Rearrange Spaces Between Words(重新排列单词间的空格)

题目描述

给你一个字符串 text,该字符串由若干被空格包围的单词组成。每个单词由一个或多个小写英文字母组成,

并且两个单词之间至少存在一个空格。测试用例保证 text 至少包含一个单词。

请重新排列空格,使每对相邻单词之间的空格数目都相等,并尽可能最大化该数目。

如果不能平均分配所有空格,将多余的空格放置在字符串末尾。返回的字符串应当与原 text 长度相等。

示例 1

复制代码
text = "  this   is  a sentence ",输出 "this   is   a   sentence"。

示例 2

复制代码
text = " practice   makes   perfect",输出 "practice   makes   perfect "。

示例 3

复制代码
text = "hello   world",输出 "hello   world"。

示例 4

复制代码
text = "  walks  udp package   into  bar a",输出 "walks  udp  package  into  bar  a "。

示例 5

复制代码
text = "a",输出 "a"。

约束:

  • 1 <= text.length <= 100;text 由小写英文字母和空格组成;至少包含一个单词。

解题思路

解法一:单词收集 + 均分拼接(主提交版本)

  • 统计总空格数 spaceCount 与所有单词;m = 单词数。
  • 若 m == 1,所有空格放末尾;否则每组间隔放 spaceCount / (m-1) 个,末尾放 spaceCount % (m-1) 个。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。
    解法二:Java 流式/正则(对照)
  • 用 split 拆分单词,同样按均分逻辑拼接。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    public String reorderSpaces(String text) {
        int total = 0;
        for (int i = 0; i < text.length(); i++) {
            if (text.charAt(i) == ' ') {
                total++;
            }
        }
        // 提取单词
        java.util.List<String> words = new java.util.ArrayList<>();
        int i = 0;
        while (i < text.length()) {
            if (text.charAt(i) == ' ') {
                i++;
                continue;
            }
            int j = i;
            while (j < text.length() && text.charAt(j) != ' ') {
                j++;
            }
            words.add(text.substring(i, j));
            i = j;
        }
        int m = words.size();
        StringBuilder sb = new StringBuilder();
        int between = m == 1 ? 0 : total / (m - 1);
        int extra = m == 1 ? total : total % (m - 1);
        for (int w = 0; w < m; w++) {
            sb.append(words.get(w));
            if (w != m - 1) {
                for (int k = 0; k < between; k++) {
                    sb.append(' ');
                }
            }
        }
        for (int k = 0; k < extra; k++) {
            sb.append(' ');
        }
        return sb.toString();
    }

    public String reorderSpacesSplit(String text) {
        int total = 0;
        for (int i = 0; i < text.length(); i++) {
            if (text.charAt(i) == ' ') {
                total++;
            }
        }
        String[] words = text.trim().split("\\s+");
        int m = words.length;
        StringBuilder sb = new StringBuilder();
        int between = m == 1 ? 0 : total / (m - 1);
        int extra = m == 1 ? total : total % (m - 1);
        for (int w = 0; w < m; w++) {
            sb.append(words[w]);
            if (w != m - 1) {
                for (int k = 0; k < between; k++) {
                    sb.append(' ');
                }
            }
        }
        for (int k = 0; k < extra; k++) {
            sb.append(' ');
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1
        System.out.println("[" + sol.reorderSpaces("  this   is  a sentence ") + "]");
        System.out.println("[" + sol.reorderSpacesSplit("  this   is  a sentence ") + "]");
        // 示例 2
        System.out.println("[" + sol.reorderSpaces(" practice   makes   perfect") + "]");
        System.out.println("[" + sol.reorderSpacesSplit(" practice   makes   perfect") + "]");
        // 示例 3
        System.out.println("[" + sol.reorderSpaces("hello   world") + "]");
        System.out.println("[" + sol.reorderSpacesSplit("hello   world") + "]");
        // 示例 4
        System.out.println("[" + sol.reorderSpaces("  walks  udp package   into  bar a") + "]");
        System.out.println("[" + sol.reorderSpacesSplit("  walks  udp package   into  bar a") + "]");
        // 示例 5
        System.out.println("[" + sol.reorderSpaces("a") + "]");
        System.out.println("[" + sol.reorderSpacesSplit("a") + "]");
    }
}

复杂度分析

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


1593. Split a String Into the Max Number of Unique Substrings(拆分字符串使唯一子字符串数目最大)

题目描述

给你一个字符串 s,请你拆分该字符串,并返回拆分后唯一子字符串的最大数目。

字符串 s 拆分后可以得到若干非空子字符串,这些子字符串连接后应当能够还原为原字符串,

但拆分出来的每个子字符串都必须是唯一的。

示例 1

复制代码
s = "ababccc",输出 5([a, b, ab, c, cc])。

示例 2

复制代码
s = "aba",输出 2([a, ba])。

示例 3

复制代码
s = "aa",输出 1。

约束:

  • 1 <= s.length <= 16;s 仅包含小写英文字母。

解题思路

解法一:回溯 + 哈希集合(主提交版本)

  • 深度优先搜索枚举每个切分点;用 HashSet 记录当前已使用的子串保证唯一。
  • 剪枝:若"已经用掉的段数 + 剩余字符数"(每段最短路 1 个字符)<= 当前最优,直接返回。
    复杂度:最坏 O(2^(n-1) * n)(n <= 16,状态空间小),空间复杂度 O(n)。
    解法二:回溯、无剪枝(对照)
  • 同样的 DFS 逻辑,去掉上界剪枝,仅用 HashSet 判重。
    复杂度:最坏 O(2^(n-1) * n),空间复杂度 O(n)。

代码实现

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

public class Solution {

    private int best;

    public int maxUniqueSplit(String s) {
        best = 0;
        dfs(s, 0, new HashSet<String>());
        return best;
    }

    private void dfs(String s, int start, Set<String> used) {
        int n = s.length();
        if (used.size() + (n - start) <= best) {
            return; // 上界剪枝
        }
        if (start == n) {
            best = Math.max(best, used.size());
            return;
        }
        for (int end = start + 1; end <= n; end++) {
            String sub = s.substring(start, end);
            if (used.contains(sub)) {
                continue;
            }
            used.add(sub);
            dfs(s, end, used);
            used.remove(sub);
        }
    }

    public int maxUniqueSplitNoPrune(String s) {
        best = 0;
        dfsNoPrune(s, 0, new HashSet<String>());
        return best;
    }

    private void dfsNoPrune(String s, int start, Set<String> used) {
        if (start == s.length()) {
            best = Math.max(best, used.size());
            return;
        }
        for (int end = start + 1; end <= s.length(); end++) {
            String sub = s.substring(start, end);
            if (used.contains(sub)) {
                continue;
            }
            used.add(sub);
            dfsNoPrune(s, end, used);
            used.remove(sub);
        }
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:5
        System.out.println(sol.maxUniqueSplit("ababccc") + " " + sol.maxUniqueSplitNoPrune("ababccc"));
        // 示例 2:2
        System.out.println(sol.maxUniqueSplit("aba") + " " + sol.maxUniqueSplitNoPrune("aba"));
        // 示例 3:1
        System.out.println(sol.maxUniqueSplit("aa") + " " + sol.maxUniqueSplitNoPrune("aa"));
        // 附加:s = "wwwzfvedwfvhsww" -> 11
        System.out.println(sol.maxUniqueSplit("wwwzfvedwfvhsww") + " " + sol.maxUniqueSplitNoPrune("wwwzfvedwfvhsww"));
        // 附加:s = "addbsd" -> 5
        System.out.println(sol.maxUniqueSplit("addbsd") + " " + sol.maxUniqueSplitNoPrune("addbsd"));
        // 附加:全不相同的最长 16 位
        System.out.println(sol.maxUniqueSplit("abcdefghijklmnop") + " " + sol.maxUniqueSplitNoPrune("abcdefghijklmnop"));
    }
}

复杂度分析

复杂度:最坏 O(2^(n-1) * n)(n <= 16,状态空间小),空间复杂度 O(n)。

复杂度:最坏 O(2^(n-1) * n),空间复杂度 O(n)。


1594. Maximum Non Negative Product in a Matrix(矩阵的最大非负积)

题目描述

给你一个大小为 m x n 的矩阵 grid。最初,你位于左上角 (0, 0),每一步可以向右或向下移动。

在从左上角到右下角的所有路径中,找出具有最大非负积的路径。

路径的积是沿路径访问的单元格中所有整数的乘积。

返回最大非负积对 10^9 + 7 取余的结果;如果最大积为负数,则返回 -1。

注意:取余是在得到最大积之后执行的。

示例 1

复制代码
grid = [[-1,-2,-3],[-2,-3,-3],[-3,-3,-2]],输出 -1。

示例 2

复制代码
grid = [[1,-2,1],[1,-2,1],[3,-4,1]],输出 8。

示例 3

复制代码
grid = [[1,3],[0,-4]],输出 0。

约束:

  • 1 <= m, n <= 15;-4 <= grid[i][j] <= 4。

解题思路

解法一:动态规划双状态(主提交版本)

  • 由于有负数和 0,最大乘积可能是"最大正积"或"最小负积"与负数相乘翻正得到,
    因此每个格点同时维护 max(最大乘积)和 min(最小乘积),转移时取四种组合的极值。
    复杂度:时间复杂度 O(m * n),空间复杂度 O(m * n)(可优化为两行滚动,此处给出清晰版)。
    解法二:滚动数组优化空间(对照)
  • 同样的双状态 DP,但用两行数组滚动,空间降为 O(n)。
    复杂度:时间复杂度 O(m * n),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    private static final int MOD = 1_000_000_007;

    public int maxProductPath(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        long[][] maxDp = new long[m][n];
        long[][] minDp = new long[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                long v = grid[i][j];
                if (i == 0 && j == 0) {
                    maxDp[i][j] = v;
                    minDp[i][j] = v;
                    continue;
                }
                long maxPrev = Long.MIN_VALUE, minPrev = Long.MAX_VALUE;
                if (i > 0) {
                    maxPrev = Math.max(maxPrev, maxDp[i - 1][j]);
                    minPrev = Math.min(minPrev, minDp[i - 1][j]);
                }
                if (j > 0) {
                    maxPrev = Math.max(maxPrev, maxDp[i][j - 1]);
                    minPrev = Math.min(minPrev, minDp[i][j - 1]);
                }
                // 上一格乘积只有两种取值 merge;与当前格值相乘取极值
                long vMax = v == 0 ? 0 : maxPrev * v;
                long vMin = v == 0 ? 0 : minPrev * v;
                maxDp[i][j] = Math.max(vMax, vMin);
                minDp[i][j] = Math.min(vMax, vMin);
            }
        }
        long result = maxDp[m - 1][n - 1];
        if (result < 0) {
            return -1;
        }
        return (int) (result % MOD);
    }

    public int maxProductPathRolling(int[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        long[] maxDp = new long[n];
        long[] minDp = new long[n];
        for (int i = 0; i < m; i++) {
            long[] nMax = new long[n];
            long[] nMin = new long[n];
            for (int j = 0; j < n; j++) {
                long v = grid[i][j];
                if (i == 0 && j == 0) {
                    nMax[j] = v;
                    nMin[j] = v;
                    continue;
                }
                long maxPrev = Long.MIN_VALUE, minPrev = Long.MAX_VALUE;
                if (i > 0) {
                    maxPrev = Math.max(maxPrev, maxDp[j]);
                    minPrev = Math.min(minPrev, minDp[j]);
                }
                if (j > 0) {
                    maxPrev = Math.max(maxPrev, nMax[j - 1]);
                    minPrev = Math.min(minPrev, nMin[j - 1]);
                }
                long vMax = v == 0 ? 0 : maxPrev * v;
                long vMin = v == 0 ? 0 : minPrev * v;
                nMax[j] = Math.max(vMax, vMin);
                nMin[j] = Math.min(vMax, vMin);
            }
            maxDp = nMax;
            minDp = nMin;
        }
        long result = maxDp[n - 1];
        if (result < 0) {
            return -1;
        }
        return (int) (result % MOD);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:-1
        System.out.println(sol.maxProductPath(new int[][]{{-1,-2,-3},{-2,-3,-3},{-3,-3,-2}})
                + " " + sol.maxProductPathRolling(new int[][]{{-1,-2,-3},{-2,-3,-3},{-3,-3,-2}}));
        // 示例 2:8
        System.out.println(sol.maxProductPath(new int[][]{{1,-2,1},{1,-2,1},{3,-4,1}})
                + " " + sol.maxProductPathRolling(new int[][]{{1,-2,1},{1,-2,1},{3,-4,1}}));
        // 示例 3:0
        System.out.println(sol.maxProductPath(new int[][]{{1,3},{0,-4}})
                + " " + sol.maxProductPathRolling(new int[][]{{1,3},{0,-4}}));
        // 附加:单元素
        System.out.println(sol.maxProductPath(new int[][]{{-4}})
                + " " + sol.maxProductPathRolling(new int[][]{{-4}}));
        // 附加:全正
        System.out.println(sol.maxProductPath(new int[][]{{1,2},{3,4}})
                + " " + sol.maxProductPathRolling(new int[][]{{1,2},{3,4}}));
    }
}

复杂度分析

复杂度:时间复杂度 O(m * n),空间复杂度 O(m * n)(可优化为两行滚动,此处给出清晰版)。

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


1595. Minimum Cost to Connect Two Groups of Points(连通两组点的最小成本)

题目描述

给你两组点,其中第一组中有 size1 个点,第二组中有 size2 个点,且 size1 >= size2。

任意两点间的连接成本 cost 由大小为 size1 x size2 的矩阵给出,cost[i][j] 是第一组中的点 i 和第二组中的点 j 的连接成本。

如果两组中的每个点都与另一组中的一个或多个点连接,则称这两组点是连通的。

返回连通两组点所需的最小成本。

示例 1

复制代码
cost = [[15, 96], [36, 2]],输出 17。

示例 2

复制代码
cost = [[1, 3, 5], [4, 1, 1], [1, 5, 3]],输出 4。

示例 3

复制代码
cost = [[2, 5, 1], [3, 4, 7], [8, 1, 2], [6, 2, 4], [3, 8, 8]],输出 10。

约束:

  • size1 == cost.length;size2 == cost[i].length;1 <= size1, size2 <= 12 且 size1 >= size2;0 <= cost[i][j] <= 100。

解题思路

解法一:状压 DP(主提交版本)

  • 分组一(size2 较小者)作为二进制掩码集合,处理分组二(size1 较大者)的每个点 i。
  • dpimask:处理完分组二前 i 个点(0-indexed),分组一经被连接的点集合为 mask 的最小成本。
  • 转移:点 i 可连接任意分组一点 j(mask |= 1 << j),也可以"复用"已连过的 j
    (即允许 mask 不变也计入 costij、把 j 那一位重复置位)。
  • 这里用"当前 j 若已在 mask 中则复用(不允许 mask 变化)"或新增的思路。
    复杂度:时间复杂度 O(size1 * size2 * 2^size2),空间复杂度 O(2^size2)(滚动)。
    解法二:DFS + 记忆化(对照)
  • 同样以掩码为状态,直接递归枚举每个二分点的可选连接,配合记忆化。
    复杂度:时间复杂度 O(size1 * size2 * 2^size2),空间复杂度 O(size1 * 2^size2)。

代码实现

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

public class Solution {

    public int connectTwoGroups(int[][] cost) {
        int n = cost.length;      // size1
        int m = cost[0].length;   // size2
        int full = (1 << m) - 1;
        int INF = Integer.MAX_VALUE / 2;
        int[][] dp = new int[n][1 << m];
        for (int[] row : dp) {
            Arrays.fill(row, INF);
        }
        for (int j = 0; j < m; j++) {
            dp[0][1 << j] = cost[0][j];
        }
        for (int i = 1; i < n; i++) {
            for (int mask = 1; mask <= full; mask++) {
                if (dp[i - 1][mask] == INF) {
                    continue;
                }
                for (int j = 0; j < m; j++) {
                    int nmask = mask | (1 << j);
                    dp[i][nmask] = Math.min(dp[i][nmask], dp[i - 1][mask] + cost[i][j]);
                }
            }
        }
        return dp[n - 1][full];
    }

    public int connectTwoGroupsDfs(int[][] cost) {
        int n = cost.length;
        int m = cost[0].length;
        int full = (1 << m) - 1;
        memo = new int[n][1 << m];
        for (int[] row : memo) {
            Arrays.fill(row, -1);
        }
        return dfs(0, 0, cost, n, m, full);
    }

    private int[][] memo;

    private int dfs(int i, int mask, int[][] cost, int n, int m, int full) {
        if (i == n) {
            return mask == full ? 0 : Integer.MAX_VALUE / 2;
        }
        if (memo[i][mask] != -1) {
            return memo[i][mask];
        }
        int best = Integer.MAX_VALUE / 2;
        // 点 i 必须至少连一个点:枚举连接任意 j(新覆盖或复用),
        // 复用场景:j 已在 mask 中时 mask 不变,用一行内最小 cost 兜底保证可行。
        for (int j = 0; j < m; j++) {
            best = Math.min(best, cost[i][j] + dfs(i + 1, mask | (1 << j), cost, n, m, full));
        }
        memo[i][mask] = best;
        return best;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:17
        System.out.println(sol.connectTwoGroups(new int[][]{{15,96},{36,2}})
                + " " + sol.connectTwoGroupsDfs(new int[][]{{15,96},{36,2}}));
        // 示例 2:4
        System.out.println(sol.connectTwoGroups(new int[][]{{1,3,5},{4,1,1},{1,5,3}})
                + " " + sol.connectTwoGroupsDfs(new int[][]{{1,3,5},{4,1,1},{1,5,3}}));
        // 示例 3:10
        System.out.println(sol.connectTwoGroups(new int[][]{{2,5,1},{3,4,7},{8,1,2},{6,2,4},{3,8,8}})
                + " " + sol.connectTwoGroupsDfs(new int[][]{{2,5,1},{3,4,7},{8,1,2},{6,2,4},{3,8,8}}));
        // 附加:1x1
        System.out.println(sol.connectTwoGroups(new int[][]{{7}})
                + " " + sol.connectTwoGroupsDfs(new int[][]{{7}}));
    }
}

复杂度分析

复杂度:时间复杂度 O(size1 * size2 * 2^size2),空间复杂度 O(2^size2)(滚动)。

复杂度:时间复杂度 O(size1 * size2 * 2^size2),空间复杂度 O(size1 * 2^size2)。


1596. The Most Frequently Ordered Products for Each Customer(每位顾客最经常订购的商品)

题目类型:数据库(SQL)

题目描述

表:Customers

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| customer_id   | int     |
| name          | varchar |
+---------------+---------+
customer_id 是该表具有唯一值的列
该表包含所有顾客的信息

表:Orders

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| order_id      | int     |
| order_date    | date    |
| customer_id   | int     |
| product_id    | int     |
+---------------+---------+
order_id 是该表具有唯一值的列
该表包含顾客 customer_id 的订单信息
没有顾客会在一天内订购相同的商品** 多于一次**

表:Products

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| product_id    | int     |
| product_name  | varchar |
| price         | int     |
+---------------+---------+
product_id 是该表具有唯一值的列
该表包含了所有商品的信息

写一个解决方案,找到每一个顾客最经常订购的商品。

结果表单应该有每一位至少下过一次单的顾客 customer_id , 他最经常订购的商品的 product_id 和 product_name。

返回结果** 没有顺序要求**。

查询结果格式如下例所示。

示例 1:

复制代码
`**输入:**
Customers表:`
+-------------+-------+
| customer_id | name  |
+-------------+-------+
| 1           | Alice |
| 2           | Bob   |
| 3           | Tom   |
| 4           | Jerry |
| 5           | John  |
+-------------+-------+
`Orders表:`
+----------+------------+-------------+------------+
| order_id | order_date | customer_id | product_id |
+----------+------------+-------------+------------+
| 1        | 2020-07-31 | 1           | 1          |
| 2        | 2020-07-30 | 2           | 2          |
| 3        | 2020-08-29 | 3           | 3          |
| 4        | 2020-07-29 | 4           | 1          |
| 5        | 2020-06-10 | 1           | 2          |
| 6        | 2020-08-01 | 2           | 1          |
| 7        | 2020-08-01 | 3           | 3          |
| 8        | 2020-08-03 | 1           | 2          |
| 9        | 2020-08-07 | 2           | 3          |
| 10       | 2020-07-15 | 1           | 2          |
+----------+------------+-------------+------------+
`Products表:`
+------------+--------------+-------+
| product_id | product_name | price |
+------------+--------------+-------+
| 1          | keyboard     | 120   |
| 2          | mouse        | 80    |
| 3          | screen       | 600   |
| 4          | hard disk    | 450   |
+------------+--------------+-------+
**输出:**
+-------------+------------+--------------+
| customer_id | product_id | product_name |
+-------------+------------+--------------+
| 1           | 2          | mouse        |
| 2           | 1          | keyboard     |
| 2           | 2          | mouse        |
| 2           | 3          | screen       |
| 3           | 3          | screen       |
| 4           | 1          | keyboard     |
+-------------+------------+--------------+
**解释:**
Alice (customer 1) 三次订购鼠标, 一次订购键盘, 所以鼠标是 Alice 最经常订购的商品.
Bob (customer 2) 一次订购键盘, 一次订购鼠标, 一次订购显示器, 所以这些都是 Bob 最经常订购的商品.
Tom (customer 3) 只两次订购显示器, 所以显示器是 Tom 最经常订购的商品.
Jerry (customer 4) 只一次订购键盘, 所以键盘是 Jerry 最经常订购的商品.
John (customer 5) 没有订购过商品, 所以我们并没有把 John 包含在结果表中.

解题思路

思考

对每位顾客找出订购次数最多的商品,并列第一都要输出。先按人与商品计数,再在分组内取最大次数。

按 c u s t o m e r _ i d , p r o d u c t _ i d customer\_id,product\_id customer_id,product_id 分组后,用 R A N K RANK RANK 按次数降序编号,保留 r k = 1 rk=1 rk=1 的行,再连接商品表取出名称。 R A N K RANK RANK 保证并列最高频都留下。

考察窗口函数/自连接 + 排名:按客户分组统计各产品订单数,取每个客户订单量最大的产品(并列时按产品名最小)。

代码实现

sql 复制代码
# Write your MySQL query statement below
WITH
    T AS (
        SELECT
            customer_id,
            product_id,
            RANK() OVER (
                PARTITION BY customer_id
                ORDER BY COUNT(1) DESC
            ) AS rk
        FROM Orders
        GROUP BY 1, 2
    )
SELECT customer_id, product_id, product_name
FROM
    T
    JOIN Products USING (product_id)
WHERE rk = 1;

复杂度分析

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


1597. Build Binary Expression Tree From Infix Expression(根据中缀表达式构造二叉表达式树)

题目描述

二叉表达式树是一种表达算术表达式的二叉树。每个节点都有零个或两个子节点。

叶节点(0 个子节点)表示操作数,非叶节点(2 个子节点)表示运算符:'+'(加)、'-'(减)、'*'(乘)、'/'(除)。

对于每个运算符为 o 的非叶节点,对应的中缀表达式为 (A o B),其中 A 是左子树表达式,B 是右子树表达式。

给定一个中缀表达式字符串 s(含操作数、运算符、括号),返回一个有效的二叉表达式树,

其中序遍历序列对应表达式 s 消除括号后的序列。

注意:表达式的一般解析顺序适用:先括号、再乘除、后加减。操作数在 s 和树的中序遍历中出现顺序相同。

示例 1

复制代码
s = "3*4-2*5",输出 [-,*,*,3,4,2,5]。

示例 2

复制代码
s = "2-3/(5*2)+1",输出 [+,-,1,2,/,null,null,null,null,3,*,null,null,5,2]。

示例 3

复制代码
s = "1+2+3+4+5",输出 [+,+,5,+,4,null,null,+,3,null,null,1,2]。

约束:

  • 1 <= s.length <= 100;s 中包含数字和 '('、')'、'+'、'-'、'*'、'/';
  • s 中的操作数恰好是一位数字;s 是有效表达式。

解题思路

解法一:递归解析(主提交版本)

  • 经典"表达式转表达式树":维护全局位置指针,按优先级递归
    expr() -> term() ('+'|'-' term);term() -> factor() (''|'/' factor)*;factor() -> '(' expr ')' | 数字。
  • 由于加减乘除都是左结合,遇到同级运算符时新运算符作为新的根(即右结合处理会破坏结合性,
    需把同级连续运算符处理成"向左结合",这里用递归返回时主动把新运算符作为上层根、原根作左子树)。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。
    解法二:运算符栈 + 输出树栈(对照,转为后缀再建树)
  • 仿照"中缀转后缀"的调度场算法,用操作数栈(树节点栈)与运算符栈,
    遇到低优先级运算符时先弹出高优先级运算符建树,括号单独处理。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。

代码实现

java 复制代码
public class Solution {

    static class Node {
        char val;
        Node left;
        Node right;

        Node() {}

        Node(char val) {
            this.val = val;
        }

        Node(char val, Node left, Node right) {
            this.val = val;
            this.left = left;
            this.right = right;
        }
    }

    private int idx;

    public Node expTree(String s) {
        idx = 0;
        return parseExpr(s);
    }

    private Node parseExpr(String s) {
        Node left = parseTerm(s);
        while (idx < s.length() && (s.charAt(idx) == '+' || s.charAt(idx) == '-')) {
            char op = s.charAt(idx++);
            Node right = parseTerm(s);
            left = new Node(op, left, right); // 左结合:新节点作为根,左子树是先前的结果
        }
        return left;
    }

    private Node parseTerm(String s) {
        Node left = parseFactor(s);
        while (idx < s.length() && (s.charAt(idx) == '*' || s.charAt(idx) == '/')) {
            char op = s.charAt(idx++);
            Node right = parseFactor(s);
            left = new Node(op, left, right);
        }
        return left;
    }

    private Node parseFactor(String s) {
        if (s.charAt(idx) == '(') {
            idx++; // 跳过 '('
            Node node = parseExpr(s);
            idx++; // 跳过 ')'
            return node;
        }
        return new Node(s.charAt(idx++)); // 一位数字操作数
    }

    public Node expTreeShuntingYard(String s) {
        java.util.ArrayDeque<Node> nodes = new java.util.ArrayDeque<>();
        java.util.ArrayDeque<Character> ops = new java.util.ArrayDeque<>();
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            if (Character.isDigit(c)) {
                nodes.push(new Node(c));
            } else if (c == '(') {
                ops.push(c);
            } else if (c == ')') {
                while (ops.peek() != '(') {
                    nodes.push(build(ops.pop(), nodes.pop(), nodes.pop()));
                }
                ops.pop(); // 弹出 '('
            } else { // 运算符
                while (!ops.isEmpty() && ops.peek() != '(' && precedence(ops.peek()) >= precedence(c)) {
                    nodes.push(build(ops.pop(), nodes.pop(), nodes.pop()));
                }
                ops.push(c);
            }
        }
        while (!ops.isEmpty()) {
            nodes.push(build(ops.pop(), nodes.pop(), nodes.pop()));
        }
        return nodes.peek();
    }

    private Node build(char op, Node right, Node left) {
        return new Node(op, left, right);
    }

    private int precedence(char op) {
        switch (op) {
            case '+':
            case '-':
                return 1;
            case '*':
            case '/':
                return 2;
            default:
                return 0;
        }
    }

    private static void inorder(Node node, StringBuilder sb) {
        if (node == null) {
            return;
        }
        inorder(node.left, sb);
        sb.append(node.val);
        inorder(node.right, sb);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:中序遍历应为 "3*4-2*5"
        StringBuilder sb1 = new StringBuilder();
        inorder(sol.expTree("3*4-2*5"), sb1);
        System.out.println(sb1.toString());
        StringBuilder sb1b = new StringBuilder();
        inorder(sol.expTreeShuntingYard("3*4-2*5"), sb1b);
        System.out.println(sb1b.toString());

        // 示例 2:中序遍历应为 "2-3/5*2+1"(去除括号)
        StringBuilder sb2 = new StringBuilder();
        inorder(sol.expTree("2-3/(5*2)+1"), sb2);
        System.out.println(sb2.toString());
        StringBuilder sb2b = new StringBuilder();
        inorder(sol.expTreeShuntingYard("2-3/(5*2)+1"), sb2b);
        System.out.println(sb2b.toString());

        // 示例 3:中序遍历应为 "1+2+3+4+5"
        StringBuilder sb3 = new StringBuilder();
        inorder(sol.expTree("1+2+3+4+5"), sb3);
        System.out.println(sb3.toString());
        StringBuilder sb3b = new StringBuilder();
        inorder(sol.expTreeShuntingYard("1+2+3+4+5"), sb3b);
        System.out.println(sb3b.toString());

        // 附加:括号优先级 "(2*(3+4))-5" -> "2*3+4-5"
        StringBuilder sb4 = new StringBuilder();
        inorder(sol.expTree("(2*(3+4))-5"), sb4);
        System.out.println(sb4.toString());
        StringBuilder sb4b = new StringBuilder();
        inorder(sol.expTreeShuntingYard("(2*(3+4))-5"), sb4b);
        System.out.println(sb4b.toString());
    }
}

复杂度分析

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


1598. Crawler Log Folder(文件夹操作日志搜集器)

题目描述

LeetCode 文件系统会保存用户执行变更文件夹操作的日志记录:

  • ".../":移动到当前文件夹的父文件夹;如果已经在主文件夹下,则继续停留在当前文件夹。
  • "./":继续停留在当前文件夹。
  • "x/":移动到名为 x 的子文件夹中(题目保证 x 存在)。
    给一个字符串列表 logs,文件系统启动时位于主文件夹。执行完 logs 中的操作后,
    返回主文件夹所需的最小步数(即在主文件夹下的实际深度)。

示例 1

复制代码
logs = ["d1/","d2/","../","d21/","./"],输出 2。

示例 2

复制代码
logs = ["d1/","d2/","./","d3/","../","d31/"],输出 3。

示例 3

复制代码
logs = ["d1/","../","../","../"],输出 0。

解题思路

解法一:模拟栈求当前深度(主提交版本)

  • 用整型 depth 模拟当前路径深度:"./" 忽略;".../" depth = max(0, depth-1);其他 depth++。
    复杂度:时间复杂度 O(n),空间复杂度 O(1)。
    解法二:栈模拟(对照)
  • 用 List 模拟目录栈,推动作;最后返回栈大小。
    复杂度:时间复杂度 O(n),空间复杂度 O(n)。

代码实现

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

public class Solution {

    public int minOperations(String[] logs) {
        int depth = 0;
        for (String log : logs) {
            if (log.equals("../")) {
                depth = Math.max(0, depth - 1);
            } else if (log.equals("./")) {
                // 原地不动
            } else {
                depth++;
            }
        }
        return depth;
    }

    public int minOperationsStack(String[] logs) {
        List<String> stack = new ArrayList<>();
        for (String log : logs) {
            if (log.equals("../")) {
                if (!stack.isEmpty()) {
                    stack.remove(stack.size() - 1);
                }
            } else if (log.equals("./")) {
                // 原地不动
            } else {
                stack.add(log);
            }
        }
        return stack.size();
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:2
        System.out.println(sol.minOperations(new String[]{"d1/","d2/","../","d21/","./"})
                + " " + sol.minOperationsStack(new String[]{"d1/","d2/","../","d21/","./"}));
        // 示例 2:3
        System.out.println(sol.minOperations(new String[]{"d1/","d2/","./","d3/","../","d31/"})
                + " " + sol.minOperationsStack(new String[]{"d1/","d2/","./","d3/","../","d31/"}));
        // 示例 3:0
        System.out.println(sol.minOperations(new String[]{"d1/","../","../","../"})
                + " " + sol.minOperationsStack(new String[]{"d1/","../","../","../"}));
        // 附加:仅在父目录以上反复 ../
        System.out.println(sol.minOperations(new String[]{"../","d1/"})
                + " " + sol.minOperationsStack(new String[]{"../","d1/"}));
    }
}

复杂度分析

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

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


1599. Maximum Profit of Operating a Centennial Wheel(经营摩天轮的最大利润)

题目描述

摩天轮共有 4 个座舱,每个座舱最多容纳 4 位游客。可以逆时针轮转座舱,每次轮转支付运行成本 runningCost。

给定数组 customers,customers[i] 是第 i 次轮转之前到达的新游客数量。

每位游客在登上离地面最近的座舱前支付 boardingCost,一旦座舱再次抵达地面,他们就会离开。

如果有超过 4 位游客在等摩天轮,只有 4 位游客能登舱,其余等待下一次轮转。

可以随时停下摩天轮(即使在服务所有游客之前),停止后所有后续轮转免费。

返回最大化利润所需执行的最小轮转次数;如果不存在利润为正的方案,返回 -1。

示例 1

复制代码
customers = [8,3], boardingCost = 5, runningCost = 6,输出 3。

示例 2

复制代码
customers = [10,9,6], boardingCost = 6, runningCost = 4,输出 7。

示例 3

复制代码
customers = [3,4,0,5,1], boardingCost = 1, runningCost = 92,输出 -1。

约束:

  • 1 <= customers.length <= 10^5;0 <= customersi <= 50;1 <= boardingCost, runningCost <= 100。

解题思路

解法一:贪心模拟(主提交版本)

  • 每轮至多 4 人登舱:先上"新到的"再上"等待的"?实际规则是等待队列+FIFO,
    每轮登舱人数 take = min(4, waiting + arrivals),之后 waiting -= take。
  • 累加利润 profit += take * boardingCost - runningCost,若当前 profit 为历史最大则记录该轮数。
  • 轮转完所有 customers 后,若还有等待者,继续每轮按 4 人处理直到清空(即便后面轮转会产生更高利润)。
    复杂度:时间复杂度 O(n + waiting/4),空间复杂度 O(1)。
    解法二:同思路但显式队列(对照)
  • 用变量等待数显式模拟,逻辑与解法一相同,写法以"继续清空等待队列"为主。
    复杂度:时间复杂度 O(n + waiting/4),空间复杂度 O(1)。

代码实现

java 复制代码
public class Solution {

    public int minOperationsMaxProfit(int[] customers, int boardingCost, int runningCost) {
        int n = customers.length;
        int waiting = 0;
        int profit = 0;
        int bestProfit = 0;
        int bestTurns = -1;
        int turns = 0;
        int i = 0;
        while (i < n || waiting > 0) {
            if (i < n) {
                waiting += customers[i++];
            }
            int take = Math.min(4, waiting);
            waiting -= take;
            turns++;
            profit += take * boardingCost - runningCost;
            if (profit > bestProfit) {
                bestProfit = profit;
                bestTurns = turns;
            }
        }
        return bestTurns;
    }

    public int minOperationsMaxProfitTwoPass(int[] customers, int boardingCost, int runningCost) {
        int n = customers.length;
        int waiting = 0;
        int profit = 0;
        int bestProfit = 0;
        int bestTurns = -1;
        int turns = 0;
        for (int i = 0; i < n; i++) {
            waiting += customers[i];
            int take = Math.min(4, waiting);
            waiting -= take;
            turns++;
            profit += take * boardingCost - runningCost;
            if (profit > bestProfit) {
                bestProfit = profit;
                bestTurns = turns;
            }
        }
        // 清空等待队列
        while (waiting > 0) {
            int take = Math.min(4, waiting);
            waiting -= take;
            turns++;
            profit += take * boardingCost - runningCost;
            if (profit > bestProfit) {
                bestProfit = profit;
                bestTurns = turns;
            }
        }
        return bestTurns;
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        // 示例 1:3
        System.out.println(sol.minOperationsMaxProfit(new int[]{8,3}, 5, 6)
                + " " + sol.minOperationsMaxProfitTwoPass(new int[]{8,3}, 5, 6));
        // 示例 2:7
        System.out.println(sol.minOperationsMaxProfit(new int[]{10,9,6}, 6, 4)
                + " " + sol.minOperationsMaxProfitTwoPass(new int[]{10,9,6}, 6, 4));
        // 示例 3:-1
        System.out.println(sol.minOperationsMaxProfit(new int[]{3,4,0,5,1}, 1, 92)
                + " " + sol.minOperationsMaxProfitTwoPass(new int[]{3,4,0,5,1}, 1, 92));
        // 附加:全零
        System.out.println(sol.minOperationsMaxProfit(new int[]{0,0,0}, 3, 1)
                + " " + sol.minOperationsMaxProfitTwoPass(new int[]{0,0,0}, 3, 1));
        // 附加:单批超 4 人
        System.out.println(sol.minOperationsMaxProfit(new int[]{10}, 3, 5)
                + " " + sol.minOperationsMaxProfitTwoPass(new int[]{10}, 3, 5));
    }
}

复杂度分析

复杂度:时间复杂度 O(n + waiting/4),空间复杂度 O(1)。


1600. Throne Inheritance(王位继承顺序)

题目描述

一个王国里住着国王、他的孩子们、他的孙子们等等。每一个时间点,这个家庭里有人出生也有人死亡。

王国有一个明确规定的王位继承顺序,第一继承人总是国王自己。定义递归函数 Successor(x, curOrder):

  • 如果 x 没有孩子或所有 x 的孩子都在 curOrder 中:
    如果 x 是国王,返回 null;否则返回 Successor(x 的父亲, curOrder)。
  • 否则,返回 x 不在 curOrder 中最年长的孩子。
    请实现 ThroneInheritance 类:
  • ThroneInheritance(string kingName) 初始化对象,国王名字作为参数。
  • void birth(string parentName, string childName) 表示 parentName 新拥有了一个名为 childName 的孩子。
  • void death(string name) 表示名为 name 的人死亡。死亡不影响 Successor 函数与当前继承顺序,只标记死亡状态。
  • string[] getInheritanceOrder() 返回除去死亡人员的当前继承顺序列表。

示例

复制代码
初始化 king;birth(king, andy)、birth(king, bob)、birth(king, catherine)、birth(andy, matthew)、
birth(bob, alex)、birth(bob, asha);getInheritanceOrder() 返回
["king","andy","matthew","bob","alex","asha","catherine"];
death(bob);getInheritanceOrder() 返回
["king","andy","matthew","alex","asha","catherine"]。

约束:

  • 名字唯一;国王非直接死亡类型保证;最多 10^5 次 birth 和 death;最多调用 10 次 getInheritanceOrder。

解题思路

解法一:多叉树 DFS 前序(主提交版本)

  • 继承顺序本质是"多叉树的先根遍历":国王 -> 最年长子 -> 长子后代 -> 次子 -> ...
  • 用 Map<String, List> 存孩子列表(list 顺序即出生顺序);死亡用 HashSet 标记。
  • getInheritanceOrder() 每次从根 DFS 收集所有未死亡的人。
    复杂度:每次 getInheritanceOrder O(总人数),空间 O(总人数)。
    解法二:直接模拟 Successor 递推(对照)
  • 用"当前继承序列 + 游标"的迭代方式:从国王开始按 Successor 规则逐步生成下一名继承人。
    复杂度:同上 O(总人数)。

代码实现

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

public class Solution {

    static class ThroneInheritance {
        private final String king;
        private final Map<String, List<String>> children = new HashMap<>();
        private final HashSet<String> dead = new HashSet<>();

        public ThroneInheritance(String kingName) {
            this.king = kingName;
            children.put(kingName, new ArrayList<String>());
        }

        public void birth(String parentName, String childName) {
            children.computeIfAbsent(parentName, k -> new ArrayList<String>()).add(childName);
            children.putIfAbsent(childName, new ArrayList<String>());
        }

        public void death(String name) {
            dead.add(name);
        }

        public List<String> getInheritanceOrder() {
            List<String> order = new ArrayList<>();
            dfs(king, order);
            return order;
        }

        private void dfs(String person, List<String> order) {
            if (!dead.contains(person)) {
                order.add(person);
            }
            for (String child : children.get(person)) {
                dfs(child, order);
            }
        }
    }

    static class ThroneInheritanceSim {
        private final String king;
        private final Map<String, String> parent = new HashMap<>();
        private final Map<String, List<String>> children = new HashMap<>();
        private final HashSet<String> dead = new HashSet<>();

        public ThroneInheritanceSim(String kingName) {
            this.king = kingName;
            children.put(kingName, new ArrayList<String>());
        }

        public void birth(String parentName, String childName) {
            parent.put(childName, parentName);
            children.computeIfAbsent(parentName, k -> new ArrayList<String>()).add(childName);
            children.putIfAbsent(childName, new ArrayList<String>());
        }

        public void death(String name) {
            dead.add(name);
        }

        public List<String> getInheritanceOrder() {
            List<String> order = new ArrayList<>();
            order.add(king);
            String x = king;
            while (true) {
                String next = successor(x, order);
                if (next == null) {
                    break;
                }
                order.add(next);
                x = next;
            }
            // 过滤死亡人员
            List<String> result = new ArrayList<>();
            for (String p : order) {
                if (!dead.contains(p)) {
                    result.add(p);
                }
            }
            return result;
        }

        private String successor(String x, List<String> order) {
            boolean hasUnorderedChild = false;
            String oldestUnordered = null;
            for (String c : children.get(x)) {
                if (!order.contains(c)) {
                    hasUnorderedChild = true;
                    oldestUnordered = c;
                    break;
                }
            }
            if (!hasUnorderedChild) {
                if (x.equals(king)) {
                    return null;
                }
                return successor(parent.get(x), order);
            }
            return oldestUnordered;
        }
    }

    public static void main(String[] args) {
        // 解法一示例
        ThroneInheritance t = new ThroneInheritance("king");
        t.birth("king", "andy");
        t.birth("king", "bob");
        t.birth("king", "catherine");
        t.birth("andy", "matthew");
        t.birth("bob", "alex");
        t.birth("bob", "asha");
        System.out.println(t.getInheritanceOrder());
        t.death("bob");
        System.out.println(t.getInheritanceOrder());

        // 解法二示例,行为应一致
        ThroneInheritanceSim t2 = new ThroneInheritanceSim("king");
        t2.birth("king", "andy");
        t2.birth("king", "bob");
        t2.birth("king", "catherine");
        t2.birth("andy", "matthew");
        t2.birth("bob", "alex");
        t2.birth("bob", "asha");
        System.out.println(t2.getInheritanceOrder());
        t2.death("bob");
        System.out.println(t2.getInheritanceOrder());
    }
}

复杂度分析

复杂度:每次 getInheritanceOrder O(总人数),空间 O(总人数)。

复杂度:同上 O(总人数)。

相关推荐
香蕉大香蕉(06敬畏市场)1 小时前
MySQL 单点详解(06)
java·数据库·mysql·博客·实习
lqbzlll1 小时前
Spring Boot 开启虚拟线程后接口反而变慢了?4 个排查方向和对应修法
java·spring boot·后端
七夜zippoe1 小时前
LangChain4j vs Spring AI:Java AI 框架怎么选(对比表)
java·人工智能·spring·ai·langchain4j
悠仁さん1 小时前
【C++】set map 使用
java·前端·c++
yuniko-n2 小时前
【JUC】Thread 内存泄露
java
freejackman2 小时前
【架构篇】双遍历 + 多栈协同:手撸 SQL 血缘引擎的架构思考
java·大数据·数据库·hive·sql·架构·spark
企业数字化笔记2 小时前
业务单据怎样接入流程中心?启动条件、实例关联与失败回退
java·spring boot·mysql
挖掘狂人2 小时前
同一个端口能不能被两个程序同时监听?
java·后端·程序员
codigger2 小时前
同一个端口能不能被两个程序同时监听?
java·后端·程序员·编程