LeetCode1141~1160题目解析

LeetCode 1141-1160. 题解合集

文章目录


1141. User Activity for the Past 30 Days I(过去 30 天的用户活动 I)

题目类型:数据库(SQL)

题目描述

表:Activity

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| user_id       | int     |
| session_id    | int     |
| activity_date | date    |
| activity_type | enum    |
+---------------+---------+
该表可能包含重复数据。
activity_type 列是 ENUM(category) 类型, 从 ('open_session', 'end_session', 'scroll_down', 'send_message') 取值。
该表记录社交媒体网站的用户活动。
注意,每个会话只属于一个用户。

编写解决方案,统计截至 2019-07-27(包含2019-07-27),近** **30 天的每日活跃用户数(当天只要有一条活动记录,即为活跃用户)。

以 任意顺序 返回结果表。

结果示例如下。

注意 :('open_session','end_session','scroll_down','send_message')中的任何活动将被视为用户在某一天活跃的有效活动。

示例 1:

复制代码
**输入:**
Activity table:
+---------+------------+---------------+---------------+
| user_id | session_id | activity_date | activity_type |
+---------+------------+---------------+---------------+
| 1       | 1          | 2019-07-20    | open_session  |
| 1       | 1          | 2019-07-20    | scroll_down   |
| 1       | 1          | 2019-07-20    | end_session   |
| 2       | 4          | 2019-07-20    | open_session  |
| 2       | 4          | 2019-07-21    | send_message  |
| 2       | 4          | 2019-07-21    | end_session   |
| 3       | 2          | 2019-07-21    | open_session  |
| 3       | 2          | 2019-07-21    | send_message  |
| 3       | 2          | 2019-07-21    | end_session   |
| 4       | 3          | 2019-06-25    | open_session  |
| 4       | 3          | 2019-06-25    | end_session   |
+---------+------------+---------------+---------------+
**输出:**
+------------+--------------+ 
| day        | active_users |
+------------+--------------+ 
| 2019-07-20 | 2            |
| 2019-07-21 | 2            |
+------------+--------------+ **
解释:**注意非活跃用户的记录不需要展示。

解题思路

思考

统计截止 2019-07-27 的 30 30 30 天内每日去重用户数。WHERE 同时约束上界与日差,再按日期 GROUP BY 并对 user_id 做 COUNT(DISTINCT ...)。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT activity_date AS day, COUNT(DISTINCT user_id) AS active_users
FROM Activity
WHERE activity_date <= '2019-07-27' AND DATEDIFF('2019-07-27', activity_date) < 30
GROUP BY 1;

复杂度分析

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


1142. User Activity for the Past 30 Days II(过去 30 天的用户活动 II)

题目类型:数据库(SQL)

题目描述

Activity 表:

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| user_id       | int     |
| session_id    | int     |
| activity_date | date    |
| activity_type | enum    |
+---------------+---------+
该表没有主键,它可能有重复的行。
activity_type 列是 ENUM 类型,可以取(" open_session"," end_session"," scroll_down"," send_message")四种活动类型之一。
该表显示了社交媒体网站的用户活动。
请注意,每个会话只属于一个用户。

编写解决方案,统计截至 2019-07-27(含)的 30 天内每个用户的平均会话数,四舍五入到小数点后两位。只统计那些会话期间用户至少进行一项活动的有效会话。

结果格式如下例所示。

示例:

复制代码
**输入:**
Activity 表:
+---------+------------+---------------+---------------+
| user_id | session_id | activity_date | activity_type |
+---------+------------+---------------+---------------+
| 1       | 1          | 2019-07-20    | open_session  |
| 1       | 1          | 2019-07-20    | scroll_down   |
| 1       | 1          | 2019-07-20    | end_session   |
| 2       | 4          | 2019-07-20    | open_session  |
| 2       | 4          | 2019-07-21    | send_message  |
| 2       | 4          | 2019-07-21    | end_session   |
| 3       | 2          | 2019-07-21    | open_session  |
| 3       | 2          | 2019-07-21    | send_message  |
| 3       | 2          | 2019-07-21    | end_session   |
| 3       | 5          | 2019-07-21    | open_session  |
| 3       | 5          | 2019-07-21    | scroll_down   |
| 3       | 5          | 2019-07-21    | end_session   |
| 4       | 3          | 2019-06-25    | open_session  |
| 4       | 3          | 2019-06-25    | end_session   |
+---------+------------+---------------+---------------+
**输出:**
+---------------------------+ 
| average_sessions_per_user |
+---------------------------+ 
| 1.33                      |
+---------------------------+
**解释:**用户 1 和 2 每人在过去 30 天有 1 个会话,而用户 3 有 2 个会话。所以平均是 (1 + 1 + 2) / 3 = 1.33 。

解题思路

思考

先按用户统计窗口内不同 session_id 个数,再对这些会话数取平均,空表时用 IFNULL 得到 0 0 0。窗口条件与上一题相同。

代码实现

sql 复制代码
# Write your MySQL query statement below
WITH
    T AS (
        SELECT
            COUNT(DISTINCT session_id) AS sessions
        FROM Activity
        WHERE activity_date <= '2019-07-27' AND DATEDIFF('2019-07-27', activity_date) < 30
        GROUP BY user_id
    )
SELECT IFNULL(ROUND(AVG(sessions), 2), 0) AS average_sessions_per_user
FROM T;

复杂度分析

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


1143. Longest Common Subsequence(最长公共子序列)

题目描述

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列,返回 0。

一个字符串的子序列是指:在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是。两个字符串的公共子序列是这两个字符串所共同拥有的子序列。

示例 1

复制代码
text1 = "abcde", text2 = "ace" → 3

示例 2

复制代码
text1 = "abc", text2 = "abc" → 3

示例 3

复制代码
text1 = "abc", text2 = "def" → 0

约束:

  • 1 <= text1.length, text2.length <= 1000;text1 和 text2 仅由小写英文字符组成。

解题思路

解法一:二维动态规划

  1. dpij 表示 text10...i-1 与 text20...j-1 的 LCS 长度;
  2. 若 text1i-1 == text2j-1:dpij = dpi-1j-1 + 1;
    否则 dpij = max(dpi-1j, dpij-1);
  3. 滚动优化可用两行数组省空间,但为清晰先用完整二维表。
    复杂度:时间 O(mn),空间 O(m n)。
    解法二:滚动数组优化空间
  4. 状态转移只依赖上一行与当前行,用一维数组滚动即可;
  5. 注意左上角值需先用变量缓存(因为会被覆盖)。
    复杂度:时间 O(m*n),空间 O(min(m,n))。

代码实现

java 复制代码
public class Solution {

    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m + 1][n + 1];
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[m][n];
    }

    public int longestCommonSubsequenceRolling(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[] dp = new int[n + 1];
        for (int i = 1; i <= m; i++) {
            int prev = 0; // dp[i-1][j-1]
            for (int j = 1; j <= n; j++) {
                int tmp = dp[j]; // dp[i-1][j](当前行覆盖前)
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    dp[j] = prev + 1;
                } else {
                    dp[j] = Math.max(dp[j], dp[j - 1]); // dp[i-1][j] vs dp[i][j-1]
                }
                prev = tmp;
            }
        }
        return dp[n];
    }

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

        System.out.println("示例1 解法一: " + s.longestCommonSubsequence("abcde", "ace") + "(期望 3)");
        System.out.println("示例1 解法二: " + s.longestCommonSubsequenceRolling("abcde", "ace") + "(期望 3)");

        System.out.println("示例2 解法一: " + s.longestCommonSubsequence("abc", "abc") + "(期望 3)");
        System.out.println("示例2 解法二: " + s.longestCommonSubsequenceRolling("abc", "abc") + "(期望 3)");

        System.out.println("示例3 解法一: " + s.longestCommonSubsequence("abc", "def") + "(期望 0)");
        System.out.println("示例3 解法二: " + s.longestCommonSubsequenceRolling("abc", "def") + "(期望 0)");
    }
}

复杂度分析

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

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


1144. Decrease Elements To Make Array Zigzag(递减元素使数组呈锯齿状)

题目描述

给你一个整数数组 nums,每次操作会从中选择一个元素并将该元素的值减少 1。

如果符合下列情况之一,则数组 A 就是锯齿数组:

  • 每个偶数索引对应的元素都大于相邻的元素,即 A0 > A1 < A2 > A3 < ...
  • 或者,每个奇数索引对应的元素都大于相邻的元素,即 A0 < A1 > A2 < A3 < ...
    返回将数组 nums 转换为锯齿数组所需的最小操作次数。

示例 1

复制代码
nums = [1,2,3] → 2(把 2 递减到 0,或把 3 递减到 1)

示例 2

复制代码
nums = [9,6,1,6,2] → 4

约束:

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

解题思路

解法一:贪心 + 两种模式取最小

  1. 锯齿只可能有两种形态:偶数位是"峰"或奇数位是"峰";
  2. 对每一种形态,遍历需要是"谷"的位置 k:若 numsk >= numsk-1 或 numsk >= numsk+1,
    则必须把 numsk 降到 min(邻居) - 1,操作数为 numsk - (min(邻居)-1),累加;
  3. 取两种形态操作数的最小值。
    注意:直接对原数组副本做减法累加更稳妥(因为一次只减 1,且贪心每次把谷降到最低即可,
    不会影响其它位置------因为谷只与邻居比较)。
    复杂度:时间 O(n),空间 O(1)(额外 O(n) 副本)。
    解法二:模拟逐次递减
  4. 直接按规则循环:找到不满足条件的谷,循环减 1 直到满足;统计总操作数;
  5. 两种模式分别模拟,取较小。适合演示"每次操作减 1"的字面含义,但复杂度较高。
    复杂度:时间 O(n * max(nums)),空间 O(1);本实现仍是逐位计算降值(等价于快速模拟)。

代码实现

java 复制代码
public class Solution {

    public int movesToMakeZigzag(int[] nums) {
        int n = nums.length;
        // 模式 A:偶数位为峰(奇数位是谷)
        int costA = 0;
        int[] a = nums.clone();
        for (int i = 1; i < n; i += 2) { // 奇数位是谷
            if (a[i] >= a[i - 1]) {
                int delta = a[i] - (a[i - 1] - 1);
                costA += delta;
                a[i] -= delta;
            }
            if (i + 1 < n && a[i] >= a[i + 1]) {
                int delta = a[i] - (a[i + 1] - 1);
                costA += delta;
                a[i] -= delta;
            }
        }
        // 模式 B:奇数位为峰(偶数位是谷)
        int costB = 0;
        int[] b = nums.clone();
        for (int i = 0; i < n; i += 2) { // 偶数位是谷
            if (i > 0 && b[i] >= b[i - 1]) {
                int delta = b[i] - (b[i - 1] - 1);
                costB += delta;
                b[i] -= delta;
            }
            if (i + 1 < n && b[i] >= b[i + 1]) {
                int delta = b[i] - (b[i + 1] - 1);
                costB += delta;
                b[i] -= delta;
            }
        }
        return Math.min(costA, costB);
    }

    public int movesToMakeZigzagFormula(int[] nums) {
        return Math.min(calc(nums, 0), calc(nums, 1));
    }

    private int calc(int[] nums, int start) {
        int n = nums.length;
        int cost = 0;
        for (int i = start; i < n; i += 2) {
            int target = Integer.MAX_VALUE;
            if (i > 0) target = Math.min(target, nums[i - 1]);
            if (i + 1 < n) target = Math.min(target, nums[i + 1]);
            if (nums[i] >= target) cost += nums[i] - (target - 1);
        }
        return cost;
    }

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

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

        int[] n2 = {9,6,1,6,2};
        System.out.println("示例2 解法一: " + s.movesToMakeZigzag(n2) + "(期望 4)");
        System.out.println("示例2 解法二: " + s.movesToMakeZigzagFormula(n2) + "(期望 4)");
    }
}

复杂度分析

复杂度:时间 O(n),空间 O(1)(额外 O(n) 副本)。

复杂度:时间 O(n * max(nums)),空间 O(1);本实现仍是逐位计算降值(等价于快速模拟)。


1145. Binary Tree Coloring Game(二叉树着色游戏)

题目描述

有两位极客玩家参与了一场「二叉树着色」的游戏。游戏中,给出二叉树的根节点 root,树上总共有 n 个节点,

且 n 为奇数,其中每个节点上的值从 1 到 n 各不相同。

最开始时:「一号」玩家从 1, `n` 中取一个值 x,「二号」玩家也从 1, `n` 中取一个值 y 且 y != x。

「一号」玩家给值为 x 的节点染上红色,而「二号」玩家给值为 y 的节点染上蓝色。

之后两位玩家轮流操作,「一号」玩家先手。每一回合,玩家选择一个被他染过色的节点,

将所选节点一个未着色的邻节点(左右子节点或父节点)进行染色。

如果当前玩家无法找到这样的节点来染色时,其回合就会被跳过。若两个玩家都没有可以染色的节点时,游戏结束。

着色节点最多的那位玩家获得胜利。

现在,假设你是「二号」玩家,根据所给出的输入,假如存在一个 y 值可以确保你赢得这场游戏,则返回 true;否则返回 false。

示例 1

复制代码
root = [1,2,3,4,5,6,7,8,9,10,11], n = 11, x = 3 → true(二号选值为 2 的节点)

示例 2

复制代码
root = [1,2,3], n = 3, x = 1 → false

约束:

  • 树中节点数目为 n;1 <= x <= n <= 100;n 是奇数;1 <= Node.val <= n;树中所有值互不相同。

解题思路

解法一:树形 DFS 计数 + 三分割分析

  1. 一号先手染色节点 x 之后,整棵树被 x 分成三块互不相通的区域:
  • x 的左子树
  • x 的右子树
  • x 的父节点侧的其余部分(n - 左子树大小 - 右子树大小 - 1)
  1. 二号第一次选 y 时,只要 y 落在上述某一块中,二号就能把这一整块染成蓝色
    (一号无法跨过 x 进入该块------x 已被红色锁定,谁也无法再染 x 的邻居中属于对方区域的格子?
    关键性质:一号从 x 出发只能扩展 x 所在连通区域,二号选在某一侧后也只会扩展那一侧;
    最终二号能获得的节点数恰好是该块的全部节点);
  2. 因此二号能赢 ⇔ 存在一块区域的节点数 > n / 2。
    复杂度:时间 O(n),空间 O(h)。
    解法二:BFS/DFS 找 x 节点再统计三块
  3. 先用一次遍历定位值为 x 的节点;
  4. 计算其左子树大小、右子树大小与父侧大小;
  5. 与解法一相同,判定最大值是否超过 n/2。
    复杂度:时间 O(n),空间 O(n)。

代码实现

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

public class Solution {

    public boolean btreeGameWinningMove(TreeNode root, int n, int x) {
        TreeNode nodeX = find(root, x);      // 定位 x 节点
        int left = sizeOf(nodeX.left);       // x 左子树大小
        int right = sizeOf(nodeX.right);     // x 右子树大小
        int top = n - left - right - 1;      // x 父侧其余节点数
        return Math.max(top, Math.max(left, right)) > n / 2;
    }

    private int sizeOf(TreeNode node) {
        if (node == null) return 0;
        return 1 + sizeOf(node.left) + sizeOf(node.right);
    }

    public boolean btreeGameWinningMoveBfs(TreeNode root, int n, int x) {
        TreeNode nodeX = find(root, x);
        int l = sizeOf(nodeX.left);
        int r = sizeOf(nodeX.right);
        int top = n - l - r - 1;
        return Math.max(top, Math.max(l, r)) > n / 2;
    }

    private TreeNode find(TreeNode node, int x) {
        if (node == null) return null;
        if (node.val == x) return node;
        TreeNode left = find(node.left, x);
        if (left != null) return left;
        return find(node.right, x);
    }

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

        // 示例 1:[1,2,3,4,5,6,7,8,9,10,11], n=11, x=3
        TreeNode r1 = build(new Integer[]{1,2,3,4,5,6,7,8,9,10,11});
        System.out.println("示例1 解法一: " + s.btreeGameWinningMove(r1, 11, 3) + "(期望 true)");
        System.out.println("示例1 解法二: " + s.btreeGameWinningMoveBfs(r1, 11, 3) + "(期望 true)");

        // 示例 2:[1,2,3], n=3, x=1
        TreeNode r2 = build(new Integer[]{1,2,3});
        System.out.println("示例2 解法一: " + s.btreeGameWinningMove(r2, 3, 1) + "(期望 false)");
        System.out.println("示例2 解法二: " + s.btreeGameWinningMoveBfs(r2, 3, 1) + "(期望 false)");
    }
}

复杂度分析

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

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


1146. Snapshot Array(快照数组)

题目描述

实现支持下列接口的「快照数组」- SnapshotArray:

  • SnapshotArray(int length):初始化一个与指定长度相等的类数组数据结构,初始时每个元素都等于 0。
  • void set(index, val):会将指定索引 index 处的元素设置为 val。
  • int snap():获取该数组的快照,并返回快照的编号 snap_id(快照号是调用 snap() 的总次数减去 1)。
  • int get(index, snap_id):根据指定的 snap_id 选择快照,并返回该快照指定索引 index 的值。

示例

复制代码
["SnapshotArray","set","snap","set","get"]
[[3],[0,5],[],[0,6],[0,0]]
  • 输出 :`null`,`null`,0,`null`,5
    约束:
  • 1 <= length <= 50000;最多进行 50000 次 set/snap/get 调用;
  • 0 <= index < length;0 <= snap_id < snap() 总次数;0 <= val <= 10^9。

解题思路

解法一:每个索引一份"变更历史" + 二分查找

  1. 对每个 index 维护一个列表,按时间顺序记录 (snap_id, val) 的变更;
  2. set 时如果该 index 最近的变更就是当前 snap_id,直接覆盖 val(无谓记录);
    否则追加 (当前 snap_id, val);
  3. get(index, snap_id) 时,在该 index 的历史列表中二分查找最后一个 snap_id <= 目标快照号
    的变更,返回其 val;在此之前(或无记录)值为 0。
    复杂度:时间 set O(1)、snap O(1)、get O(log 变更数);空间 O(变更总数)。
    解法二:全量快照列表
  4. 每次 snap 时把当前数组拷贝一份存到快照列表;
  5. get 直接返回快照列表snap_idindex。
  6. 实现直观,但空间 O(snap 数 * length),仅适合小数据演示。
    复杂度:时间 snap O(length)、get O(1);空间 O(snap 数 * length)。

代码实现

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

public class Solution {

    public static class SnapshotArray {
        private int snapId = 0;
        // 每个索引的一个历史记录列表:[snap_id, val]
        private List<int[]>[] hist;
        // 每个索引在当前快照中的值缓存,用于 set 合并
        private int[] cur;

        @SuppressWarnings("unchecked")
        public SnapshotArray(int length) {
            hist = new List[length];
            for (int i = 0; i < length; i++) {
                hist[i] = new ArrayList<>();
                hist[i].add(new int[]{0, 0}); // 初始快照 0 值为 0
            }
            cur = new int[length];
        }

        public void set(int index, int val) {
            // 若最后一次记录就是当前 snap_id,覆盖(不产生新记录)
            List<int[]> list = hist[index];
            int lastId = list.get(list.size() - 1)[0];
            if (lastId == snapId) {
                list.get(list.size() - 1)[1] = val;
            } else {
                list.add(new int[]{snapId, val});
            }
            cur[index] = val;
        }

        public int snap() {
            return snapId++;
        }

        public int get(int index, int snap_id) {
            List<int[]> list = hist[index];
            // 二分找最后一个 snap_id <= 目标 的记录
            int lo = 0, hi = list.size() - 1, ans = 0;
            while (lo <= hi) {
                int mid = (lo + hi) >>> 1;
                if (list.get(mid)[0] <= snap_id) {
                    ans = mid;
                    lo = mid + 1;
                } else {
                    hi = mid - 1;
                }
            }
            return list.get(ans)[1];
        }
    }

    public static class SnapshotArrayFull {
        private int snapId = 0;
        private final List<int[]> shots = new ArrayList<>();
        private int[] arr;

        public SnapshotArrayFull(int length) {
            arr = new int[length];
        }

        public void set(int index, int val) {
            arr[index] = val;
        }

        public int snap() {
            shots.add(arr.clone());
            return snapId++;
        }

        public int get(int index, int snap_id) {
            return shots.get(snap_id)[index];
        }
    }

    public static void main(String[] args) {
        // 示例流程
        SnapshotArray sa = new SnapshotArray(3);
        sa.set(0, 5);
        int s0 = sa.snap();
        sa.set(0, 6);
        int g0 = sa.get(0, s0);
        System.out.println("示例 解法一: snap=" + s0 + "(期望 0) get=" + g0 + "(期望 5)");

        SnapshotArrayFull sb = new SnapshotArrayFull(3);
        sb.set(0, 5);
        int s1 = sb.snap();
        sb.set(0, 6);
        System.out.println("示例 解法二: snap=" + s1 + "(期望 0) get=" + sb.get(0, s1) + "(期望 5)");

        // 附加用例
        SnapshotArray sc = new SnapshotArray(1);
        sc.set(0, 1);
        int ss0 = sc.snap();
        int ss1 = sc.snap();
        sc.set(0, 3);
        int ss2 = sc.snap();
        System.out.println("附加: snap0=" + ss0 + " snap1=" + ss1 + " snap2=" + ss2
                + " get(0,0)=" + sc.get(0, 0) + " get(0,1)=" + sc.get(0, 1) + " get(0,2)=" + sc.get(0, 2));
    }
}

复杂度分析

复杂度:时间 set O(1)、snap O(1)、get O(log 变更数);空间 O(变更总数)。

  1. 实现直观,但空间 O(snap 数 * length),仅适合小数据演示。

复杂度:时间 snap O(length)、get O(1);空间 O(snap 数 * length)。


1147. Longest Chunked Palindrome Decomposition(段式回文)

题目描述

你会得到一个字符串 text。你应该把它分成 k 个子字符串 (subtext1, subtext2, ..., subtextk),要求满足:

  • subtext_i 是非空字符串
  • 所有子字符串的连接等于 text(即 subtext1 + subtext2 + ... + subtextk == text)
  • 对于所有 i 的有效值(1 <= i <= k),subtext_i == subtext_{k-i+1} 均成立(对称相等)
    返回 k 可能的最大值。

示例 1

复制代码
text = "ghiabcdefhelloadamhelloabcdefghi" → 7((ghi)(abcdef)(hello)(adam)(hello)(abcdef)(ghi))

示例 2

复制代码
text = "merchant" → 1

示例 3

复制代码
text = "antaprezatepzapreanta" → 11

约束:

  • 1 <= text.length <= 1000;text 仅由小写英文字符组成。

解题思路

解法一:双端贪心

  1. 维护左右两个游标,贪心地在两端寻找最短的对称段:
    固定左起点 l,从右往左找最小的 r 使 textl...r == 对称位置等长子串;
  2. 每找到一组对称段,k 加 2(首尾各一段),继续处理中间剩余部分;
  3. 若最后剩余单个子串(长度 > 0),k 加 1。
    复杂度:时间 O(n^2)(每个位置可能比较 O(n) 子串),空间 O(1)(不含比较临时串的额外开销,substring 会复制)。
    解法二:递归分治
  4. 递归函数 solve(s):返回 s 能切成的最多段数;
  5. 枚举 k = 1...s.length/2,若前缀 s0...k-1 == 后缀 slen-k...len-1,则
    答案 = 2 + solve(sk ... len-k-1);否则返回 1;取最大的 k 得到最优;
  6. 用记忆化避免重复计算重叠子问题(实际字符串不同,可无需缓存,这里加缓存避免 TLE 更保险)。
    复杂度:时间 O(n^2)(枚举 + 比较),空间 O(n)(递归栈 + 缓存)。

代码实现

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

public class Solution {

    public int longestDecomposition(String text) {
        int n = text.length();
        int ans = 0;
        int l = 0, r = n - 1;
        while (l <= r) {
            if (l == r) { ans++; break; }          // 剩单个字符也算一段
            int len = 1;
            boolean found = false;
            while (l + len - 1 < r - len + 1) {
                // 比较前缀 text[l..l+len-1] 与后缀 text[r-len+1..r]
                if (eq(text, l, r - len + 1, len)) {
                    ans += 2;
                    l += len;
                    r -= len;
                    found = true;
                    break;
                }
                len++;
            }
            if (!found) { // 整个剩余部分作为一段
                ans++;
                break;
            }
        }
        return ans;
    }

    private boolean eq(String s, int i, int j, int len) {
        for (int k = 0; k < len; k++) {
            if (s.charAt(i + k) != s.charAt(j + k)) return false;
        }
        return true;
    }

    public int longestDecompositionRec(String text) {
        return rec(text);
    }

    private int rec(String s) {
        int n = s.length();
        if (n == 0) return 0;
        for (int k = 1; k <= n / 2; k++) {
            String pre = s.substring(0, k);
            String suf = s.substring(n - k);
            if (pre.equals(suf)) {
                return 2 + rec(s.substring(k, n - k));
            }
        }
        return 1;
    }

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

        String t1 = "ghiabcdefhelloadamhelloabcdefghi";
        System.out.println("示例1 解法一: " + s.longestDecomposition(t1) + "(期望 7)");
        System.out.println("示例1 解法二: " + s.longestDecompositionRec(t1) + "(期望 7)");

        String t2 = "merchant";
        System.out.println("示例2 解法一: " + s.longestDecomposition(t2) + "(期望 1)");
        System.out.println("示例2 解法二: " + s.longestDecompositionRec(t2) + "(期望 1)");

        String t3 = "antaprezatepzapreanta";
        System.out.println("示例3 解法一: " + s.longestDecomposition(t3) + "(期望 11)");
        System.out.println("示例3 解法二: " + s.longestDecompositionRec(t3) + "(期望 11)");

        String t4 = "aaa";
        System.out.println("aaa 解法一: " + s.longestDecomposition(t4) + "(期望 3)");
        System.out.println("aaa 解法二: " + s.longestDecompositionRec(t4) + "(期望 3)");
    }
}

复杂度分析

复杂度:时间 O(n^2)(每个位置可能比较 O(n) 子串),空间 O(1)(不含比较临时串的额外开销,substring 会复制)。

复杂度:时间 O(n^2)(枚举 + 比较),空间 O(n)(递归栈 + 缓存)。


1148. Article Views I(文章浏览 I)

题目类型:数据库(SQL)

题目描述

Views 表:

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| article_id    | int     |
| author_id     | int     |
| viewer_id     | int     |
| view_date     | date    |
+---------------+---------+
此表可能会存在重复行。(换句话说,在 SQL 中这个表没有主键)
此表的每一行都表示某人在某天浏览了某位作者的某篇文章。
请注意,同一人的 author_id 和 viewer_id 是相同的。

请查询出所有浏览过自己文章的作者。

结果按照作者的 id 升序排列。

查询结果的格式如下所示:

示例 1:

复制代码
**输入:**
Views 表:
+------------+-----------+-----------+------------+
| article_id | author_id | viewer_id | view_date  |
+------------+-----------+-----------+------------+
| 1          | 3         | 5         | 2019-08-01 |
| 1          | 3         | 6         | 2019-08-02 |
| 2          | 7         | 7         | 2019-08-01 |
| 2          | 7         | 6         | 2019-08-02 |
| 4          | 7         | 1         | 2019-07-22 |
| 3          | 4         | 4         | 2019-07-21 |
| 3          | 4         | 4         | 2019-07-21 |
+------------+-----------+-----------+------------+

**输出:**
+------+
| id   |
+------+
| 4    |
| 7    |
+------+

解题思路

思考

作者阅读自己文章即 author_id = viewer_id。WHERE 筛出这些行,DISTINCT 去掉同一作者的重复浏览,再按 id 排序。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT DISTINCT author_id AS id
FROM Views
WHERE author_id = viewer_id
ORDER BY 1;

复杂度分析

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


1149. Article Views II(文章浏览 II)

题目类型:数据库(SQL)

题目描述

表: Views

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| article_id    | int     |
| author_id     | int     |
| viewer_id     | int     |
| view_date     | date    |
+---------------+---------+
此表可能会存在重复行。
此表的每一行都表示某人在某天浏览了某位作者的某篇文章。 
请注意,同一人的 author_id 和 viewer_id 是相同的。

编写解决方案来找出在同一天阅读至少两篇文章的人。

结果按照 id 升序排序。

结果的格式如下。

示例 1:

复制代码
**输入:**
Views 表:
+------------+-----------+-----------+------------+
| article_id | author_id | viewer_id | view_date  |
+------------+-----------+-----------+------------+
| 1          | 3         | 5         | 2019-08-01 |
| 3          | 4         | 5         | 2019-08-01 |
| 1          | 3         | 6         | 2019-08-02 |
| 2          | 7         | 7         | 2019-08-01 |
| 2          | 7         | 6         | 2019-08-02 |
| 4          | 7         | 1         | 2019-07-22 |
| 3          | 4         | 4         | 2019-07-21 |
| 3          | 4         | 4         | 2019-07-21 |
+------------+-----------+-----------+------------+
**输出:**
+------+
| id   |
+------+
| 5    |
| 6    |
+------+

解题思路

思考

同一天浏览不少于两篇不同文章的人。按 viewer_id, view_date 分组,HAVING COUNT(DISTINCT article_id) > 1 即满足,外层再 DISTINCT 观看者以免跨日重复。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT DISTINCT viewer_id AS id
FROM Views
GROUP BY viewer_id, view_date
HAVING COUNT(DISTINCT article_id) > 1
ORDER BY 1;

复杂度分析

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


1150. Check If a Number Is Majority Element in a Sorted Array(检查一个数是否在数组中占绝大多数)

题目描述

给出一个按非递减顺序排列的数组 nums,和一个目标数值 target。假如数组 nums 中绝大多数元素的数值都等于 target,

则返回 true,否则请返回 false。所谓占绝大多数,是指在长度为 N 的数组中出现必须超过 N/2 次。

示例 1

复制代码
nums = [2,4,5,5,5,5,5,6,6], target = 5 → true(5 出现 5 次 > 9/2)

示例 2

复制代码
nums = [10,100,101,101], target = 101 → false(2 次 = 4/2 不满足)

约束:

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

解题思路

解法一:二分查找下界与上界

  1. 数组有序,用二分找到第一个 >= target 的位置 first 和第一个 > target 的位置 last;
  2. target 出现次数 = last - first,判断是否 > n / 2。
    复杂度:时间 O(log n),空间 O(1)。
    解法二:线性扫描计数
  3. 直接遍历统计 target 出现次数;
  4. 判断 > n / 2。小数据下简单直观。
    复杂度:时间 O(n),空间 O(1)。

代码实现

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

public class Solution {

    public boolean isMajorityElement(int[] nums, int target) {
        int n = nums.length;
        int first = lowerBound(nums, target);
        if (first == n || nums[first] != target) return false;
        int last = upperBound(nums, target);
        return (last - first) > n / 2;
    }

    private int lowerBound(int[] nums, int target) {
        int lo = 0, hi = nums.length;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (nums[mid] >= target) hi = mid;
            else lo = mid + 1;
        }
        return lo;
    }

    private int upperBound(int[] nums, int target) {
        int lo = 0, hi = nums.length;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (nums[mid] > target) hi = mid;
            else lo = mid + 1;
        }
        return lo;
    }

    public boolean isMajorityElementScan(int[] nums, int target) {
        int cnt = 0;
        for (int v : nums) if (v == target) cnt++;
        return cnt > nums.length / 2;
    }

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

        int[] a1 = {2,4,5,5,5,5,5,6,6};
        System.out.println("示例1 解法一: " + s.isMajorityElement(a1, 5) + "(期望 true)");
        System.out.println("示例1 解法二: " + s.isMajorityElementScan(a1, 5) + "(期望 true)");

        int[] a2 = {10,100,101,101};
        System.out.println("示例2 解法一: " + s.isMajorityElement(a2, 101) + "(期望 false)");
        System.out.println("示例2 解法二: " + s.isMajorityElementScan(a2, 101) + "(期望 false)");

        int[] a3 = {5,5,5};
        System.out.println("全等 解法一: " + s.isMajorityElement(a3, 5) + "(期望 true)");
        System.out.println("全等 解法二: " + s.isMajorityElementScan(a3, 5) + "(期望 true)");
    }
}

复杂度分析

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

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


1151. Minimum Swaps to Group All 1's Together(最少交换次数来组合所有的 1)

题目描述

给出一个二进制数组 data,你需要通过交换位置,将数组中任何位置上的 1 组合到一起,

并返回所有可能中所需最少的交换次数。

示例 1

复制代码
data = [1,0,1,0,1] → 1

示例 2

复制代码
data = [0,0,0,1,0] → 0

示例 3

复制代码
data = [1,0,1,0,1,0,0,1,1,0,1] → 3

示例 4

复制代码
data = [1,0,1,0,1,0,1,1,1,0,1,0,0,1,1,1,0,0,1,1,1,0,1,0,1,1,0,0,0,1,1,1,1,0,0,1] → 8

约束:

  • 1 <= data.length <= 10^5;data`i` == 0 或 1。

解题思路

解法一:滑动窗口

  1. 设一共有 ones 个 1,最终所有 1 必然聚集在某个长度为 ones 的连续窗口中;
  2. 对每个长度 ones 的窗口,需要交换的次数 = 窗口内 0 的个数(把 0 换成窗口外的 1);
  3. 滑动窗口维护窗口内 1 的个数,取 max(窗口内 1 个数),答案 = ones - max;
    (窗口内 1 越多,需要换进来的 0 越少)
    复杂度:时间 O(n),空间 O(1)。
    解法二:前缀和
  4. 前缀和数组统计 1 的个数;
  5. 枚举窗口起点 i,窗口 i, i+ones-1 内 0 的个数 = ones - (prei+ones - prei),取最小值。
    复杂度:时间 O(n),空间 O(n)。

代码实现

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

public class Solution {

    public int minSwaps(int[] data) {
        int ones = 0;
        for (int v : data) ones += v;
        if (ones <= 1) return 0;
        int n = data.length;
        int curOnes = 0;
        for (int i = 0; i < ones; i++) curOnes += data[i];
        int maxOnes = curOnes;
        for (int i = ones; i < n; i++) {
            curOnes += data[i] - data[i - ones];
            maxOnes = Math.max(maxOnes, curOnes);
        }
        return ones - maxOnes;
    }

    public int minSwapsPrefix(int[] data) {
        int n = data.length;
        int ones = 0;
        for (int v : data) ones += v;
        if (ones <= 1) return 0;
        int[] pre = new int[n + 1];
        for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] + data[i - 1];
        int best = ones;
        for (int i = 0; i + ones <= n; i++) {
            int inOnes = pre[i + ones] - pre[i];
            best = Math.min(best, ones - inOnes);
        }
        return best;
    }

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

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

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

        int[] d3 = {1,0,1,0,1,0,0,1,1,0,1};
        System.out.println("示例3 解法一: " + s.minSwaps(d3) + "(期望 3)");
        System.out.println("示例3 解法二: " + s.minSwapsPrefix(d3) + "(期望 3)");
    }
}

复杂度分析

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

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


1152. Analyze User Website Visit Pattern(用户网站访问行为分析)

题目描述

给定两个字符串数组 username 和 website 和一个整数数组 timestamp。给定的数组长度相同,

其中元组 username\[`i`, website`i`, timestamp[i]] 表示用户 username`i` 在时间 timestamp[i] 访问了网站 website`i`。

访问模式是包含三个网站的列表(不一定是完全不同的)。

一种访问模式的得分是访问该模式中所有网站的用户数量,这些网站在该模式中出现的顺序相同。

返回得分最大的访问模式。如果有多个访问模式具有相同的最大分数,则返回字典序最小的。

注意,模式中的网站不需要连续访问,只需按照模式中出现的顺序访问即可。

示例 1

复制代码
username = ["joe","joe","joe","james","james","james","james","mary","mary","mary"],
timestamp = [1,2,3,4,5,6,7,8,9,10],
website = ["home","about","career","home","cart","maps","home","home","about","career"]
→ ["home","about","career"]

示例 2

复制代码
username = ["ua","ua","ua","ub","ub","ub"], timestamp = [1,2,3,4,5,6],
website = ["a","b","a","a","b","c"] → ["a","b","a"]

约束:

  • 3 <= username.length <= 50;timestamp/website 数组等长;所有元组不重复;
  • 保证至少有一个用户访问了至少三个网站。

解题思路

解法一:用户分组 + 组合去重计数

  1. 按用户名分组,组内按 timestamp 升序排序网站访问序列;
  2. 对每个用户,从访问序列中枚举所有三元组 (i<j<k) 作为该用户访问过的模式(用 Set 去重------
    同一用户对同一模式只计一次分);
  3. 用 Map<模式, 分数> 累加各用户贡献,最后取分数最大、字典序最小的模式。
    复杂度:时间 O(U * V^3)(实际数据量很小,U 为人数 V 为人均访问数)。
    解法二:全局排序后按用户收集
  4. 先把 (username, website, timestamp) 三元组按时间全局排序;
  5. 再按用户分组收集按时间有序的网站列表;
  6. 与解法一相同地枚举三元组并计数,取最优模式。
    复杂度:时间 O(N log N + U*V^3),空间 O(N)。

代码实现

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

public class Solution {

    public List<String> mostVisitedPattern(String[] username, int[] timestamp, String[] website) {
        int n = username.length;
        // 按用户分组收集 (时间, 网站)
        Map<String, List<int[]>> byUser = new HashMap<>();
        for (int i = 0; i < n; i++) {
            byUser.computeIfAbsent(username[i], k -> new ArrayList<>())
                    .add(new int[]{timestamp[i], i});
        }
        Map<String, Integer> score = new HashMap<>();
        for (List<int[]> visits : byUser.values()) {
            visits.sort((a, b) -> a[0] - b[0]); // 按时间升序
            int m = visits.size();
            if (m < 3) continue;
            Set<String> seen = new HashSet<>(); // 同一用户对同一模式去重
            for (int i = 0; i < m; i++) {
                for (int j = i + 1; j < m; j++) {
                    for (int k = j + 1; k < m; k++) {
                        String pat = website[visits.get(i)[1]] + "#"
                                + website[visits.get(j)[1]] + "#"
                                + website[visits.get(k)[1]];
                        if (seen.add(pat)) {
                            score.put(pat, score.getOrDefault(pat, 0) + 1);
                        }
                    }
                }
            }
        }
        String best = null;
        int bestScore = -1;
        for (Map.Entry<String, Integer> e : score.entrySet()) {
            if (e.getValue() > bestScore
                    || (e.getValue() == bestScore && e.getKey().compareTo(best) < 0)) {
                best = e.getKey();
                bestScore = e.getValue();
            }
        }
        String[] parts = best.split("#");
        return Arrays.asList(parts);
    }

    public List<String> mostVisitedPatternSorted(String[] username, int[] timestamp, String[] website) {
        int n = username.length;
        Integer[] idx = new Integer[n];
        for (int i = 0; i < n; i++) idx[i] = i;
        Arrays.sort(idx, (a, b) -> {
            if (username[a].equals(username[b])) return timestamp[a] - timestamp[b];
            return username[a].compareTo(username[b]);
        });
        Map<String, List<String>> seq = new LinkedHashMap<>();
        for (int i : idx) {
            seq.computeIfAbsent(username[i], k -> new ArrayList<>()).add(website[i]);
        }
        Map<String, Integer> score = new HashMap<>();
        for (List<String> ws : seq.values()) {
            int m = ws.size();
            if (m < 3) continue;
            Set<String> seen = new HashSet<>();
            for (int i = 0; i < m; i++) {
                for (int j = i + 1; j < m; j++) {
                    for (int k = j + 1; k < m; k++) {
                        String pat = ws.get(i) + "#" + ws.get(j) + "#" + ws.get(k);
                        if (seen.add(pat)) {
                            score.put(pat, score.getOrDefault(pat, 0) + 1);
                        }
                    }
                }
            }
        }
        String best = null;
        int bestScore = -1;
        for (Map.Entry<String, Integer> e : score.entrySet()) {
            if (e.getValue() > bestScore
                    || (e.getValue() == bestScore && e.getKey().compareTo(best) < 0)) {
                best = e.getKey();
                bestScore = e.getValue();
            }
        }
        return Arrays.asList(best.split("#"));
    }

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

        String[] u1 = {"joe","joe","joe","james","james","james","james","mary","mary","mary"};
        int[] t1 = {1,2,3,4,5,6,7,8,9,10};
        String[] w1 = {"home","about","career","home","cart","maps","home","home","about","career"};
        System.out.println("示例1 解法一: " + s.mostVisitedPattern(u1, t1, w1) + "(期望 [home, about, career])");
        System.out.println("示例1 解法二: " + s.mostVisitedPatternSorted(u1, t1, w1) + "(期望 [home, about, career])");

        String[] u2 = {"ua","ua","ua","ub","ub","ub"};
        int[] t2 = {1,2,3,4,5,6};
        String[] w2 = {"a","b","a","a","b","c"};
        System.out.println("示例2 解法一: " + s.mostVisitedPattern(u2, t2, w2) + "(期望 [a, b, a])");
        System.out.println("示例2 解法二: " + s.mostVisitedPatternSorted(u2, t2, w2) + "(期望 [a, b, a])");
    }
}

复杂度分析

复杂度:时间 O(U * V^3)(实际数据量很小,U 为人数 V 为人均访问数)。

复杂度:时间 O(N log N + U*V^3),空间 O(N)。


1153. String Transforms Into Another String(字符串转化)

题目描述

给出两个长度相同的字符串 str1 和 str2。请你帮忙判断字符串 str1 能不能在零次或多次转化后变成字符串 str2。

每一次转化时,你可以将 str1 中出现的所有相同字母变成其他任何小写英文字母。

只有在字符串 str1 能够通过上述方式顺利转化为字符串 str2 时才能返回 true。

示例 1

复制代码
str1 = "aabcc", str2 = "ccdee" → true('c'→'e',然后 'b'→'d',最后 'a'→'c')

示例 2

复制代码
str1 = "leetcode", str2 = "codeleet" → false

约束:

  • 1 <= str1.length == str2.length <= 10^4;str1 和 str2 中都只会出现小写英文字母。

解题思路

解法一:映射检查 + 26 字母换空间

  1. 建立映射 map26,str1 中每个字符必须映射到唯一的字符:若已有映射且与当前要求不符则 false;
  2. 若 str2 使用了全部 26 个小写字母(即 str1 的 26 个字符映射成 26 个不同字符),
    则不存在可转换的"中转"字母,会死锁------此时若 str1 != str2 则 false;
    (因为任何单步替换都会连环破坏其他映射,但若 str1 == str2 则零次转化即可)
  3. 特殊判断:只有当 str2 包含全部 26 种字母且存在映射冲突(str1 != str2)时才无解。
    更准确的充要条件:str1 中每个字符映射唯一,且若 str2 中 26 个字母全部出现并且
    str1 的 26 个字符也全部不同(即 str1 无重复字符映射到多个)......
    标准结论:检查映射一致性 + 特殊情况「str2 包含 26 个不同字母且 str1 != str2 时为 false」。
    复杂度:时间 O(n),空间 O(26)。
    解法二:并查集建模
  4. 每个小写字母是一个节点,str1i → str2i 建立"转化边";
  5. 若 str1i 已被映射到别的字符而出现冲突 → false;
  6. 检查图中是否存在自环长度>1 的环导致无中转字符:若某个连通分量是单纯环且覆盖全部 26 节点,
    则无法转化(除非 str1 == str2);实现上用「str2 用满 26 字母且 str1 != str2」特判即可。
    复杂度:时间 O(n),空间 O(26)。

代码实现

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

public class Solution {

    public boolean canConvert(String str1, String str2) {
        if (str1.equals(str2)) return true;
        int n = str1.length();
        int[] map = new int[26];
        Arrays.fill(map, -1);
        boolean[] usedInStr2 = new boolean[26];
        int distinct2 = 0;
        for (int i = 0; i < n; i++) {
            int a = str1.charAt(i) - 'a';
            int b = str2.charAt(i) - 'a';
            if (map[a] == -1) {
                map[a] = b;
            } else if (map[a] != b) {
                return false; // 同一字符被要求映射到不同字符
            }
            if (!usedInStr2[b]) {
                usedInStr2[b] = true;
                distinct2++;
            }
        }
        // str2 若包含了全部 26 个字母,则没有空闲字母做"中转",且 str1 != str2 时必然死锁
        return distinct2 < 26;
    }

    public boolean canConvertUnion(int[] map, String str1, String str2) {
        // 此处演示不带并查集的等价判据,避免过度设计;实际用与方法一相同的映射判据
        return canConvert(str1, str2);
    }

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

        System.out.println("示例1 解法一: " + s.canConvert("aabcc", "ccdee") + "(期望 true)");
        System.out.println("示例2 解法一: " + s.canConvert("leetcode", "codeleet") + "(期望 false)");

        // 附加用例
        System.out.println("相同串 解法一: " + s.canConvert("abc", "abc") + "(期望 true)");
        System.out.println("全26死锁 解法一: " + s.canConvert("abcdefghijklmnopqrstuvwxyz",
                "bcdefghijklmnopqrstuvwxyza") + "(期望 false)");
        System.out.println("26->26但str2不全: " + s.canConvert(
                "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
                "bcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyza") + "(期望 false)");
    }
}

复杂度分析

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


1154. Day of the Year(一年中的第几天)

题目描述

给你一个字符串 date,按 YYYY-MM-DD 格式表示一个现行公元纪年法日期。返回该日期是当年的第几天。

示例 1

复制代码
date = "2019-01-09" → 9

示例 2

复制代码
date = "2019-02-10" → 41

约束:

  • date.length == 10;date4 == date7 == '-';其他字符都是数字;
  • date 表示的范围从 1900 年 1 月 1 日至 2019 年 12 月 31 日。

解题思路

解法一:查表 + 闰年判断

  1. 解析出年、月、日;
  2. 累加前 month-1 个月的天数,加上日;二月按闰年调整(能被 400 整除或能被 4 整除但不能被 100 整除)。
    复杂度:时间 O(1),空间 O(1)。
    解法二:LocalDate 日期库
  3. 用 LocalDate.parse 解析,getDayOfYear() 直接得到当年第几天;
  4. 代码最简,适合展示标准库用法。
    复杂度:时间 O(1),空间 O(1)。

代码实现

java 复制代码
public class Solution {

    private static final int[] DAYS = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

    public int dayOfYear(String date) {
        int y = Integer.parseInt(date.substring(0, 4));
        int m = Integer.parseInt(date.substring(5, 7));
        int d = Integer.parseInt(date.substring(8, 10));
        int ans = 0;
        for (int i = 0; i < m - 1; i++) ans += DAYS[i];
        if (m > 2 && isLeap(y)) ans++;
        return ans + d;
    }

    private boolean isLeap(int y) {
        return (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0);
    }

    public int dayOfYearLibrary(String date) {
        return java.time.LocalDate.parse(date).getDayOfYear();
    }

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

        System.out.println("示例1 解法一: " + s.dayOfYear("2019-01-09") + "(期望 9)");
        System.out.println("示例1 解法二: " + s.dayOfYearLibrary("2019-01-09") + "(期望 9)");

        System.out.println("示例2 解法一: " + s.dayOfYear("2019-02-10") + "(期望 41)");
        System.out.println("示例2 解法二: " + s.dayOfYearLibrary("2019-02-10") + "(期望 41)");

        System.out.println("闰年 解法一: " + s.dayOfYear("2008-03-01") + "(期望 61)");
        System.out.println("闰年 解法二: " + s.dayOfYearLibrary("2008-03-01") + "(期望 61)");

        System.out.println("平年 解法一: " + s.dayOfYear("2007-03-01") + "(期望 60)");
        System.out.println("平年 解法二: " + s.dayOfYearLibrary("2007-03-01") + "(期望 60)");
    }
}

复杂度分析

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


1155. Number of Dice Rolls With Target Sum(掷骰子等于目标和的方法数)

题目描述

这里有 n 个一样的骰子,每个骰子上都有 k 个面,分别标号为 1 到 k。

给定三个整数 n、k 和 target,请返回投掷骰子的所有可能得到的结果(共有 k^n 种方式),

使得骰子面朝上的数字总和等于 target。由于答案可能很大,需要对 10^9 + 7 取模。

示例 1

复制代码
n = 1, k = 6, target = 3 → 1

示例 2

复制代码
n = 2, k = 6, target = 7 → 6

示例 3

复制代码
n = 30, k = 30, target = 500 → 222616187

约束:

  • 1 <= n, k <= 30;1 <= target <= 1000。

解题思路

解法一:二维动态规划

  1. dpis 表示投掷前 i 个骰子,总和为 s 的方法数;
  2. 转移:dpis = sum(dpi-1s-f) for f in 1...k;
  3. 边界 dp00 = 1;最终 dpntarget。
    复杂度:时间 O(n * target * k),空间 O(n * target)(可滚动优化到 O(target))。
    解法二:滚动数组优化 + 前缀和加速
  4. 只用一维数组滚动;对每个 i 重新计算新 dp;
  5. 用滑动窗口/前缀和把内层 k 求和降为 O(1):
    对当前层,维护长度 k 的窗口和即可。
    复杂度:时间 O(n * target),空间 O(target)。

代码实现

java 复制代码
public class Solution {

    private static final int MOD = 1_000_000_007;

    public int numRollsToTarget(int n, int k, int target) {
        int[][] dp = new int[n + 1][target + 1];
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {
            for (int s = 0; s <= target; s++) {
                for (int f = 1; f <= k; f++) {
                    if (s >= f) {
                        dp[i][s] = (int) ((dp[i][s] + (long) dp[i - 1][s - f]) % MOD);
                    }
                }
            }
        }
        return dp[n][target];
    }

    public int numRollsToTargetRolling(int n, int k, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int i = 1; i <= n; i++) {
            int[] ndp = new int[target + 1];
            long window = 0;
            // ndp[s] = sum_{f=1..k} dp_prev[s-f],即窗口 [s-k, s-1] 内 dp 之和
            for (int s = 1; s <= target; s++) {
                window += dp[s - 1];               // 新元素 dp[s-1] 进入窗口
                if (s - k - 1 >= 0) window -= dp[s - k - 1]; // 滑出窗口外元素
                window = (window % MOD + MOD) % MOD;
                ndp[s] = (int) window;
            }
            dp = ndp;
        }
        return dp[target];
    }

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

        System.out.println("示例1 解法一: " + s.numRollsToTarget(1, 6, 3) + "(期望 1)");
        System.out.println("示例1 解法二: " + s.numRollsToTargetRolling(1, 6, 3) + "(期望 1)");

        System.out.println("示例2 解法一: " + s.numRollsToTarget(2, 6, 7) + "(期望 6)");
        System.out.println("示例2 解法二: " + s.numRollsToTargetRolling(2, 6, 7) + "(期望 6)");

        System.out.println("示例3 解法一: " + s.numRollsToTarget(30, 30, 500) + "(期望 222616187)");
        System.out.println("示例3 解法二: " + s.numRollsToTargetRolling(30, 30, 500) + "(期望 222616187)");
    }
}

复杂度分析

复杂度:时间 O(n * target * k),空间 O(n * target)(可滚动优化到 O(target))。

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


1156. Swap For Longest Repeated Character Substring(单字符重复子串的最大长度)

题目描述

如果字符串中的所有字符都相同,那么这个字符串是单字符重复的字符串。

给你一个字符串 text,你只能交换其中两个字符一次或者什么都不做,然后得到一些单字符重复的子串。

返回其中最长的子串的长度。

示例 1

复制代码
text = "ababa" → 3

示例 2

复制代码
text = "aaabaaa" → 6

示例 3

复制代码
text = "aaabbaaa" → 4

示例 4

复制代码
text = "aaaaa" → 5

示例 5

复制代码
text = "abcdef" → 1

约束:

  • 1 <= text.length <= 20000;text 仅由小写英文字母组成。

解题思路

解法一:分组连续段 + 合并相邻端

  1. 把 text 压缩为连续同字符段:seg0 为段长度、seg1 为字符;
  2. 对每个字符 c:
  • 若总出现次数 totalc,单个最长连续段 len 即为候选(无需交换);
  • 若存在两个相邻段且中间隔一个其他字符(如 aabaa 的 a-a 段),且 totalc > 两段和,
    则可从别处换一个 c 来连通:候选 = 两段和 + 1;否则候选 = 两段和;
  1. 取全局最大。
    复杂度:时间 O(n),空间 O(n)。
    解法二:滑动窗口 + 频率统计(最多一个非 c 字符)
  2. 对每个字符 c,滑动窗口保持窗口内最多 1 个非 c 字符,且窗口外至少还有一个 c 可换入;
  3. 记录窗口内 c 的个数,那些 c 紧邻段长度即候选;维护最大。
  4. 实现简洁但需特判交换来源;这里用解法一的贪心更清晰,解法二给出另一种等价扫描。
    复杂度:时间 O(26 * n),空间 O(1)。

代码实现

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

public class Solution {

    public int maxRepOpt1(String text) {
        int n = text.length();
        int[] total = new int[26];
        for (int i = 0; i < n; i++) total[text.charAt(i) - 'a']++;

        // 压缩连续段
        List<int[]> segs = new ArrayList<>();
        int i = 0;
        while (i < n) {
            int j = i;
            while (j < n && text.charAt(j) == text.charAt(i)) j++;
            segs.add(new int[]{j - i, text.charAt(i) - 'a'});
            i = j;
        }

        int ans = 0;
        for (int s = 0; s < segs.size(); s++) {
            int[] cur = segs.get(s);
            int len = cur[0], ch = cur[1];
            // 不交换:最长连续段,但不能超过总数
            ans = Math.max(ans, Math.min(len + (s > 0 || s < segs.size() - 1 ? 1 : 0) - 0, total[ch]));
            // 更准确:len 本身,或与相邻相同字符段通过一次交换连通
            int cand = len;
            if (total[ch] > len) cand = len + 1; // 窗口外还有同字符可换入
            ans = Math.max(ans, cand);

            // 中间隔一个字符的两个同类段(如 a b a 中间一个 b)
            if (s + 2 < segs.size()
                    && segs.get(s + 2)[1] == ch
                    && segs.get(s + 1)[0] == 1) {
                int merged = len + segs.get(s + 2)[0]; // 两段和
                if (total[ch] > merged) merged++;      // 别处还有一个 c 可换到中间
                ans = Math.max(ans, merged);
            }
        }
        return ans == 0 ? 1 : ans; // n >= 1,至少 1
    }

    public int maxRepOpt1Sliding(String text) {
        int n = text.length();
        int[] total = new int[26];
        for (int i = 0; i < n; i++) total[text.charAt(i) - 'a']++;
        int ans = 1;
        for (int c = 0; c < 26; c++) {
            int ch = (char) ('a' + c);
            int l = 0, other = 0, cnt = 0;
            for (int r = 0; r < n; r++) {
                if (text.charAt(r) == ch) cnt++;
                else other++;
                while (other > 1) { // 窗口内至多 1 个非 c
                    if (text.charAt(l) == ch) cnt--;
                    else other--;
                    l++;
                }
                // 若窗口外还有 c,则可以多换 1 个进来
                if (total[c] > cnt) ans = Math.max(ans, cnt + 1);
                else ans = Math.max(ans, cnt);
            }
        }
        return ans;
    }

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

        System.out.println("示例1 解法一: " + s.maxRepOpt1("ababa") + "(期望 3)");
        System.out.println("示例1 解法二: " + s.maxRepOpt1Sliding("ababa") + "(期望 3)");

        System.out.println("示例2 解法一: " + s.maxRepOpt1("aaabaaa") + "(期望 6)");
        System.out.println("示例2 解法二: " + s.maxRepOpt1Sliding("aaabaaa") + "(期望 6)");

        System.out.println("示例3 解法一: " + s.maxRepOpt1("aaabbaaa") + "(期望 4)");
        System.out.println("示例3 解法二: " + s.maxRepOpt1Sliding("aaabbaaa") + "(期望 4)");

        System.out.println("示例4 解法一: " + s.maxRepOpt1("aaaaa") + "(期望 5)");
        System.out.println("示例4 解法二: " + s.maxRepOpt1Sliding("aaaaa") + "(期望 5)");

        System.out.println("示例5 解法一: " + s.maxRepOpt1("abcdef") + "(期望 1)");
        System.out.println("示例5 解法二: " + s.maxRepOpt1Sliding("abcdef") + "(期望 1)");
    }
}

复杂度分析

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

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


1157. Online Majority Element In Subarray(子数组中占绝大多数的元素)

题目描述

设计一个数据结构,有效地找到给定子数组的多数元素。

子数组的多数元素是在子数组中出现 threshold 次数或次数以上的元素。

实现 MajorityChecker 类:

  • MajorityChecker(int[] arr) 会用给定的数组 arr 对 MajorityChecker 初始化。
  • int query(int left, int right, int threshold) 返回子数组 arr`left`...`right` 中至少出现 threshold 次的元素,
    如果不存在这样的元素则返回 -1。

示例

复制代码
["MajorityChecker", "query", "query", "query"]
[[[1, 1, 2, 2, 1, 1]], [0, 5, 4], [0, 3, 3], [2, 3, 2]]
  • 输出 :`null`, 1, -1, 2
    约束:
  • 1 <= arr.length <= 210^4;1 <= arr[i] <= 2 10^4;0 <= left <= right < arr.length;
  • threshold <= right-left+1;2*threshold > right-left+1;query 调用最多 10^4。

解题思路

解法一:线段树摩尔投票 + 位置表二分验证

  1. 关键性质:由于 2*threshold > 区间长度,区间内最多只有一个元素的出现次数 >= threshold;
    且若存在,该元素必然是该区间的「多数元素」(摩尔投票候选);
  2. 线段树每个节点维护 (candidate, count):该区间摩尔投票后剩余的候选及其净票数,
    合并规则:若两段候选相同则票数相加,否则取票数大的候选并相减;
  3. query 时线段树区间查询得到候选值,再用该值的位置列表二分得到区间内真实出现次数,
    若 >= threshold 则返回,否则 -1。
    复杂度:建树 O(n),query 时间 O(log n)(线段树)+ O(log n)(二分)= O(log n);空间 O(n)。
    解法二:随机采样候选 + 位置表验证
  4. 区间长度 len,若存在多数元素则随机取一个下标,命中该元素的概率 > 1/2;
  5. 采样 20 次均未命中的概率 < 2^-20,可忽略;
  6. 每个候选用位置列表二分验证真实出现次数是否 >= threshold。
    复杂度:query 时间 O(k * log n)(k 为采样次数,k=20),空间 O(n)。

代码实现

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

public class Solution {

    public static class MajorityChecker {
        private int[] arr;
        private int n;
        private Map<Integer, List<Integer>> pos;
        private int[] treeCand;
        private int[] treeCnt;

        public MajorityChecker(int[] arr) {
            this.arr = arr;
            this.n = arr.length;
            this.pos = new HashMap<>();
            for (int i = 0; i < n; i++) {
                pos.computeIfAbsent(arr[i], k -> new ArrayList<>()).add(i);
            }
            treeCand = new int[4 * n];
            treeCnt = new int[4 * n];
            build(1, 0, n - 1);
        }

        private void build(int node, int l, int r) {
            if (l == r) {
                treeCand[node] = arr[l];
                treeCnt[node] = 1;
                return;
            }
            int mid = (l + r) >>> 1;
            build(node * 2, l, mid);
            build(node * 2 + 1, mid + 1, r);
            // 摩尔投票合并
            if (treeCand[node * 2] == treeCand[node * 2 + 1]) {
                treeCand[node] = treeCand[node * 2];
                treeCnt[node] = treeCnt[node * 2] + treeCnt[node * 2 + 1];
            } else if (treeCnt[node * 2] >= treeCnt[node * 2 + 1]) {
                treeCand[node] = treeCand[node * 2];
                treeCnt[node] = treeCnt[node * 2] - treeCnt[node * 2 + 1];
            } else {
                treeCand[node] = treeCand[node * 2 + 1];
                treeCnt[node] = treeCnt[node * 2 + 1] - treeCnt[node * 2];
            }
        }

        public int query(int left, int right, int threshold) {
            int[] res = queryTree(1, 0, n - 1, left, right);
            int cand = res[0];
            if (cand == -1) return -1;
            List<Integer> list = pos.get(cand);
            int lo = lowerBound(list, left), hi = upperBound(list, right);
            return (hi - lo) >= threshold ? cand : -1;
        }

        private int[] queryTree(int node, int l, int r, int ql, int qr) {
            if (ql <= l && r <= qr) {
                return new int[]{treeCand[node], treeCnt[node]};
            }
            int mid = (l + r) >>> 1;
            if (qr <= mid) return queryTree(node * 2, l, mid, ql, qr);
            if (ql > mid) return queryTree(node * 2 + 1, mid + 1, r, ql, qr);
            int[] a = queryTree(node * 2, l, mid, ql, qr);
            int[] b = queryTree(node * 2 + 1, mid + 1, r, ql, qr);
            // 合并
            if (a[0] == -1) return b;
            if (b[0] == -1) return a;
            if (a[0] == b[0]) return new int[]{a[0], a[1] + b[1]};
            if (a[1] >= b[1]) return new int[]{a[0], a[1] - b[1]};
            return new int[]{b[0], b[1] - a[1]};
        }

        private int lowerBound(List<Integer> list, int target) {
            int lo = 0, hi = list.size();
            while (lo < hi) {
                int mid = (lo + hi) >>> 1;
                if (list.get(mid) >= target) hi = mid;
                else lo = mid + 1;
            }
            return lo;
        }

        private int upperBound(List<Integer> list, int target) {
            int lo = 0, hi = list.size();
            while (lo < hi) {
                int mid = (lo + hi) >>> 1;
                if (list.get(mid) > target) hi = mid;
                else lo = mid + 1;
            }
            return lo;
        }
    }

    public static class MajorityCheckerRandom {
        private int[] arr;
        private Map<Integer, List<Integer>> pos;
        private java.util.Random rand = new java.util.Random();

        public MajorityCheckerRandom(int[] arr) {
            this.arr = arr;
            this.pos = new HashMap<>();
            for (int i = 0; i < arr.length; i++) {
                pos.computeIfAbsent(arr[i], k -> new ArrayList<>()).add(i);
            }
        }

        public int query(int left, int right, int threshold) {
            int len = right - left + 1;
            for (int t = 0; t < 20; t++) {
                int idx = left + rand.nextInt(len);
                int cand = arr[idx];
                List<Integer> list = pos.get(cand);
                int lo = lowerBound(list, left), hi = upperBound(list, right);
                if (hi - lo >= threshold) return cand;
            }
            return -1;
        }

        private int lowerBound(List<Integer> list, int target) {
            int lo = 0, hi = list.size();
            while (lo < hi) {
                int mid = (lo + hi) >>> 1;
                if (list.get(mid) >= target) hi = mid;
                else lo = mid + 1;
            }
            return lo;
        }

        private int upperBound(List<Integer> list, int target) {
            int lo = 0, hi = list.size();
            while (lo < hi) {
                int mid = (lo + hi) >>> 1;
                if (list.get(mid) > target) hi = mid;
                else lo = mid + 1;
            }
            return lo;
        }
    }

    public static void main(String[] args) {
        // 示例
        MajorityChecker mc = new MajorityChecker(new int[]{1,1,2,2,1,1});
        System.out.println("示例 解法一 query(0,5,4): " + mc.query(0, 5, 4) + "(期望 1)");
        System.out.println("示例 解法一 query(0,3,3): " + mc.query(0, 3, 3) + "(期望 -1)");
        System.out.println("示例 解法一 query(2,3,2): " + mc.query(2, 3, 2) + "(期望 2)");

        MajorityCheckerRandom mr = new MajorityCheckerRandom(new int[]{1,1,2,2,1,1});
        System.out.println("示例 解法二 query(0,5,4): " + mr.query(0, 5, 4) + "(期望 1)");
        System.out.println("示例 解法二 query(0,3,3): " + mr.query(0, 3, 3) + "(期望 -1)");
        System.out.println("示例 解法二 query(2,3,2): " + mr.query(2, 3, 2) + "(期望 2)");
    }
}

复杂度分析

复杂度:建树 O(n),query 时间 O(log n)(线段树)+ O(log n)(二分)= O(log n);空间 O(n)。

复杂度:query 时间 O(k * log n)(k 为采样次数,k=20),空间 O(n)。


1158. Market Analysis I(市场分析 I)

题目类型:数据库(SQL)

题目描述

表: Users

复制代码
+----------------+---------+
| Column Name    | Type    |
+----------------+---------+
| user_id        | int     |
| join_date      | date    |
| favorite_brand | varchar |
+----------------+---------+
user_id 是此表主键(具有唯一值的列)。
表中描述了购物网站的用户信息,用户可以在此网站上进行商品买卖。

表: Orders

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| order_id      | int     |
| order_date    | date    |
| item_id       | int     |
| buyer_id      | int     |
| seller_id     | int     |
+---------------+---------+
order_id 是此表主键(具有唯一值的列)。
item_id 是 Items 表的外键(reference 列)。
(buyer_id,seller_id)是 User 表的外键。

表:Items

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| item_id       | int     |
| item_brand    | varchar |
+---------------+---------+
item_id 是此表的主键(具有唯一值的列)。

编写解决方案找出每个用户的注册日期和在 **2019 **年作为买家的订单总数。

以 任意顺序 返回结果表。

查询结果格式如下。

示例 1:

复制代码
**输入:**
Users 表:
+---------+------------+----------------+
| user_id | join_date  | favorite_brand |
+---------+------------+----------------+
| 1       | 2018-01-01 | Lenovo         |
| 2       | 2018-02-09 | Samsung        |
| 3       | 2018-01-19 | LG             |
| 4       | 2018-05-21 | HP             |
+---------+------------+----------------+
Orders 表:
+----------+------------+---------+----------+-----------+
| order_id | order_date | item_id | buyer_id | seller_id |
+----------+------------+---------+----------+-----------+
| 1        | 2019-08-01 | 4       | 1        | 2         |
| 2        | 2018-08-02 | 2       | 1        | 3         |
| 3        | 2019-08-03 | 3       | 2        | 3         |
| 4        | 2018-08-04 | 1       | 4        | 2         |
| 5        | 2018-08-04 | 1       | 3        | 4         |
| 6        | 2019-08-05 | 2       | 2        | 4         |
+----------+------------+---------+----------+-----------+
Items 表:
+---------+------------+
| item_id | item_brand |
+---------+------------+
| 1       | Samsung    |
| 2       | Lenovo     |
| 3       | LG         |
| 4       | HP         |
+---------+------------+
**输出:**
+-----------+------------+----------------+
| buyer_id  | join_date  | orders_in_2019 |
+-----------+------------+----------------+
| 1         | 2018-01-01 | 1              |
| 2         | 2018-02-09 | 2              |
| 3         | 2018-01-19 | 0              |
| 4         | 2018-05-21 | 0              |
+-----------+------------+----------------+

解题思路

思考

每位用户都要输出,即使 2019 2019 2019 年没有订单。Users 左连「买家且年份为 2019 2019 2019」的 Orders,再按用户分组计数;无匹配订单的用户计数为 0 0 0。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT
    u.user_id AS buyer_id,
    u.join_date,
    COUNT(order_id) AS orders_in_2019
FROM
    Users AS u
    LEFT JOIN Orders AS o ON u.user_id = o.buyer_id AND YEAR(order_date) = 2019
GROUP BY user_id;

复杂度分析

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


1159. Market Analysis II(市场分析 II)

题目类型:数据库(SQL)

题目描述

表: Users

复制代码
+----------------+---------+
| Column Name    | Type    |
+----------------+---------+
| user_id        | int     |
| join_date      | date    |
| favorite_brand | varchar |
+----------------+---------+
user_id 是该表的主键(具有唯一值的列)。
表中包含一位在线购物网站用户的个人信息,用户可以在该网站出售和购买商品。

表: Orders

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| order_id      | int     |
| order_date    | date    |
| item_id       | int     |
| buyer_id      | int     |
| seller_id     | int     |
+---------------+---------+
order_id 是该表的主键(具有唯一值的列)。
item_id 是 Items 表的外键(reference 列)。
buyer_id 和 seller_id 是 Users 表的外键。

表: Items

复制代码
+---------------+---------+
| Column Name   | Type    |
+---------------+---------+
| item_id       | int     |
| item_brand    | varchar |
+---------------+---------+
item_id 是该表的主键(具有唯一值的列)。

编写一个解决方案,为每个用户找出他们出售的第二件商品(按日期)的品牌是否是他们最喜欢的品牌。如果用户售出的商品少于两件,则该用户的结果为否。保证卖家不会在一天内卖出一件以上的商品。

以 任意顺序 返回结果表。

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

示例 1:

复制代码
**输入:**
Users table:
+---------+------------+----------------+
| user_id | join_date  | favorite_brand |
+---------+------------+----------------+
| 1       | 2019-01-01 | Lenovo         |
| 2       | 2019-02-09 | Samsung        |
| 3       | 2019-01-19 | LG             |
| 4       | 2019-05-21 | HP             |
+---------+------------+----------------+
Orders table:
+----------+------------+---------+----------+-----------+
| order_id | order_date | item_id | buyer_id | seller_id |
+----------+------------+---------+----------+-----------+
| 1        | 2019-08-01 | 4       | 1        | 2         |
| 2        | 2019-08-02 | 2       | 1        | 3         |
| 3        | 2019-08-03 | 3       | 2        | 3         |
| 4        | 2019-08-04 | 1       | 4        | 2         |
| 5        | 2019-08-04 | 1       | 3        | 4         |
| 6        | 2019-08-05 | 2       | 2        | 4         |
+----------+------------+---------+----------+-----------+
Items table:
+---------+------------+
| item_id | item_brand |
+---------+------------+
| 1       | Samsung    |
| 2       | Lenovo     |
| 3       | LG         |
| 4       | HP         |
+---------+------------+
**输出:**
+-----------+--------------------+
| seller_id | 2nd_item_fav_brand |
+-----------+--------------------+
| 1         | no                 |
| 2         | yes                |
| 3         | yes                |
| 4         | no                 |
+-----------+--------------------+
**解释:**
id 为 1 的用户的查询结果是 no,因为他什么也没有卖出
id为 2 和 3 的用户的查询结果是 yes,因为他们卖出的第二件商品的品牌是他们最喜爱的品牌
id为 4 的用户的查询结果是 no,因为他卖出的第二件商品的品牌不是他最喜爱的品牌

解题思路

思考

比较每位卖家第二笔订单的商品品牌是否为其偏爱品牌。对 orders 按卖家、order_date 排名,取 r k = 2 rk=2 rk=2 的行左连用户与商品;无第二笔订单则为 no。

代码实现

sql 复制代码
# Write your MySQL query statement below
SELECT
    u.user_id AS seller_id,
    CASE
        WHEN u.favorite_brand = i.item_brand THEN 'yes'
        ELSE 'no'
    END AS 2nd_item_fav_brand
FROM
    users AS u
    LEFT JOIN (
        SELECT
            order_date,
            item_id,
            seller_id,
            RANK() OVER (
                PARTITION BY seller_id
                ORDER BY order_date
            ) AS rk
        FROM orders
    ) AS o
        ON u.user_id = o.seller_id AND o.rk = 2
    LEFT JOIN items AS i ON o.item_id = i.item_id;

复杂度分析

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


1160. Find Words That Can Be Formed by Characters(拼写单词)

题目描述

给定一个字符串数组 words 和一个字符串 chars。

如果字符串可以由 chars 中的字符组成(每个字符在每个 words 中只能使用一次),则认为它是好的。

返回 words 中所有好的字符串的长度之和。

示例 1

复制代码
words = ["cat","bt","hat","tree"], chars = "atach" → 6("cat" 与 "hat" 可拼出)

示例 2

复制代码
words = ["hello","world","leetcode"], chars = "welldonehoneyr" → 10("hello" 与 "world")

约束:

  • 1 <= words.length <= 1000;1 <= words[i].length, chars.length <= 100;仅含小写字母。

解题思路

解法一:字母计数对比

  1. 统计 chars 中每个字母的出现次数 need;
  2. 对每个单词统计其字母频次,若每个字母频次都不超过 need,则单词是好词,累加其长度。
    复杂度:时间 O(C + sum(单词长度)),空间 O(26)。
    解法二:复制计数数组 + 逐词扣减
  3. 对每个单词先复制基础频次数组,然后逐字母扣减:若某字母在副本中已为 0,则词不合法;
  4. 全部扣减成功则累加长度。实现直观,适合教学。
    复杂度:时间 O(C + sum(len * 26)),空间 O(26)。

代码实现

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

public class Solution {

    public int countCharacters(String[] words, String chars) {
        int[] base = new int[26];
        for (char c : chars.toCharArray()) base[c - 'a']++;
        int ans = 0;
        for (String w : words) {
            if (canForm(w, base)) ans += w.length();
        }
        return ans;
    }

    private boolean canForm(String w, int[] base) {
        int[] cnt = new int[26];
        for (char c : w.toCharArray()) cnt[c - 'a']++;
        for (int i = 0; i < 26; i++) {
            if (cnt[i] > base[i]) return false;
        }
        return true;
    }

    public int countCharactersCopy(String[] words, String chars) {
        int[] base = new int[26];
        for (char c : chars.toCharArray()) base[c - 'a']++;
        int ans = 0;
        for (String w : words) {
            int[] copy = base.clone();
            boolean ok = true;
            for (char c : w.toCharArray()) {
                if (copy[c - 'a'] == 0) { ok = false; break; }
                copy[c - 'a']--;
            }
            if (ok) ans += w.length();
        }
        return ans;
    }

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

        String[] w1 = {"cat","bt","hat","tree"};
        System.out.println("示例1 解法一: " + s.countCharacters(w1, "atach") + "(期望 6)");
        System.out.println("示例1 解法二: " + s.countCharactersCopy(w1, "atach") + "(期望 6)");

        String[] w2 = {"hello","world","leetcode"};
        System.out.println("示例2 解法一: " + s.countCharacters(w2, "welldonehoneyr") + "(期望 10)");
        System.out.println("示例2 解法二: " + s.countCharactersCopy(w2, "welldonehoneyr") + "(期望 10)");
    }
}

复杂度分析

复杂度:时间 O(C + sum(单词长度)),空间 O(26)。

复杂度:时间 O(C + sum(len * 26)),空间 O(26)。

相关推荐
木井巳1 小时前
【动态规划: 路径问题】下降路径最小和
java·算法·leetcode·动态规划·推荐算法
秋饼1 小时前
MySQL 分库分表完全指南:ShardingSphere-JDBC 实战、分片键选型、场景边界、20 个生产坑与 30 道面试点
java·mysql·面试·shardingsphere·分库分表·后端架构
郭小李1 小时前
spring 的 @CookieValue 注解 用法
java·后端·spring
天远Date Lab2 小时前
零信任架构实战:基于天远企业股权变更构建自动化招投标供应商股东穿透审查网关
java·人工智能·架构·自动化
ynchyong2 小时前
培养程序化思维
leetcode·程序化思维·刷题经验
weixin_399380692 小时前
TongWeb7.0.4.9M11如何接入到idea(by lqw)
java·ide·intellij-idea
paopaokaka_luck2 小时前
社区服务平台(加权协同过滤推荐、Jieba中文词频分析、WebSocket实时聊天、求助处理闭环、 活动积分闭环、ECharts多维分析)
java·前端·spring boot·mysql·echarts·mybatis
.Bryce.2 小时前
T2-LLM评测不等于Agent评测
android·java·人工智能·后端·gpt·agent·1024程序员节
Wang's Blog2 小时前
Java 项目实战: 交友项目 - 用户体系集成:查询用户环信账号密码
java·交友