Skip to main content

Problems

Start at the top. The first group is the merge sweep itself - sort by start, extend the last interval when the next one touches it, append otherwise - and every group after it is that identical loop with a different thing measured at the end. Learn the sweep once and the rest of the page is deciding what to do with merged.

Merge overlapping

The merged list itself is the answer, so this is the loop worth memorising. Everything below reuses it verbatim.

56. Merge Intervals

Medium·
Explanation

Sort the intervals by start time first - once sorted, every interval that overlaps the most recently merged interval (merged[-1]) is guaranteed to appear immediately after it, so a single left-to-right scan is enough. Two helper lambdas name the two operations: doesIntersect(a, b) is the overlap test (a[1] >= b[0] and b[1] >= a[0]), and getMerged(a, b) is the union of two overlapping intervals (min of the starts, max of the ends). For each inter, if it intersects merged[-1], replace it with getMerged(merged[-1], inter). Otherwise inter starts a new, disjoint run - append it to merged as-is.

Analysis
Time
O(n + n log n)
  • A single O(n) merge scan, plus O(n log n) to sort first - n + n log n.
Space
O(sort + n)
  • merged holds up to n intervals in the worst case, when nothing overlaps at all, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 56 MERGE INTERVALS INTERACTIVE
visualization loads as you reach it
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort()
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
 
merged = []
for inter in intervals:
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
return merged

57. Insert Interval

Medium·
Explanation

The input is already sorted and disjoint, so newInterval needs to be merged into at most one contiguous run - no full sort is needed, just a single left-to-right scan. Three helper lambdas name the operations: doesIntersect(a, b) tests overlap, getMerged(a, b) returns the union of two overlapping intervals, and isInGapStrictly(left, right, a) checks whether a fits entirely in the gap between left and right without touching either. For each inter, if it overlaps newInterval, fold newInterval into it via getMerged; otherwise, if newInterval fits in the gap before inter, insert it ahead of inter. Either way, inter (possibly just merged with newInterval) is then merged into merged[-1] if it overlaps, or appended as a new run. A trailing check after the loop catches newInterval landing entirely after the last interval.

Analysis
Time
O(n)
  • A single left-to-right scan over intervals, with each lambda doing O(1) work.
Space
O(n)
  • merged holds up to n + 1 intervals in the worst case, when newInterval doesn't overlap anything.
FIG. 57 INSERT INTERVAL INTERACTIVE
visualization loads as you reach it
class Solution:
def insert(
self, intervals: List[List[int]], newInterval: List[int]
) -> List[List[int]]:
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
isInGapStrictly = lambda left, right, a: a[0] > left[1] and a[1] < right[0]
 
merged = [[-1, -1]]
for inter in intervals:
if doesIntersect(newInterval, inter):
inter = getMerged(newInterval, inter)
elif isInGapStrictly(merged[-1], inter, newInterval):
merged.append(newInterval)
 
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
last_inter = intervals[-1] if intervals else [-1, -1]
if isInGapStrictly(last_inter, [float("inf"), float("inf")], newInterval):
merged.append(newInterval)
return merged[1:]

228. Summary Ranges

Easy·
3 Approachesclick to switch
Explanation

nums is already sorted and distinct, so this reuses the same doesIntersect/getMerged interval-merge machinery as [[56. Merge Intervals]] by treating each value i as the half-open interval [i-1, i] - two values land in the same run exactly when their intervals touch, which happens precisely when they're consecutive integers. Walk nums once: fold inter into merged[-1] via getMerged whenever it intersects, otherwise start a new [i, i] run. A second pass formats each run - a single-element run (i == j) prints as just the number, anything wider prints as "i->j".

Analysis
Time
O(2n)
  • One O(n) pass to build merged, one O(n) pass to format res - 2n. No sorting needed since nums is already sorted.
Space
O(n)
  • merged and res each hold up to n entries, when every value starts its own run.
FIG. 228 SUMMARY RANGES TWO PASS INTERACTIVE
visualization loads as you reach it
class Solution:
def summaryRanges(self, nums: List[int]) -> List[str]:
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
 
merged = []
for i in nums:
inter = [i - 1, i]
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append([i, i])
res = []
for i, j in merged:
if i == j:
res.append(str(i))
else:
res.append(f"{i}->{j}")
return res

Do any overlap

Now throw the list away and keep a boolean. Because merged only ever grows when the next interval doesn't touch the last one, "did anything overlap" is just "did the merged list come out shorter than the input" - which means you can answer it without building the list at all, bailing out the first time the sweep would have merged. With only two intervals the sweep collapses to a single disjoint test.

2446. Determine if Two Events Have Conflict

Easy·
Explanation

The overlap test is symmetric - doesIntersect(a, b) is a[1] >= b[0] and b[1] >= a[0] - so no swapping or fixed ordering is needed. toMinutes converts each "HH:MM" bound into a single comparable integer; mapping it over event1 and event2 turns them into [start, end] integer pairs a and b, which doesIntersect compares directly. Using >= rather than > means touching endpoints (one event ending exactly when the other starts) count as a conflict.

Analysis
Time
O(1)
  • Every event has exactly two fixed-length "HH:MM" strings - parsing and comparing them is a constant number of operations regardless of input.
Space
O(1)
  • Only two fixed-size, 2-element lists (a, b) are held at once.
FIG. 2446 DETERMINE IF TWO EVENTS HAVE CONFLICT INTERACTIVE
visualization loads as you reach it
class Solution:
def haveConflict(self, event1: List[str], event2: List[str]) -> bool:
def toMinutes(hhmm):
hh, mm = map(int, hhmm.split(":"))
return hh * 60 + mm
 
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
 
a = list(map(toMinutes, event1))
b = list(map(toMinutes, event2))
return doesIntersect(a, b)

252. Meeting Rooms

Easy·
3 Approachesclick to switch
Explanation

Sort by start, then merge exactly like [[56. Merge Intervals]], but with a strict overlap test - doesIntersectStrictly(a, b) = a[1] > b[0] and b[1] > a[0] - so meetings that merely touch at an endpoint ([1,5] then [5,10]) don't count as conflicting. If inter intersects merged[-1], fold it in via getMerged; otherwise inter starts a new run and gets appended as-is. If no two meetings ever overlapped, every interval became its own run, so merged ends up exactly as long as intervals. Any overlap collapses at least two entries into one, shrinking merged - comparing the two lengths is enough to answer.

Analysis
Time
O(n + n log n)
  • A single O(n) merge scan, plus O(n log n) to sort first - n + n log n.
Space
O(sort + n)
  • merged holds up to n intervals in the worst case, when nothing overlaps at all, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 252 MEETING ROOMS MERGE INTERACTIVE
visualization loads as you reach it
class Solution:
def canAttendMeetings(self, intervals: List[List[int]]) -> bool:
intervals.sort()
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersectStrictly = lambda a, b: (a[1] > b[0] and b[1] > a[0])
 
merged = []
for inter in intervals:
if merged and doesIntersectStrictly(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
return len(merged) == len(intervals)

729. My Calendar I

Medium·
Explanation

self.intervals is kept sorted and disjoint at all times, so a booking is accepted exactly when it fits into one of the gaps between consecutive stored intervals. The [-1, -1] sentinel at the front means every real interval has a left neighbour, so the scan can start at i = 1 and compare the pair (intervals[i-1], intervals[i]) without a special case for the front. Bookings are half-open - [10, 20) and [20, 30) do not conflict - which is why the gap test is the inclusive isInGap (>=, <=) and the overlap test is the strict doesIntersectStrictly (>). Walking left to right, the first gap that swallows newInterval wins and it is spliced in there; if instead newInterval strictly overlaps intervals[i], the booking is rejected. Reaching the end of the loop means it sits past every stored interval, so it is appended.

Analysis
Time
O(2n)
  • book scans up to n stored intervals, and the accepting insert shifts up to n of them - two linear passes over the list per call.
  • n is the number of bookings already accepted, so n calls cost O(n^2) overall.
Space
O(n)
  • self.intervals holds one entry per accepted booking, plus the sentinel.
FIG. 729 MY CALENDAR I INTERACTIVE
visualization loads as you reach it
class MyCalendar:
 
def __init__(self):
self.intervals = [[-1, -1]]
 
def book(self, startTime: int, endTime: int) -> bool:
doesIntersectStrictly = lambda a, b: (a[1] > b[0] and b[1] > a[0])
isInGap = lambda left, right, a: a[0] >= left[1] and a[1] <= right[0]
newInterval = [startTime, endTime]
 
for i in range(1, len(self.intervals)):
if isInGap(self.intervals[i - 1], self.intervals[i], newInterval):
self.intervals.insert(i, newInterval)
return True
if doesIntersectStrictly(self.intervals[i], newInterval):
return False
self.intervals.append(newInterval)
return True
 
 
# Your MyCalendar object will be instantiated and called as such:
# obj = MyCalendar()
# param_1 = obj.book(startTime,endTime)

Count the merged groups

A step further than a plain overlap flag: instead of stopping at the first merge, run the whole sweep and count how many disjoint groups merged ends up with. Each group is independent of every other, so a question like "how many ways can you 2-color the ranges so no overlapping pair shares a color" is just 2 raised to that group count.

2580. Count Ways to Group Overlapping Ranges

Medium·
2 Approachesclick to switch
Explanation

Run the same merge sweep as Merge Intervals: sort by start, and for each inter, merge it into merged[-1] when it intersects, otherwise append it as a new run. Every group in merged is independent of every other group, so each one can go into group 1 or group 2 on its own - 2^len(merged) total assignments, taken mod 1e9 + 7.

Analysis
Time
O(n + n log n)
  • A single O(n) merge scan, plus O(n log n) to sort first - n + n log n.
Space
O(sort + n)
  • merged holds up to n intervals in the worst case, when nothing overlaps at all, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 2580 COUNT WAYS TO GROUP OVERLAPPING RANGES INTERACTIVE
visualization loads as you reach it
class Solution:
def countWays(self, ranges: List[List[int]]) -> int:
ranges.sort()
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
merged = []
for inter in ranges:
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
x = len(merged)
return (2**x) % ((10**9) + 7)

How much is covered

Same idea, but the measurement is a number rather than a flag: the union's total length, how many integer points it contains, or whether it swallows a given target range. As above, nothing is returned, so each of these has a version that never allocates a list and just accumulates while merging.

495. Teemo Attacking

Easy·
3 Approachesclick to switch
Explanation

Each attack at time i poisons the inclusive window [i, i + duration - 1] - the same shape as Merge Intervals, just with the interval computed on the fly instead of given directly, using the same doesIntersect/getMerged helpers. If the next attack's window intersects the currently open one, fold it in via getMerged; otherwise it opens a new, disjoint window. Once merged holds only disjoint windows, a second pass sums each window's length with intervalSize(a) = a[1] - a[0] + 1.

Analysis
Time
O(2n)
  • Two full linear passes: one to merge (O(n)), one to sum the merged windows (O(n)) - 2n. No sort needed at all - timeSeries is already non-decreasing, unlike the general interval-merge problems this technique is borrowed from.
Space
O(n)
  • merged holds up to n disjoint windows in the worst case, when no two attacks' windows touch.
FIG. 495 TEEMO ATTACKING MERGE THEN SUM INTERACTIVE
visualization loads as you reach it
class Solution:
def findPoisonedDuration(self, timeSeries: List[int], duration: int) -> int:
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
intervalSize = lambda a: a[1] - a[0] + 1
 
merged = []
ans = 0
for i in timeSeries:
inter = [i, i + duration - 1]
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
for inter in merged:
ans += intervalSize(inter)
return ans

2848. Points That Intersect With Cars

Easy·
3 Approachesclick to switch
Explanation

First merge overlapping intervals exactly as in Merge Intervals: sort by start, then fold inter into merged[-1] via getMerged whenever doesIntersect says the two overlap, otherwise append it as a new run. Once merged holds only disjoint runs, a second pass adds up how many integer points each run covers - j - i + 1, since both endpoints are inclusive.

Analysis
Time
O(2n + n log n)
  • Two separate linear passes - one to merge (O(n)), one to sum the merged runs (O(n)) - plus O(n log n) to sort first: 2n + n log n.
Space
O(sort + n)
  • merged holds up to n disjoint intervals in the worst case, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 2848 POINTS THAT INTERSECT WITH CARS MERGE THEN SUM INTERACTIVE
visualization loads as you reach it
class Solution:
def numberOfPoints(self, nums: List[List[int]]) -> int:
nums.sort()
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: a[1] >= b[0] and b[1] >= a[0]
 
merged = []
for inter in nums:
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
res = 0
for i, j in merged:
res += j - i + 1
return res

1893. Check if All the Integers in a Range Are Covered

Easy·
5 Approachesclick to switch
Explanation

Sort ranges and merge overlapping ones via doesIntersect/getMerged exactly as in [[56. Merge Intervals]], except each inter is first widened to [inter[0]-1, inter[1]] so that touching ranges (e.g. [1,2] and [3,4]) merge into one run instead of staying separate - the -1 closes the one-unit gap between consecutive integer ranges. Once merged holds only disjoint (widened) runs, a second pass checks each one: shifting back to [i, j] = [inter[0]+1, inter[1]] undoes the widening, and [left, right] is covered exactly when some run's [i, j] contains it entirely - left >= i and right <= j.

Analysis
Time
O(2n + n log n)
  • Two separate linear passes - one to merge (O(n)), one to check every merged run for containment (O(n)) - plus O(n log n) to sort first: 2n + n log n.
Space
O(sort + n)
  • merged holds up to n disjoint runs in the worst case, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 1893 CHECK IF ALL THE INTEGERS IN A RANGE ARE COVERED MERGE THEN CHECK INTERACTIVE
visualization loads as you reach it
class Solution:
def isCovered(self, ranges: List[List[int]], left: int, right: int) -> bool:
ranges.sort()
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: a[1] >= b[0] and b[1] >= a[0]
merged = []
 
for inter in ranges:
inter = [inter[0] - 1, inter[1]]
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
for inter in merged:
i, j = inter[0] + 1, inter[1]
if left >= i or right <= j:
if left >= i and right <= j:
return True
return False

452. Minimum Number of Arrows to Burst Balloons

Medium·
2 Approachesclick to switch
Explanation

Sort the balloons by start so overlapping runs sit next to each other. Two lambdas name the two checks: doesIntersect(a, b) tests overlap (a[1] >= b[0] and b[1] >= a[0]), and getIntersection(a, b) shrinks the overlap window to (max of starts, min of ends). For each inter, if it overlaps the last entry in intersection, one arrow already covers both - narrow that entry's window instead of adding a new one. Otherwise inter needs its own arrow - append it. The final arrow count is len(intersection).

Analysis
Time
O(n + n log n)
  • A single O(n) scan over the sorted points, plus O(n log n) to sort first - n + n log n.
Space
O(sort + n)
  • intersection holds up to n windows in the worst case, when no balloons overlap at all, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 452 MINIMUM NUMBER OF ARROWS TO BURST BALLOONS INTERACTIVE
visualization loads as you reach it
class Solution:
def findMinArrowShots(self, points: List[List[int]]) -> int:
points.sort()
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
getIntersection = lambda a, b: (max(a[0], b[0]), min(a[1], b[1]))
 
intersection = []
for inter in points:
if intersection and doesIntersect(intersection[-1], inter):
intersection[-1] = getIntersection(intersection[-1], inter)
else:
intersection.append(inter)
return len(intersection)

435. Non-overlapping Intervals

Medium·
2 Approachesclick to switch
Explanation

Sort by start, then sweep. doesIntersectStrictly(a, b) treats touching endpoints as fine - only a true overlap (a[1] > b[0] and b[1] > a[0]) counts. When inter overlaps the last kept interval, one of the two has to be removed - shrink the kept slot to getIntersection(intersection[-1], inter) (the narrower of the two end points, min(a[1], b[1]), survives), since that leaves the most room for what comes next, and bump count. Otherwise inter is disjoint from everything kept so far - append it. count ends up as the minimum number of removals.

Analysis
Time
O(n + n log n)
  • A single O(n) scan over the sorted intervals, plus O(n log n) to sort first - n + n log n.
Space
O(sort + n)
  • intersection holds up to n kept intervals in the worst case, when nothing overlaps at all, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 435 NON OVERLAPPING INTERVALS INTERACTIVE
visualization loads as you reach it
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort()
doesIntersectStrictly = lambda a, b: (a[1] > b[0] and b[1] > a[0])
getIntersection = lambda a, b: (max(a[0], b[0]), min(a[1], b[1]))
 
intersection = []
count = 0
for inter in intervals:
if intersection and doesIntersectStrictly(intersection[-1], inter):
intersection[-1] = getIntersection(intersection[-1], inter)
count += 1
else:
intersection.append(inter)
return count

1288. Remove Covered Intervals

Medium·
2 Approachesclick to switch
Explanation

Sort by start ascending, breaking ties by end descending (-i[1]) so that when two intervals share a start, the longer one is considered first - guaranteeing any interval that follows with the same start is trivially contained. doesContain(a, b) returns whether a fully covers b (a[0] <= b[0] and b[1] <= a[1]). Sweep the sorted intervals: if the last kept interval already contains inter, skip it (nothing to do, hence the ... no-op); otherwise append inter to merged, since it introduces coverage nothing kept so far provides. merged ends up holding only the intervals that survive, so its length is the answer.

Analysis
Time
O(n + n log n)
  • A single O(n) scan over the sorted intervals, plus O(n log n) to sort first - n + n log n.
Space
O(sort + n)
  • merged holds up to n kept intervals in the worst case, when nothing is covered, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 1288 REMOVE COVERED INTERVALS INTERACTIVE
visualization loads as you reach it
class Solution:
def removeCoveredIntervals(self, intervals: List[List[int]]) -> int:
intervals.sort(key=lambda i: (i[0], -i[1]))
count = 0
doesContain = lambda a, b: (a[0] <= b[0] and b[1] <= a[1])
merged = []
for inter in intervals:
if merged and doesContain(merged[-1], inter):
...
else:
merged.append(inter)
return len(merged)

What is not covered

The complement. Merge as usual, but report the gaps between consecutive merged intervals rather than the intervals themselves. The only real work beyond the previous group is the two boundary gaps: one before the first interval and one after the last, both clipped to whatever outer range the problem gives you.

163. Missing Ranges

Easy·
3 Approachesclick to switch
Explanation

nums is already sorted and distinct, so this reuses the [i-1, i] trick from [[228. Summary Ranges]]: each value n becomes the interval [n-1, n], and two values fall in the same covered run exactly when they're consecutive. Bracket the whole range with two sentinel points, [lower-1, lower-1] before lower and [upper, upper] after upper, so a gap at either edge of [lower, upper] is found by the exact same logic as a gap in the middle. Merge overlapping intervals via doesIntersect/getMerged exactly as in [[56. Merge Intervals]]. Once merged holds only disjoint covered runs (plus the two sentinels), a second pass checks every adjacent pair: the missing range between them is [merged[i][1]+1, merged[i+1][0]], kept only when that start doesn't exceed that end.

Analysis
Time
O(2n)
  • Two separate linear passes - one to merge (O(n)), one to scan adjacent merged pairs for gaps (O(n)) - 2n. No sort needed since nums is already sorted.
Space
O(n)
  • merged holds up to n disjoint runs plus the two sentinels, and ans holds up to as many gaps, both in the worst case.
FIG. 163 MISSING RANGES MERGE THEN FIND GAPS INTERACTIVE
visualization loads as you reach it
class Solution:
def findMissingRanges(
self, nums: List[int], lower: int, upper: int
) -> List[List[int]]:
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: a[1] >= b[0] and b[1] >= a[0]
merged = [[lower - 1, lower - 1]]
for n in nums:
inter = [n - 1, n]
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
merged.append([upper, upper])
# print(merged)
ans = []
for i in range(len(merged) - 1):
i, j = merged[i][1] + 1, merged[i + 1][0]
if i <= j:
ans.append([i, j])
return ans

2655. Find Maximal Uncovered Ranges

Medium·
3 Approachesclick to switch
Explanation

ranges covers parts of [0, n-1], so sort by start and merge overlapping ranges via doesIntersect/getMerged exactly as in [[56. Merge Intervals]]. Bracket the covered runs with two sentinel ranges, [-1, -1] before 0 and [n, n] after n-1, so an uncovered stretch at either edge of [0, n-1] is found by the exact same logic as one in the middle. Once merged holds only disjoint covered runs (plus the two sentinels), a second pass checks every adjacent pair: the uncovered range between them is [merged[i][1]+1, merged[i+1][0]-1], kept only when that start doesn't exceed that end.

Analysis
Time
O(2n + n log n)
  • Two separate linear passes - one to merge (O(n)), one to scan adjacent merged pairs for gaps (O(n)) - plus O(n log n) to sort first: 2n + n log n.
Space
O(sort + n)
  • merged holds up to n disjoint runs plus the two sentinels, and ans holds up to as many gaps, both in the worst case, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. FIND MAXIMAL UNCOVERED RANGES MERGE THEN FIND GAPS INTERACTIVE
visualization loads as you reach it
class Solution:
def findMaximalUncoveredRanges(
self, n: int, ranges: List[List[int]]
) -> List[List[int]]:
ranges.sort()
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: a[1] >= b[0] and b[1] >= a[0]
merged = [[-1, -1]]
 
for inter in ranges:
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
merged.append([n, n])
ans = []
for i in range(len(merged) - 1):
i, j = merged[i][1] + 1, merged[i + 1][0] - 1
if i <= j:
ans.append([i, j])
return ans

3169. Count Days Without Meetings

Medium·
3 Approachesclick to switch
Explanation

Bracket the whole range with two sentinel meetings, [0, 0] before day 1 and [days+1, days+1] after the last day, so the free stretch before the first real meeting and after the last one falls out of the same logic as every gap in between. Merge overlapping meetings exactly as in [[56. Merge Intervals]]: sort by start, then fold inter into merged[-1] whenever they intersect, otherwise append it as a new run. Once merged holds only disjoint runs (plus the two sentinels), a second pass sums the free days strictly between every adjacent pair - getGap(a, b), i.e. max(a0, b0) - min(a1, b1) - 1.

Analysis
Time
O(2n + n log n)
  • Two separate linear passes - one to merge (O(n)), one to sum the gaps between merged runs (O(n)) - plus O(n log n) to sort first: 2n + n log n.
Space
O(sort + n)
  • merged holds up to n disjoint intervals plus the two sentinels in the worst case, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 3169 COUNT DAYS WITHOUT MEETINGS MERGE THEN SUM INTERACTIVE
visualization loads as you reach it
class Solution:
def countDays(self, days: int, meetings: List[List[int]]) -> int:
meetings.sort()
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
getGap = lambda a, b: (max(a[0], b[0]) - min(a[1], b[1]) - 1)
 
merged = [[0, 0]]
for inter in meetings:
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
merged.append([days + 1, days + 1])
no_meeting_days = 0
for i in range(len(merged) - 1):
no_meeting_days += getGap(merged[i], merged[i + 1])
return no_meeting_days

3964. Minimum Lights to Illuminate a Road

Medium·
4 Approachesclick to switch
Explanation

Every working light at index i with range v illuminates [i - v, i + v], clipped to the road [0, n-1]; a sentinel [n, n] is appended so the sweep always closes out the final gap. doesIntersect checks whether an interval touches the last merged span, and getMerged extends that span when it does. When the next interval is disjoint, getGap measures the dark stretch between the last merged span and it; a single new light of range 1 covers 3 positions, so (gap + 2) // 3 new lights are needed to close it before the sweep starts a fresh merged span.

Analysis
Time
O(n + n log n)
  • Building intervals is a single O(n) pass over lights, plus O(n log n) to sort them, plus another O(n) sweep to merge and count gaps - n + n log n after collapsing the two same-order n passes into 2n, written here alongside the sort term.
Space
O(sort + n)
  • intervals and merged each hold up to n + 1 entries in the worst case, on top of the sort's own working memory.
  • Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 3964 MINIMUM LIGHTS TO ILLUMINATE A ROAD INTERACTIVE
visualization loads as you reach it
class Solution:
def minLights(self, lights: list[int]) -> int:
intervals = []
n = len(lights)
for i, v in enumerate(lights):
if v:
intervals.append([max(0, i - v), min(n - 1, i + v)])
intervals.append([n, n])
intervals.sort()
 
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
getGap = lambda a, b: max(a[0], b[0]) - min(a[1], b[1])
 
merged = [[-1, -1]]
ans = 0
for inter in intervals:
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
gap = getGap(merged[-1], inter) - 1
ans += (gap + 2) // 3
merged.append(inter)
return ans

759. Employee Free Time

Hard·
4 Approachesclick to switch
Explanation

Flatten every employee's schedule into one list of (start, end) tuples and sort it into busy. Merge overlapping intervals via doesIntersect/getMerged exactly as in [[56. Merge Intervals]]. Once merged holds only the disjoint busy runs across the whole company, a second pass walks every adjacent pair and reports the gap between them as an Interval via getGapInterval.

Analysis
Time
O(2n + n log n)
  • busy is built and sorted over all n intervals across every employee - O(n log n).
  • One linear pass merges busy into merged (O(n)), and a second linear pass over merged reports the gaps (O(n)) - two separate linear passes: 2n + n log n.
Space
O(sort + n)
  • busy and merged each hold up to n intervals in the worst case, on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort()/sorted() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. EMPLOYEE FREE TIME MERGE THEN FIND GAPS INTERACTIVE
visualization loads as you reach it
class Solution:
def employeeFreeTime(self, schedule: "[[Interval]]") -> "[Interval]":
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
getGapInterval = lambda a, b: Interval(min(a[1], b[1]), max(a[0], b[0]))
 
busy = sorted((sch.start, sch.end) for emp in schedule for sch in emp)
merged = []
for inter in busy:
if merged and doesIntersect(merged[-1], inter):
merged[-1] = getMerged(merged[-1], inter)
else:
merged.append(inter)
ans = []
for i in range(len(merged) - 1):
ans.append(getGapInterval(merged[i], merged[i + 1]))
return ans

Peak concurrent overlap

Not merge-based at all: turning each interval into a +1 at its start and a -1 at its end and sweeping the sorted events gives a running count of how many intervals are active at any instant. The maximum that count ever reaches is the answer whenever the question is "how many things overlap at once", rather than "do any overlap" or "what's covered".

253. Meeting Rooms II

Medium·
3 Approachesclick to switch
Explanation

Instead of merging intervals, turn each one into two timestamped events - (start, +1) and (end, -1) - and sort all 2n of them by point. Sweeping left to right and accumulating active tracks exactly how many meetings are in progress at that instant; because a (point, -1) sorts before a (point, +1) at the same tick, a meeting ending at 5 frees its room before one starting at 5 claims it, so touching meetings never double-count. maxi is the largest active ever reaches, which is the fewest rooms that can cover every meeting simultaneously.

Analysis
Time
O(2n + n log n)
  • Building the 2n events and the final scan over them are each a single O(n) pass, which combine into 2n - plus O(n log n) to sort the events.
Space
O(sort + n)
  • events holds 2n tuples, which is O(n), on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 253 MEETING ROOMS II INTERACTIVE
visualization loads as you reach it
class Solution:
def minMeetingRooms(self, intervals: List[List[int]]) -> int:
def streamEvents(intervals):
events = []
for start, end in intervals:
events.extend([(start, 1), (end, -1)])
return sorted(events)
 
active = maxi = 0
for point, curr in streamEvents(intervals):
active += curr
maxi = max(maxi, active)
return maxi

1094. Car Pooling

Medium·
3 Approachesclick to switch
Explanation

Same sweep as [[253. Meeting Rooms II]], but the delta carried by each event is the passenger count instead of a flat ±1: every trip becomes a (start, +num) and an (end, -num). Sweeping the sorted events and accumulating active tracks exactly how many passengers are in the car at that instant - a passenger's seat frees up at their drop-off point before a new pickup at that same point claims it, since -num sorts before +num at equal points. The moment active exceeds capacity the car is already overfull, so return False immediately rather than waiting to find a peak.

Analysis
Time
O(2n + n log n)
  • Building the 2n events and the final scan over them are each a single O(n) pass, which combine into 2n - plus O(n log n) to sort the events.
Space
O(sort + n)
  • events holds 2n tuples, which is O(n), on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 1094 CAR POOLING SWEEP INTERACTIVE
visualization loads as you reach it
class Solution:
def carPooling(self, trips: List[List[int]], capacity: int) -> bool:
def streamEvents(intervals):
events = []
for num, start, end in intervals:
events.extend([(start, num), (end, -num)])
return sorted(events)
 
active = 0
for point, curr in streamEvents(trips):
active += curr
if active > capacity:
return False
return True

2406. Divide Intervals Into Minimum Number of Groups

Medium·
2 Approachesclick to switch
Explanation

Same sweep as [[253. Meeting Rooms II]], but each interval here is inclusive on both ends, so two intervals that only touch ([1, 3] and [3, 5]) still intersect at the shared point 3 and need separate groups. That's why the end event fires at b+1 rather than b: pushing the -1 one point past the interval's own end keeps it active through its last point instead of releasing early at a tick another interval might still be occupying. Sweeping the sorted events and accumulating active then tracks how many intervals are simultaneously open at that instant, and the largest active ever reaches is the minimum number of groups needed.

Analysis
Time
O(2n + n log n)
  • Building the 2n events and the final scan over them are each a single O(n) pass, which combine into 2n - plus O(n log n) to sort the events.
Space
O(sort + n)
  • line holds 2n pairs, which is O(n), on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 2406 DIVIDE INTERVALS SWEEP INTERACTIVE
visualization loads as you reach it
class Solution:
def minGroups(self, intervals: List[List[int]]) -> int:
def streamEvents(intervals):
line = []
for a, b in intervals:
line.extend([[a, 1], [b + 1, -1]])
return sorted(line)
 
active = maxi = 0
for point, curr in streamEvents(intervals):
active += curr
maxi = max(maxi, active)
return maxi

1419. Minimum Number of Frogs Croaking

Medium·
2 Approachesclick to switch
Explanation

Each 'c' opens a new frog's croak and each 'k' closes one, so buckets[idx] records +1/-1 at the index where that happens and active accumulates it into a running count of frogs mid-croak - maxi is the largest that count ever reaches. Alongside that, counter tallies how many of each letter have been seen, and the string is only a valid sequence of croaks if those tallies stay non-increasing in the order c >= r >= o >= a >= k at every prefix - a 'r' can't appear before its 'c', an 'o' before its 'r', and so on. Any violation returns -1 immediately, and a mismatched final c/k count (a croak left open) also returns -1.

Analysis
Time
O(n)
  • A single pass over croakOfFrogs, doing O(1) work per character, where n = len(croakOfFrogs).
Space
O(n)
  • buckets is allocated to length n even though only the current index is ever read or written.
  • counter holds at most the 5 fixed keys of "croak", independent of n.
FIG. 1419 MIN FROGS BUCKETS INTERACTIVE
visualization loads as you reach it
class Solution:
def minNumberOfFrogs(self, croakOfFrogs: str) -> int:
buckets = [0] * (len(croakOfFrogs))
counter = collections.defaultdict(int)
croak = "croak"
maxi = active = 0
for idx, char in enumerate(croakOfFrogs):
if char == "c":
buckets[idx] += 1
elif char == "k":
buckets[idx] -= 1
active += buckets[idx]
counter[char] += 1
maxi = max(maxi, active)
if not (
counter["c"]
>= counter["r"]
>= counter["o"]
>= counter["a"]
>= counter["k"]
):
return -1
return maxi if counter["c"] == counter["k"] else -1

1854. Maximum Population Year

Easy·
2 Approachesclick to switch
Explanation

Same sweep as [[253. Meeting Rooms II]]: every [start, end) log becomes a (start, +1) and an (end, -1), and sweeping the sorted events tracks how many people are alive at that instant. Sorting the raw tuples puts a tie's -1 before its 1 at the same year, so a death is processed before a birth landing on the same year - the running active never briefly double-counts. maxi starts at (-1, 1930) below any possible population, and is only replaced the moment active beats the current best, so the first year to reach the peak wins ties.

Analysis
Time
O(2n + n log n)
  • Building the 2n events and the final scan over them are each a single O(n) pass, which combine into 2n - plus O(n log n) to sort the events.
Space
O(sort + n)
  • line holds 2n pairs, which is O(n), on top of the sort's own working memory.
  • Sorting algorithms are typically O(log n) space (in-place, recursion-stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 1854 MAXIMUM POPULATION YEAR SWEEP INTERACTIVE
visualization loads as you reach it
class Solution:
def maximumPopulation(self, logs: List[List[int]]) -> int:
def streamEvents(intervals):
line = []
for start, end in intervals:
line.extend([[start, 1], [end, -1]])
return sorted(line, key=lambda i: i)
 
maxi = [-1, 1930]
active = 0
for point, curr in streamEvents(logs):
active += curr
# print(point, curr)
if maxi[0] < active:
maxi = (active, point)
return maxi[1]

370. Range Addition

Medium·
2 Approachesclick to switch
Explanation

Same sweep as [[253. Meeting Rooms II]], but instead of a flat ±1 the delta is each update's own weight, and instead of one running peak the running active value has to be written into every position of the output, not just checked at a point. Every update becomes a (start, +weight) and an (end+1, -weight) (the +1 makes the range inclusive of end), and the sorted events are swept in order. Between two consecutive event points the value never changes, so before applying the next event's delta, res[prev:point+1] is backfilled with the active value that was already in force across that whole flat stretch; only after that does active pick up curr and res[point] get overwritten with the new value that starts exactly there.

Analysis
Time
O(2n + n log n + length)
  • Building the 2n events and sweeping all of them is one O(n) pass each - 2n combined - plus O(n log n) to sort the events.
  • The inner for loop never revisits an index: across the whole sweep it backfills every position of res from 0 to length exactly once, an O(length) pass on top.
Space
O(sort + n + length)
  • line holds 2n pairs, O(n).
  • res is length + 1 elements, O(length).
  • Sorting algorithms are typically O(log n) space (in-place, recursion-stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 370 RANGE ADDITION SWEEP INTERACTIVE
visualization loads as you reach it
class Solution:
def getModifiedArray(self, length: int, updates: List[List[int]]) -> List[int]:
def streamEvents(intervals):
line = []
for start, end, weight in intervals:
line.extend([[start, weight], [end + 1, -weight]])
return sorted(line)
 
res = [0] * (length + 1)
prev = None
active = 0
for point, curr in streamEvents(updates):
start = point if prev is None else prev
for i in range(start, point + 1):
res[i] = active
active += curr
res[point] = active
prev = point
return res[:length]

2381. Shifting Letters II

Medium·
Explanation

Same difference array as [[370. Range Addition]], but the delta at each index is a shift direction instead of an arbitrary weight: direction is normalized to -1/+1, added at start and subtracted at end+1. A single left-to-right scan over buckets then accumulates active into the net number of forward shifts every character has accumulated by that index - every shift whose range covers point and hasn't ended yet contributes its direction. Each original character is looked up in alphabet_map, shifted by active positions, and wrapped with %26 before being appended to ans.

Analysis
Time
O(4n + m + 26)
  • Allocating buckets is one O(n) pass; slicing buckets[:-1] copies another O(n); the scan over that slice is a third O(n) pass; "".join(ans) is a fourth O(n) pass - 4n combined, where n = len(s).
  • Building buckets from the shifts is a separate O(m) pass, where m = len(shifts).
  • alphabet_map is built once from the 26-character alphabet, O(26), independent of both n and m.
Space
O(2n + 26)
  • buckets and ans are each O(n).
  • alphabet_map holds exactly the 26 letters of the alphabet, O(26), independent of n.
FIG. 2381 SHIFTING LETTERS II INTERACTIVE
visualization loads as you reach it
class Solution:
def shiftingLetters(self, s: str, shifts: List[List[int]]) -> str:
buckets = [0] * (len(s) + 1)
for start, end, direction in shifts:
direction = -1 if direction == 0 else 1
buckets[start] += direction
buckets[end + 1] -= direction
ans = []
alphabets = "abcdefghijklmnopqrstuvwxyz"
alphabet_map = {j: i for i, j in enumerate(alphabets)}
active = 0
for point, curr in enumerate(buckets[:-1]):
active += curr
char = alphabets[(alphabet_map[s[point]] + active) % 26]
ans.append(char)
return "".join(ans)

2237. Count Positions on Street With Required Brightness

Medium·
Explanation

Same fixed-size difference array as [[370. Range Addition]]: each light illuminates [center - range, center + range], clipped to the street's own bounds [0, n-1] first, and that clipped interval becomes a +1 at start and a -1 at end+1 in buckets. A single left-to-right scan then accumulates active into the actual brightness at every position, and a position counts the moment its brightness meets or beats requirement[point].

Analysis
Time
O(3n + m)
  • Allocating buckets is one O(n) pass; slicing buckets[:-2] copies another O(n); the accumulating scan over that slice is a third O(n) pass - 3n combined.
  • Building buckets from the lights is a separate O(m) pass, where m = len(lights).
Space
O(n)
  • buckets is allocated to n + 2 elements, independent of m.
FIG. 2237 COUNT POSITIONS ON STREET WITH REQUIRED BRIGHTNESS INTERACTIVE
visualization loads as you reach it
class Solution:
def meetRequirement(
self, n: int, lights: List[List[int]], requirement: List[int]
) -> int:
buckets = [0] * (n + 2)
for center, rang in lights:
start = max(0, center - rang)
end = min(n - 1, center + rang)
buckets[start] += 1
buckets[end + 1] -= 1
active = 0
count = 0
for point, curr in enumerate(buckets[:-2]):
active += curr
if active >= requirement[point]:
count += 1
return count

2779. Maximum Beauty of an Array After Applying Operation

Medium·
Explanation

Every i in nums can be replaced by anything in [i-k, i+k], so streamEvents turns each element into a +1 event at i-k and a -1 event at i+k+1 - the interval of values it could become, made half-open so the endpoint itself still counts. Sweeping the sorted events, active is the number of elements whose ranges cover the current point at once, and the highest active ever gets is the largest group that can all be turned into the same value.

Analysis
Time
O(2n + n log n)
  • streamEvents builds 2n events from the n elements of nums, then sorts them - O(n log n).
  • The sweep loop then walks all 2n events once - a second linear pass over n: 2n + n log n.
Space
O(sort + n)
  • streamEvents builds a line of 2n events before sorting it - O(n) - on top of the sort's own working memory.
FIG. MAXIMUM BEAUTY OF AN ARRAY AFTER APPLYING OPERATION INTERACTIVE
visualization loads as you reach it
class Solution:
def maximumBeauty(self, nums: List[int], k: int) -> int:
def streamEvents(interval):
line = []
for i in interval:
line.extend([[i - k, 1], [i + k + 1, -1]])
return sorted(line)
 
active = 0
maxi = 0
for point, curr in streamEvents(nums):
active += curr
maxi = max(maxi, active)
return maxi

Weighted concurrent sum

Same +1/-1 sweep, but each event carries a weight instead of a flat unit, so the running total is a sum rather than a count - and every distinct stretch between consecutive event points, not just the peak, is reported alongside whatever that sum was during it.

1943. Describe the Painting

Medium·
Explanation

streamEvents turns every [start, end, weight] segment into a +weight event at its start and a -weight event at its end, sorted into one timeline - the running total is now a color sum rather than a plain overlap count. Sweeping that timeline: whenever the point moves (point != prev) and the sum so far (active) is non-zero, [prev, point, active] is emitted as one mixed segment before prev advances to point and active picks up the new event's weight.

Analysis
Time
O(2n + n log n)
  • streamEvents builds 2n events from the n segments, then sorts them - O(n log n).
  • The sweep loop then walks all 2n events once - a second linear pass over n: 2n + n log n.
Space
O(sort + n)
  • streamEvents builds a line of 2n events before sorting it - O(n) - on top of the sort's own working memory.
FIG. DESCRIBE THE PAINTING INTERACTIVE
visualization loads as you reach it
class Solution:
def splitPainting(self, segments: List[List[int]]) -> List[List[int]]:
def streamEvents(intervals):
line = []
for start, end, weight in intervals:
line.extend([[start, weight], [end, -weight]])
return sorted(line)
 
ans = []
active = 0
prev = None
for point, curr in streamEvents(segments):
if prev and point != prev and active:
ans.append([prev, point, active])
prev = point
active += curr
return ans

Concurrent count at query points

Same +1/-1 sweep as above, but instead of the single overall peak, the running count is needed at several specific instants - one per query point. Sort the queries too, and drain every query that falls before the current event point as the sweep passes it, recording whatever active is at that moment.

2251. Number of Flowers in Full Bloom

Hard·
Explanation

Same +1/-1 sweep as [[253. Meeting Rooms II]], but instead of tracking a single peak, the running active count is needed at several specific instants - each person's arrival time. Turn every flower into (start, +1) and (end+1, -1) (the +1 on the end makes the interval inclusive), sort those 2n events, and sort people too so both walks move only forward. Before each event's point advances active, drain every still-pending person (prev <= people_sorted[finder] < point) and record the current active as their answer - that person arrived before this event changed anything, so the count is still accurate for them. finder never resets, so each person is visited exactly once across the whole sweep.

Analysis
Time
O(2n + 2m + n log n + m log m)
  • Building the 2n events is one O(n) pass; sweeping all of them is another O(n) pass - 2n combined.
  • Every person is drained by the inner while exactly once across the whole sweep, and the final list comprehension is another single pass over people - 2m combined.
  • Sorting the 2n events costs O(n log n), and sorting people costs O(m log m).
Space
O(sort + n + m)
  • The events list built inside streamEvents holds 2n tuples, O(n).
  • people_sorted and ans are each O(m).
  • Sorting algorithms are typically O(log n) space (in-place, recursion-stack only), but Python's list.sort() is Timsort, which allocates up to O(n) auxiliary space in the worst case - that's what sort stands for here.
FIG. 2251 NUMBER OF FLOWERS IN FULL BLOOM INTERACTIVE
visualization loads as you reach it
class Solution:
def fullBloomFlowers(
self, flowers: List[List[int]], people: List[int]
) -> List[int]:
def streamEvents(interval):
events = []
for start, end in flowers:
events.extend([[start, 1], [end + 1, -1]])
return sorted(events)
 
people_sorted = sorted(people)
active = 0
prev = 0
ans = {}
finder = 0
for point, curr in streamEvents(flowers):
while finder < len(people_sorted) and prev <= people_sorted[finder] < point:
ans[people_sorted[finder]] = active
finder += 1
active += curr
prev = point
return [ans.get(i, 0) for i in people]

Two sorted lists

The one group that abandons the sweep. When the input arrives as two already-sorted lists there is nothing to sort and nothing to merge - you walk both with two pointers, emit the shared stretch of the current pair if there is one, and advance whichever interval ends first, because it can never intersect anything later.

986. Interval List Intersections

Medium·
Explanation

Both lists are already sorted and disjoint within themselves, so a single two-pointer sweep is enough - no sorting needed. At each step, look at a = firstList[i] and b = secondList[j]: if they overlap, their intersection is [max(a[0], b[0]), min(a[1], b[1])]. Whichever of the two intervals ends first can never overlap anything further in the other list (everything past it starts later), so its pointer is the one that advances - i if a ends first, otherwise j.

Analysis
Time
O(n + m)
  • Each step advances i or j by one, and the loop stops once either pointer runs out - at most n + m total advances across both lists.
Space
O(k)
  • common holds the k intersections found; no extra structure scales with the input beyond the output itself.
FIG. 986 INTERVAL LIST INTERSECTIONS INTERACTIVE
visualization loads as you reach it
class Solution:
def intervalIntersection(
self, firstList: List[List[int]], secondList: List[List[int]]
) -> List[List[int]]:
common = []
getMerged = lambda a, b: (min(a[0], b[0]), max(a[1], b[1]))
getIntersection = lambda a, b: (max(a[0], b[0]), min(a[1], b[1]))
doesIntersect = lambda a, b: (a[1] >= b[0] and b[1] >= a[0])
i = j = 0
while i < len(firstList) and j < len(secondList):
a, b = firstList[i], secondList[j]
if doesIntersect(a, b):
inter = getIntersection(a, b)
common.append(inter)
if a[1] < b[1]:
i += 1
else:
j += 1
return common