普通视图

发现新文章,点击刷新页面。
昨天 — 2025年9月18日LeetCode 每日一题题解

[Python3/Java/C++/Go/TypeScript] 一题一解:哈希表 + 有序集合(清晰题解)

作者 lcbin
2025年9月18日 07:07

方法一:哈希表 + 有序集合

我们用一个哈希表 $\text{d}$ 来存储任务信息,键为任务 ID,值为一个二元组 $(\text{userId}, \text{priority})$,表示该任务所属的用户 ID 以及任务的优先级。

我们用一个有序集合 $\text{st}$ 来存储当前系统中的所有任务,元素为一个二元组 $(-\text{priority}, -\text{taskId})$,表示任务的优先级和任务 ID 的相反数。我们将优先级和任务 ID 取相反数是为了让优先级最高且任务 ID 最大的任务在有序集合中排在最前面。

对于每个操作,我们可以按如下方式进行处理:

  • 初始化:对于每个任务 $(\text{userId}, \text{taskId}, \text{priority})$,我们将其添加到哈希表 $\text{d}$ 和有序集合 $\text{st}$ 中。
  • 添加任务:将任务 $(\text{userId}, \text{taskId}, \text{priority})$ 添加到哈希表 $\text{d}$ 和有序集合 $\text{st}$ 中。
  • 编辑任务:从哈希表 $\text{d}$ 中获取任务 ID对应的用户 ID 和旧优先级,然后从有序集合 $\text{st}$ 中删除旧的任务信息,再将新的任务信息添加到哈希表 $\text{d}$ 和有序集合 $\text{st}$ 中。
  • 删除任务:从哈希表 $\text{d}$ 中获取任务 ID 对应的优先级,然后从有序集合 $\text{st}$ 中删除任务信息,并从哈希表 $\text{d}$ 中删除任务。
  • 执行最高优先级任务:如果有序集合 $\text{st}$ 为空,返回 -1。否则,从有序集合 $\text{st}$ 中取出第一个元素,获取任务 ID,然后从哈希表 $\text{d}$ 中获取对应的用户 ID,并将任务从哈希表 $\text{d}$ 和有序集合 $\text{st}$ 中删除,最后返回用户 ID。

###python

class TaskManager:

    def __init__(self, tasks: List[List[int]]):
        self.d = {}
        self.st = SortedList()
        for task in tasks:
            self.add(*task)

    def add(self, userId: int, taskId: int, priority: int) -> None:
        self.d[taskId] = (userId, priority)
        self.st.add((-priority, -taskId))

    def edit(self, taskId: int, newPriority: int) -> None:
        userId, priority = self.d[taskId]
        self.st.discard((-priority, -taskId))
        self.d[taskId] = (userId, newPriority)
        self.st.add((-newPriority, -taskId))

    def rmv(self, taskId: int) -> None:
        _, priority = self.d[taskId]
        self.d.pop(taskId)
        self.st.remove((-priority, -taskId))

    def execTop(self) -> int:
        if not self.st:
            return -1
        taskId = -self.st.pop(0)[1]
        userId, _ = self.d[taskId]
        self.d.pop(taskId)
        return userId


# Your TaskManager object will be instantiated and called as such:
# obj = TaskManager(tasks)
# obj.add(userId,taskId,priority)
# obj.edit(taskId,newPriority)
# obj.rmv(taskId)
# param_4 = obj.execTop()

###java

class TaskManager {
    private final Map<Integer, int[]> d = new HashMap<>();
    private final TreeSet<int[]> st = new TreeSet<>((a, b) -> {
        if (a[0] == b[0]) {
            return b[1] - a[1];
        }
        return b[0] - a[0];
    });

    public TaskManager(List<List<Integer>> tasks) {
        for (var task : tasks) {
            add(task.get(0), task.get(1), task.get(2));
        }
    }

    public void add(int userId, int taskId, int priority) {
        d.put(taskId, new int[] {userId, priority});
        st.add(new int[] {priority, taskId});
    }

    public void edit(int taskId, int newPriority) {
        var e = d.get(taskId);
        int userId = e[0], priority = e[1];
        st.remove(new int[] {priority, taskId});
        st.add(new int[] {newPriority, taskId});
        d.put(taskId, new int[] {userId, newPriority});
    }

    public void rmv(int taskId) {
        var e = d.remove(taskId);
        int priority = e[1];
        st.remove(new int[] {priority, taskId});
    }

    public int execTop() {
        if (st.isEmpty()) {
            return -1;
        }
        var e = st.pollFirst();
        var t = d.remove(e[1]);
        return t[0];
    }
}

/**
 * Your TaskManager object will be instantiated and called as such:
 * TaskManager obj = new TaskManager(tasks);
 * obj.add(userId,taskId,priority);
 * obj.edit(taskId,newPriority);
 * obj.rmv(taskId);
 * int param_4 = obj.execTop();
 */

###cpp

class TaskManager {
private:
    unordered_map<int, pair<int, int>> d;
    set<pair<int, int>> st;

public:
    TaskManager(vector<vector<int>>& tasks) {
        for (const auto& task : tasks) {
            add(task[0], task[1], task[2]);
        }
    }

    void add(int userId, int taskId, int priority) {
        d[taskId] = {userId, priority};
        st.insert({-priority, -taskId});
    }

    void edit(int taskId, int newPriority) {
        auto [userId, priority] = d[taskId];
        st.erase({-priority, -taskId});
        st.insert({-newPriority, -taskId});
        d[taskId] = {userId, newPriority};
    }

    void rmv(int taskId) {
        auto [userId, priority] = d[taskId];
        st.erase({-priority, -taskId});
        d.erase(taskId);
    }

    int execTop() {
        if (st.empty()) {
            return -1;
        }
        auto e = *st.begin();
        st.erase(st.begin());
        int taskId = -e.second;
        int userId = d[taskId].first;
        d.erase(taskId);
        return userId;
    }
};

/**
 * Your TaskManager object will be instantiated and called as such:
 * TaskManager* obj = new TaskManager(tasks);
 * obj->add(userId,taskId,priority);
 * obj->edit(taskId,newPriority);
 * obj->rmv(taskId);
 * int param_4 = obj->execTop();
 */

###go

type TaskManager struct {
d  map[int][2]int
st *redblacktree.Tree[int, int]
}

func encode(priority, taskId int) int {
return (priority << 32) | taskId
}

func comparator(a, b int) int {
if a > b {
return -1
} else if a < b {
return 1
}
return 0
}

func Constructor(tasks [][]int) TaskManager {
tm := TaskManager{
d:  make(map[int][2]int),
st: redblacktree.NewWith[int, int](comparator),
}
for _, task := range tasks {
tm.Add(task[0], task[1], task[2])
}
return tm
}

func (this *TaskManager) Add(userId int, taskId int, priority int) {
this.d[taskId] = [2]int{userId, priority}
this.st.Put(encode(priority, taskId), taskId)
}

func (this *TaskManager) Edit(taskId int, newPriority int) {
if e, ok := this.d[taskId]; ok {
priority := e[1]
this.st.Remove(encode(priority, taskId))
this.d[taskId] = [2]int{e[0], newPriority}
this.st.Put(encode(newPriority, taskId), taskId)
}
}

func (this *TaskManager) Rmv(taskId int) {
if e, ok := this.d[taskId]; ok {
priority := e[1]
delete(this.d, taskId)
this.st.Remove(encode(priority, taskId))
}
}

func (this *TaskManager) ExecTop() int {
if this.st.Empty() {
return -1
}
it := this.st.Iterator()
it.Next()
taskId := it.Value()
if e, ok := this.d[taskId]; ok {
delete(this.d, taskId)
this.st.Remove(it.Key())
return e[0]
}
return -1
}

/**
 * Your TaskManager object will be instantiated and called as such:
 * obj := Constructor(tasks);
 * obj.Add(userId,taskId,priority);
 * obj.Edit(taskId,newPriority);
 * obj.Rmv(taskId);
 * param_4 := obj.ExecTop();
 */

###ts

class TaskManager {
    private d: Map<number, [number, number]>;
    private pq: PriorityQueue<[number, number]>;

    constructor(tasks: number[][]) {
        this.d = new Map();
        this.pq = new PriorityQueue<[number, number]>((a, b) => {
            if (a[0] === b[0]) {
                return b[1] - a[1];
            }
            return b[0] - a[0];
        });
        for (const task of tasks) {
            this.add(task[0], task[1], task[2]);
        }
    }

    add(userId: number, taskId: number, priority: number): void {
        this.d.set(taskId, [userId, priority]);
        this.pq.enqueue([priority, taskId]);
    }

    edit(taskId: number, newPriority: number): void {
        const e = this.d.get(taskId);
        if (!e) return;
        const userId = e[0];
        this.d.set(taskId, [userId, newPriority]);
        this.pq.enqueue([newPriority, taskId]);
    }

    rmv(taskId: number): void {
        this.d.delete(taskId);
    }

    execTop(): number {
        while (!this.pq.isEmpty()) {
            const [priority, taskId] = this.pq.dequeue();
            const e = this.d.get(taskId);
            if (e && e[1] === priority) {
                this.d.delete(taskId);
                return e[0];
            }
        }
        return -1;
    }
}

/**
 * Your TaskManager object will be instantiated and called as such:
 * var obj = new TaskManager(tasks)
 * obj.add(userId,taskId,priority)
 * obj.edit(taskId,newPriority)
 * obj.rmv(taskId)
 * var param_4 = obj.execTop()
 */

时间复杂度方面,初始化操作需要 $O(n \log n)$ 的时间,其中 $n$ 是初始任务的数量。每个添加、编辑、删除和执行操作都需要 $O(\log m)$ 的时间,其中 $m$ 是当前系统中的任务数量。由于总操作次数不超过 $2 \times 10^5$,因此整体时间复杂度是可接受的。空间复杂度 $O(n + m)$,用于存储哈希表和有序集合。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

每日一题-设计任务管理器🟡

2025年9月18日 00:00

一个任务管理器系统可以让用户管理他们的任务,每个任务有一个优先级。这个系统需要高效地处理添加、修改、执行和删除任务的操作。

请你设计一个 TaskManager 类:

  • TaskManager(vector<vector<int>>& tasks) 初始化任务管理器,初始化的数组格式为 [userId, taskId, priority] ,表示给 userId 添加一个优先级为 priority 的任务 taskId 。

  • void add(int userId, int taskId, int priority) 表示给用户 userId 添加一个优先级为 priority 的任务 taskId ,输入 保证 taskId 不在系统中。

  • void edit(int taskId, int newPriority) 更新已经存在的任务 taskId 的优先级为 newPriority 。输入 保证 taskId 存在于系统中。

  • void rmv(int taskId) 从系统中删除任务 taskId 。输入 保证 taskId 存在于系统中。

  • int execTop() 执行所有用户的任务中优先级 最高 的任务,如果有多个任务优先级相同且都为 最高 ,执行 taskId 最大的一个任务。执行完任务后,taskId 从系统中 删除 。同时请你返回这个任务所属的用户 userId 。如果不存在任何任务,返回 -1 。

注意 ,一个用户可能被安排多个任务。

 

示例 1:

输入:
["TaskManager", "add", "edit", "execTop", "rmv", "add", "execTop"]
[[[[1, 101, 10], [2, 102, 20], [3, 103, 15]]], [4, 104, 5], [102, 8], [], [101], [5, 105, 15], []]

输出:
[null, null, null, 3, null, null, 5]

解释:

TaskManager taskManager = new TaskManager([[1, 101, 10], [2, 102, 20], [3, 103, 15]]); // 分别给用户 1 ,2 和 3 初始化一个任务。
taskManager.add(4, 104, 5); // 给用户 4 添加优先级为 5 的任务 104 。
taskManager.edit(102, 8); // 更新任务 102 的优先级为 8 。
taskManager.execTop(); // 返回 3 。执行用户 3 的任务 103 。
taskManager.rmv(101); // 将系统中的任务 101 删除。
taskManager.add(5, 105, 15); // 给用户 5 添加优先级为 15 的任务 105 。
taskManager.execTop(); // 返回 5 。执行用户 5 的任务 105 。

 

提示:

  • 1 <= tasks.length <= 105
  • 0 <= userId <= 105
  • 0 <= taskId <= 105
  • 0 <= priority <= 109
  • 0 <= newPriority <= 109
  • add ,edit ,rmv 和 execTop 的总操作次数 加起来 不超过 2 * 105 次。
  • 输入保证 taskId 是合法的。

3408. 设计任务管理器

作者 stormsunshine
2025年1月5日 13:36

解法一

思路和算法

任务管理器需要按照优先级降序和编号降序的顺序维护所有任务,并根据任务编号找到相应的任务并更新优先级。为了实现上述功能,需要维护如下信息。

  • 存储所有任务的有序集合,顺序为当优先级不同时按照优先级降序,当优先级相同时按照编号降序。

  • 存储任务编号与任务的对应关系的哈希表。

构造方法中,初始化有序集合与哈希表,将数组 $\textit{tasks}$ 中的所有任务加入有序集合并将任务编号与任务的对应关系存入哈希表。

对于 $\textit{add}$ 操作,根据用户编号 $\textit{userId}$、任务编号 $\textit{taskId}$ 和优先级 $\textit{priority}$ 创建任务,将该任务加入有序集合,并将任务编号 $\textit{taskId}$ 与该任务的对应关系存入哈希表。

对于 $\textit{edit}$ 操作,从哈希表中得到任务编号 $\textit{taskId}$ 对应的任务,将该任务从有序集合中移除,然后将该任务的优先级更新为 $\textit{newPriority}$,最后将该任务重新加入有序集合。

对于 $\textit{rmv}$ 操作,从哈希表中得到任务编号 $\textit{taskId}$ 对应的任务,将该任务从有序集合中移除,将任务编号 $\textit{taskId}$ 从哈希表中移除。

对于 $\textit{execTop}$ 操作,做法如下。

  • 当有序集合不为空时,得到有序集合中的首个任务并从有序集合中移除,将该任务的编号从哈希表中移除,返回该任务的用户编号。

  • 当有序集合为空时,不存在任何任务,返回 $-1$。

代码

###Java

class TaskManager {
    private TreeSet<Task> tasksSet;
    private Map<Integer, Task> idsTasks;

    public TaskManager(List<List<Integer>> tasks) {
        tasksSet = new TreeSet<Task>();
        idsTasks = new HashMap<Integer, Task>();
        for (List<Integer> task : tasks) {
            int userId = task.get(0), taskId = task.get(1), priority = task.get(2);
            add(userId, taskId, priority);
        }
    }

    public void add(int userId, int taskId, int priority) {
        Task task = new Task(userId, taskId, priority);
        tasksSet.add(task);
        idsTasks.put(taskId, task);
    }

    public void edit(int taskId, int newPriority) {
        Task task = idsTasks.get(taskId);
        tasksSet.remove(task);
        task.setPriority(newPriority);
        tasksSet.add(task);
    }

    public void rmv(int taskId) {
        Task task = idsTasks.get(taskId);
        tasksSet.remove(task);
        idsTasks.remove(taskId);
    }

    public int execTop() {
        if (!tasksSet.isEmpty()) {
            Task task = tasksSet.removeFirst();
            idsTasks.remove(task.getTaskId());
            return task.getUserId();
        } else {
            return -1;
        }
    }
}

class Task implements Comparable<Task> {
    private int userId;
    private int taskId;
    private int priority;

    public Task(int userId, int taskId, int priority) {
        this.userId = userId;
        this.taskId = taskId;
        this.priority = priority;
    }

    public int getUserId() {
        return userId;
    }

    public int getTaskId() {
        return taskId;
    }

    public int getPriority() {
        return priority;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public void setTaskId(int taskId) {
        this.taskId = taskId;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    @Override
    public int compareTo(Task task2) {
        return this.priority != task2.priority ? task2.priority - this.priority : task2.taskId - this.taskId;
    }
}

复杂度分析

  • 时间复杂度:构造方法的时间复杂度是 $O(n \log n)$,其余各项操作的时间复杂度是 $O(\log n)$,其中 $n$ 是任务总数。构造方法需要根据给定的任务列表更新有序集合与哈希表,其余各项操作需要对一项任务更新有序集合与哈希表,每项任务的更新时间是 $O(\log n)$。

  • 空间复杂度:$O(n)$,其中 $n$ 是任务总数。有序集合与哈希表的空间是 $O(n)$。

解法二

思路和算法

可以使用优先队列和延迟删除替代有序集合,实现任务管理器。优先队列的队首元素是优先级最高和编号最大的任务。

构造方法中,初始化优先队列与哈希表,将数组 $\textit{tasks}$ 中的所有任务加入优先队列并将任务编号与任务的对应关系存入哈希表。

对于 $\textit{add}$ 操作,根据用户编号 $\textit{userId}$、任务编号 $\textit{taskId}$ 和优先级 $\textit{priority}$ 创建任务,将该任务加入优先队列,并将任务编号 $\textit{taskId}$ 与该任务的对应关系存入哈希表。

对于 $\textit{edit}$ 操作,从哈希表中得到任务编号 $\textit{taskId}$ 对应的任务,新建一个用户编号和任务编号与原任务相同、优先级为 $\textit{newPriority}$ 的任务,将新任务加入优先队列,将任务编号 $\textit{taskId}$ 对应新任务存入哈希表。

对于 $\textit{rmv}$ 操作,将任务编号 $\textit{taskId}$ 从哈希表中移除。

对于 $\textit{execTop}$ 操作,做法如下。

  1. 当优先队列不为空且优先队列的队首元素应移除时,将优先队列的队首元素移除,重复该操作直到优先队列为空或优先队列的队首元素不应移除。优先队列的队首元素应移除的条件是以下两个条件之一成立。

    • 哈希表中不存在优先队列的队首元素对应的任务编号,该情况为任务被移除。

    • 优先队列的队首元素的优先级与优先队列的队首元素对应的任务编号对应的任务的用户编号不相等或优先级不相等,该情况为任务优先级更新过,优先队列的随后元素是优先级更新前的任务。

  2. 根据优先队列是否为空,执行相应的操作。

    • 当优先队列不为空时,将优先队列的队首元素的任务取出,将该任务的编号从哈希表中移除,返回该任务的用户编号。

    • 当优先队列为空时,不存在任何任务,返回 $-1$。

代码

###Java

class TaskManager {
    private PriorityQueue<Task> pq;
    private Map<Integer, Task> idsTasks;

    public TaskManager(List<List<Integer>> tasks) {
        pq = new PriorityQueue<Task>();
        idsTasks = new HashMap<Integer, Task>();
        for (List<Integer> task : tasks) {
            int userId = task.get(0), taskId = task.get(1), priority = task.get(2);
            add(userId, taskId, priority);
        }
    }

    public void add(int userId, int taskId, int priority) {
        Task task = new Task(userId, taskId, priority);
        pq.offer(task);
        idsTasks.put(taskId, task);
    }

    public void edit(int taskId, int newPriority) {
        Task task = idsTasks.get(taskId);
        Task newTask = new Task(task.getUserId(), taskId, newPriority);
        pq.offer(newTask);
        idsTasks.put(taskId, newTask);
    }

    public void rmv(int taskId) {
        idsTasks.remove(taskId);
    }

    public int execTop() {
        while (!pq.isEmpty() && shouldRemove(pq.peek())) {
            pq.poll();
        }
        if (!pq.isEmpty()) {
            Task task = pq.poll();
            idsTasks.remove(task.getTaskId());
            return task.getUserId();
        } else {
            return -1;
        }
    }

    private boolean shouldRemove(Task task) {
        return !idsTasks.containsKey(task.getTaskId()) || task.getUserId() != idsTasks.get(task.getTaskId()).getUserId() || task.getPriority() != idsTasks.get(task.getTaskId()).getPriority();
    }
}

class Task implements Comparable<Task> {
    private int userId;
    private int taskId;
    private int priority;

    public Task(int userId, int taskId, int priority) {
        this.userId = userId;
        this.taskId = taskId;
        this.priority = priority;
    }

    public int getUserId() {
        return userId;
    }

    public int getTaskId() {
        return taskId;
    }

    public int getPriority() {
        return priority;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public void setTaskId(int taskId) {
        this.taskId = taskId;
    }

    public void setPriority(int priority) {
        this.priority = priority;
    }

    @Override
    public int compareTo(Task task2) {
        return this.priority != task2.priority ? task2.priority - this.priority : task2.taskId - this.taskId;
    }
}

###C#

public class TaskManager {
    private PriorityQueue<Task, Task> pq;
    private IDictionary<int, Task> idsTasks;

    public TaskManager(IList<IList<int>> tasks) {
        pq = new PriorityQueue<Task, Task>();
        idsTasks = new Dictionary<int, Task>();
        foreach (IList<int> task in tasks) {
            int userId = task[0], taskId = task[1], priority = task[2];
            Add(userId, taskId, priority);
        }
    }

    public void Add(int userId, int taskId, int priority) {
        Task task = new Task(userId, taskId, priority);
        pq.Enqueue(task, task);
        if (!idsTasks.ContainsKey(taskId)) {
            idsTasks.Add(taskId, task);
        } else {
            idsTasks[taskId] = task;
        }
    }

    public void Edit(int taskId, int newPriority) {
        Task task = idsTasks[taskId];
        Task newTask = new Task(task.UserId, taskId, newPriority);
        pq.Enqueue(newTask, newTask);
        idsTasks[taskId] = newTask;
    }

    public void Rmv(int taskId) {
        idsTasks.Remove(taskId);
    }

    public int ExecTop() {
        while (pq.Count > 0 && ShouldRemove(pq.Peek())) {
            pq.Dequeue();
        }
        if (pq.Count > 0) {
            Task task = pq.Dequeue();
            idsTasks.Remove(task.TaskId);
            return task.UserId;
        } else {
            return -1;
        }
    }

    private bool ShouldRemove(Task task) {
        return !idsTasks.ContainsKey(task.TaskId) || task.UserId != idsTasks[task.TaskId].UserId || task.Priority != idsTasks[task.TaskId].Priority;
    }
}

class Task : IComparable<Task> {
    public int UserId { get; set; }
    public int TaskId { get; set; }
    public int Priority { get; set; }

    public Task(int userId, int taskId, int priority) {
        UserId = userId;
        TaskId = taskId;
        Priority = priority;
    }

    public int CompareTo(Task task2) {
        return this.Priority != task2.Priority ? task2.Priority - this.Priority : task2.TaskId - this.TaskId;
    }
}

复杂度分析

  • 时间复杂度:构造方法的时间复杂度是 $O(n \log n)$,方法 $\textit{add}$ 和 $\textit{edit}$ 的时间复杂度是 $O(\log n)$,方法 $\textit{rmv}$ 的时间复杂度是 $O(1)$,方法 $\textit{execTop}$ 的均摊时间复杂度是 $O(\log n)$,其中 $n$ 是任务总数。构造方法需要根据给定的任务列表更新优先队列与哈希表,方法 $\textit{add}$ 和 $\textit{edit}$ 需要对一项任务更新优先队列与哈希表,每项任务的更新时间是 $O(\log n)$,方法 $\textit{rmv}$ 更新哈希表的时间是 $O(1)$,方法 $\textit{execTop}$ 需要执行延迟删除,每个元素最多加入优先队列和从优先队列中取出各一次因此均摊时间是 $O(\log n)$。

  • 空间复杂度:$O(n)$,其中 $n$ 是任务总数。优先队列与哈希表的空间是 $O(n)$。

懒删除堆(Python/Java/C++/Go/JS/Rust)

作者 endlesscheng
2025年1月5日 13:33

为了实现 $\texttt{execTop}$,用最大堆维护优先级、任务编号、用户编号。

为了实现堆的懒更新和懒删除,用一个哈希表记录每个任务编号对应的最新的优先级和用户编号。哈希表的 key 是任务编号,value 是优先级和用户编号组成的二元组。

  • $\texttt{edit}$:直接把一个新的优先级、任务编号、用户编号三元组入堆。同时更新哈希表。
  • $\texttt{rmv}$:把元素从哈希表中删掉。更简单的写法是,把优先级改成 $-1$。
  • $\texttt{execTop}$:不断弹出「货不对板」的堆顶,也就是任务编号和哈希表中记录的数据不一致的堆顶。直到堆为空或者找到一致的数据。

注 1:如果你学过 Dijkstra 算法,其中的堆就是懒更新堆。

注 2:题目保证输入的 $\textit{tasks}$ 数组中没有重复的 $\textit{taskId}$。

###py

class TaskManager:
    def __init__(self, tasks: List[List[int]]):
        self.mp = {taskId: (priority, userId) for userId, taskId, priority in tasks}
        self.h = [(-priority, -taskId, userId) for userId, taskId, priority in tasks]  # 取相反数,变成最大堆
        heapify(self.h)

    def add(self, userId: int, taskId: int, priority: int) -> None:
        self.mp[taskId] = (priority, userId)
        heappush(self.h, (-priority, -taskId, userId))

    def edit(self, taskId: int, newPriority: int) -> None:
        # 懒修改
        self.add(self.mp[taskId][1], taskId, newPriority)

    def rmv(self, taskId: int) -> None:
        # 懒删除
        self.mp[taskId] = (-1, -1)

    def execTop(self) -> int:
        while self.h:
            priority, taskId, userId = heappop(self.h)
            if self.mp[-taskId] == (-priority, userId):
                self.rmv(-taskId)
                return userId
            # else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
        return -1

###java

class TaskManager {
    // taskId -> (priority, userId)
    private final Map<Integer, int[]> mp = new HashMap<>();

    // (priority, taskId, userId)
    private final PriorityQueue<int[]> pq =
            new PriorityQueue<>((a, b) -> a[0] != b[0] ? b[0] - a[0] : b[1] - a[1]);

    public TaskManager(List<List<Integer>> tasks) {
        for (List<Integer> task : tasks) {
            add(task.get(0), task.get(1), task.get(2));
        }
    }

    public void add(int userId, int taskId, int priority) {
        mp.put(taskId, new int[]{priority, userId});
        pq.offer(new int[]{priority, taskId, userId});
    }

    public void edit(int taskId, int newPriority) {
        // 懒修改
        int userId = mp.get(taskId)[1];
        add(userId, taskId, newPriority);
    }

    public void rmv(int taskId) {
        // 懒删除
        mp.get(taskId)[0] = -1;
    }

    public int execTop() {
        while (!pq.isEmpty()) {
            int[] top = pq.poll();
            int priority = top[0], taskId = top[1], userId = top[2];
            int[] p = mp.get(taskId);
            if (p[0] == priority && p[1] == userId) {
                rmv(taskId);
                return userId;
            }
            // else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
        }
        return -1;
    }
}

###cpp

class TaskManager {
    unordered_map<int, pair<int, int>> mp; // taskId -> (priority, userId)
    priority_queue<tuple<int, int, int>> pq; // (priority, taskId, userId)

public:
    TaskManager(vector<vector<int>>& tasks) {
        for (auto& task : tasks) {
            add(task[0], task[1], task[2]);
        }
    }

    void add(int userId, int taskId, int priority) {
        mp[taskId] = {priority, userId};
        pq.emplace(priority, taskId, userId);
    }

    void edit(int taskId, int newPriority) {
        // 懒修改
        add(mp[taskId].second, taskId, newPriority);
    }

    void rmv(int taskId) {
        // 懒删除
        mp[taskId].first = -1;
    }

    int execTop() {
        while (!pq.empty()) {
            auto [priority, taskId, userId] = pq.top();
            pq.pop();
            if (mp[taskId] == pair(priority, userId)) {
                rmv(taskId);
                return userId;
            }
            // else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
        }
        return -1;
    }
};

###go

type pair struct{ priority, userId int }

type TaskManager struct {
mp map[int]pair // taskId -> (priority, userId)
h  *hp          // (priority, taskId, userId)
}

func Constructor(tasks [][]int) TaskManager {
n := len(tasks)
mp := make(map[int]pair, n) // 预分配空间
h := make(hp, n)
for i, t := range tasks {
userId, taskId, priority := t[0], t[1], t[2]
mp[taskId] = pair{priority, userId}
h[i] = tuple{priority, taskId, userId}
}
heap.Init(&h)
return TaskManager{mp, &h}
}

func (t *TaskManager) Add(userId, taskId, priority int) {
t.mp[taskId] = pair{priority, userId}
heap.Push(t.h, tuple{priority, taskId, userId})
}

func (t *TaskManager) Edit(taskId, newPriority int) {
// 懒修改
t.Add(t.mp[taskId].userId, taskId, newPriority)
}

func (t *TaskManager) Rmv(taskId int) {
// 懒删除
t.mp[taskId] = pair{-1, -1}
}

func (t *TaskManager) ExecTop() int {
for t.h.Len() > 0 {
top := heap.Pop(t.h).(tuple)
priority, taskId, userId := top.priority, top.taskId, top.userId
if t.mp[taskId] == (pair{priority, userId}) {
t.Rmv(taskId)
return userId
}
// else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
}
return -1
}

type tuple struct{ priority, taskId, userId int }
type hp []tuple
func (h hp) Len() int           { return len(h) }
func (h hp) Less(i, j int) bool { return cmp.Or(h[i].priority-h[j].priority, h[i].taskId-h[j].taskId) > 0 }
func (h hp) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *hp) Push(v any)        { *h = append(*h, v.(tuple)) }
func (h *hp) Pop() any          { a := *h; v := a[len(a)-1]; *h = a[:len(a)-1]; return v }

###js

class TaskManager {
    constructor(tasks) {
        // taskId -> [priority, userId]
        this.mp = new Map();

        // 最大堆 [priority, taskId, userId]
        this.pq = new PriorityQueue((a, b) => a[0] !== b[0] ? b[0] - a[0] : b[1] - a[1]);

        for (const [userId, taskId, priority] of tasks) {
            this.add(userId, taskId, priority);
        }
    }

    add(userId, taskId, priority) {
        this.mp.set(taskId, [priority, userId]);
        this.pq.enqueue([priority, taskId, userId]);
    }

    edit(taskId, newPriority) {
        // 懒修改
        const userId = this.mp.get(taskId)[1];
        this.add(userId, taskId, newPriority);
    }

    rmv(taskId) {
        // 懒删除
        this.mp.get(taskId)[0] = -1;
    }

    execTop() {
        while (!this.pq.isEmpty()) {
            const [priority, taskId, userId] = this.pq.dequeue();
            const [p, u] = this.mp.get(taskId);
            if (p === priority && u === userId) {
                this.rmv(taskId);
                return userId;
            }
            // else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
        }
        return -1;
    }
}

###rust

use std::collections::{BinaryHeap, HashMap};

struct TaskManager {
    mp: HashMap<i32, (i32, i32)>, // taskId -> (priority, userId)
    h: BinaryHeap<(i32, i32, i32)>, // (priority, taskId, userId)
}

impl TaskManager {
    fn new(tasks: Vec<Vec<i32>>) -> Self {
        let mut manager = Self {
            mp: HashMap::new(),
            h: BinaryHeap::new(),
        };
        for task in tasks {
            manager.add(task[0], task[1], task[2]);
        }
        manager
    }

    fn add(&mut self, user_id: i32, task_id: i32, priority: i32) {
        self.mp.insert(task_id, (priority, user_id));
        self.h.push((priority, task_id, user_id));
    }

    fn edit(&mut self, task_id: i32, new_priority: i32) {
        // 懒修改
        let user_id = self.mp.get(&task_id).unwrap().1;
        self.add(user_id, task_id, new_priority);
    }

    fn rmv(&mut self, task_id: i32) {
        // 懒删除
        *self.mp.get_mut(&task_id).unwrap() = (-1, -1);
    }

    fn exec_top(&mut self) -> i32 {
        while let Some((priority, task_id, user_id)) = self.h.pop() {
            let (p, u) = self.mp[&task_id];
            if p == priority && u == user_id {
                self.rmv(task_id);
                return user_id;
            }
            // else 货不对板,堆顶和 mp 中记录的不一样,说明堆顶数据已被修改或删除,不做处理
        }
        -1
    }
}

复杂度分析

  • 时间复杂度:
    • 初始化:$\mathcal{O}(n)$ 或者 $\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{tasks}$ 的长度。Python 和 Go 使用了堆化,复杂度是 $\mathcal{O}(n)$ 的。
    • $\texttt{add}$ 和 $\texttt{edit}$:$\mathcal{O}(\log (n+q))$,其中 $q$ 是 $\texttt{add}$ 和 $\texttt{edit}$ 的调用次数之和。
    • $\texttt{rmv}$:$\mathcal{O}(1)$。
    • $\texttt{execTop}$:均摊 $\mathcal{O}(\log (n+q))$。每个元素至多入堆出堆各一次。
  • 空间复杂度:$\mathcal{O}(n+q)$。

专题训练

见下面数据结构题单的「§5.6 懒删除堆」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

昨天以前LeetCode 每日一题题解

设计数字容器系统(双哈希表与二进制树)

作者 heng-deng-shi
2022年7月25日 08:46

按照题目的要求,数值和索引的取值范围都是[1, 10^9]
要能够在指定的索引处赋值某个数值或修改某个数值,以及找出某个数值所对应的所有索引中最小的那个。那么,对于设计出的数据结构,就有几方面的要求。

  1. 在取值范围内的每个索引,要么还没有存放任何数值,否则只能对应一个数值。
  2. 在取值范围内的每个数值,如果还没有被放入任何索引(也可能是中途被覆盖掉了),则查不出它的任何索引值,即返回无效值-1,否则,它可以对应有多个索引值,且要求能够在较快的时间内找出这些索引值当中的最小值。

这里采用两个哈希表,来处理索引和数值之间的对应关系。另外采用一个二进制树,来存储一个数值所对应的可能的多个索引值。

  1. 索引的哈希表,键值就是索引本身。由于一个索引最多只能对应一个数值,所以,除了键值本身以外,哈希表里面还存储索引值对应的数值。
  2. 数值的哈希表,键值就是数值本身。除了键值以外,哈希表里面还存储一棵二进制树,这棵树里面存储有可能多个的索引值。
    二进制树的具体操作,下面再描述。
  3. 开始创建对象时,两个哈希表都初始化为空表。
  4. 在往某个索引修改数值时,分下面几种情况进行处理。
    4.1 如果这个索引还没有存储过任何数值,则新建一个索引哈希节点。哈希节点里面的数值就等于新赋值的number
    (4.1.1) 如果这个数值对应的数值哈希节点不存在,说明是新出现的数值,则新建一个数值哈希节点,哈希节点中的二进制树也新建,把该索引值存储到二进制树中。
    (4.1.2) 如果这个数值对应的数值哈希节点已经存在,则把当前这个索引值,直接加到它的二进制树中。
    4.2 如果这个索引已经有存储过的数值。
    (4.2.1) 如果新赋值的数值,等于该索引处旧的数值,则什么都不会发生变化,不做任何处理。
    (4.2.2) 如果新赋值的数值,不等于该索引处旧的数值,则先把该索引值,从旧数值的数值哈希节点对应的二进制树中抹去(如果二进制树里面唯一的索引值被抹去了,则这个数值哈希节点也对应地要删除)。再看新的数值对应的数值哈希节点是否存在。
    >>> 4.2.2.1 如果新的数值对应的数值哈希节点不存在,说明是新出现的数值,则新建一个数值哈希节点,哈希节点中的二进制树也新建,把该索引值存储到二进制树中。
    >>> 4.2.2.2 如果新的数值对应的数值哈希节点已经存在,则把当前这个索引值,直接加到它的二进制树中。

【二进制树】
二进制树其实就是一棵二叉树,如下图所示,该数据结构将数值以二进制的方式进行处理,包括添加、删除、查询最大最小值(或者第k大值)等等。
本题中,取值范围[1, 10^9]内的数,最多可能占据30个bits,但为了方便解释,这里以一共4个bits的数值为例(即图中所示),本质道理上是一样的。
从最低位bit0到最高位bit3,共4个bits。假设现在要存储2, 3, 5, 10, 11这几个数值到二进制树中去。
对这些数值,它们的二进制(保留4个bits)依次为0010, 0011, 0101, 1010, 1011,各自从最高的bit3开始进行处理。

  1. 最高位是1的,从根节点往右走,最高位是0的,从根节点往左走,然后分别给各自的左右节点的计数加一。
  2. 然后是bit2,同上,该位是1的,往当前节点的右分支走,该位是0的,往当前节点的左分支走。同时,分别给各自的左右节点的计数加一。
  3. 依次类推,直到bit0为止。
  4. 沿途过程中,如果往左或往右时,对应的分支为NULL,则新建一个分支即可。
  5. 直到最后bit0结束后,每一个数值,都在二进制树中留下了自己的痕迹。上述的2, 3, 5, 10, 11这几个值,在一共4个bits的二进制树留下的痕迹,就如图所示。

相反的,如果需要从二进制树中移除某个数值,其过程和添加数值相反看待就可以,即,上面的计数加一,改成了计数减一。当某个分支的计数值减为零的时候,该分支删除,并赋为NULL即可。

当需要查询整棵二进制树中的最小值时,只要从根节点开始往下,每一个bit依次查看,能往左走的(即左分支不为NULL),尽量往左走,除非左分支为NULL,才往右分支走。按照这个逻辑,走到最低的bit0时,就是最小值的路径,依次把各个bit赋值上,就是结果。在下图的示例中,从根节点开始往下查询,一开始从根节点往下查看bit3时,往左,从bit3往下查看bit2时,也往左,从bit2往下查看bit1时,发现只能往右,此时结果的bit1置为1,最后从bit1往下查看bit0时,还是往左,这样到结束时,得到的二进制结果就是0010,即十进制的2

截图_16587109215246.png

时间复杂度:O(N*logE),每个哈希节点的添加、删除、修改处理的时间复杂度是O(1),二进制树中添加、删除、查询的时间复杂度是O(logE)
空间复杂度:O(N*logE),索引哈希表的空间复杂度是O(N),数值哈希表中由于附带了二进制树,其空间复杂度是O(N*logE)
其中,N是总的操作次数,E表示数值和索引的取值范围[1, 10^9]

#define INVALID_VALUE -1
#define HIGHEST_BIT 0x20000000

/* 二进制树的节点定义。 */
typedef struct BinaryNode_s
{
    int counter;
    struct BinaryNode_s *left;
    struct BinaryNode_s *right;
}
BinaryNode;

/* 数值哈希节点的定义。 */
typedef struct
{
    int number;
    BinaryNode *binary;
    UT_hash_handle hh;
}
HashNumberNode;

/* 索引哈希节点的定义。 */
typedef struct
{
    int index;
    int number;
    UT_hash_handle hh;
}
HashIndexNode;

/* 对象中存储两个哈希表。 */
typedef struct
{
    HashIndexNode *indexTable;
    HashNumberNode *numberTable;
}
NumberContainers;

/* 几个自定义函数的声明,具体实现见下。 */
extern void insertIntoBinary(BinaryNode *binary, int index);
extern void removeFromBinary(BinaryNode *binary, int index);
extern int findSmallestFromBinary(BinaryNode *binary);
extern void deleteTree(BinaryNode *binary);

/* 创建对象,初始化时两个哈希表均为空。 */
NumberContainers *numberContainersCreate()
{
    NumberContainers *obj = (NumberContainers *)malloc(sizeof(NumberContainers));
    obj->indexTable = NULL;
    obj->numberTable = NULL;
    return obj;
}

/* 某个索引处进行赋值。 */
void numberContainersChange(NumberContainers *obj, int index, int number)
{
    HashIndexNode *indexNode = NULL;
    HashNumberNode *numberNode = NULL;
    HASH_FIND_INT(obj->indexTable, &index, indexNode);
    /* 如果该索引是第一次赋值。 */
    if(NULL == indexNode)
    {
        /* 新建一个索引哈希节点。 */
        indexNode = (HashIndexNode *)malloc(sizeof(HashIndexNode));
        indexNode->index = index;
        indexNode->number = number;
        HASH_ADD_INT(obj->indexTable, index, indexNode);
        /* 查询这个赋值的数值,对应的数值哈希节点是否已经存在。 */
        HASH_FIND_INT(obj->numberTable, &number, numberNode);
        if(NULL == numberNode)
        {
            /* 如果还不存在,则先新建一个数值哈希节点。 */
            numberNode = (HashNumberNode *)malloc(sizeof(HashNumberNode));
            numberNode->number = number;
            /* 二进制树的根节点,是默认存在的,先创建好。
            这里使用calloc代替malloc,是暗含了初始化counter为0,和左右分支为NULL的过程。 */
            numberNode->binary = (BinaryNode *)calloc(1, sizeof(BinaryNode));
            HASH_ADD_INT(obj->numberTable, number, numberNode);
        }
        /* 将索引值加入到对应的二进制树中。 */
        insertIntoBinary(numberNode->binary, index);
    }
    /* 否则,只有当新的数值不等于旧的数值时,才有必要进行处理,否则什么都不会发生变化。 */
    else if(indexNode->number != number)
    {
        /* 从旧的数值对应的二进制树中,把这个索引值删除。 */
        HASH_FIND_INT(obj->numberTable, &indexNode->number, numberNode);
        if(NULL != numberNode)
        {
            removeFromBinary(numberNode->binary, index);
            /* 如果删除这个索引值之后,这个数值不剩下任何索引值了,则表示谁也不要它了,它就可以伤心地离开了。 */
            if(NULL == numberNode->binary->left && NULL == numberNode->binary->right)
            {
                HASH_DEL(obj->numberTable, numberNode);
                free(numberNode->binary);
                free(numberNode);
            }
        }
        /* 把新的数值赋值给该索引哈希节点。 */
        indexNode->number = number;
        /* 同时查看新的数值,对应的数值哈希节点是否已经存在。 */
        HASH_FIND_INT(obj->numberTable, &number, numberNode);
        if(NULL == numberNode)
        {
            /* 如果不存在,则新建数值的哈希节点。 */
            numberNode = (HashNumberNode *)malloc(sizeof(HashNumberNode));
            numberNode->number = number;
            /* 同上,根节点默认存在,且使用calloc代替malloc申请空间。 */
            numberNode->binary = (BinaryNode *)calloc(1, sizeof(BinaryNode));
            HASH_ADD_INT(obj->numberTable, number, numberNode);
        }
        /* 把这个索引值加到新的数值哈希节点对应的二进制树中。 */
        insertIntoBinary(numberNode->binary, index);
    }
    return;
}

/* 查找某个数值对应的最小索引值。 */
int numberContainersFind(NumberContainers *obj, int number)
{
    int x = INVALID_VALUE;
    HashNumberNode *numberNode = NULL;
    HASH_FIND_INT(obj->numberTable, &number, numberNode);
    /* 当这个数值哈希节点存在时,才可能找到最小索引值,否则就返回初始化的-1。 */
    if(NULL != numberNode)
    {
        x = findSmallestFromBinary(numberNode->binary);
    }
    return x;
}

/* 释放对象。先释放两个哈希表,再释放对象。 */
void numberContainersFree(NumberContainers *obj)
{
    HashIndexNode *indexNode = NULL, *it = NULL;
    HashNumberNode *numberNode = NULL, *nt = NULL;
    HASH_ITER(hh, obj->indexTable, indexNode, it)
    {
        HASH_DEL(obj->indexTable, indexNode);
        free(indexNode);
    }
    HASH_ITER(hh, obj->numberTable, numberNode, nt)
    {
        HASH_DEL(obj->numberTable, numberNode);
        if(NULL != numberNode->binary)
        {
            deleteTree(numberNode->binary);
        }
        free(numberNode);
    }
    free(obj);
    return;
}

/* 几个自定义函数的实现,见下。 */

/* 将索引值加入到对应的二进制树中。 */
void insertIntoBinary(BinaryNode *binary, int index)
{
    int bit = HIGHEST_BIT;
    BinaryNode *node = binary;
    /* 从最高bit开始,逐位往下。 */
    while(0 != bit)
    {
        /* 该位为1的,往右分支走。且计数加一。 */
        if(index & bit)
        {
            if(NULL == node->right)
            {
                node->right = (BinaryNode *)calloc(1, sizeof(BinaryNode));
            }
            node->right->counter++;
            node = node->right;
        }
        /* 该位为0的,往左分支走。且计数加一。 */
        else
        {
            if(NULL == node->left)
            {
                node->left = (BinaryNode *)calloc(1, sizeof(BinaryNode));
            }
            node->left->counter++;
            node = node->left;
        }
        /* 下一位。 */
        bit = bit >> 1;
    }
    return;
}

/* 从二进制树中删除某个索引值。 */
void removeFromBinary(BinaryNode *binary, int index)
{
    int bit = HIGHEST_BIT;
    BinaryNode *node = binary;
    /* 从最高bit开始,逐位往下。 */
    while(0 != bit && NULL != node)
    {
        /* 该位为1的,往右分支走。且计数减一。 */
        if(index & bit)
        {
            node->right->counter--;
            if(0 == node->right->counter)
            {
                deleteTree(node->right);
                node->right = NULL;
            }
            node = node->right;
        }
        /* 该位为0的,往左分支走。且计数减一。 */
        else
        {
            node->left->counter--;
            if(0 == node->left->counter)
            {
                deleteTree(node->left);
                node->left = NULL;
            }
            node = node->left;
        }
        /* 下一位。 */
        bit = bit >> 1;
    }
    return;
}

/* 在二进制树中查找最小的索引值。 */
int findSmallestFromBinary(BinaryNode *binary)
{
    int bit = HIGHEST_BIT, x = 0;
    BinaryNode *node = binary;
    /* 从最高bit开始,逐位往下。 */
    while(0 != bit && NULL != node)
    {
        /* 能往左就尽量往左,左边的数值小。 */
        if(NULL != node->left)
        {
            node = node->left;
        }
        /* 没有左分支的话,就只能往右了。往右时,该bit为1,要加上。这里用或操作代替加法也可以。 */
        else
        {
            x += bit;
            node = node->right;
        }
        /* 下一位。 */
        bit = bit >> 1;
    }
    return x;
}

/* 删除树,或某个子树。 */
void deleteTree(BinaryNode *binary)
{
    if(NULL != binary->left)
    {
        deleteTree(binary->left);
    }
    if(NULL != binary->right)
    {
        deleteTree(binary->right);
    }
    free(binary);
    return;
}

每日一题-设计数字容器系统🟡

2025年9月17日 00:00

设计一个数字容器系统,可以实现以下功能:

  • 在系统中给定下标处 插入 或者 替换 一个数字。
  • 返回 系统中给定数字的最小下标。

请你实现一个 NumberContainers 类:

  • NumberContainers() 初始化数字容器系统。
  • void change(int index, int number) 在下标 index 处填入 number 。如果该下标 index 处已经有数字了,那么用 number 替换该数字。
  • int find(int number) 返回给定数字 number 在系统中的最小下标。如果系统中没有 number ,那么返回 -1 。

 

示例:

输入:
["NumberContainers", "find", "change", "change", "change", "change", "find", "change", "find"]
[[], [10], [2, 10], [1, 10], [3, 10], [5, 10], [10], [1, 20], [10]]
输出:
[null, -1, null, null, null, null, 1, null, 2]

解释:
NumberContainers nc = new NumberContainers();
nc.find(10); // 没有数字 10 ,所以返回 -1 。
nc.change(2, 10); // 容器中下标为 2 处填入数字 10 。
nc.change(1, 10); // 容器中下标为 1 处填入数字 10 。
nc.change(3, 10); // 容器中下标为 3 处填入数字 10 。
nc.change(5, 10); // 容器中下标为 5 处填入数字 10 。
nc.find(10); // 数字 10 所在的下标为 1 ,2 ,3 和 5 。因为最小下标为 1 ,所以返回 1 。
nc.change(1, 20); // 容器中下标为 1 处填入数字 20 。注意,下标 1 处之前为 10 ,现在被替换为 20 。
nc.find(10); // 数字 10 所在下标为 2 ,3 和 5 。最小下标为 2 ,所以返回 2 。

 

提示:

  • 1 <= index, number <= 109
  • 调用 change 和 find 的 总次数 不超过 105 次。

竞赛时 时间紧,临时加一层缓存就好AC了 这样会快很多

作者 es-7
2022年7月24日 18:55

批注 2022-07-24 185846.png

var NumberContainers = function() {
    this.map = new Map()
    this.obj = {}
    this.cache = {}
};


NumberContainers.prototype.change = function(index, number) {
   if(this.obj[index] !== undefined){
       let v = this.obj[index]
       let set = this.map.get(v)
       if(set) set.delete(index)
       delete this.cache[v]
   }
    this.obj[index] = number 
    this.map.set(number, (this.map.get(number) || new Set()).add(index))
    delete this.cache[number]
};

NumberContainers.prototype.find = function(number) {
    if(this.cache[number]) return this.cache[number]
    let set = this.map.get(number),ret 
    if(!set || set.size === 0) ret = -1
    else ret = Math.min(...set)
    this.cache[number] = ret
    return ret
};


两种方法:有序集合 / 懒删除堆(Python/Java/C++/Go)

作者 endlesscheng
2022年7月24日 07:52

方法一:哈希表 + 有序集合

为了实现 $\texttt{find}$,我们需要对每个 $\textit{number}$ 创建一个有序集合,维护这个 $\textit{number}$ 对应的所有下标。用有序集合可以快速地获取最小下标。

对于 $\texttt{change}$,如果 $\textit{index}$ 处有数字,我们需要先删除旧的数字,所以还需要知道每个 $\textit{index}$ 对应的 $\textit{number}$ 是多少,这可以用一个哈希表记录。

具体来说,创建一个哈希表 $\textit{indexToNumber}$,以及一个哈希表套有序集合 $\textit{numberToIndices}$。

对于 $\texttt{change}$:

  • 如果 $\textit{index}$ 处有数字 $x$,那么从 $\textit{numberToIndices}[x]$ 中删除 $\textit{index}$(删除旧的数据)。
  • 然后,更新(或者插入)$\textit{indexToNumber}[\textit{index}] = \textit{number}$,往 $\textit{numberToIndices}[\textit{number}]$ 中添加 $\textit{index}$。

对于 $\texttt{find}$,获取 $\textit{numberToIndices}[\textit{number}]$ 中的最小元素即可。

class NumberContainers:
    def __init__(self):
        self.index_to_number = {}
        # from sortedcontainers import SortedSet
        self.number_to_indices = defaultdict(SortedSet)

    def change(self, index: int, number: int) -> None:
        # 移除旧数据
        old_number = self.index_to_number.get(index, None)
        if old_number is not None:
            self.number_to_indices[old_number].discard(index)

        # 添加新数据
        self.index_to_number[index] = number
        self.number_to_indices[number].add(index)

    def find(self, number: int) -> int:
        indices = self.number_to_indices[number]
        return indices[0] if indices else -1
class NumberContainers {
    private final Map<Integer, Integer> indexToNumber = new HashMap<>();
    private final Map<Integer, TreeSet<Integer>> numberToIndices = new HashMap<>();

    public void change(int index, int number) {
        // 移除旧数据
        Integer oldNumber = indexToNumber.get(index);
        if (oldNumber != null) {
            numberToIndices.get(oldNumber).remove(index);
        }

        // 添加新数据
        indexToNumber.put(index, number);
        numberToIndices.computeIfAbsent(number, _ -> new TreeSet<>()).add(index);
    }

    public int find(int number) {
        TreeSet<Integer> indices = numberToIndices.get(number);
        return indices == null || indices.isEmpty() ? -1 : indices.first();
    }
}
class NumberContainers {
    unordered_map<int, int> index_to_number;
    unordered_map<int, set<int>> number_to_indices;

public:
    void change(int index, int number) {
        // 移除旧数据
        auto it = index_to_number.find(index);
        if (it != index_to_number.end()) {
            number_to_indices[it->second].erase(index);
        }

        // 添加新数据
        index_to_number[index] = number;
        number_to_indices[number].insert(index);
    }

    int find(int number) {
        auto it = number_to_indices.find(number);
        return it == number_to_indices.end() || it->second.empty() ? -1 : *it->second.begin();
    }
};
// import "github.com/emirpasic/gods/v2/trees/redblacktree"
type NumberContainers struct {
indexToNumber   map[int]int
numberToIndices map[int]*redblacktree.Tree[int, struct{}]
}

func Constructor() NumberContainers {
return NumberContainers{map[int]int{}, map[int]*redblacktree.Tree[int, struct{}]{}}
}

func (n NumberContainers) Change(index, number int) {
// 移除旧数据
if oldNumber, ok := n.indexToNumber[index]; ok {
n.numberToIndices[oldNumber].Remove(index)
}

// 添加新数据
n.indexToNumber[index] = number
if n.numberToIndices[number] == nil {
n.numberToIndices[number] = redblacktree.New[int, struct{}]()
}
n.numberToIndices[number].Put(index, struct{}{})
}

func (n NumberContainers) Find(number int) int {
indices, ok := n.numberToIndices[number]
if !ok || indices.Empty() {
return -1
}
return indices.Left().Key
}

复杂度分析

  • 时间复杂度:
    • 初始化 $\mathcal{O}(1)$。
    • $\texttt{change}$:$\mathcal{O}(\log q)$,其中 $q$ 是 $\texttt{change}$ 的调用次数。
    • $\texttt{find}$:$\mathcal{O}(\log q)$ 或者 $\mathcal{O}(1)$,取决于有序集合是否额外维护最小值。
  • 空间复杂度:$\mathcal{O}(q)$。

方法二:哈希表 + 懒删除堆

$\textit{numberToIndices}$ 改成哈希表套最小堆。

对于 $\texttt{change}$,不删除旧数据。

对于 $\texttt{find}$,查看堆顶是否等于 $\textit{number}$,若不相同,则意味着堆顶是之前没有删除的旧数据,弹出堆顶;否则堆顶就是答案。

class NumberContainers:
    def __init__(self):
        self.index_to_number = {}
        self.number_to_indices = defaultdict(list)

    def change(self, index: int, number: int) -> None:
        # 添加新数据
        self.index_to_number[index] = number
        heappush(self.number_to_indices[number], index)

    def find(self, number: int) -> int:
        indices = self.number_to_indices[number]
        while indices and self.index_to_number[indices[0]] != number:
            heappop(indices)  # 堆顶货不对板,说明是旧数据,删除
        return indices[0] if indices else -1
class NumberContainers {
    private final Map<Integer, Integer> indexToNumber = new HashMap<>();
    private final Map<Integer, PriorityQueue<Integer>> numberToIndices = new HashMap<>();

    public void change(int index, int number) {
        // 添加新数据
        indexToNumber.put(index, number);
        numberToIndices.computeIfAbsent(number, _ -> new PriorityQueue<>()).offer(index);
    }

    public int find(int number) {
        PriorityQueue<Integer> indices = numberToIndices.get(number);
        if (indices == null) {
            return -1;
        }
        while (!indices.isEmpty() && indexToNumber.get(indices.peek()) != number) {
            indices.poll(); // 堆顶货不对板,说明是旧数据,删除
        }
        return indices.isEmpty() ? -1 : indices.peek();
    }
}
class NumberContainers {
    unordered_map<int, int> index_to_number;
    unordered_map<int, priority_queue<int, vector<int>, greater<int>>> number_to_indices;

public:
    void change(int index, int number) {
        // 添加新数据
        index_to_number[index] = number;
        number_to_indices[number].push(index);
    }

    int find(int number) {
        auto& indices = number_to_indices[number];
        while (!indices.empty() && index_to_number[indices.top()] != number) {
            indices.pop(); // 堆顶货不对板,说明是旧数据,删除
        }
        return indices.empty() ? -1 : indices.top();
    }
};
type NumberContainers struct {
indexToNumber   map[int]int
numberToIndices map[int]*hp
}

func Constructor() NumberContainers {
return NumberContainers{map[int]int{}, map[int]*hp{}}
}

func (n NumberContainers) Change(index, number int) {
// 添加新数据
n.indexToNumber[index] = number
if _, ok := n.numberToIndices[number]; !ok {
n.numberToIndices[number] = &hp{}
}
heap.Push(n.numberToIndices[number], index)
}

func (n NumberContainers) Find(number int) int {
indices, ok := n.numberToIndices[number]
if !ok {
return -1
}
for indices.Len() > 0 && n.indexToNumber[indices.IntSlice[0]] != number {
heap.Pop(indices) // 堆顶货不对板,说明是旧数据,删除
}
if indices.Len() == 0 {
return -1
}
return indices.IntSlice[0]
}

type hp struct{ sort.IntSlice }
func (h *hp) Push(v any) { h.IntSlice = append(h.IntSlice, v.(int)) }
func (h *hp) Pop() any   { a := h.IntSlice; v := a[len(a)-1]; h.IntSlice = a[:len(a)-1]; return v }

复杂度分析

  • 时间复杂度:
    • 初始化 $\mathcal{O}(1)$。
    • $\texttt{change}$:$\mathcal{O}(\log q)$,其中 $q$ 是 $\texttt{change}$ 的调用次数。
    • $\texttt{find}$:均摊 $\mathcal{O}(\log q)$。
  • 空间复杂度:$\mathcal{O}(q)$。

专题训练

见下面数据结构题单的「§5.6 懒删除堆」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

「三哈希构造」Java 80ms

作者 hu-li-hu-wai
2022年7月24日 07:18

方法:「哈希构造」

截屏2022-07-24 07.23.12.png

思路及算法

双向映射很容易想到, 用哈希表保存 索引 和 数值的关系. 关键是一旦出现了 同下标的更新 应该如何处理
我们用 numMap 保存正向 索引 —> 数值, 而 数值 —> 索引 我们拆为

  • minIndex 表示 —> 最小索引 的映射
  • indexsList 表示 —> 所有索引 的映射

原因在于最小索引需要频繁更新, 而找最小索引只有当最小索引被删了才需要遍历更新(发生频率低)

具体逻辑见代码

代码

###java

class Solution {
  class NumberContainers {

    HashMap<Integer, Integer> numMap = new HashMap<>();//保存正向 索引 —> 数值
    HashMap<Integer, Integer> minIndex = new HashMap<>();//保存反向 数值 —> 最小索引 的映射
    HashMap<Integer, Set<Integer>> indexsList = new HashMap<>();//保存反向 数值 —> 所有索引 的映射

    public NumberContainers() {

    }

    public void change(int index, int number) {
      if (numMap.containsKey(index)) {//1.如果存在index
        int prev = numMap.remove(index);
        Set<Integer> prevIndexsList = indexsList.get(prev);
        prevIndexsList.remove(index);
        if (prevIndexsList.isEmpty()) {//只有一个数, 删完了, 需要清空key
          indexsList.remove(prev);
          minIndex.remove(prev);
        } else if (minIndex.get(prev) == index) {//如果删的就是最小索引,需要再找一次最小索引
          Set<Integer> indexs = indexsList.get(prev);
          int min = Integer.MAX_VALUE;
          for (int x : indexs) min = Math.min(min, x);
          minIndex.put(prev, min);
        }
      }
      numMap.put(index, number);//更新正向映射
      Integer min = minIndex.getOrDefault(number, Integer.MAX_VALUE);
      min = Math.min(min, index);
      minIndex.put(number, min);//更新 最小索引 映射
      Set<Integer> list = indexsList.getOrDefault(number, new HashSet<>());
      list.add(index);
      indexsList.put(number, list);//更新 所有索引 映射
    }

    public int find(int number) {
      return minIndex.getOrDefault(number, -1);//返回相当简洁
    }
  }
}

结语
如果对您有帮助,欢迎点赞、收藏、关注 沪里户外,让更多的小伙伴看到,祝大家offer多多,AC多多

❌
❌