Start learning free

LESSON 387 OF 536 · HEAP

Task Scheduler in JavaScript: O(T log 26) with a Max-Heap and Queue

Practice O(T log 26) Read the full explanation

A CPU has to run a list of tasks. Each task is an uppercase letter from A to Z; equal letters are copies of the same task. Running one task takes one unit of time, and in each unit the CPU either runs one task or stays idle.

There is a cooldown: two copies of the same task must be at least n units apart, so after running a task the CPU needs n other units (other tasks, or idle) before it can run that task again. The tasks can run in any order.

Return the fewest units of time the CPU needs to run all the tasks.

 

Example 1:

Input: tasks = ["A","A","A","B","B","B"], n = 2
Output: 8
Explanation: One best order is A B idle A B idle A B.
After an A, two other units must pass before the next A, and B has the same rule. With only two kinds of task, the CPU has to idle twice.

Example 2:

Input: tasks = ["A","A","A","B","B","B"], n = 0
Output: 6
Explanation: With no cooldown, any order works, for example A A A B B B, and no unit is idle.

Example 3:

Input: tasks = ["A","A","A","A","A","A","B","C","D","E","F","G"], n = 2
Output: 16
Explanation: One best order is A B C A D E A F G A idle idle A idle idle A.
The six A's need two units between each other; the other tasks fill the first gaps, and the CPU idles in the rest.

 

Constraints:

  • 1 <= tasks.length <= 105
  • Every task is an uppercase English letter.
  • 0 <= n <= 100

Note:

Your algorithm should run in O(T log 26) time, where T is the number of tasks (log 26 because a heap here never holds more than 26 kinds of task), and use O(1) extra space: at most 26 counts.

The tutorial simulates the CPU with a max-heap: in each unit, run the task with the most copies left, then let it wait in a queue until its cooldown is over. When no task is ready, the time jumps straight to the moment the first waiting task is ready again. (The answer also has a counting shortcut: if the most frequent task appears m times and c tasks appear that often, it is the bigger of tasks.length and (m - 1) · (n + 1) + c.)

STEP 1 OF 15
    Ctrl + Enter to check

    JAVASCRIPT · HEAP

    Task Scheduler in JavaScript, explained

    Problem overview

    A CPU must run a list of tasks, each an uppercase letter from "A" to "Z". Equal letters are copies of the same task. Running a task takes one unit of time, and in every unit the CPU either runs one task or stays idle. Two copies of the same task must be at least n units apart: after running a task, the CPU needs n other units, whether tasks or idle time, before it can run that task again. The tasks can run in any order.

    The method leastInterval(tasks, n) returns the fewest units of time needed to run all the tasks.

    For tasks = ["A","A","A","B","B","B"] and n = 2 the answer is 8. One best order is A B idle A B idle A B: with only two kinds of task, the CPU has to idle twice. With n = 0 there is no cooldown and the answer is 6. For ["A","A","A","A","A","A","B","C","D","E","F","G"] and n = 2 the answer is 16: the six A's need two units between each pair, the other tasks fill the first gaps, and the CPU idles in the rest.

    The statement asks for O(T log 26) time, where T is the number of tasks, and O(1) extra space: at most 26 counts.

    Approach: always run the task with the most copies left

    Which letter a task is doesn't matter, only how many copies of it there are. So the first step is to count the copies of each letter.

    Then simulate the CPU one unit at a time with a greedy rule: run the task that has the most copies left. Its copies need the most room between them, so starting them early leaves the fewest idle units at the end. A max-heap of counts gives the biggest count on top.

    After a task runs, it can't run again for a while. A second structure holds it during that time: a queue of waiting tasks, each stored as [left, readyTime], where left is the number of copies still to run and readyTime is the time unit at whose end the cooldown is over. A task run at time t can run again at t + n + 1, after n other units, so its ready time is t + n. Tasks join the queue in the order they run, so they leave it in order of ready time: the front is always the first one to become ready.

    When the heap is empty but tasks are still cooling down, the CPU idles. Rather than counting idle units one at a time, the simulation jumps straight to the moment the first waiting task is ready.

    There is also a counting shortcut: if the most frequent task appears m times and c different tasks appear that often, the answer is the larger of tasks.length and (m - 1) * (n + 1) + c. This lesson builds the simulation, which also shows how the schedule works.

    Step by step

    1. Count the copies. Create count with new Array(26).fill(0): one place per letter, index 0 for A up to 25 for Z.
    2. Loop over tasks with for...of.
    3. Add one to the letter's place. task.charCodeAt(0) gives the letter's code, and 65 is the code of "A", so subtracting 65 gives an index from 0 to 25.
    4. Create the heap: a MaxHeap from the library the lesson provides. It will hold counts, biggest on top.
    5. Loop over count, taking each count as c.
    6. Insert only positive counts. A letter that never appears has count 0 and nothing to run.
    7. Create the queue cooling as an empty array. Each entry will be [left, readyTime].
    8. Start time at 0. It counts the units used so far and ends as the answer.
    9. Loop while work remains: while the heap isn't empty or cooling holds a task.
    10. Add one to time. A new unit starts.
    11. If the heap is empty, nothing can run, so the CPU idles. Set time to the first waiting task's ready time, cooling[0][1]. That counts all the idle units in between at once.
    12. Otherwise, run the biggest task. left is heap.extract() - 1: the copies it still has after this unit.
    13. Queue it if copies remain. If left > 0, push [left, time + n] onto cooling. A task with no copies left is done and isn't queued.
    14. Release a ready task. If cooling isn't empty and its front entry's ready time equals time, remove it with shift() and insert its count, item 0, back into the heap.
    15. Return time. It is the unit in which the last task ran.

    The solution in JavaScript

    var algorithms = require('algorithms')
    class Solution {
        leastInterval(tasks, n) {
            let count = new Array(26).fill(0);
            for (let task of tasks) {
                count[task.charCodeAt(0) - 65]++;
            }
            let heap = new algorithms.DataStructures.Heap.MaxHeap();
            for (let c of count) {
                if (c > 0) {
                    heap.insert(c);
                }
            }
            let cooling = [];
            let time = 0;
            while (!heap.isEmpty() || cooling.length > 0) {
                time++;
                if (heap.isEmpty()) {
                    time = cooling[0][1];
                } else {
                    let left = heap.extract() - 1;
                    if (left > 0) {
                        cooling.push([left, time + n]);
                    }
                }
                if (cooling.length > 0 && cooling[0][1] == time) {
                    heap.insert(cooling.shift()[0]);
                }
            }
            return time;
        }
    }
    

    The JavaScript details worth a closer look:

    • The heap library. JavaScript has no built-in heap. The starter code loads one with require('algorithms'), whose MaxHeap has insert, extract, isEmpty and n. With no compare function it compares numbers, so the biggest count is extracted first.
    • Letters to indices. A string has no numeric value, so charCodeAt(0) returns the code of the character at position 0. "A".charCodeAt(0) is 65 and "Z".charCodeAt(0) - 65 is 25.
    • The compound update. count[...]++ reads, adds one and writes back, on an array of zeros that fill(0) prepared. Without the fill, the places would hold undefined, and undefined + 1 is NaN.
    • shift() returns the removed entry. cooling.shift() removes the first entry and returns it, and [0] then reads its count. Removing from the front of an array is slower than from the end, but cooling never holds more than 26 entries.
    • Two-level indexing. cooling[0][1] is the second item, the ready time, of the first entry.
    • == on numbers. Both sides are numbers, so == behaves like === here.

    Tracing tasks = [A, A, A, B, B, B], n = 2

    The counts are 3 and 3. Each row shows one pass of the loop: what the CPU did in that unit, the queue afterwards, and how many counts are in the heap.

    timeactioncooling afterwardsheap size
    1run, 2 copies left[[2,3]]1
    2run, 2 copies left[[2,3],[2,4]]0
    3idle (heap empty), ready task with 2 copies returns[[2,4]]1
    4run, 1 copy left; ready task with 2 copies returns[[1,6]]1
    5run, 1 copy left[[1,6],[1,7]]0
    6idle (heap empty), ready task with 1 copy returns[[1,7]]1
    7run, 0 copies left; ready task with 1 copy returns[]1
    8run, 0 copies left[]0

    Now the heap and the queue are both empty, the loop ends, and the method returns 8.

    Complexity

    Time: O(T log 26). Counting the tasks takes O(T). Each task is run once, which costs one extraction from the heap, and a task that still has copies is inserted again later. So there are about T heap operations in total. The heap never holds more than 26 counts, so each operation costs O(log 26). Idle stretches are skipped in one step, so they add no loop passes.

    Space: O(1). The array count has 26 places, and the heap and the queue each hold at most 26 entries.

    Edge cases and common mistakes

    • No cooldown. With n = 0 the answer is just the number of tasks: ["A","A","A","B","B","B"] gives 6. A task run at time t is ready at t, and the release check at the end of the same loop pass puts it straight back into the heap.
    • One task. ["A"] with n = 100 gives 1: the cooldown matters only between copies.
    • A task and its copy. ["A","A"] with n = 3 gives 5: A, three idle units, A.
    • Many kinds, no idling. ["A","B","C","D"] with n = 5 gives 4. Different tasks never wait for each other.
    • Enough variety to fill every gap. ["A","A","A","B"] with n = 2 gives 7, and ["A","A","B","B"] with n = 1 gives 4 (no idle unit needed).
    • Off by one in the ready time. A task run at time can run again at time + n + 1. The queue stores time + n, the unit at whose end it is released, so the task is back in the heap before unit time + n + 1 starts.
    • Counting idle units one by one. This also works but costs a loop pass per idle unit. The jump to cooling[0][1] skips them all.

    Testing

    • ["A","A","A","B","B","B"], n = 2 gives 8.
    • ["A","A","A","B","B","B"], n = 0 gives 6.
    • ["A","A","A","A","A","A","B","C","D","E","F","G"], n = 2 gives 16.
    • ["A"], n = 100 gives 1.
    • ["A","A"], n = 3 gives 5.
    • ["A","A","A","B"], n = 2 gives 7.
    Keep going with JavaScript500+ step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free