InterviewKata
Pattern 08 of 24

Overlapping Intervals

Sort the ranges by where they start, then walk left to right. Once sorted, you only ever compare an interval with the one just before it.

1. What is this pattern for?

Imagine your calendar is a mess of sticky notes, one per meeting: "9–10", "9:30–11", "1–2"… dropped in random order. To find your busy blocks, you first line the notes up by start time. Then you go down the line: if the next note starts before the current block ends, you stick it onto that block (stretching it if needed). If it starts later, there's a gap, and a new block begins. One pass and your day is clean.

An interval is a pair [start, end]: a meeting, a booking, a range of numbers. Many problems ask how a collection of intervals overlap: merge them, insert a new one, count how many overlap at once, remove the fewest to make them disjoint, or find the free gaps.

Checking every pair is O(n²). The trick is to sort first (O(n log n)). After sorting by start, overlaps can only happen between neighbors in the line, so one linear sweep finishes the job.

Two intervals a and b overlap exactly when a.start ≤ b.end and b.start ≤ a.end. After sorting by start, that becomes simply next.start ≤ current.end.

2. How it works (bisected)

  1. Sort by start (for merging, inserting, counting rooms) or by end (for the greedy "keep the most non-overlapping" problems). This is the step that makes everything else linear.
  2. Keep a "current" interval: usually merged[-1], the last block in your answer.
  3. For each next interval, ask one question: does it start at or before the current one ends? (s ≤ cur_end)
  4. Overlap → extend: cur_end = max(cur_end, e). Use max because the new interval may sit entirely inside the current one.
  5. No overlap → close and open: the current block is final. Start a new one with [s, e].
  6. Need "how many at once"? Keep a min-heap of end times (the rooms in use), or sweep sorted +1/−1 events. The biggest count you see is the answer.

Counting simultaneous overlaps (how many rooms do we need?): sort by start, and keep a min-heap of end times. The heap's top tells you which room frees up first.

3. Seeing a problem: how to spot Overlapping Intervals

Trigger words

  • "intervals", "ranges", "meetings", "bookings", "schedules", "time slots"
  • "merge overlapping", "insert a new interval"
  • "minimum number of rooms / platforms / arrows"
  • "remove the fewest intervals so none overlap"
  • "common free time", "intersection of two lists of intervals"

Shape of the input

  • A list of pairs [start, end] (sometimes objects with .start and .end).
  • Usually unsorted, which is your cue to sort first.
  • The answer depends on which ranges touch or cross each other.
  • The brute force compares every pair of intervals: O(n²).
The one question to ask"If I lined these ranges up by start time, would I only need to compare each one with the one before it?" If yes, sort and sweep.

4. When to use it, and when not to

SituationBest toolWhy
Merge / insert / find gaps among rangesSort by start + sweepAfter sorting, overlaps are only between neighbors
Max number of ranges overlapping at one moment (rooms)Sort + min-heap of ends, or a +1/−1 sweepThe heap = rooms currently busy. See Top K Elements for heaps
Keep the most non-overlapping ranges / fewest arrowsSort by end + greedyFinishing earliest leaves the most room for the rest
Intersect two already-sorted interval listsTwo PointersAdvance whichever interval ends first
Many range sum updates/queries on positions (not intervals as objects)Prefix Sum / difference arrayAdd at start, subtract after end, prefix-sum once
"Is there a booking that conflicts with [s, e)?" asked online, many timesSorted container + Binary SearchFind the neighbor in O(log n) instead of re-sorting

Cost: sorting dominates: O(n log n) time. The sweep is O(n). Space is O(n) for the output (or O(log n)–O(n) for the sort itself). The rooms heap adds O(n log n) time and O(n) space.

5. The Python template (memorize this)

import heapq


# A) Merge: sort by start, then extend-or-append
def get_start(interval):
    # Sort key helper: the start of an interval is its first number.
    return interval[0]


def merge_template(intervals):
    # Sort a copy by start time. After sorting, an interval can only
    # overlap the block we built just before it.
    sorted_intervals = sorted(intervals, key=get_start)
    merged = []

    for i in range(len(sorted_intervals)):
        # Read the two ends of the current interval into named variables.
        start = sorted_intervals[i][0]
        end = sorted_intervals[i][1]

        if len(merged) == 0:
            # Very first interval: nothing to compare with, so open a block.
            merged.append([start, end])
        else:
            # The block we are currently building is the last one in merged.
            last_block = merged[len(merged) - 1]
            if start <= last_block[1]:
                # Starts before (or exactly when) the block ends: overlap.
                # Stretch the block. max() matters: [1,10] then [2,3]
                # must stay [1,10], not shrink to [1,3].
                last_block[1] = max(last_block[1], end)
            else:
                # Starts after the block ends: there is a gap, so the
                # last block is finished and this interval opens a new one.
                merged.append([start, end])
    return merged


# B) How many overlap at once? Sort by start + min-heap of end times
def max_overlap(intervals):
    # Intervals are half-open [start, end): one ending at 5 does NOT
    # clash with one starting at 5.
    # heapq turns a plain list into a "min-heap":
    #   heapq.heappush(heap, x)  adds x to the heap
    #   heapq.heappop(heap)      removes and returns the SMALLEST item
    #   heap[0]                  peeks at the smallest item (no removal)
    sorted_intervals = sorted(intervals, key=get_start)
    room_ends = []                      # end time of every room in use

    for i in range(len(sorted_intervals)):
        start = sorted_intervals[i][0]
        end = sorted_intervals[i][1]

        if len(room_ends) > 0 and room_ends[0] <= start:
            # The room that frees up earliest is already free by now.
            # Reuse it: remove its old end time, add the new end time.
            heapq.heappop(room_ends)
            heapq.heappush(room_ends, end)
        else:
            # Every room is still busy (or there are none): open a new room.
            heapq.heappush(room_ends, end)

    # Rooms are never closed, so the heap size is the most ever needed.
    return len(room_ends)


# C) Greedy "keep the most non-overlapping": sort by END
def get_end(interval):
    # Sort key helper: the end of an interval is its second number.
    return interval[1]


def max_non_overlapping(intervals):
    # Earliest finish first: keeping the interval that ends soonest
    # leaves the most room for the ones after it.
    sorted_intervals = sorted(intervals, key=get_end)
    count = 0
    last_kept_end = float("-inf")       # nothing kept yet

    for i in range(len(sorted_intervals)):
        start = sorted_intervals[i][0]
        end = sorted_intervals[i][1]
        if start >= last_kept_end:
            # It begins after the last kept one finished: keep it.
            count = count + 1
            last_kept_end = end
        # Otherwise it clashes with what we kept, so we skip it.
    return count

What the code is doing, line by line:

  1. get_start / get_end are two-line helpers that return an interval's first or second number. Passing one as key= to sorted tells Python what to sort by. (Writing them as named functions is the long-hand form of a lambda.)
  2. A) merge_template: sorted(...) returns a new sorted list, leaving the input alone. We walk it with index i and read start and end.
  3. A) first interval: when merged is empty there is nothing to compare with, so the interval opens the first block.
  4. A) every later interval: last_block = merged[len(merged) - 1] is the block we are building. If start <= last_block[1] they overlap, so the block's end becomes max(old end, end). Otherwise there is a gap and [start, end] becomes a new block.
  5. B) max_overlap: the comment explains the three heap operations. room_ends holds one end time per room; room_ends[0] is the room that frees up soonest.
  6. B) reuse or open: if room_ends[0] <= start, that room is free, so we pop its old end and push the new one (room count unchanged). Otherwise we push, which opens a new room. The final heap size is the most intervals that were ever overlapping at once.
  7. C) max_non_overlapping: sort by end. last_kept_end starts at minus infinity, so the first interval is always kept.
  8. C) keep or skip: if start >= last_kept_end, the interval fits after the last kept one, so we add 1 to count and update last_kept_end. Otherwise it clashes and we skip it. "Fewest to remove" is simply len(intervals) - count.

6. How to remember it forever

Memory hook: "Line up the sticky notes"Sort by start, then each note either sticks to the block above it (overlap → stretch with max) or starts a new block (gap).
How this hook works: what “Line up the sticky notes” means

What the slogan means, phrase by phrase

“Sticky notes”
Intervals [start, end], given in random order.
“Line up”
Sort by start first. After that, a note can only overlap the block directly above it, never one further back.
“sticks to the block above it (overlap → stretch with max)”
If start <= block_end, the note joins that block, and its end becomes max(block_end, end).
“or starts a new block (gap)”
If it starts after the block ends, there's free time in between, so a new block begins.

Why this picture fits the pattern: Interval problems are hopeless in random order: anything might overlap anything. Sorting by start turns them into a single top-to-bottom pass that compares each note with one block.

The story → the code

In the storyIn the code
The pile of notesintervals
Line them upsorted(intervals, key=get_start)
The block abovemerged[-1]
Sticks: stretch itmerged[-1][1] = max(merged[-1][1], end)
Gap: new blockmerged.append([start, end])

Watch it on numbers

[[8, 10], [1, 3], [2, 6], [15, 18]].

  1. Line them up by start: [1, 3], [2, 6], [8, 10], [15, 18].
  2. [1, 3] opens the first block.
  3. [2, 6] starts at 2 ≤ 3, so it sticks and the block stretches to [1, 6].
  4. [8, 10] starts at 8 > 6: a gap, so a new block. [15, 18] is also a gap and a new block.
  5. Result [[1, 6], [8, 10], [15, 18]].

The trap it saves you from

Writing end instead of max(block_end, end). A note inside the block, such as [2, 3] under [1, 10], would *shrink* it to [1, 3]. Also read whether touching ends ([1, 2] and [2, 3]) count as overlapping.

It still works when…

"Merge by START, choose by END." To keep as many non-overlapping notes as possible (435), line them up by end and keep each one that starts after the last kept end. For Meeting Rooms II, a heap of end times counts how many blocks are open at once.

Picture

Colored bars on a timeline, stacked by start time. Slide a vertical ruler from left to right: bars that cross the ruler at the same time are the overlaps.

Chant

"Sort the starts; if it starts before I end, stretch to the max; else, a new friend."

Which sort?

"Merge by START, choose by END." Merging and rooms sort by start. Greedy keep/remove problems sort by end.

7. LeetCode problems, solved step by step

New to this pattern? Start with theseSolve 252. Meeting Rooms, then 56. Merge Intervals, then 57. Insert Interval. They are marked Start here below. Read the explanation, press Play on the step-through, then close the page and solve it yourself on LeetCode.
Medium 56. Merge Intervals Start here Google Meta Amazon Cisco Microsoft Apple Netflix Array

Problem: Given a list of intervals, merge all overlapping ones and return the non-overlapping result. Touching intervals like [1,4] and [4,5] count as overlapping.

Example 1
Input: intervals = [[1,3],[2,6],[8,10],[15,18]]
Output: [[1,6],[8,10],[15,18]]
Explanation: Since intervals [1,3] and [2,6] overlap, merge them into [1,6].
Example 2
Input: intervals = [[1,4],[4,5]]
Output: [[1,5]]
Explanation: Intervals [1,4] and [4,5] are considered overlapping.

Constraints: 1 ≤ intervals.length ≤ 10⁴ · intervals[i].length == 2 · 0 ≤ starti ≤ endi ≤ 10⁴

Hint 1

If the intervals were sorted by start, where could an interval overlap?

Hint 2

After sorting, each interval either overlaps the last merged block (start ≤ its end) or starts a new block.

From brute force to the pattern: Comparing every pair and merging repeatedly until nothing changes is O(n²) or worse. Sorting by start puts every possible overlap next to each other, so one sweep merges everything: O(n log n).

Spot it: The word "merge" plus a list of ranges: this is the canonical problem of the pattern.

Key insight: Once sorted by start, an interval can only overlap the last merged block. Either stretch that block's end with max, or start a new block. (The first visualization above runs exactly this.)

Approach:

  1. Sort the intervals by start.
  2. Put a copy of the first interval into merged.
  3. For each next [start, end]: if start ≤ last_block[1], they overlap, so set last_block[1] = max(last_block[1], end).
  4. Otherwise there's a gap: append [start, end] as a new block.
  5. Return merged.
from typing import List


def get_start(interval):
    # Sort key helper: the start of an interval is its first number.
    return interval[0]


class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        # Step 1: sort by start so overlapping intervals end up side by side.
        intervals.sort(key=get_start)

        # Step 2: the first interval opens the first merged block.
        # We build a NEW list [start, end] so that stretching the block
        # later never changes the caller's input list.
        first_start = intervals[0][0]
        first_end = intervals[0][1]
        merged = []
        merged.append([first_start, first_end])

        # Step 3: visit every other interval from left to right.
        for i in range(1, len(intervals)):
            start = intervals[i][0]
            end = intervals[i][1]
            last_block = merged[len(merged) - 1]    # block being built
            if start <= last_block[1]:
                # Overlap (touching counts): stretch the block's end.
                # max: [1,10] followed by [2,3] must stay [1,10].
                last_block[1] = max(last_block[1], end)
            else:
                # Gap: the old block is final; this interval opens a new one.
                merged.append([start, end])
        return merged

What the code is doing, line by line:

  1. get_start returns an interval's first number. intervals.sort(key=get_start) uses it to sort by start time.
  2. First block: we read the first interval's two ends and append a brand-new list [first_start, first_end] to merged. Making a new list (instead of reusing intervals[0]) means later stretching can't accidentally change the input.
  3. The for loop starts at index 1 and reads each interval's start and end.
  4. last_block is the last item of merged (index len(merged) - 1). Since the input is sorted, this is the only block the current interval could overlap.
  5. If start <= last_block[1], the interval begins before the block ends (or exactly at its end), so they overlap. We stretch the block's end with max, which also handles an interval sitting fully inside the block.
  6. Otherwise there is a gap, so we append [start, end] as a new block.
  7. return merged.

Dry run on the example intervals = [[1,3],[2,6],[8,10],[15,18]] (already sorted; merged starts as [[1,3]]):

i[start, end]last_block[1]testmerged after
1[2,6]32 <= 3: overlap, end = max(3, 6) = 6[[1,6]]
2[8,10]68 > 6: gap, new block[[1,6],[8,10]]
3[15,18]1015 > 10: gap, new block[[1,6],[8,10],[15,18]]

Answer: [[1,6],[8,10],[15,18]].

Edge cases to test:

  • Touching intervals, e.g. [1, 4] and [4, 5] → [1, 5].
  • One interval inside another, e.g. [1, 10] and [2, 3].
  • Unsorted input.

Complexity: O(n log n) time, O(n) output space.

Medium 57. Insert Interval Start here Google Amazon Microsoft Array

Problem: The intervals are already sorted and don't overlap. Insert newInterval, merging wherever needed, and return the result.

Example 1
Input: intervals = [[1,3],[6,9]], newInterval = [2,5]
Output: [[1,5],[6,9]]
Example 2
Input: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
Output: [[1,2],[3,10],[12,16]]
Explanation: Because the new interval [4,8] overlaps with [3,5],[6,7],[8,10].

Constraints: 0 ≤ intervals.length ≤ 10⁴ · intervals[i].length == 2 · 0 ≤ starti ≤ endi ≤ 10⁵ · intervals is sorted by starti in ascending order. · newInterval.length == 2 · 0 ≤ start ≤ end ≤ 10⁵

Hint 1

The list is already sorted and non-overlapping. Split it into three parts relative to newInterval.

Hint 2

Copy the intervals that end before it, merge everything that overlaps it, then copy the rest.

From brute force to the pattern: Appending newInterval and re-running Merge Intervals is O(n log n). Because the input is already sorted, one linear pass with three phases is O(n).

Spot it: It's sorted already, so don't re-sort. A single pass splits the list into three zones.

Key insight: (1) Copy intervals that end before the new one starts. (2) Absorb every interval that starts before the new one ends, growing it with min/max. (3) Copy the rest.

Approach:

  1. Walk with an index i. While intervals[i] ends before the new interval starts, copy it to the output (zone 1).
  2. While intervals[i] starts at or before the new interval's end, absorb it: new_start = min(new_start, …), new_end = max(new_end, …) (zone 2).
  3. Append the grown [new_start, new_end].
  4. Copy every remaining interval (zone 3).
from typing import List


class Solution:
    def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
        result = []
        i = 0
        count = len(intervals)
        # The new interval may grow as it swallows neighbours, so keep its
        # two ends in variables we can update.
        new_start = newInterval[0]
        new_end = newInterval[1]

        # Zone 1: intervals that END before the new one STARTS.
        # They are entirely to the left, so copy them unchanged.
        while i < count and intervals[i][1] < new_start:
            result.append(intervals[i])
            i = i + 1

        # Zone 2: intervals that START at or before the new one ENDS.
        # They overlap, so absorb each one by widening the new interval.
        while i < count and intervals[i][0] <= new_end:
            new_start = min(new_start, intervals[i][0])
            new_end = max(new_end, intervals[i][1])
            i = i + 1

        # The (possibly grown) new interval goes in right here.
        result.append([new_start, new_end])

        # Zone 3: everything left is entirely to the right: copy it.
        while i < count:
            result.append(intervals[i])
            i = i + 1
        return result

What the code is doing, line by line:

  1. Setup: result is the output list, i walks through intervals, and count is how many there are. new_start and new_end hold the new interval's ends, because they may grow.
  2. Zone 1 loop: while the current interval ends before new_start, it can't touch the new one, so copy it and move on.
  3. Zone 2 loop: while the current interval starts at or before new_end, it overlaps. Widen the new interval with min on the start and max on the end, then move on. The interval itself is not copied; it has been swallowed.
  4. result.append([new_start, new_end]) places the merged new interval exactly between the left zone and the right zone.
  5. Zone 3 loop: every remaining interval starts after new_end, so copy them all.

Dry run on the example intervals = [[1,3],[6,9]], newInterval = [2,5]:

stepiintervals[i]zone / testnew_start, new_endresult
zone 1 check0[1,3]3 < 2? no, so zone 1 is empty2, 5[]
zone 20[1,3]1 <= 5: absorb1, 5[]
zone 2 check1[6,9]6 <= 5? no, stop1, 5[]
append new1--1, 5[[1,5]]
zone 31[6,9]copy1, 5[[1,5],[6,9]]

Answer: [[1,5],[6,9]].

Edge cases to test:

  • An empty list → [newInterval].
  • newInterval before all or after all intervals.
  • newInterval covering every interval.

Complexity: O(n) time, O(n) output space.

Medium 435. Non-overlapping Intervals Google Amazon Microsoft Array

Problem: Return the minimum number of intervals to remove so the rest don't overlap. Touching ([1,2], [2,3]) is allowed.

Example 1
Input: intervals = [[1,2],[2,3],[3,4],[1,3]]
Output: 1
Explanation: [1,3] can be removed and the rest of the intervals are non-overlapping.
Example 2
Input: intervals = [[1,2],[1,2],[1,2]]
Output: 2
Explanation: You need to remove two [1,2] to make the rest of the intervals non-overlapping.

Constraints: 1 ≤ intervals.length ≤ 10⁵ · intervals[i].length == 2 · -5 * 10⁴ ≤ starti < endi ≤ 5 * 10⁴

Hint 1

Equivalently: keep as many non-overlapping intervals as possible.

Hint 2

Sort by end time and greedily keep each interval that starts at or after the last kept end.

From brute force to the pattern: Trying every subset to remove is exponential; DP over sorted intervals is O(n²). Greedy by earliest end time is optimal (it leaves the most room for the rest): O(n log n).

Spot it: "remove the fewest" = "keep the most". That's the classic activity-selection greedy: sort by end.

Key insight: Always keep the interval that finishes earliest, because it leaves the most room for everything after it. Any later interval that starts before the kept end has to go.

Approach:

  1. Sort by end time.
  2. Track last_kept_end = end of the last interval you kept (start at −∞).
  3. For each [start, end]: if start ≥ last_kept_end, keep it and set last_kept_end = end. Otherwise remove it (removed = removed + 1). It conflicts, and since it ends later than the one you kept, dropping it is never worse.
  4. Return removed.
from typing import List


def get_end(interval):
    # Sort key helper: the end of an interval is its second number.
    return interval[1]


class Solution:
    def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
        # Greedy: sort by END so the interval that finishes first comes first.
        intervals.sort(key=get_end)
        removed = 0
        last_kept_end = float("-inf")       # nothing kept yet

        for i in range(len(intervals)):
            start = intervals[i][0]
            end = intervals[i][1]
            if start >= last_kept_end:
                # Fits after the last kept interval (touching is allowed): keep it.
                last_kept_end = end
            else:
                # Starts before the kept one ends: conflict. It also ends
                # later (we sorted by end), so removing it is never worse.
                removed = removed + 1
        return removed

What the code is doing, line by line:

  1. get_end returns an interval's end. Using it as the sort key orders intervals by finishing time.
  2. removed = 0 counts deletions. last_kept_end = -inf means "nothing kept yet", so the first interval is always kept.
  3. In the loop we read start and end of the current interval.
  4. If start >= last_kept_end, it begins after (or exactly when) the last kept one ends, so we keep it and remember its end.
  5. Otherwise it overlaps the last kept interval. Because we sorted by end, it finishes no earlier than the kept one, so it is the better one to throw away: removed = removed + 1.
  6. return removed.

Dry run on the example intervals = [[1,2],[2,3],[3,4],[1,3]] (sorted by end: [[1,2],[2,3],[1,3],[3,4]]):

intervalstart >= last_kept_end?decisionlast_kept_endremoved
[1,2]1 >= -inf: yeskeep20
[2,3]2 >= 2: yeskeep30
[1,3]1 >= 3: noremove31
[3,4]3 >= 3: yeskeep41

Answer: 1.

Edge cases to test:

  • Touching intervals like [1, 2] and [2, 3] do not overlap.
  • Identical intervals.
  • Nothing overlaps → 0.

Complexity: O(n log n) time, O(1) extra space.

Medium 253. Meeting Rooms II Google Meta Amazon Cisco Microsoft Netflix ArrayHeap

Problem: Given meeting times [start, end), return the minimum number of conference rooms needed.

Example 1
Input: intervals = [[0,30],[5,10],[15,20]]
Output: 2
Explanation: The meeting [0,30] overlaps both others, but [5,10] and [15,20] can share a room.
Example 2
Input: intervals = [[7,10],[2,4]]
Output: 1

Constraints: 1 ≤ intervals.length ≤ 10⁴ · 0 ≤ startᵢ < endᵢ ≤ 10⁶

Hint 1

The answer is the maximum number of meetings happening at the same moment.

Hint 2

Sort by start; keep a min-heap of end times of the rooms in use. Reuse a room if its earliest end is ≤ the next start.

From brute force to the pattern: Checking, for every meeting, how many others overlap it is O(n²). Sorting by start and keeping room end times in a min-heap gives the maximum overlap in O(n log n) (or sort starts and ends separately and sweep).

Spot it: "minimum rooms" = the maximum number of meetings happening at the same moment.

Key insight: Sort by start. Keep a min-heap of end times for the rooms in use. If the earliest-ending room is free by the time the next meeting starts (ends[0] ≤ s), reuse it. Otherwise open a new room. The heap's final size is the answer. (See the second visualization above.)

Approach:

  1. Sort meetings by start.
  2. Keep a min-heap ends with the end time of every room in use.
  3. For each meeting [s, e): if ends[0] ≤ s, the earliest-finishing room is free, so replace its end with e.
  4. Otherwise push e. A new room opens.
  5. The heap's size at the end is the number of rooms needed.
import heapq
from typing import List


def get_start(interval):
    # Sort key helper: the start of an interval is its first number.
    return interval[0]


class Solution:
    def minMeetingRooms(self, intervals: List[List[int]]) -> int:
        # heapq keeps a plain list arranged as a "min-heap":
        #   heapq.heappush(heap, x)  adds x
        #   heapq.heappop(heap)      removes and returns the smallest item
        #   heap[0]                  is always the smallest item
        intervals.sort(key=get_start)       # handle meetings in start order
        room_ends = []                      # end time of every busy room

        for i in range(len(intervals)):
            start = intervals[i][0]
            end = intervals[i][1]
            if len(room_ends) > 0 and room_ends[0] <= start:
                # The room that frees up first is already free: reuse it.
                # Take out its old end time, put in this meeting's end time.
                heapq.heappop(room_ends)
                heapq.heappush(room_ends, end)
            else:
                # Every room is busy right now: open a new room.
                heapq.heappush(room_ends, end)

        # One heap entry per room ever opened.
        return len(room_ends)

What the code is doing, line by line:

  1. The heap comment explains the three heap operations we use. The key fact: room_ends[0] is always the earliest end time, which is the room that frees up first.
  2. intervals.sort(key=get_start) processes meetings in the order they begin.
  3. room_ends = []: no rooms are open yet.
  4. In the loop we read the meeting's start and end.
  5. If a room is free (the earliest end is <= start; meetings are half-open, so ending at 10 and starting at 10 is fine), we pop that old end time and push the new one. The room count stays the same. (The one-call shortcut for this is heapq.heapreplace.)
  6. Otherwise every room is still in use, so we push the end time, which opens a new room.
  7. return len(room_ends): rooms are reused but never removed, so the heap size equals the number of rooms needed.

Dry run on the example intervals = [[0,30],[5,10],[15,20]]:

meetingroom_ends[0] beforeactionroom_ends after (sorted)
[0,30](empty)open new room[30]
[5,10]30 > 5: busyopen new room[10, 30]
[15,20]10 <= 15: freereuse: pop 10, push 20[20, 30]

Answer: len(room_ends) = 2.

Edge cases to test:

  • A meeting ending exactly when another starts can reuse the room.
  • All meetings overlap → n rooms.
  • A single meeting → 1.

Complexity: O(n log n) time, O(n) space.

Hard 759. Employee Free Time Google Amazon Array

Problem: Each employee has a sorted list of non-overlapping working intervals. Return the finite intervals when all employees are free, in sorted order.

Example 1
Input: schedule = [[[1,2],[5,6]],[[1,3]],[[4,10]]]
Output: [[3,4]]
Explanation: Everyone is free from 3 to 4. Times before 1 and after 10 are infinite and don't count.
Example 2
Input: schedule = [[[1,3],[6,7]],[[2,4]],[[2,5],[9,12]]]
Output: [[5,6],[7,9]]

Constraints: 1 ≤ schedule.length, schedule[i].length ≤ 50 · 0 ≤ schedule[i].start < schedule[i].end ≤ 10⁸

Hint 1

Everyone's busy times together form one big list of intervals. What are the gaps in it?

Hint 2

Flatten and sort by start (or merge the sorted lists with a heap); sweep with the furthest end seen so far, and every gap is free time.

From brute force to the pattern: Marking every time unit as busy or free is impossible for times up to 10⁸. Sorting all intervals and sweeping once finds every gap in O(N log N); a heap over the employees' sorted lists makes it O(N log k).

Spot it: "Common free time" is the gaps between the merged busy blocks of everyone combined.

Key insight: Flatten all intervals, sort by start, and sweep while tracking the furthest end seen so far. Whenever the next start is strictly greater than end, the stretch [end, start] is a moment nobody is working. (A k-way heap merge gives O(n log k); plain sorting is simpler.)

Approach:

  1. Flatten every employee's intervals into one list and sort it by start.
  2. Set end = the first interval's end. This is the end of the current merged busy block.
  3. For each next interval: if start > end, record the free gap [end, start].
  4. Then extend the busy block: end = max(end, interval.end).
  5. Return the recorded gaps.
"""
class Interval:
    def __init__(self, start=None, end=None):
        self.start = start
        self.end = end
"""


def get_interval_start(interval):
    # Sort key helper for Interval objects: use the .start field.
    return interval.start


class Solution:
    def employeeFreeTime(self, schedule: '[[Interval]]') -> '[Interval]':
        # Step 1: flatten. Put every employee's intervals into one list.
        all_intervals = []
        for employee_index in range(len(schedule)):
            employee_intervals = schedule[employee_index]
            for j in range(len(employee_intervals)):
                all_intervals.append(employee_intervals[j])

        # Step 2: sort that list by start time.
        all_intervals.sort(key=get_interval_start)

        # Step 3: sweep, remembering where the current busy block ends.
        free = []
        busy_until = all_intervals[0].end
        for i in range(1, len(all_intervals)):
            current = all_intervals[i]
            if current.start > busy_until:
                # Nobody is working between busy_until and current.start.
                free.append(Interval(busy_until, current.start))
            # Extend the busy block if this interval goes further right.
            busy_until = max(busy_until, current.end)
        return free

What the code is doing, line by line:

  1. The docstring shows LeetCode's Interval class: each interval has .start and .end.
  2. get_interval_start returns interval.start; we pass it to sort as the key.
  3. Step 1 (two nested loops): the outer loop visits each employee, the inner loop visits that employee's intervals, and every interval is appended to all_intervals. Who worked doesn't matter, only when.
  4. Step 2: sort everything by start time, exactly like Merge Intervals.
  5. Step 3: busy_until is the end of the busy block we are building, starting with the first interval's end.
  6. For each later interval: if it starts strictly after busy_until, there is a gap where nobody works, so we record Interval(busy_until, current.start). Either way we then stretch busy_until with max, just like merging.

Dry run on the example [[[1,3],[6,7]], [[2,4]], [[2,5],[9,12]]] (flattened and sorted: [1,3], [2,4], [2,5], [6,7], [9,12]; start with busy_until = 3):

currentstart > busy_until?free afterbusy_until after
[2,4]2 > 3: no[]4
[2,5]2 > 4: no[]5
[6,7]6 > 5: yes, gap [5,6][[5,6]]7
[9,12]9 > 7: yes, gap [7,9][[5,6],[7,9]]12

Example: [[[1,3],[6,7]], [[2,4]], [[2,5],[9,12]]] → busy blocks [1,5], [6,7], [9,12] → free [[5,6],[7,9]].

Edge cases to test:

  • Overlapping intervals from different employees.
  • Intervals that touch (no gap between them).
  • No common free time → [].

Complexity: O(n log n) time for n total intervals, O(n) space.

8. Common mistakes

Forgetting to sortThe whole "only compare with the previous one" idea only works on sorted input. Unsorted [[2,6],[1,3]] would never merge.
Using e instead of max(last_end, e)For [1,10] then [2,3], writing last[1] = e shrinks the block to [1,3]. Always take the max.
Closed vs half-open boundariesDo [1,4] and [4,5] overlap? In 56 yes (<=). For meetings [s, e) (253, 435), no (use ends[0] <= s to reuse, s >= end to keep). Read the problem and choose the comparison on purpose.
Sorting by the wrong keyGreedy "keep the most / remove the fewest" must sort by end. Sorting by start lets one long early interval block many short ones.
Mutating the input while it's aliasedmerged = [intervals[0]] then merged[-1][1] = … also changes intervals[0]. Copy with intervals[0][:] if the caller still needs the input.

9. Self-check quiz

Merge [[8,10],[1,3],[2,6]].

Sorted: [1,3], [2,6], [8,10]. 2 ≤ 3 → [1,6]. 8 > 6 → new block.

To remove the fewest intervals so the rest don't overlap, sort by…

Keeping the interval that finishes first leaves the most room for the rest. That's the activity-selection greedy.

Meetings [1,5) [2,6) [4,8) [7,9) [9,12). Minimum rooms?

At time 4, three meetings run at once: [1,5), [2,6) and [4,8). Later meetings reuse freed rooms, so the heap never grows past 3.

Two closed intervals a and b overlap exactly when…

Each must start before the other ends. The opposite (no overlap) is "a ends before b starts, or b ends before a starts".

10. Practice list (in order)

#ProblemLevelData structureTwist
252Meeting RoomsEasyArrayWarm-up: sort, check neighbors for any overlap
228Summary RangesEasyArrayBuild intervals from consecutive runs
56Merge IntervalsMediumArrayCore template
57Insert IntervalMediumArrayAlready sorted: three zones, O(n)
986Interval List IntersectionsMediumArrayTwo pointers: advance the one that ends first
435Non-overlapping IntervalsMediumArraySort by end, greedy
452Minimum Number of Arrows to Burst BalloonsMediumArraySame greedy; touching counts as overlap
253Meeting Rooms IIMediumArrayHeapMin-heap of end times
1094Car PoolingMediumArraySweep line / difference array with capacities
729My Calendar IMediumArrayOnline inserts: sorted list + bisect
763Partition LabelsMediumStringHash MapEach letter spans an interval [first, last]; cut where the merged interval ends
1288Remove Covered IntervalsMediumArraySort by start ascending, end descending; an interval is covered if its end ≤ the max end so far
436Find Right IntervalMediumArraySort the starts with their indices; binary-search the smallest start ≥ each end
646Maximum Length of Pair ChainMediumArraySort by right end; greedily take each pair that starts after the last chosen end
2021Brightest Position on StreetMediumArrayHash MapEach lamp lights [p − r, p + r]: sweep +1 at the start and −1 after the end, in sorted order
759Employee Free TimeHardArrayMerge everyone, report the gaps
1851Minimum Interval to Include Each QueryHardArrayHeapSort queries offline + heap by size
218The Skyline ProblemHardArrayHeapSweep events + max-heap of heights
715Range ModuleHardArrayKeep a sorted list of disjoint intervals; merge on add, split on remove, bisect on query

Worked solutions for the practice list

Every problem in the table that isn't solved in section 7 is worked out below. Click a problem name in the table to jump to it.

Easy 252. Meeting Rooms Start here Amazon Apple Array

Problem: Given meeting times [start, end), can one person attend all of them?

Example 1
Input: intervals = [[0,30],[5,10],[15,20]]
Output: false
Explanation: [0,30] overlaps both other meetings.
Example 2
Input: intervals = [[7,10],[2,4]]
Output: true

Constraints: 0 ≤ intervals.length ≤ 10⁴ · 0 ≤ startᵢ < endᵢ ≤ 10⁶

Hint 1

After sorting by start time, which meetings could clash?

Hint 2

Only neighbours: check whether each meeting starts before the previous one ends.

From brute force to the pattern: Comparing every pair of meetings is O(n²). Sorting by start means only adjacent meetings can overlap, so one pass checks it: O(n log n).

Spot it: Possible exactly when no two meetings overlap. Sort by start, and any overlap has to show up between neighbors.

Approach:

  1. Sort by start.
  2. For each meeting after the first: if it starts before the previous one ends, return False.
  3. Return True.
from typing import List


class Solution:
    def canAttendMeetings(self, intervals: List[List[int]]) -> bool:
        # Sort by start time. Python sorts lists of lists by their first
        # number (then by the second number when the first ones tie).
        intervals.sort()

        # Compare every meeting with the one right before it.
        for i in range(1, len(intervals)):
            current_start = intervals[i][0]
            previous_end = intervals[i - 1][1]
            if current_start < previous_end:
                # It starts before the previous meeting ends: a clash.
                return False
        # No neighbours clash, so no meetings clash at all.
        return True

What the code is doing, line by line:

  1. intervals.sort() sorts the meetings. A list of lists is sorted by first number, so this is "sort by start time".
  2. The for loop starts at index 1, because meeting 0 has nobody before it to clash with.
  3. current_start and previous_end name the two numbers we need to compare.
  4. If current_start < previous_end, this meeting begins while the previous one is still going, so one person can't attend both: return False. (Equal is fine: [1,5) then [5,8) do not clash.)
  5. return True: after sorting, any clash would have to be between neighbours, and we found none.

Dry run on the example intervals = [[0,30],[5,10],[15,20]] (already sorted):

icurrent_startprevious_endcurrent_start < previous_end?action
1530yesreturn False

Answer: False (the meeting at 5 starts while [0,30) is still running).

Edge cases to test:

  • No meetings → True.
  • Back-to-back meetings, e.g. [1, 2] and [2, 3] → True.
  • Unsorted input.

Complexity: O(n log n) time, O(1) extra space.

Medium 986. Interval List Intersections Meta Amazon Microsoft Array

Problem: Two lists of closed intervals are each sorted and internally disjoint. Return their intersection: every range that's covered by both lists.

Example 1
Input: firstList = [[0,2],[5,10],[13,23],[24,25]], secondList = [[1,5],[8,12],[15,24],[25,26]]
Output: [[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]
Example 2
Input: firstList = [[1,3],[5,9]], secondList = []
Output: []

Constraints: 0 ≤ firstList.length, secondList.length ≤ 1000 · firstList.length + secondList.length ≥ 1 · 0 ≤ starti < endi ≤ 10⁹ · endi < starti+1 · 0 ≤ startj < endj ≤ 10⁹ · endj < startj+1

Hint 1

Both lists are sorted and disjoint. Use one pointer per list.

Hint 2

The overlap of two intervals is [max(starts), min(ends)] if non-empty; then advance the interval that ends first.

From brute force to the pattern: Comparing every interval of A with every interval of B is O(m · n). Two pointers walk both sorted lists once, because the interval that ends first can't overlap anything later: O(m + n).

Spot it: Both lists are already sorted, so walk them together with Two Pointers. The overlap of two intervals is [max(starts), min(ends)] when that's non-empty.

Approach:

  1. Start both pointers at 0: i = 0 (into firstList) and j = 0 (into secondList).
  2. Compute overlap_start = max(firstList[i][0], secondList[j][0]) and overlap_end = min(firstList[i][1], secondList[j][1]). If overlap_start ≤ overlap_end, record [overlap_start, overlap_end].
  3. Advance the pointer whose interval ends first. It can't overlap anything further in the other list.
  4. Stop when either list runs out.
from typing import List


class Solution:
    def intervalIntersection(self, firstList: List[List[int]], secondList: List[List[int]]) -> List[List[int]]:
        i = 0                               # position in firstList
        j = 0                               # position in secondList
        result = []
        while i < len(firstList) and j < len(secondList):
            # The overlap starts at the LATER of the two starts ...
            overlap_start = max(firstList[i][0], secondList[j][0])
            # ... and ends at the EARLIER of the two ends.
            overlap_end = min(firstList[i][1], secondList[j][1])
            if overlap_start <= overlap_end:
                # Non-empty overlap: record it.
                result.append([overlap_start, overlap_end])
            # Whichever interval ends first cannot overlap anything else
            # in the other list, so move past it.
            if firstList[i][1] < secondList[j][1]:
                i = i + 1
            else:
                j = j + 1
        return result

What the code is doing, line by line:

  1. i = 0, j = 0: one pointer per list, each on its first interval. result collects the overlaps.
  2. The while loop runs as long as both pointers are still inside their lists. Once one list is used up, nothing else can overlap.
  3. overlap_start is the later of the two starts and overlap_end is the earlier of the two ends. That is exactly the shared part of two intervals.
  4. If overlap_start <= overlap_end the shared part is non-empty (a single point like [5,5] counts, since the intervals are closed), so we append it.
  5. Moving a pointer: the interval that ends first is finished, because everything later in the other list starts even later. We step past it with i = i + 1 or j = j + 1.

Dry run on the example firstList = [[0,2],[5,10],[13,23],[24,25]], secondList = [[1,5],[8,12],[15,24],[25,26]]:

ijfirstList[i]secondList[j]overlap_startoverlap_endrecorded?move
00[0,2][1,5]12[1,2]i
10[5,10][1,5]55[5,5]j
11[5,10][8,12]810[8,10]i
21[13,23][8,12]1312no (13 > 12)j
22[13,23][15,24]1523[15,23]i
32[24,25][15,24]2424[24,24]j
33[24,25][25,26]2525[25,25]i

Then i = 4 runs off the end. Result: [[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]].

Edge cases to test:

  • One list empty → [].
  • Single-point overlaps, e.g. [5, 5].
  • One interval covering many in the other list.

Complexity: O(m + n) time, O(1) extra space (besides the output).

Medium 452. Minimum Number of Arrows to Burst Balloons Google Amazon Array

Problem: Each balloon spans [x_start, x_end] on the x-axis. A vertical arrow shot at x bursts every balloon with start ≤ x ≤ end. What's the fewest arrows that burst them all?

Example 1
Input: points = [[10,16],[2,8],[1,6],[7,12]]
Output: 2
Explanation: The balloons can be burst by 2 arrows: - Shoot an arrow at x = 6, bursting the balloons [2,8] and [1,6]. - Shoot an arrow at x = 11, bursting the balloons [10,16] and [7,12].
Example 2
Input: points = [[1,2],[3,4],[5,6],[7,8]]
Output: 4
Explanation: One arrow needs to be shot for each balloon for a total of 4 arrows.

Constraints: 1 ≤ points.length ≤ 10⁵ · points[i].length == 2 · -2³¹ ≤ xstart < xend ≤ 2³¹ - 1

Hint 1

One arrow at x bursts every balloon whose range contains x. Where should the first arrow go?

Hint 2

Sort by end; shoot at the end of the first balloon, skip every balloon it bursts, repeat.

From brute force to the pattern: Trying arrow positions or subsets is far too slow. Greedy by earliest end is optimal: shooting at the smallest end bursts the most balloons that must be burst anyway. O(n log n).

Spot it: Same greedy as 435: sort by end. Shooting at the earliest end bursts the most balloons that are still standing. Touching counts as overlapping here.

Approach:

  1. Sort balloons by end.
  2. Keep arrow_x = position of the last arrow (start at −∞).
  3. For each balloon: if start > arrow_x, the last arrow misses it, so shoot a new arrow at its end.
  4. Return the arrow count.
from typing import List


def get_balloon_end(balloon):
    # Sort key helper: a balloon [x_start, x_end] ends at its second number.
    return balloon[1]


class Solution:
    def findMinArrowShots(self, points: List[List[int]]) -> int:
        # Greedy: handle the balloon that ends first, first.
        points.sort(key=get_balloon_end)
        arrows = 0
        arrow_x = float("-inf")             # no arrow shot yet

        for i in range(len(points)):
            balloon_start = points[i][0]
            balloon_end = points[i][1]
            if balloon_start > arrow_x:
                # The last arrow is left of this balloon: it misses.
                # Shoot a new arrow at this balloon's END, as far right
                # as possible, so it also hits as many later balloons
                # as possible.
                arrows = arrows + 1
                arrow_x = balloon_end
            # Otherwise start <= arrow_x <= end: already burst.
        return arrows

What the code is doing, line by line:

  1. get_balloon_end is a tiny helper that returns a balloon's end. Passing it as key= makes sort order balloons by end.
  2. arrows = 0, arrow_x = -inf: no arrows yet, and the "last arrow" is infinitely far left so the first balloon always needs a new one.
  3. In the loop we read the balloon's start and end by index.
  4. If balloon_start > arrow_x, the last arrow passes to the left of this balloon. We shoot a new arrow at balloon_end; that is the rightmost spot that still hits this balloon, so it has the best chance of hitting later ones too.
  5. Otherwise the balloon contains arrow_x (its end is at least arrow_x because we sorted by end), so it is already burst and we do nothing.

Dry run on the example points = [[10,16],[2,8],[1,6],[7,12]] (sorted by end: [[1,6],[2,8],[7,12],[10,16]]):

balloonstart > arrow_x?arrowsarrow_x
[1,6]1 > -inf: yes, shoot16
[2,8]2 > 6: no, already burst16
[7,12]7 > 6: yes, shoot212
[10,16]10 > 12: no, already burst212

Answer: 2.

Edge cases to test:

  • Touching balloons like [1, 2] and [2, 3] share one arrow.
  • All balloons overlap → 1.
  • Huge coordinates (sorting by end avoids overflow issues in other languages).

Complexity: O(n log n) time, O(1) extra space.

Medium 1094. Car Pooling Google Amazon Array

Problem: A car with capacity seats drives east. Trip [num, from, to] picks up num passengers at from and drops them off at to. Can every trip be served?

Example 1
Input: trips = [[2,1,5],[3,3,7]], capacity = 4
Output: false
Explanation: At kilometer 1, 2 passengers are picked up, so the car holds 2. At kilometer 3, 3 more are picked up, so the car holds 5. Since 5 > capacity = 4, the trips cannot all be completed.
Example 2
Input: trips = [[2,1,5],[3,3,7]], capacity = 5
Output: true
Explanation: At kilometer 1, the car holds 2 passengers. At kilometer 3, the car holds 5 passengers. At kilometer 5, the first 2 are dropped off, so the car holds 3. At kilometer 7, the last 3 are dropped off, so the car holds 0. The maximum occupancy is 5, which never exceeds capacity = 5.

Constraints: 1 ≤ trips.length ≤ 1000 · trips[i].length == 3 · 1 ≤ numPassengersi ≤ 100 · 0 ≤ fromi < toi ≤ 1000 · 1 ≤ capacity ≤ 10⁵

Hint 1

Track the number of passengers at each location.

Hint 2

Record +passengers at each pickup and −passengers at each drop-off, then sweep left to right checking the running total.

From brute force to the pattern: Simulating each trip over every kilometre is O(n · distance). A difference array (or sorted events) adds each trip in O(1) and checks capacity in one sweep: O(n + max location).

Spot it: This is "how much overlaps at the busiest point", weighted by passengers. Use a sweep line: +num at pickup, −num at drop-off.

Approach:

  1. Create events (from, +num) and (to, −num).
  2. Sort them. At the same location, the negative drop-off sorts first, so seats free up before new riders board.
  3. Sweep and keep a running load. If it ever exceeds capacity, return False.
  4. Return True.
from typing import List


class Solution:
    def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
        # Turn every trip into two events (location, change in riders).
        events = []
        for i in range(len(trips)):
            passengers = trips[i][0]
            pickup = trips[i][1]
            dropoff = trips[i][2]
            events.append((pickup, passengers))      # riders get in
            events.append((dropoff, -passengers))    # riders get out

        # Sort by location. Tuples compare by the first item, then the
        # second, so at the same spot the negative drop-off comes first:
        # seats are freed before new riders board.
        events.sort()

        # Drive east, keeping a running count of riders in the car.
        load = 0
        for i in range(len(events)):
            change = events[i][1]
            load = load + change
            if load > capacity:
                return False                # too many riders at once
        return True

What the code is doing, line by line:

  1. Building events: for each trip we read passengers, pickup and dropoff by index, then add two tuples: (pickup, +passengers) and (dropoff, -passengers).
  2. events.sort() orders events by location. When two events share a location, the smaller second value wins, so a drop-off (negative) is processed before a pickup (positive). That frees seats first.
  3. load = 0: the car starts empty.
  4. The sweep loop reads each event's change, adds it to load, and returns False the moment load goes over capacity.
  5. return True: we got through every event without overflowing.

Dry run on the example trips = [[2,1,5],[3,3,7]], capacity = 4:

event (location, change)load afterload > 4?
(1, +2)2no
(3, +3)5yes: return False

Sorted events were [(1,2), (3,3), (5,-2), (7,-3)]; the last two are never reached. Answer: False.

Edge cases to test:

  • A drop-off and a pickup at the same location: drop off first.
  • A single trip over capacity → False.
  • Trips that don't overlap at all.

Complexity: O(n log n) time, O(n) space. (A difference array over locations ≤ 1000 gives O(n + 1000).)

Medium 729. My Calendar I Amazon Array

Problem: Implement book(start, end) for half-open events [start, end). Accept and store the booking only if it doesn't overlap any existing one.

Example 1
Input: ["MyCalendar", "book", "book", "book"] [[], [10, 20], [15, 25], [20, 30]]
Output: [null, true, false, true]
Explanation: MyCalendar myCalendar = new MyCalendar(); myCalendar.book(10, 20); // return True myCalendar.book(15, 25); // return False, It can not be booked because time 15 is already booked by another event. myCalendar.book(20, 30); // return True, The event can be booked, as the first event takes every time less than 20, but not including 20.

Constraints: 0 ≤ start < end ≤ 10⁹ · At most 1000 calls will be made to book.

Hint 1

A new booking [start, end) clashes with an existing [s, e) when start < e and s < end.

Hint 2

Check every existing booking (or keep them sorted and check only the neighbours).

From brute force to the pattern: Checking all existing bookings is O(n) per call, which is fine for these limits. A sorted list with binary search (or a balanced BST) makes it O(log n) per call.

Spot it: Intervals arrive one at a time (online), so you can't sort once. Keep the bookings sorted, and a new booking only needs to be checked against its two neighbors. Find them with binary search.

Approach:

  1. Keep two parallel sorted lists: starts and ends.
  2. Binary-search starts for the first position whose start is greater than the new start. That position position is where the new booking would go (this is what bisect.bisect_right computes; here it is written out by hand).
  3. Conflict with the left neighbor if ends[position−1] > start. Conflict with the right neighbor if starts[position] < end.
  4. If there's no conflict, insert at position in both lists and return True.
class MyCalendar:
    def __init__(self):
        self.starts = []                    # booking starts, kept sorted
        self.ends = []                      # ends, same order as starts

    def book(self, start: int, end: int) -> bool:
        # Binary search: find the first position whose start is
        # GREATER than the new start. The new booking would go there,
        # so its neighbours are at position - 1 (left) and position (right).
        low = 0
        high = len(self.starts)
        while low < high:
            middle = (low + high) // 2
            if self.starts[middle] <= start:
                low = middle + 1            # answer is to the right of middle
            else:
                high = middle               # middle might be the answer
        position = low

        # Left neighbour: does it end after we start? Then it overlaps.
        if position > 0 and self.ends[position - 1] > start:
            return False
        # Right neighbour: does it start before we end? Then it overlaps.
        if position < len(self.starts) and self.starts[position] < end:
            return False

        # No clash: insert into both lists at the same position.
        # list.insert is O(n), which is fine for 1000 calls.
        self.starts.insert(position, start)
        self.ends.insert(position, end)
        return True

What the code is doing, line by line:

  1. __init__ creates two empty lists. self.starts[k] and self.ends[k] together describe booking number k, and the bookings are kept sorted by start.
  2. The binary search keeps a window [low, high). If the middle start is <= start, the answer must be to its right, so low = middle + 1. Otherwise the middle could be the answer, so high = middle. When low == high, that is position.
  3. Left neighbour check: the booking just before position starts at or before us. If it ends after we start (ends[position - 1] > start), the two overlap, so we refuse.
  4. Right neighbour check: the booking at position starts after us. If it starts before we end (starts[position] < end), they overlap, so we refuse.
  5. Insert: no clash with either neighbour means no clash at all (the list is sorted and non-overlapping), so we insert into both lists at position and return True.

Dry run on the example book(10,20), book(15,25), book(20,30):

callpositioncheckreturnsstarts / ends after
book(10, 20)0no neighboursTrue[10] / [20]
book(15, 25)1left neighbour ends at 20 > 15: overlapFalse[10] / [20]
book(20, 30)1left ends at 20, not > 20; no right neighbourTrue[10, 20] / [20, 30]

Edge cases to test:

  • Back-to-back bookings, e.g. [10, 20) then [20, 30) → both allowed.
  • A booking inside an existing one.
  • A booking covering an existing one.

Complexity: O(log n) search + O(n) insert per booking, O(n) space. (A balanced BST / SortedList makes inserts O(log n).)

Hard 1851. Minimum Interval to Include Each Query Google ArrayHeap

Problem: For each query point q, return the size (right − left + 1) of the smallest interval that contains q, or -1 if no interval does.

Example 1
Input: intervals = [[1,4],[2,4],[3,6],[4,4]], queries = [2,3,4,5]
Output: [3,3,1,4]
Explanation: The queries are processed as follows: - Query = 2: The interval [2,4] is the smallest interval containing 2. The answer is 4 - 2 + 1 = 3. - Query = 3: The interval [2,4] is the smallest interval containing 3. The answer is 4 - 2 + 1 = 3. - Query = 4: The interval [4,4] is the smallest interval containing 4. The answer is 4 - 4 + 1 = 1. - Query = 5: The interval [3,6] is the smallest interval containing 5. The answer is 6 - 3 + 1 = 4.
Example 2
Input: intervals = [[2,3],[2,5],[1,8],[20,25]], queries = [2,19,5,22]
Output: [2,-1,4,6]
Explanation: The queries are processed as follows: - Query = 2: The interval [2,3] is the smallest interval containing 2. The answer is 3 - 2 + 1 = 2. - Query = 19: None of the intervals contain 19. The answer is -1. - Query = 5: The interval [2,5] is the smallest interval containing 5. The answer is 5 - 2 + 1 = 4. - Query = 22: The interval [20,25] is the smallest interval containing 22. The answer is 25 - 20 + 1 = 6.

Constraints: 1 ≤ intervals.length ≤ 10⁵ · 1 ≤ queries.length ≤ 10⁵ · intervals[i].length == 2 · 1 ≤ lefti ≤ righti ≤ 10⁷ · 1 ≤ queries[j] ≤ 10⁷

Hint 1

Process the queries in increasing order (remember their original positions).

Hint 2

Sort the intervals by start; for each query, push intervals that have started into a heap keyed by size, and pop those that already ended.

From brute force to the pattern: For every query, scanning all intervals is O(n · q). Sorting both and using a min-heap of (size, end) answers each query in O(log n): O((n + q) log n).

Spot it: Answer the queries offline in sorted order. As q grows, intervals "start" (left ≤ q) and "expire" (right < q), which is a sweep. "Smallest live interval" → min-heap by size.

Approach:

  1. Sort intervals by left end. Process the distinct queries in increasing order.
  2. For each q, push (size, right) for every interval with left ≤ q.
  3. Pop from the heap while its top has right < q. It has expired, and it will stay expired for every bigger query too.
  4. The heap's top size (or -1) is the answer for q. Map answers back to the original query order.
import heapq
from typing import List


class Solution:
    def minInterval(self, intervals: List[List[int]], queries: List[int]) -> List[int]:
        # heapq keeps a plain list arranged as a "min-heap":
        #   heapq.heappush(heap, item) adds an item
        #   heapq.heappop(heap)        removes and returns the smallest item
        #   heap[0]                    is always the smallest item
        # Our items are tuples (size, right). Tuples compare by their
        # first value, so heap[0] is always the SMALLEST interval.

        # Sort intervals by their left end.
        intervals.sort()

        # Build the distinct query values in increasing order.
        sorted_queries = []
        for i in range(len(queries)):
            sorted_queries.append(queries[i])
        sorted_queries.sort()
        unique_queries = []
        for i in range(len(sorted_queries)):
            value = sorted_queries[i]
            if len(unique_queries) == 0 or unique_queries[len(unique_queries) - 1] != value:
                unique_queries.append(value)

        heap = []                           # (size, right) of started intervals
        answer_for = {}                     # query value -> answer
        next_interval = 0                   # first interval not yet pushed

        for q_index in range(len(unique_queries)):
            query = unique_queries[q_index]

            # Push every interval that has started (left <= query).
            while next_interval < len(intervals) and intervals[next_interval][0] <= query:
                left = intervals[next_interval][0]
                right = intervals[next_interval][1]
                size = right - left + 1
                heapq.heappush(heap, (size, right))
                next_interval = next_interval + 1

            # Throw away smallest intervals that ended before query.
            # Later queries are even bigger, so they stay useless.
            while len(heap) > 0 and heap[0][1] < query:
                heapq.heappop(heap)

            # The top of the heap is now the smallest interval holding query.
            if len(heap) > 0:
                answer_for[query] = heap[0][0]
            else:
                answer_for[query] = -1

        # Put the answers back in the ORIGINAL query order.
        result = []
        for i in range(len(queries)):
            result.append(answer_for[queries[i]])
        return result

What the code is doing, line by line:

  1. The heap comment explains heappush, heappop and heap[0]. Each heap item is a tuple (size, right), and tuples compare by their first value, so the smallest interval is always on top.
  2. intervals.sort() puts intervals in order of their left end, so we can add them one by one as the query grows.
  3. Distinct sorted queries: we copy queries, sort the copy, then keep a value only if it differs from the last one we kept. That way each query value is answered once, from smallest to largest.
  4. heap, answer_for, next_interval: the heap of started intervals, a dictionary from query value to its answer, and the index of the first interval not yet pushed.
  5. First inner while: push every interval whose left end is <= query, with its size right - left + 1. Because queries only grow, an interval pushed once never has to be pushed again.
  6. Second inner while: while the smallest interval ends before the query (right < query), it cannot contain this query or any later one, so pop it for good.
  7. The if/else stores the top size as the answer, or -1 when the heap is empty.
  8. Last loop: walk the original queries list and look each one up in answer_for, so the answers come back in the order they were asked.

Dry run on the example intervals = [[1,4],[2,4],[3,6],[4,4]], queries = [2,3,4,5]:

querypushed (size, right)poppedheap after (sorted)answer
2(4,4) from [1,4], (3,4) from [2,4]none(3,4), (4,4)3
3(4,6) from [3,6]none(3,4), (4,4), (4,6)3
4(1,4) from [4,4]none(1,4), (3,4), (4,4), (4,6)1
5none(1,4), (3,4), (4,4) (all end at 4 < 5)(4,6)4

Result in original order: [3, 3, 1, 4].

Edge cases to test:

  • A query no interval contains → −1.
  • Duplicate queries.
  • Intervals of length 1 (start == end).

Complexity: O(n log n + q log q) time, O(n + q) space.

Hard 218. The Skyline Problem Microsoft ArrayHeap

Problem: Buildings are [left, right, height] rectangles on the ground. Return the skyline's "key points": [x, height] wherever the outline's height changes.

Example 1
Input: buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]
Output: [[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]]
Explanation: Figure A shows the buildings of the input. Figure B shows the skyline formed by those buildings. The red points in figure B represent the key points in the output list.
Example 2
Input: buildings = [[0,2,3],[2,5,3]]
Output: [[0,3],[5,0]]

Constraints: 1 ≤ buildings.length ≤ 10⁴ · 0 ≤ lefti < righti ≤ 2³¹ - 1 · 1 ≤ heighti ≤ 2³¹ - 1 · buildings is sorted by lefti in non-decreasing order.

Hint 1

The skyline only changes at a building's left or right edge. Process those edges in x-order.

Hint 2

Keep a max-heap of the heights of buildings covering the current x; record a key point whenever the maximum height changes.

From brute force to the pattern: Checking the height at every x coordinate is far too slow for large coordinates. A sweep over sorted edges with a max-heap (lazy removal) gives O(n log n).

Spot it: Sweep left to right over building edges. At each x, the skyline's height is the tallest building that is currently "live", so use a max-heap of live heights. When that maximum changes, record a key point.

Approach:

  1. Make events: (left, −height, right) for starts and (right, 0, 0) for ends. Sorting puts taller starts first at the same x.
  2. Keep a max-heap live of (−height, right), seeded with the ground (0, ∞).
  3. At each event, push the building if it's a start. Then lazily pop tops whose right ≤ x.
  4. If the current max height differs from the last key point's height, append [x, height].
import heapq
from typing import List


class Solution:
    def getSkyline(self, buildings: List[List[int]]) -> List[List[int]]:
        # heapq is a MIN-heap: heapq.heappop(heap) removes the smallest
        # item and heap[0] is the smallest. We need the TALLEST building,
        # so we store heights as NEGATIVE numbers: the tallest building
        # then has the smallest (most negative) value and sits at heap[0].
        # This is the "negative-number max-heap trick".

        # Make two events per building.
        events = []
        for i in range(len(buildings)):
            left = buildings[i][0]
            right = buildings[i][1]
            height = buildings[i][2]
            # Start event: (x, -height, right). At the same x, a taller
            # building has a smaller -height, so it is handled first.
            events.append((left, -height, right))
            # End event: (x, 0, 0). It adds nothing; it just makes us
            # clean up buildings that have ended at this x.
            events.append((right, 0, 0))
        events.sort()

        result = []
        # Live buildings as (-height, right). The ground (height 0)
        # never ends, so the heap is never empty.
        live = []
        heapq.heappush(live, (0, float("inf")))

        for i in range(len(events)):
            event_x = events[i][0]
            negative_height = events[i][1]
            right = events[i][2]

            if negative_height != 0:
                # A building starts here: it becomes live.
                heapq.heappush(live, (negative_height, right))

            # Remove tallest buildings that already ended (right <= event_x).
            # We only need to clean the top, because only the top
            # decides the current height ("lazy deletion").
            while live[0][1] <= event_x:
                heapq.heappop(live)

            # Current skyline height = tallest live building.
            current_height = -live[0][0]

            # Record a key point only if the height changed.
            if len(result) == 0:
                result.append([event_x, current_height])
            elif result[len(result) - 1][1] != current_height:
                result.append([event_x, current_height])
        return result

What the code is doing, line by line:

  1. The comment at the top explains the max-heap trick. Python's heapq only gives you the smallest item. Storing -height makes the tallest building the smallest number, so it sits at live[0].
  2. Building the events: for each building we read left, right and height by index. A start event is (left, -height, right). An end event is (right, 0, 0). It carries no building; it just tells the sweep to stop at right and tidy up.
  3. events.sort() orders the events left to right. Tuples compare item by item, so at the same x a taller start (more negative) comes first, and starts (negative) come before end markers (0).
  4. live starts with the ground (0, inf). Its height is 0 and it never ends, so live[0] always exists.
  5. In the loop we read the three fields of the event by index. If negative_height != 0, this is a start event, so we push the building into live.
  6. The while loop pops the top building while it has already ended (right <= event_x). Ended buildings deeper in the heap can stay for now; they get removed later if they ever reach the top. That is called lazy deletion.
  7. current_height = -live[0][0] flips the sign back to get the real height of the tallest live building.
  8. Recording: if result is empty, or the last key point has a different height, the outline changed here, so we append [event_x, current_height]. Otherwise the height is unchanged and nothing is recorded.

Dry run on the example buildings = [[2,9,10],[3,7,15],[5,12,12],[15,20,10],[19,24,8]]:

eventpoppedlive heap (sorted)current_heightresult change
(2, -10, 9)none(-10,9), (0,inf)10add [2,10]
(3, -15, 7)none(-15,7), (-10,9), (0,inf)15add [3,15]
(5, -12, 12)none(-15,7), (-12,12), (-10,9), (0,inf)15none (same height)
(7, 0, 0)(-15,7)(-12,12), (-10,9), (0,inf)12add [7,12]
(9, 0, 0)none (top (-12,12) is still live; (-10,9) waits lazily)(-12,12), (-10,9), (0,inf)12none
(12, 0, 0)(-12,12), (-10,9)(0,inf)0add [12,0]
(15, -10, 20)none(-10,20), (0,inf)10add [15,10]
(19, -8, 24)none(-10,20), (-8,24), (0,inf)10none
(20, 0, 0)(-10,20)(-8,24), (0,inf)8add [20,8]
(24, 0, 0)(-8,24)(0,inf)0add [24,0]

Result: [[2,10],[3,15],[7,12],[12,0],[15,10],[20,8],[24,0]].

Edge cases to test:

  • Buildings sharing an edge with the same height (don't emit duplicate points).
  • Nested buildings.
  • Adjacent buildings of different heights.

Complexity: O(n log n) time, O(n) space.

Easy 228. Summary Ranges Google Amazon Netflix Array

Problem: nums is sorted with unique values. Return the smallest list of ranges that covers all the numbers exactly: "a->b" for a run a..b, or "a" for a single number.

Example 1
Input: nums = [0,1,2,4,5,7]
Output: ["0->2","4->5","7"]
Explanation: The ranges are: [0,2] --> "0->2" [4,5] --> "4->5" [7,7] --> "7"
Example 2
Input: nums = [0,2,3,4,6,8,9]
Output: ["0","2->4","6","8->9"]
Explanation: The ranges are: [0,0] --> "0" [2,4] --> "2->4" [6,6] --> "6" [8,9] --> "8->9"

Constraints: 0 ≤ nums.length ≤ 20 · -2³¹ ≤ nums[i] ≤ 2³¹ - 1 · All the values of nums are unique. · nums is sorted in ascending order.

Hint 1

Walk through the sorted numbers and group consecutive ones.

Hint 2

Close a range whenever the next number isn't exactly one more than the current one.

From brute force to the pattern: This is already a linear scan; the only work is careful formatting: "a" for a single number and "a->b" for a range. O(n).

Spot it: The reverse of merging: build intervals. Consecutive numbers (next == current + 1) belong to the same interval, and any gap closes it.

Approach:

  1. Walk with index i. Each run starts at start = nums[i].
  2. While the next number is exactly one more than the current, extend the run.
  3. Close the run: write "start" if it holds one number, otherwise "start->end".
  4. Continue from the number after the run.
class Solution:
    def summaryRanges(self, nums: List[int]) -> List[str]:
        ranges = []
        i = 0
        while i < len(nums):
            start = nums[i]
            # extend the run while the numbers are consecutive
            while i + 1 < len(nums) and nums[i + 1] == nums[i] + 1:
                i = i + 1
            end = nums[i]
            if start == end:
                ranges.append(str(start))
            else:
                ranges.append(str(start) + "->" + str(end))
            # move to the first number of the next run
            i = i + 1
        return ranges

What the code is doing, line by line:

  1. while i < len(nums): each pass of this loop produces one range.
  2. start = nums[i]: the first number of the run.
  3. Inner while i + 1 < len(nums) and nums[i + 1] == nums[i] + 1: keep stepping while the next number continues the run. The bounds check comes first so we never read past the end.
  4. end = nums[i]: the last number of the run.
  5. if start == end: a single number is written alone; otherwise use the "a->b" form.
  6. i = i + 1: step past the run.

Dry run on the example:

nums = [0, 1, 2, 4, 5, 7] (LeetCode Example 1). Returns ["0->2", "4->5", "7"].

run starts atrun coverswritten
00, 1, 2 (stops: 4 ≠ 3)"0->2"
44, 5 (stops: 7 ≠ 6)"4->5"
77 (end of array)"7"

Edge cases to test:

  • An empty array → [].
  • A single number.
  • Negative numbers, e.g. [-3, -2] → ["-3->-2"].

Complexity: O(n) time, O(1) extra space besides the output.

Medium 763. Partition Labels Google Amazon Microsoft StringHash Map

Problem: Split s into as many parts as possible so each letter appears in at most one part. Return the part sizes.

Example 1
Input: s = "ababcbacadefegdehijhklij"
Output: [9,7,8]
Explanation: The partition is "ababcbaca", "defegde", "hijhklij". This is a partition so that each letter appears in at most one part. A partition like "ababcbacadefegde", "hijhklij" is incorrect, because it splits s into less parts.
Example 2
Input: s = "eccbbbbdec"
Output: [10]

Constraints: 1 ≤ s.length ≤ 500 · s consists of lowercase English letters.

Hint 1

For each letter, where does it first and last appear?

Hint 2

Extend the current part to the last occurrence of every letter in it; cut when you reach that point.

From brute force to the pattern: Trying cut positions and checking that no letter crosses them is O(n²). Each letter's last index tells you exactly how far the current part must reach, so one pass finds every cut.

Spot it: Each letter occupies the interval from its first to its last occurrence. A part must contain whole intervals, so overlapping intervals merge, and a part ends exactly where the merged interval ends: Merge Intervals in disguise.

Why not another pattern? Trying every cut and checking letter sets is O(n²). Building explicit intervals and merging them is equivalent but the greedy scan is shorter.

Approach:

  1. Record each letter's last index.
  2. Scan s, extending end to the last index of every letter seen.
  3. When i == end, the current part is complete: record its size and start a new part.
class Solution:
    def partitionLabels(self, s: str) -> List[int]:
        last = {c: i for i, c in enumerate(s)}  # last index of each letter
        sizes = []
        start = 0
        end = 0  # the current part must reach at least here
        for i, c in enumerate(s):
            end = max(end, last[c])  # this letter forces the part to extend to its last occurrence
            if i == end:
                sizes.append(end - start + 1)  # every letter seen so far is finished
                start = i + 1
        return sizes

What the code is doing, line by line:

  1. last[c]: the end of letter c's interval.
  2. end is the end of the merged interval for the current part.
  3. When the scan reaches end, no letter in the part appears later, so cutting here is safe and as early as possible.

Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.

Edge cases to test:

  • All letters distinct → parts of size 1.
  • One letter at both ends → a single part.
  • A single character.

Complexity: O(n) time, O(1) space (26 letters).

Medium 1288. Remove Covered Intervals Google Amazon Microsoft Array

Problem: Remove every interval that is covered by another (c ≤ a and b ≤ d). Return how many remain.

Example 1
Input: intervals = [[1,4],[3,6],[2,8]]
Output: 2
Explanation: Interval [3,6] is covered by [2,8], therefore it is removed.
Example 2
Input: intervals = [[1,4],[2,3]]
Output: 1

Constraints: 1 ≤ intervals.length ≤ 1000 · intervals[i].length == 2 · 0 ≤ li < ri ≤ 10⁵ · All the given intervals are unique.

Hint 1

Sort by start. What must be true about an earlier interval that covers the current one?

Hint 2

Track the maximum end seen; break ties in start by putting longer intervals first.

From brute force to the pattern: Comparing every pair is O(n²). After sorting, one running maximum decides coverage for each interval: O(n log n).

Spot it: Interval relationships → sort by start; then a covering interval always comes before the intervals it covers.

Why not another pattern? Checking every pair is O(n²).

Approach:

  1. Sort by start ascending and, for equal starts, by end descending (the longest first).
  2. Scan, keeping max_end. An interval with end ≤ max_end is covered; otherwise it survives and updates max_end.
class Solution:
    def removeCoveredIntervals(self, intervals: List[List[int]]) -> int:
        intervals = sorted(intervals, key=lambda iv: (iv[0], -iv[1]))  # longest first on equal starts
        remaining = 0
        max_end = 0
        for start, end in intervals:
            if end > max_end:
                remaining = remaining + 1  # not covered by anything before it
                max_end = end
            # else: an earlier interval starts no later and ends no earlier: covered
        return remaining

What the code is doing, line by line:

  1. After sorting, every earlier interval starts at or before the current one.
  2. So the current interval is covered exactly when some earlier interval ends at or after it: end <= max_end.
  3. Sorting equal starts by descending end makes the longer interval come first, so it correctly covers the shorter ones.

Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.

Edge cases to test:

  • Identical intervals (one covers the other).
  • Equal starts with different ends.
  • No interval covers another.

Complexity: O(n log n) time, O(n) space for the sorted copy.

Hard 715. Range Module Google Array

Problem: Track half-open ranges [left, right): addRange covers a range, removeRange uncovers it, and queryRange asks whether every number in a range is currently covered.

Example 1
Input: ["RangeModule", "addRange", "removeRange", "queryRange", "queryRange", "queryRange"] [[], [10, 20], [14, 16], [10, 14], [13, 15], [16, 17]]
Output: [null, null, null, true, false, true]
Explanation: RangeModule rangeModule = new RangeModule(); rangeModule.addRange(10, 20); rangeModule.removeRange(14, 16); rangeModule.queryRange(10, 14); // return True,(Every number in [10, 14) is being tracked) rangeModule.queryRange(13, 15); // return False,(Numbers like 14, 14.03, 14.17 in [13, 15) are not being tracked) rangeModule.queryRange(16, 17); // return True, (The number 16 in [16, 17) is still being tracked, despite the remove operation)

Constraints: 1 ≤ left < right ≤ 10⁹ · At most 10⁴ calls will be made to addRange, queryRange, and removeRange.

Hint 1

Store the covered numbers as a sorted list of disjoint intervals.

Hint 2

Adding merges overlapping intervals; removing splits them; a query only needs the interval that starts at or before left.

From brute force to the pattern: Marking every integer is impossible for ranges up to 10⁹. Keeping disjoint intervals means each operation only touches the intervals that overlap the given range.

Spot it: A set of numbers stored as disjoint, sorted intervals: adding merges overlapping intervals, removing splits them, and querying finds the one interval that could contain the range. That's interval merging plus binary search.

Why not another pattern? A boolean array over all numbers is impossible (up to 10⁹). A segment tree with lazy propagation works but is far more code.

Approach:

  1. Store a sorted list of disjoint intervals.
  2. addRange: drop every interval that overlaps or touches [left, right), extend the new range to cover them, insert it.
  3. removeRange: drop overlapping intervals, but keep their parts sticking out on the left or right.
  4. queryRange: binary-search the last interval starting at or before left; covered if it ends at or after right.
from bisect import bisect_right

class RangeModule:
    def __init__(self):
        self.intervals = []  # sorted, disjoint [start, end) pairs

    def addRange(self, left: int, right: int) -> None:
        kept = []
        for s, e in self.intervals:
            if e < left or s > right:
                kept.append([s, e])  # no overlap and not touching: keep as is
            else:
                left = min(left, s)  # merge it into the new range
                right = max(right, e)
        kept.append([left, right])
        kept.sort()
        self.intervals = kept

    def queryRange(self, left: int, right: int) -> bool:
        starts = [s for s, _ in self.intervals]
        i = bisect_right(starts, left) - 1  # last interval starting at or before left
        return i >= 0 and self.intervals[i][1] >= right

    def removeRange(self, left: int, right: int) -> None:
        kept = []
        for s, e in self.intervals:
            if e <= left or s >= right:
                kept.append([s, e])  # untouched
            else:
                if s < left:
                    kept.append([s, left])  # the part sticking out on the left
                if e > right:
                    kept.append([right, e])  # the part sticking out on the right
        self.intervals = kept

What the code is doing, line by line:

  1. Intervals are half-open, so [10, 14) and [14, 20) touch; addRange merges touching intervals too (e < left is the strict test).
  2. addRange grows [left, right) to swallow every overlapping interval, then inserts it and re-sorts.
  3. queryRange: only the interval with the largest start ≤ left can contain left; the range is covered if that interval also reaches right.
  4. removeRange keeps the parts of each overlapping interval that lie outside [left, right).

Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.

Edge cases to test:

  • Adding a range that touches an existing one (merge them).
  • Removing from the middle of an interval (it splits in two).
  • Querying an empty module → False.

Complexity: O(n log n) per addRange, O(n) per removeRange, O(n) per queryRange here (the list of starts is rebuilt; a sorted container makes these O(log n) amortised).

Medium 436. Find Right Interval Amazon Array

Problem: For each interval, return the index of the interval with the smallest start ≥ its end (or −1).

Example 1
Input: intervals = [[1,2]]
Output: [-1]
Explanation: There is only one interval in the collection, so it outputs -1.
Example 2
Input: intervals = [[3,4],[2,3],[1,2]]
Output: [-1,0,1]
Explanation: There is no right interval for [3,4]. The right interval for [2,3] is [3,4] since start0 = 3 is the smallest start that is >= end1 = 3. The right interval for [1,2] is [2,3] since start1 = 2 is the smallest start that is >= end2 = 2.

Constraints: 1 ≤ intervals.length ≤ 2 * 10⁴ · intervals[i].length == 2 · -10⁶ ≤ starti ≤ endi ≤ 10⁶ · The start point of each interval is unique.

Hint 1

For each end, you need the smallest start that is at least that end.

Hint 2

Sort the starts and binary-search.

From brute force to the pattern: All pairs: O(n²); sort + binary search: O(n log n).

Spot it: An intervals question answered by sorting starts and binary search: for each end, find the first start at or after it.

Why not another pattern? Checking all pairs is O(n²).

Approach:

  1. starts = sorted((start, index)).
  2. For each interval: bisect_left(starts, (end, −1)); return that index or −1.
from bisect import bisect_left

class Solution:
    def findRightInterval(self, intervals: List[List[int]]) -> List[int]:
        starts = sorted((s, i) for i, (s, _) in enumerate(intervals))
        result = []
        for _, end in intervals:
            j = bisect_left(starts, (end, -1))  # first start >= end
            result.append(starts[j][1] if j < len(starts) else -1)
        return result

What the code is doing, line by line:

  1. Starts are unique, so (end, −1) sorts before any (end, i), finding a start equal to end too.
  2. An interval can be its own right interval when start == end.

Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.

Edge cases to test:

  • Single interval.
  • start == end (itself).
  • No right interval → −1.

Complexity: O(n log n) time, O(n) space.

Medium 646. Maximum Length of Pair Chain Cisco Array

Problem: A pair [c, d] can follow [a, b] if b < c. Return the longest chain (pairs in any order).

Example 1
Input: pairs = [[1,2],[2,3],[3,4]]
Output: 2
Explanation: The longest chain is [1,2] -> [3,4].
Example 2
Input: pairs = [[1,2],[7,8],[4,5]]
Output: 3
Explanation: The longest chain is [1,2] -> [4,5] -> [7,8].

Constraints: n == pairs.length · 1 ≤ n ≤ 1000 · -1000 ≤ lefti < righti ≤ 1000

Hint 1

Which known interval problem is this?

Hint 2

Sort by end and pick greedily.

From brute force to the pattern: LIS DP is O(n²); the greedy is O(n log n).

Spot it: It's interval scheduling: choose as many non-overlapping intervals as possible → sort by end and take greedily (like Non-overlapping Intervals).

Why not another pattern? The LIS-style DP is O(n²); the greedy is O(n log n).

Approach:

  1. Sort by the second element.
  2. Take a pair if its start is greater than the last taken end.
class Solution:
    def findLongestChain(self, pairs: List[List[int]]) -> int:
        chain = 0
        end = float("-inf")
        for a, b in sorted(pairs, key=lambda p: p[1]):  # earliest end first
            if a > end:
                chain += 1  # it fits after the last chosen pair
                end = b
        return chain

What the code is doing, line by line:

  1. Choosing the pair that ends earliest leaves the most room for the rest (exchange argument).
  2. Strict > because the chain needs b < c.

Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.

Edge cases to test:

  • Touching pairs (b == c) can't chain.
  • Nested pairs.
  • One pair.

Complexity: O(n log n) time, O(1) extra space.

Medium 2021. Brightest Position on Street Microsoft ArrayHash Map

Problem: Lamps light [position − range, position + range]. Return the smallest position with the most lamps lighting it.

Example 1
Input: lights = [[-3,2],[1,2],[3,3]]
Output: -1
Explanation: Lamps cover [−5, −1], [−1, 3] and [0, 6]. Brightness 2 is the maximum (at −1 and at 0..3); −1 is the smallest such position.
Example 2
Input: lights = [[1,0],[0,1]]
Output: 1
Explanation: Position 1 is lit by both lamps.
Example 3
Input: lights = [[1,2]]
Output: -1
Explanation: Every position in [−1, 3] has brightness 1; −1 is the smallest.

Constraints: 1 ≤ lights.length ≤ 10⁵ · lights[i].length == 2 · -10⁸ ≤ positioni ≤ 10⁸ · 0 ≤ rangei ≤ 10⁸

Hint 1

Brightness changes only at interval ends.

Hint 2

Use a difference map and sweep the sorted points.

From brute force to the pattern: Checking every candidate against every lamp is O(n²); the sweep is O(n log n).

Spot it: Overlapping intervals → a sweep line over a difference map: +1 where a lamp starts, −1 just after it ends; scan the sorted points.

Why not another pattern? Checking each candidate position against every lamp is O(n²).

Approach:

  1. delta[p − r] += 1, delta[p + r + 1] −= 1.
  2. Sweep sorted keys with a running sum; keep the first position with the maximum.
from collections import Counter

class Solution:
    def brightestPosition(self, lights: List[List[int]]) -> int:
        delta = Counter()
        for p, r in lights:
            delta[p - r] += 1  # light starts here
            delta[p + r + 1] -= 1  # and stops after p + r
        best_pos, best = None, -1
        current = 0
        for x in sorted(delta):
            current += delta[x]
            if current > best:
                best, best_pos = current, x  # first (smallest) position with this brightness
        return best_pos

What the code is doing, line by line:

  1. Brightness only changes at the stored points, so the maximum is reached at one of them.
  2. Strict > keeps the smallest position on ties.

Dry run: use the Step through the solution player under the code. It runs this exact code on Example 1 and shows every line with the variables at that moment.

Edge cases to test:

  • Range 0 (a single point).
  • Ties (smallest position).
  • Negative positions.

Complexity: O(n log n) time, O(n) space.