Sort the intervals by start value first — that single step guarantees any overlap only happens with the immediately preceding merged interval, enabling a clean O(n) sweep instead of an O(n²) nested comparison.
Given an array of intervals [[start, end], …], merge all overlapping intervals and return the result. Two intervals overlap when one starts at or before the other ends.
Example: [[1,3],[2,6],[8,10],[15,18]] → [[1,6],[8,10],[15,18]] because [1,3] and [2,6]overlap (2 ≤ 3), so they fuse into [1,6]. The remaining two pairs have gaps, so they stay separate.
current.start against last.end. One forward pass, no backtracking.intervals.sort((a, b) => a[0] - b[0]). This is the enabling step — overlaps become local (adjacent).[start, end]:start <= last.end — overlap: update last.end = max(last.end, end). Use maxbecause a fully-contained interval wouldn't extend the end.[start, end] as a new entry.Math.max when extending. If the new interval is fully contained within the last merged one (e.g. [1,10] followed by [2,5]), a naive last.end = end would incorrectly shrink the result to [1,5].The sweep itself is O(n) — each interval is pushed or merged exactly once. Total is O(n log n) for the sort. In-place mutation of the input is possible but messy; the idiomatic approach uses a separate output array.
[1,3], [2,6], [8,10], [15,18]. First we sort by start value — that is the key step that makes a single sweep possible.1▶function merge(intervals: number[][]): number[][] {2 // Sort by start so any overlap must be with the immediately previous merged interval3 intervals.sort((a, b) => a[0] - b[0]);45▶ const merged: number[][] = [];67 for (const [start, end] of intervals) {8 if (merged.length === 0) {9 merged.push([start, end]);10 continue;11 }1213 const last = merged[merged.length - 1];1415 if (start <= last[1]) {16 // Overlapping: extend the end if needed17 last[1] = Math.max(last[1], end);18 } else {19 // Gap: start a fresh interval20 merged.push([start, end]);21 }22 }2324 return merged;25}
function merge(intervals: number[][]): number[][] {
// Sort by start so any overlap must be with the immediately previous merged interval
intervals.sort((a, b) => a[0] - b[0]);
const merged: number[][] = [];
for (const [start, end] of intervals) {
if (merged.length === 0) {
merged.push([start, end]);
continue;
}
const last = merged[merged.length - 1];
if (start <= last[1]) {
// Overlapping: extend the end if needed
last[1] = Math.max(last[1], end);
} else {
// Gap: start a fresh interval
merged.push([start, end]);
}
}
return merged;
}[start, end]must be the one immediately before it in the sorted list. A later interval can't start earlier, so it can't retroactively extend an already-closed gap.[start, end] unconditionally and continue to the next.start <= last[1] means the current interval begins inside or at the boundary of the last merged one. Extend the end with Math.max(last[1], end) — the max handles full containment (e.g. [1,10] swallowing [2,5]).start > last[1]means there's a real gap. The previous merged interval is now closed forever; push a fresh one.function merge(intervals: number[][]): number[][] {
// Sort by start so any overlap must be with the immediately previous merged interval
intervals.sort((a, b) => a[0] - b[0]);
const merged: number[][] = [];
for (const [start, end] of intervals) {
if (merged.length === 0) {
merged.push([start, end]);
continue;
}
const last = merged[merged.length - 1];
if (start <= last[1]) {
// Overlapping: extend the end if needed
last[1] = Math.max(last[1], end);
} else {
// Gap: start a fresh interval
merged.push([start, end]);
}
}
return merged;
}No sorting: keep scanning the list for any two intervals that overlap, merge them into one, and restart — repeat until a full pass finds nothing left to combine.
function merge(intervals: number[][]): number[][] {
// Work on a copy so we do not mutate the caller's array.
let result = intervals.map(([s, e]) => [s, e]);
let merged = true;
while (merged) {
merged = false;
outer: for (let i = 0; i < result.length; i++) {
for (let j = i + 1; j < result.length; j++) {
const [s1, e1] = result[i];
const [s2, e2] = result[j];
// Overlap iff each starts no later than the other ends.
if (s1 <= e2 && s2 <= e1) {
// Fuse j into i, then drop j and rescan from scratch.
result[i] = [Math.min(s1, s2), Math.max(e1, e2)];
result.splice(j, 1);
merged = true;
break outer;
}
}
}
}
return result;
}O(n²) scan and there can be O(n) fuses, so this is cubic in the worst case. Sorting by start time first guarantees overlaps are adjacent, collapsing it to a single O(n log n) pass.| "merge overlapping intervals" | sort by start → sweep last.end |
| start ≤ last end | overlap: extend last[1] = max(last[1], end) |
| start > last end | gap: push new interval |
| "insert/remove/count intervals" | same sort-by-start skeleton |
function merge(intervals: number[][]): number[][] {
intervals.sort((a, b) => a[0] - b[0]); // sort by start
const merged: number[][] = [];
for (const [start, end] of intervals) {
const last = merged[merged.length - 1];
if (!last || start > last[1]) {
merged.push([start, end]); // no overlap: new interval
} else {
last[1] = Math.max(last[1], end); // overlap: extend end
}
}
return merged;
}current.start against last.end. What does current.start <= last.end mean?[1,10] and the next sorted interval is [3,5]. What should the merged list contain after processing [3,5]?[[1,4],[4,5]]. What is the output?