2353.设计食物评分系统

目标

设计一个支持下述操作的食物评分系统:

  • 修改 系统中列出的某种食物的评分。
  • 返回系统中某一类烹饪方式下评分最高的食物。

实现 FoodRatings 类:

  • FoodRatings(String[] foods, String[] cuisines, int[] ratings) 初始化系统。食物由 foods、cuisines 和 ratings 描述,长度均为 n 。
  • foods[i] 是第 i 种食物的名字。
  • cuisines[i] 是第 i 种食物的烹饪方式。
  • ratings[i] 是第 i 种食物的最初评分。
  • void changeRating(String food, int newRating) 修改名字为 food 的食物的评分。
  • String highestRated(String cuisine) 返回指定烹饪方式 cuisine 下评分最高的食物的名字。如果存在并列,返回 字典序较小 的名字。

注意,字符串 x 的字典序比字符串 y 更小的前提是:x 在字典中出现的位置在 y 之前,也就是说,要么 x 是 y 的前缀,或者在满足 x[i] != y[i] 的第一个位置 i 处,x[i] 在字母表中出现的位置在 y[i] 之前。

示例:

输入
["FoodRatings", "highestRated", "highestRated", "changeRating", "highestRated", "changeRating", "highestRated"]
[[["kimchi", "miso", "sushi", "moussaka", "ramen", "bulgogi"], ["korean", "japanese", "japanese", "greek", "japanese", "korean"], [9, 12, 8, 15, 14, 7]], ["korean"], ["japanese"], ["sushi", 16], ["japanese"], ["ramen", 16], ["japanese"]]
输出
[null, "kimchi", "ramen", null, "sushi", null, "ramen"]

解释
FoodRatings foodRatings = new FoodRatings(["kimchi", "miso", "sushi", "moussaka", "ramen", "bulgogi"], ["korean", "japanese", "japanese", "greek", "japanese", "korean"], [9, 12, 8, 15, 14, 7]);
foodRatings.highestRated("korean"); // 返回 "kimchi"
                                    // "kimchi" 是分数最高的韩式料理,评分为 9 。
foodRatings.highestRated("japanese"); // 返回 "ramen"
                                      // "ramen" 是分数最高的日式料理,评分为 14 。
foodRatings.changeRating("sushi", 16); // "sushi" 现在评分变更为 16 。
foodRatings.highestRated("japanese"); // 返回 "sushi"
                                      // "sushi" 是分数最高的日式料理,评分为 16 。
foodRatings.changeRating("ramen", 16); // "ramen" 现在评分变更为 16 。
foodRatings.highestRated("japanese"); // 返回 "ramen"
                                      // "sushi" 和 "ramen" 的评分都是 16 。
                                      // 但是,"ramen" 的字典序比 "sushi" 更小。

说明:

  • 1 <= n <= 2 * 10^4
  • n == foods.length == cuisines.length == ratings.length
  • 1 <= foods[i].length, cuisines[i].length <= 10
  • foods[i]、cuisines[i] 由小写英文字母组成
  • 1 <= ratings[i] <= 10^8
  • foods 中的所有字符串 互不相同
  • 在对 changeRating 的所有调用中,food 是系统中食物的名字。
  • 在对 highestRated 的所有调用中,cuisine 是系统中 至少一种 食物的烹饪方式。
  • 最多调用 changeRating 和 highestRated 总计 2 * 10^4 次

思路

设计一个食物评分系统,返回指定类别评分最高的食物,支持修改食物的评分。

要知道类别中评分最高的食物,优先队列/TreeSet 的元素应为 (rating, food) 键值对,根据评分从大到小排序,如果评分相同根据食物的字典序排列。

修改食物评分后需要更新对应类别的评分排名,因此需要维护 (food, cuisine) 的映射关系。如果使用懒加载,还需要记录食物最新的评分,维护 (food, rating)。如果使用红黑树,需要根据更新前的评分删除树中数据,同样需要维护 (food, rating)

有人使用优先队列超时是因为删除元素的复杂度是 O(n)。考虑使用懒删除或者使用 有序集合 TreeSet。有序集合查找最大/最小节点的复杂度是 O(logn),最大/小节点是最右/左叶子节点,查找复杂度是树的高度。

代码


/**
 * @date 2025-02-28 0:10
 */
public class FoodRatings {

    Map<String, PriorityQueue<String[]>> map;
    Map<String, String> foodMap;
    Map<String, Integer> ratingMap;

    public FoodRatings(String[] foods, String[] cuisines, int[] ratings) {
        int n = foods.length;
        map = new HashMap<>(n);
        foodMap = new HashMap<>(n);
        ratingMap = new HashMap<>(n);
        for (int i = 0; i < n; i++) {
            foodMap.put(foods[i], cuisines[i]);
            ratingMap.put(foods[i], ratings[i]);
            map.putIfAbsent(cuisines[i], new PriorityQueue<>((a, b) -> {
                int compare = Integer.parseInt(b[0]) - Integer.parseInt(a[0]);
                if (compare != 0) {
                    return compare;
                }
                return a[1].compareTo(b[1]);
            }));
            map.get(cuisines[i]).offer(new String[]{String.valueOf(ratings[i]), foods[i]});
        }
    }

    public void changeRating(String food, int newRating) {
        ratingMap.put(food, newRating);
        map.get(foodMap.get(food)).offer(new String[]{String.valueOf(newRating), food});
    }

    public String highestRated(String cuisine) {
        PriorityQueue<String[]> q = map.get(cuisine);
        while (Integer.parseInt(q.peek()[0]) != ratingMap.get(q.peek()[1])) {
            q.poll();
        }
        return q.peek()[1];
    }
}

性能

729.我的日程安排表I

目标

实现一个 MyCalendar 类来存放你的日程安排。如果要添加的日程安排不会造成 重复预订 ,则可以存储这个新的日程安排。

当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生 重复预订 。

日程可以用一对整数 startTime 和 endTime 表示,这里的时间是半开区间,即 [startTime, endTime), 实数 x 的范围为, startTime <= x < endTime 。

实现 MyCalendar 类:

  • MyCalendar() 初始化日历对象。
  • boolean book(int startTime, int endTime) 如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true 。否则,返回 false 并且不要将该日程安排添加到日历中。

示例:

输入:
["MyCalendar", "book", "book", "book"]
[[], [10, 20], [15, 25], [20, 30]]
输出:
[null, true, false, true]
解释:
MyCalendar myCalendar = new MyCalendar();
myCalendar.book(10, 20); // return True
myCalendar.book(15, 25); // return False ,这个日程安排不能添加到日历中,因为时间 15 已经被另一个日程安排预订了。
myCalendar.book(20, 30); // return True ,这个日程安排可以添加到日历中,因为第一个日程安排预订的每个时间都小于 20 ,且不包含时间 20 。

说明:

  • 0 <= start < end <= 10^9
  • 每个测试用例,调用 book 方法的次数最多不超过 1000 次。

提示:

  • Store the events as a sorted list of intervals. If none of the events conflict, then the new event can be added.

思路

判断给定区间是否与已有区间相交,如果不相交将其加入已有区间。

最直接的想法是枚举每一个区间,判断是否相交,如果不相交则加入集合。判断区间 [a, b)[c, d) 是否相交,可以固定一个区间,然后让另一个区间滑动,可以发现相交需要满足 d > a && c < b,注意取等号是不相交的。

当然也可以使用二叉搜索树。

TreeSet 中查找特定元素的 API:

  • ceiling 返回的是 大于等于 target 的最小元素/ null
  • floor 返回的是 小于等于 target 的最大元素/ null
  • higher 返回的是 大于 target 的最小元素 / null
  • lower 返回的是 小于 target 的最大元素 / null

代码


/**
 * @date 2025-01-02 9:58
 */
class MyCalendar {

    TreeSet<int[]> ts = new TreeSet<>((a, b) -> a[0] - b[0]);

    public MyCalendar() {

    }

    public boolean book(int startTime, int endTime) {
        int[] interval = {startTime, endTime};
        if (ts.isEmpty()) {
            ts.add(interval);
            return true;
        }
        int[] param = new int[]{endTime, 0};
        // 查找 起点 大于等于 endTime 的 起点最小的区间,即要插入间隙的右边区间 right
        int[] right = ts.ceiling(param);
        // 如果 right 是第一个元素 或者 前面一个区间的右边界 end 小于等于 startTime,说明不相交
        if (right == ts.first() || ts.lower(param)[1] <= startTime) {
            ts.add(interval);
            return true;
        }
        return false;
    }

}

性能

938.二叉搜索树的范围和

目标

给定二叉搜索树的根结点 root,返回值位于范围 [low, high] 之间的所有结点的值的和。

说明:

  • 树中节点数目在范围 [1, 2 * 10^4] 内
  • 1 <= Node.val <= 10^5
  • 1 <= low <= high <= 10^5
  • 所有 Node.val 互不相同

思路

二叉搜索树,也叫二叉查找树(Binary Search Tree, BST)。BST是一颗二叉树,其中的每个节点都含有一个可比较的Key,并且每个节点的Key都大于其左子树中的任意节点的Key,而小于其右子树的任意节点的Key。

比较每个节点是否在给定的范围内,如果节点Key小于low去左子树找,大于high则去右子树找,如果在二者之间,累加和,继续遍历左右子树。

代码

/**
 * @date 2024/2/26 10:37
 */
public class RangeSumBST {
    public 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;
        }

        @Override
        public String toString() {
            return "TreeNode{" +
                    "val=" + val +
                    ", left=" + left +
                    ", right=" + right +
                    '}';
        }
    }

    public int sum = 0;

    /** 省去了节点为空的判断嵌套*/
    public int rangeSumBST_v1(TreeNode root, int low, int high) {
        if (root == null) {
            return 0;
        }
        if (low > root.val) {
            rangeSumBST_v1(root.right, low, high);
        }
        if (high < root.val) {
            rangeSumBST_v1(root.left, low, high);
        }
        if (high >= root.val && low <= root.val) {
            sum += root.val;
            rangeSumBST_v1(root.left, low, high);
            rangeSumBST_v1(root.right, low, high);
        }
        return sum;
    }

    public int rangeSumBST(TreeNode root, int low, int high) {
        if (low > root.val) {
            if (root.right != null) {
                rangeSumBST(root.right, low, high);
            }
        }
        if (high < root.val) {
            if (root.left != null) {
                rangeSumBST(root.left, low, high);
            }
        }
        if (high >= root.val && low <= root.val){
            sum += root.val;
            if (root.left != null) {
                rangeSumBST(root.left, low, high);
            }
            if (root.right != null) {
                rangeSumBST(root.right, low, high);
            }
        }
        return sum;
    }
}

性能

235.二叉搜索树的最近公共祖先

目标

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。

思路

我第一次想到的方法是先找到这两个指定节点,然后交替求取位置索引较大节点的父节点。然后比较,如果值相等则为公共祖先。但是,题目给出的树结构中不包含指向父节点的引用,很自然地想到先将值保存到数组中,中序遍历二叉搜索树得到正序序列。然后二分查找出index,计算父节点index:左孩是奇数index,父index为(child-1)/2,右孩是偶数index,父index为(child-2)/2,注意排除root,index为0。接着比较父节点的值,还应该考虑输入节点本身就是父子关系的情况,如果pindex > qindex,应先求p节点的父index,因为index大,层数可能更高。再与qindex对应的值比较,不相等的话再求q节点的父index,依此类推。题目中有提到,树中的值都是唯一的。这个想法看起来正确,但真正去实现的时候就会发现是不可行的。首先是存储空间问题,要在数组中保留树的结构,在有许多空节点的情况下是不可行的。这个在 二叉搜索树最近节点查询 中提到过。然后是二分查找需要先排序的问题,这本身就打乱了树节点的index关系。

这几天刷题有一个感受,就是如果算法实现起来太过复杂那么一定是有问题的,很有可能是求解的方向不对。

于是我换了一种思路,还是先找到这两个节点,但是在找的过程中记录下访问的路径,然后再找到路径节点中所有相同节点中的最后一个即可。那么路径应该保存到哪里,又如何获取相同节点序列的最后一个呢?

在 Java 的标准库中,java.util.Stack 类是使用数组实现的。然而,从 Java 6 开始,java.util.Stack 类被标记为遗留(legacy),并建议使用 java.util.Deque 接口及其实现(如 ArrayDeque 或 LinkedList)来替代。Deque 接口表示双端队列,它支持在两端插入和删除元素,因此非常适合实现栈和队列。----AI的回答

无论是使用数组还是链表,最后查找的时候,都可以从头开始同时比较,直到第一个不相等节点出现即可。无需额外申请空间。

如果从尾开始比较的话,需要借助HashSet,将其中一个路径序列存入集合,然后循环弹另一个路径栈,第一个在集合中的被弹出元素就是最近的公共祖先。需要额外的空间。有的时候条件反转一下可以简化处理逻辑,而有时则相反!

另外数组长度固定,超过的话就会有重新分配空间的开销。

代码

public TreeNode lowestCommonAncestor_v1(TreeNode root, TreeNode p, TreeNode q) {
        Stack<TreeNode> pStack = findPath(root, p.val, new Stack<>());
        Stack<TreeNode> qStack = findPath(root, q.val, new Stack<>());
        HashSet<Integer> set = new HashSet<>();
        while (!pStack.empty()){
            set.add(pStack.pop().val);
        }
        TreeNode res = null;
        while (!qStack.empty()){
            res = qStack.pop();
            if (set.contains(res.val)) {
                break;
            }
        }
        return res;
    }

public Stack<TreeNode> findPath(TreeNode subRoot, int value, Stack<TreeNode> stack) {
    stack.push(subRoot);
    if (subRoot.val == value) {
        return stack;
    } else if (subRoot.val > value) {
        return findPath(subRoot.left, value, stack);
    } else {
        return findPath(subRoot.right, value, stack);
    }
}

/**
 * 还有一个更好的做法是一次遍历同时比较p,q,如果一个大于等于,一个小于等于 当前节点值,表明已经/准备分叉了
 * 这个是看了题解之后发现的
 */
public TreeNode lowestCommonAncestor_v2(TreeNode root, TreeNode p, TreeNode q) {
    if (root.val > p.val && root.val > q.val) {
        return lowestCommonAncestor_v2(root.left, p, q);
    } else if (root.val < p.val && root.val < q.val) {
        return lowestCommonAncestor_v2(root.right, p, q);
    } else {
        return root;
    }
}

性能

2476.二叉搜索树最近节点查询

目标

给你一个 二叉搜索树 的根节点 root ,和一个由正整数组成、长度为 n 的数组 queries 。

请你找出一个长度为 n 的 二维 答案数组 answer ,其中 answer[i] = [mini, maxi] :

  • mini 是树中小于等于 queries[i] 的 最大值 。如果不存在这样的值,则使用 -1 代替。
  • maxi 是树中大于等于 queries[i] 的 最小值 。如果不存在这样的值,则使用 -1 代替。

返回数组 answer 。

说明:

  • 树中节点的数目在范围 [2, 10^5] 内
  • 1 <= Node.val <= 10^6
  • n == queries.length
  • 1 <= n <= 10^5
  • 1 <= queries[i] <= 10^6

思路

这个题目求的是最接近给定值的节点值。一个朴素的想法是将搜索树的值都列出来,然后从中查找前后的值。这里列出值无需保留树的结构,虽然节点数目范围是[2,10^5],但如果考虑一个极端的情况,树的深度就是10^5-1,保留树的结构就大约需要2^(10^5)约等于10^3010个元素。二叉搜索树如果采用中序遍历结果就是正序的。但是考虑到存在null值,数组可能填不满,这样就破坏了有序性。想要使用二分查找还要先排序。Arrays.binarySearch 的结果如果找到相应的值则返回对应的index>=0。如果没有找到,则返回-insertion point-1。所谓插入点,其前一个位置的值小于搜索的值。例如 arr = [2, 3, 4, 5, 6, 7, 9] 搜索值为8,使用二分查找则返回 -6-1,arr[5]的值是7,8应该插入到7后面,即插入点为6。因此,如果没查询到,可以使用-index-1得到最近的大于搜索值的位置,这里需要注意数组的边界。

代码

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

/**
 * @date 2024-02-24 18:57
 */
public class ClosestNodes {
    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;
        }

        @Override
        public String toString() {
            return "TreeNode{" +
                    "val=" + val +
                    ", left=" + left +
                    ", right=" + right +
                    '}';
        }
    }

    public final int MAX = 100000;
    public int[] values = new int[MAX];
    public int i = 0;

    public List<List<Integer>> closestNodes(TreeNode root, List<Integer> queries) {
        Arrays.fill(values, -1);
        traverseSubTree(root);
        // 遍历之后values并非全部有序,因为存在null节点,没初始化 值为0
        Arrays.sort(values);
        List<List<Integer>> res = new ArrayList<>(queries.size());
        for (Integer query : queries) {
            List<Integer> tmp = new ArrayList<>(2);
            int ri = Arrays.binarySearch(values, query);
            if (ri < 0) {
                ri = -ri - 1;
                if (ri == values.length) {
                    tmp.add(values[values.length -1]);
                    tmp.add(-1);
                } else if(ri == 0){
                    tmp.add(-1);
                    tmp.add(values[0]);
                } else {
                    tmp.add(values[ri - 1]);
                    tmp.add(values[ri]);
                }
            } else {
                // 如果存在
                tmp.add(values[ri]);
                tmp.add(values[ri]);
            }
            res.add(tmp);
        }
        return res;
    }

    public void traverseSubTree(TreeNode subTree) {
        if (subTree.left != null) {
            traverseSubTree(subTree.left);
        }
        values[i++] = subTree.val;
        if (subTree.right != null) {
            traverseSubTree(subTree.right);
        }
    }
}

性能