Skip to content

Interval Scheduling: Code Templates in 6 Languages

If you have not read the concept guide yet, start there for the intuition and complexity analysis. This page gives you the code you can memorize and adapt during an interview. The main template handles maximum non-overlapping selection. The variations cover merging, resource counting, and the weighted case where the greedy rule no longer works.

Main Template: Activity Selection (Earliest Finish Time)

The core interval scheduling template. Sort by end time, then greedily take every interval that does not overlap the last one taken. This is the engine behind Non-overlapping Intervals and Meeting Rooms , and it is the answer to most “maximum number of tasks” questions.

function activitySelection(intervals) {
    // Sorting by end time puts the earliest-finishing interval
    // first, which leaves the most room for everything after it.
    intervals.sort((a, b) => a.end - b.end);

    const selected = [];
    let lastEnd = -Infinity;

    for (const interval of intervals) {
        // Half-open semantics: an interval that starts exactly
        // when the last one ends does not conflict.
        if (interval.start >= lastEnd) {
            selected.push(interval);
            lastEnd = interval.end;
        }
    }

    return selected;
}
Templates only help if you can recall them without staring at the screen. Turn these snippets into flashcards for daily mobile review.

Code Breakdown

Key Variables

  • lastEnd: the end time of the most recently accepted interval. Every later decision compares against this single value, which is the entire state the loop needs.
  • selected: the result list of accepted intervals.
  • The sort: sorting by end time is what makes the greedy choice safe. Without it, an interval that ends late could be accepted early and block several short intervals that end sooner.

Visual Mechanism

    stateDiagram-v2
    [*] --> Sort: Sort intervals by end time
    Sort --> Scan: Loop over intervals
    Scan --> Keep: start >= lastEnd
    Scan --> Drop: start < lastEnd
    Keep --> Update: record interval end
    Update --> Scan: next interval
    Drop --> Scan: next interval
    Scan --> Done: no intervals left
    Done --> [*]
  

Critical Sections

The initialization sets lastEnd to negative infinity so the first interval is always accepted. Any real start time is larger than it.

The acceptance check is where the boundary semantics live. start >= lastEnd accepts intervals that merely touch, which matches half-open intervals. Problems that treat touching as overlap use > instead. This is the one line you change when a problem changes its overlap rule.

The advance step updates lastEnd only when an interval is accepted. A skipped interval must not move it, because a rejected interval is not part of the schedule and cannot block later ones.

Variations

1. Merge Intervals

When intervals must be combined instead of selected, sort by start time instead. After that, each interval can only overlap the last merged one.

Use this for Merge Intervals .

function merge(intervals) {
    if (intervals.length === 0) return [];

    // Sorting by start time means any overlap with a previous
    // interval always involves the last entry in the result.
    intervals.sort((a, b) => a.start - b.start);
    const merged = [intervals[0]];

    for (let i = 1; i < intervals.length; i++) {
        const last = merged[merged.length - 1];
        const current = intervals[i];

        if (current.start <= last.end) {
            // Overlap: widen the merged interval to cover both.
            last.end = Math.max(last.end, current.end);
        } else {
            // No overlap: the current interval starts a new block.
            merged.push(current);
        }
    }

    return merged;
}
    graph LR
    I["[1,3], [2,6], [8,10], [15,18]"] --> S["Sort by start time"]
    S --> M["[2,6] overlaps [1,3], widen to [1,6]"]
    M --> A["[8,10] is separate, add"]
    A --> B["[15,18] is separate, add"]
    B --> R["Result: [1,6], [8,10], [15,18]"]
  

2. Meeting Rooms II (Minimum Resources)

When the question is how many resources are needed, sort the starts and ends separately and walk both with two pointers. Every start that finds no free end opens a new room.

Use this for Meeting Rooms II .

function minMeetingRooms(intervals) {
    // Sorting starts and ends independently turns the problem
    // into merging two timelines, one for meetings beginning
    // and one for meetings ending.
    const starts = intervals.map(i => i.start).sort((a, b) => a - b);
    const ends = intervals.map(i => i.end).sort((a, b) => a - b);

    let rooms = 0;
    let endIdx = 0;

    for (const start of starts) {
        if (start < ends[endIdx]) {
            // A meeting starts before the earliest room frees
            // up, so a new room is needed.
            rooms++;
        } else {
            // The earliest-ending meeting is done, so its room
            // can be reused.
            endIdx++;
        }
    }

    return rooms;
}
    graph LR
    I["Meetings: 0-30, 5-10, 15-20"] --> S["starts: 0, 5, 15 and ends: 10, 20, 30"]
    S --> P1["start 0 < end 10, open a room, count 1"]
    P1 --> P2["start 5 < end 10, open a room, count 2"]
    P2 --> P3["start 15 >= end 10, first room is free, reuse"]
    P3 --> R["Answer: 2 rooms"]
  

3. Weighted Interval Scheduling

When intervals carry a value and you must maximize the total, the greedy rule fails and dynamic programming takes over. Sort by end time, then for each interval choose between skipping it and taking it plus the best compatible schedule that ends before it starts. This runs in

O(N log N)
time with
O(N)
space.

function weightedIntervalScheduling(intervals) {
    // intervals is an array of [start, end, value].
    // Sorting by end time lets each interval find its compatible
    // predecessors with binary search.
    intervals.sort((a, b) => a[1] - b[1]);
    const n = intervals.length;

    const ends = intervals.map(interval => interval[1]);
    // dp[i] is the best total value using only the first i intervals.
    const dp = new Array(n + 1).fill(0);

    // First index whose end time is greater than target.
    const bisectRight = (arr, target) => {
        let lo = 0, hi = arr.length;
        while (lo < hi) {
            const mid = (lo + hi) >> 1;
            if (arr[mid] <= target) lo = mid + 1;
            else hi = mid;
        }
        return lo;
    };

    for (let i = 1; i <= n; i++) {
        const [start, , value] = intervals[i - 1];

        // Only intervals that finish at or before start can
        // precede this one in a valid schedule.
        let k = bisectRight(ends, start);
        if (k > i - 1) k = i - 1; // an interval cannot precede itself

        const take = value + dp[k];
        const skip = dp[i - 1];
        dp[i] = Math.max(take, skip);
    }

    return dp[n];
}
    graph TD
    A["Sort intervals by end time"] --> B{"For each interval"}
    B --> C["Skip: keep the best value so far"]
    B --> D["Take: value plus the best compatible schedule before it"]
    C --> E["dp[i] holds the larger of the two"]
    D --> E
  
Each variation above is a separate review card in the app. Drill them individually so you do not mix up the conditions under pressure.

Now head to the practice problems to apply these templates to real interview questions.

Now that you have the templates, the next step is application. Head to the practice problems or start reviewing these templates with spaced repetition .