Design Task Manager
MedPrompt
There is a task management system that allows users to manage their tasks, each associated with a priority. The system should efficiently handle adding, modifying, executing, and removing tasks.
Implement the TaskManager class:
-
TaskManager(vector<vector<int>>& tasks)initializes the task manager with a list of user-task-priority triples. Each element in the input list is of the form[userId, taskId, priority], which adds a task to the specified user with the given priority. -
void add(int userId, int taskId, int priority)adds a task with the specifiedtaskIdandpriorityto the user withuserId. It is guaranteed thattaskIddoes not exist in the system. -
void edit(int taskId, int newPriority)updates the priority of the existingtaskIdtonewPriority. It is guaranteed thattaskIdexists in the system. -
void rmv(int taskId)removes the task identified bytaskIdfrom the system. It is guaranteed thattaskIdexists in the system. -
int execTop()executes the task with the highest priority across all users. If there are multiple tasks with the same highest priority, execute the one with the highesttaskId. After executing, thetaskIdis removed from the system. Return theuserIdassociated with the executed task. If no tasks are available, return -1.
Note that a user may be assigned multiple tasks.
Example 1:
Input:
["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], []]
Output:
[null, null, null, 3, null, null, 5]
Explanation
TaskManager taskManager = new TaskManager([[1, 101, 10], [2, 102, 20], [3, 103, 15]]); // Initializes with three tasks for Users 1, 2, and 3.taskManager.add(4, 104, 5); // Adds task 104 with priority 5 for User 4.
taskManager.edit(102, 8); // Updates priority of task 102 to 8.
taskManager.execTop(); // return 3. Executes task 103 for User 3.
taskManager.rmv(101); // Removes task 101 from the system.
taskManager.add(5, 105, 15); // Adds task 105 with priority 15 for User 5.
taskManager.execTop(); // return 5. Executes task 105 for User 5.
Constraints:
1 <= tasks.length <= 1050 <= userId <= 1050 <= taskId <= 1050 <= priority <= 1090 <= newPriority <= 109- At most
2 * 105calls will be made in total toadd,edit,rmv, andexecTopmethods. - The input is generated such that
taskIdwill be valid.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach uses a simple HashMap to store the tasks. While operations like adding, editing, and removing tasks are very fast (constant time), finding the task with the highest priority requires scanning through all the tasks currently in the system. This makes the execTop operation inefficient for a large number of tasks.
Algorithm
- Data Structure: Use a
HashMap<Integer, int[]>where the key istaskIdand the value is an array[userId, priority]. - Initialization: In the constructor, iterate through the initial list of tasks and populate the
HashMap. add(userId, taskId, priority): Insert a new entry into theHashMap. This is anO(1)operation.edit(taskId, newPriority): Access the task by itstaskIdin theHashMapand update its priority value. This is anO(1)operation.rmv(taskId): Remove the entry corresponding totaskIdfrom theHashMap. This is anO(1)operation.execTop():- If the map is empty, return -1.
- Initialize variables to track the best task found so far (e.g.,
maxPriority,bestTaskId,bestUserId). - Iterate through every task in the
HashMap. - For each task, compare its priority (and
taskIdfor tie-breaking) with the current best task. - If the current task is better, update the tracking variables.
- After checking all tasks, the best task has been identified. Remove it from the
HashMapand return itsuserId.
Walkthrough
Data Structure
The core of this approach is a java.util.HashMap that maps each taskId to an integer array containing its userId and priority. This provides O(1) average-case time complexity for lookups, insertions, and deletions by taskId.
Method Implementations
add,edit,rmv: These methods directly translate to theHashMap'sput,get/put, andremoveoperations, respectively. SincetaskIdis guaranteed to exist foreditandrmv, these operations are straightforward and efficient.execTop: This is the performance bottleneck. To find the highest priority task, we have no choice but to perform a full linear scan of all the values in theHashMap. We iterate through each task, keeping track of the one with the highest priority seen so far. If two tasks have the same priority, the one with the largertaskIdis chosen. Once the entire map has been traversed, the winning task is removed from the map, and itsuserIdis returned.
import java.util.HashMap;import java.util.Map; class TaskManager { private Map<Integer, int[]> taskMap; // taskId -> {userId, priority} public TaskManager(int[][] tasks) { taskMap = new HashMap<>(); for (int[] task : tasks) { add(task[0], task[1], task[2]); } } public void add(int userId, int taskId, int priority) { taskMap.put(taskId, new int[]{userId, priority}); } public void edit(int taskId, int newPriority) { // The problem guarantees taskId exists. taskMap.get(taskId)[1] = newPriority; } public void rmv(int taskId) { taskMap.remove(taskId); } public int execTop() { if (taskMap.isEmpty()) { return -1; } int bestTaskId = -1; int maxPriority = -1; int bestUserId = -1; for (Map.Entry<Integer, int[]> entry : taskMap.entrySet()) { int currentTaskId = entry.getKey(); int[] taskDetails = entry.getValue(); int currentPriority = taskDetails[1]; if (currentPriority > maxPriority) { maxPriority = currentPriority; bestTaskId = currentTaskId; bestUserId = taskDetails[0]; } else if (currentPriority == maxPriority) { if (currentTaskId > bestTaskId) { bestTaskId = currentTaskId; bestUserId = taskDetails[0]; } } } taskMap.remove(bestTaskId); return bestUserId; }}Complexity
Time
- `add`, `edit`, `rmv`: `O(1)` on average. - `execTop`: `O(N)`, where N is the number of tasks. This is because we must iterate through all tasks to find the maximum.
Space
O(N), where N is the number of tasks in the system. The `HashMap` stores an entry for each task.
Trade-offs
Pros
Simple to understand and implement.
Extremely fast
O(1)time complexity foradd,edit, andrmvoperations.
Cons
The
execTopmethod is very inefficient with a time complexity ofO(N), whereNis the number of tasks. This will lead to a 'Time Limit Exceeded' error on large test cases.
This optimized approach uses a combination of a Max-Heap (implemented as a PriorityQueue) and a HashMap to achieve much better performance for the execTop operation. The PriorityQueue keeps tasks ordered by priority, allowing for logarithmic time access to the top task. To handle edit and rmv operations efficiently without incurring the high cost of modifying an arbitrary element in a heap, a 'lazy deletion' strategy is employed.
Algorithm
- Data Structures:
- A
PriorityQueue<int[]>(Max-Heap) to store tasks as[priority, taskId, userId]. It's ordered by priority, thentaskId, to make finding the top task efficient. - A
HashMap<Integer, int[]>to maptaskIdto[userId, priority]. This map serves as the single source of truth for a task's current state.
- A
add(userId, taskId, priority): Add the task to both theHashMapand thePriorityQueue.edit(taskId, newPriority): Update the task's priority in theHashMap. Then, add a new entry for this task with the updated priority to thePriorityQueue. The old entry in the queue is not removed; it becomes 'stale'.rmv(taskId): Remove the task from theHashMap. The corresponding entry in thePriorityQueuebecomes 'stale'.execTop():- Repeatedly look at the top element of the
PriorityQueue. - Check if this task is 'stale' by comparing it with the information in the
HashMap. A task is stale if it's not in the map, or if its priority in the queue doesn't match the current priority in the map. - If the top task is stale, remove it from the queue and repeat.
- If the top task is valid, it's the one to be executed. Remove it from both the queue and the map, and return its
userId.
- Repeatedly look at the top element of the
Walkthrough
Data Structures and Strategy
We use two main data structures:
PriorityQueue<int[]>: A max-heap that stores task information as[priority, taskId, userId]. The custom comparator ensures that elements with higher priority are at the top. For ties in priority, the element with the highertaskIdtakes precedence. This makes finding the best task anO(log M)operation.HashMap<Integer, int[]>: This map stores the ground truth for each task, mapping ataskIdto its[userId, priority]. It allows forO(1)updates and lookups.
The key idea is lazy deletion. When a task is edited or removed, we don't try to find and remove the old entry from the heap (which is an O(M) operation). Instead, we mark it as invalid by either updating (edit) or removing (rmv) its entry in the HashMap. The execTop method is then responsible for filtering out these 'stale' tasks from the top of the heap before executing a valid one.
Method Implementations
add: A new task is added to both theHashMapand thePriorityQueue.edit: The task's priority is updated in theHashMap. A new entry representing the updated task is added to thePriorityQueue. The old entry is now stale.rmv: The task is removed from theHashMap. Its entry in thePriorityQueueis now stale.execTop: We peek at the top of thePriorityQueue. We check its validity against theHashMap. If the task doesn't exist in the map or its priority is outdated, wepollit and check the next one. This continues until a valid task is found at the top. This valid task is then removed from both data structures, and itsuserIdis returned.
import java.util.HashMap;import java.util.Map;import java.util.PriorityQueue; class TaskManager { // Max-heap to get the top task efficiently. Stores {priority, taskId, userId} private PriorityQueue<int[]> pq; // Map to store the current valid state of a task. taskId -> {userId, priority} private Map<Integer, int[]> taskMap; public TaskManager(int[][] tasks) { // Comparator for the max-heap: // 1. Higher priority first. // 2. If priorities are equal, higher taskId first. pq = new PriorityQueue<>((a, b) -> { if (a[0] != b[0]) { return b[0] - a[0]; // Descending priority } return b[1] - a[1]; // Descending taskId }); taskMap = new HashMap<>(); for (int[] task : tasks) { add(task[0], task[1], task[2]); } } public void add(int userId, int taskId, int priority) { taskMap.put(taskId, new int[]{userId, priority}); pq.offer(new int[]{priority, taskId, userId}); } public void edit(int taskId, int newPriority) { int[] taskDetails = taskMap.get(taskId); taskDetails[1] = newPriority; // No need for taskMap.put() as the array is modified by reference // Add the new version of the task to the priority queue. // The old version is now "stale" and will be ignored by execTop. pq.offer(new int[]{newPriority, taskId, taskDetails[0]}); } public void rmv(int taskId) { // Simply remove from the map. The entry in the PQ becomes "stale". taskMap.remove(taskId); } public int execTop() { // Clean up stale tasks from the top of the queue while (!pq.isEmpty()) { int[] topTask = pq.peek(); int priority = topTask[0]; int taskId = topTask[1]; // A task is valid if it exists in the map and its priority matches. if (taskMap.containsKey(taskId) && taskMap.get(taskId)[1] == priority) { // Valid task found, execute it pq.poll(); // Remove from pq taskMap.remove(taskId); // Remove from map return topTask[2]; // Return userId } else { // This is a stale task (either removed or edited), discard it. pq.poll(); } } // No valid tasks left return -1; }}Complexity
Time
- Let M be the total number of `add` and `edit` calls made. - `add`, `edit`: `O(log M)` due to insertion into the priority queue. - `rmv`: `O(1)` as it only involves a hash map removal. - `execTop`: Amortized `O(log M)`. While a single call might remove multiple stale entries, each task version is pushed and popped from the heap only once over the entire sequence of operations.
Space
O(N + M), where N is the number of active tasks and M is the total number of `add` and `edit` operations. The `HashMap` takes `O(N)` space, and the `PriorityQueue` can take up to `O(M)` space due to stale entries.
Trade-offs
Pros
Highly efficient
execTopoperation with logarithmic time complexity.All operations (
add,edit,rmv,execTop) have efficient time complexities suitable for large-scale problems.
Cons
More complex to implement due to the need to manage two data structures and handle stale entries.
Uses more memory than the brute-force approach because the
PriorityQueuecan temporarily store stale entries for tasks that have been edited or removed.
Solutions
Solution
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(); */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(); */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()Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.