Intervals

Merge overlapping time ranges, test any two for overlap, and count how many run at once. Sort first, then sweep once from left to right.

15 minLesson 4 of 5
On this page
  1. When do two intervals overlap
  2. The idea: sort by start, then sweep
  3. How many at the same time
  4. Common mistakes
  5. Check yourself
  6. What to practise next

Your calendar has five meetings today, entered in no particular order: 13–15, 9–11, 10–12, 16–17 and 14–16. A colleague asks when you are busy. They do not want five meetings. They want the busy stretches: 9 to 12, and 13 to 17.

Each meeting is an interval, a start and an end. Intervals show up everywhere: bookings, shifts, video segments, ranges of IP addresses, the lines a code review comment covers. Most interval problems are solved the same way: sort them, then walk through once.

When do two intervals overlap

Start with two intervals, a and b. There are many ways to overlap (partly, fully inside, identical) and only two ways not to: a ends before b starts, or b ends before a starts. So test the two easy cases and flip the result.

Overlap a b Touching a b depends on the problem Apart a b a.end < b.start
Two intervals miss each other only when one ends before the other starts. Everything else is an overlap.
// Touching counts as overlapping here. Use < instead of <= if it should not.
function overlaps(a, b) {
  return a[0] <= b[1] && b[0] <= a[1];
}
# Touching counts as overlapping here. Use < instead of <= if it should not.
def overlaps(a, b):
    return a[0] <= b[1] and b[0] <= a[1]

Whether touching intervals overlap is a rule of the problem, not of the code. In a calendar, 14–16 and 16–17 make one busy stretch. For room bookings, a meeting ending at 16 does not block one starting at 16. Decide once, then use the same < or <= everywhere.

The idea: sort by start, then sweep

Comparing every pair of meetings is O(n²). Sorting by start time fixes that. Once sorted, a meeting can only overlap the block you are building right now, never an earlier block, because everything earlier has already ended.

So keep one open block. For each next meeting, ask one question: does it start before the open block ends? If yes, stretch the block's end. If no, close the block and open a new one.

Merge five meetings into busy blocksUse the arrows or ← → keys
sort meetings by startbusy = [first meeting]for each next meeting (start, end):  last = the final block in busy  if start <= last.end: last.end = max(last.end, end)  else: add (start, end) as a new block
  1. meetings
    09–11i
    110–12
    213–15
    314–16
    416–17
    busy
    09–11
    block end
    11

    1Sorted by start time. The first meeting opens the first busy block.

  2. meetings
    09–11
    110–12i
    213–15
    314–16
    416–17
    busy
    09–12
    block end
    12

    210–12 starts at 10, before the block ends at 11. They overlap, so stretch the block's end to 12.

  3. meetings
    09–11
    110–12
    213–15i
    314–16
    416–17
    busy
    09–12
    113–15
    block end
    15

    313–15 starts at 13, after the block ends at 12. There is a gap, so the block is finished. Start a new one.

  4. meetings
    09–11
    110–12
    213–15
    314–16i
    416–17
    busy
    09–12
    113–16
    block end
    16

    414–16 starts at 14, inside the block that ends at 15. Stretch the end to 16.

  5. meetings
    09–11
    110–12
    213–15
    314–16
    416–17i
    busy
    09–12
    113–17
    block end
    17

    516–17 starts at 16, exactly when the block ends. In a calendar that is still one busy stretch, so stretch the end to 17.

  6. meetings
    09–11
    110–12
    213–15
    314–16
    416–17
    busy
    09–12
    113–17
    blocks
    2

    6Five meetings became two busy blocks, in one pass after sorting. Each meeting was looked at once.

1 / 6
function mergeBusy(meetings) {
  if (meetings.length === 0) return [];
  const sorted = [...meetings].sort((a, b) => a[0] - b[0]);
  const busy = [[...sorted[0]]];
  for (const [start, end] of sorted.slice(1)) {
    const last = busy[busy.length - 1];
    if (start <= last[1]) {
      last[1] = Math.max(last[1], end); // overlaps or touches: stretch
    } else {
      busy.push([start, end]);          // gap: new block
    }
  }
  return busy;
}
 
mergeBusy([[13, 15], [9, 11], [10, 12], [16, 17], [14, 16]]);
// [[9, 12], [13, 17]]
def merge_busy(meetings):
    if not meetings:
        return []
    ordered = sorted(meetings, key=lambda m: m[0])
    busy = [list(ordered[0])]
    for start, end in ordered[1:]:
        last = busy[-1]
        if start <= last[1]:
            last[1] = max(last[1], end)  # overlaps or touches: stretch
        else:
            busy.append([start, end])    # gap: new block
    return busy
 
merge_busy([[13, 15], [9, 11], [10, 12], [16, 17], [14, 16]])
# [[9, 12], [13, 17]]

The code copies the first meeting instead of using it directly. Otherwise stretching the block would quietly change the caller's data.

TimeO(n log n)
SpaceO(n)

Sorting dominates. The sweep is one O(n) pass, and the output can hold all n intervals.

How many at the same time

A different question: the office has to host meetings 9–12, 10–11, 11–13 and 13–14. How many rooms does it need? That is the largest number of meetings running at any one moment.

Stop thinking of meetings and think of events. Each meeting is a +1 when it starts and a −1 when it ends. Sort all the events by time and walk through them with a counter. The highest the counter ever reaches is the answer. When an end and a start happen at the same time, process the end first: the person leaving at 11 frees the room for the one arriving at 11.

Count rooms with a sweep over eventsUse the arrows or ← → keys
events = (start, +1) and (end, −1) for each meetingsort events by time; at a tie, −1 before +1for each event:  inUse += change; most = max(most, inUse)
  1. events
    09 +i
    110 +
    211 −
    311 +
    412 −
    513 −
    613 +
    714 −
    inUse
    1
    most
    1

    19: the 9–12 meeting starts. One room in use.

  2. events
    09 +
    110 +i
    211 −
    311 +
    412 −
    513 −
    613 +
    714 −
    inUse
    2
    most
    2

    210: the 10–11 meeting starts while 9–12 is still running. Two rooms.

  3. events
    09 +
    110 +
    211 −i
    311 +
    412 −
    513 −
    613 +
    714 −
    inUse
    1
    most
    2

    311: two things happen at 11. The end comes first in the sorted order: 10–11 leaves, freeing a room.

  4. events
    09 +
    110 +
    211 −
    311 +i
    412 −
    513 −
    613 +
    714 −
    inUse
    2
    most
    2

    411: now 11–13 starts and takes the room 10–11 just left. Still two rooms, never three.

  5. events
    09 +
    110 +
    211 −
    311 +
    412 −i
    513 −
    613 +
    714 −
    inUse
    1
    most
    2

    512: 9–12 ends.

  6. events
    09 +
    110 +
    211 −
    311 +
    412 −
    513 −i
    613 +
    714 −
    inUse
    0
    most
    2

    613: 11–13 ends first, again because ends sort before starts.

  7. events
    09 +
    110 +
    211 −
    311 +
    412 −
    513 −
    613 +i
    714 −
    inUse
    1
    most
    2

    713: 13–14 starts in the freed room.

  8. events
    09 +
    110 +
    211 −
    311 +
    412 −
    513 −
    613 +
    714 −i
    inUse
    0
    most
    2

    814: 13–14 ends. The busiest moment needed 2 rooms, so 2 rooms are enough for the whole day.

1 / 8
function roomsNeeded(meetings) {
  const events = [];
  for (const [start, end] of meetings) {
    events.push([start, 1], [end, -1]);
  }
  // by time; at a tie, -1 (an end) sorts before +1 (a start)
  events.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
  let inUse = 0;
  let most = 0;
  for (const [, change] of events) {
    inUse += change;
    most = Math.max(most, inUse);
  }
  return most;
}
 
roomsNeeded([[9, 12], [10, 11], [11, 13], [13, 14]]); // 2
def rooms_needed(meetings):
    events = []
    for start, end in meetings:
        events.append((start, 1))
        events.append((end, -1))
    # tuples sort by time, then by change, so -1 (an end) comes before +1
    events.sort()
    in_use = most = 0
    for _, change in events:
        in_use += change
        most = max(most, in_use)
    return most
 
rooms_needed([[9, 12], [10, 11], [11, 13], [13, 14]])  # 2

Counting rooms costs O(n log n) time for the sort and O(n) space for the 2n events. A third classic question, "keep as many meetings as possible with no overlaps", is solved by sorting on end time instead; it is worked through in Greedy Algorithms.

Common mistakes

Check yourself

Quick check

Merge [1, 4], [2, 3], [5, 7], [6, 9], with touching counted as overlapping. What comes out?

Meetings 1–5, 2–6 and 5–8, and a room is free the moment a meeting ends. How many rooms are needed?

Why does merging sort by start time?

What to practise next

Interval problems mix with the other techniques in this module: sort-and-sweep is the same habit you saw in Greedy Algorithms. Next, Bit Manipulation changes gear completely and works on the individual bits of a number.