Practice Lab

Merge Intervals

Merge Intervals is a sorting and scanning problem. Given a list of intervals, the goal is to combine every overlapping range into one clean range.

The pattern is simple: sort intervals by start time, then keep comparing the current interval with the last interval already added to the result.

Problem

Given an array of intervals where intervals[i] = [start, end], merge all overlapping intervals and return the non-overlapping result.

Example:

intervals = [[1,3], [2,6], [8,10], [15,18]]
result    = [[1,6], [8,10], [15,18]]

The intervals [1,3] and [2,6] overlap, so they become [1,6].

First Attempt

public int[][] merge(int[][] intervals) {
    if (intervals.length <= 1) return intervals;

    Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));

    List<int[]> res = new ArrayList<>();

    for (int i = 0; i < intervals.length; i++) {
        if (res.isEmpty() || res.get(res.size() - 1)[1] < intervals[i][0]) {
            res.add(intervals[i]);
        } else {
            res.get(res.size() - 1)[1] = Math.max(
                    res.get(res.size() - 1)[1],
                    intervals[i][1]
            );
        }
    }

    return res.toArray(new int[res.size()][2]);
}

This is already the right idea.

The important move is sorting by the start of each interval before trying to merge anything.

Clean Version

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

class Solution {
    public int[][] merge(int[][] intervals) {
        if (intervals.length <= 1) {
            return intervals;
        }

        Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));

        List<int[]> merged = new ArrayList<>();

        for (int[] current : intervals) {
            if (merged.isEmpty()) {
                merged.add(current);
                continue;
            }

            int[] last = merged.get(merged.size() - 1);

            if (last[1] < current[0]) {
                merged.add(current);
            } else {
                last[1] = Math.max(last[1], current[1]);
            }
        }

        return merged.toArray(new int[merged.size()][2]);
    }
}

Why Sorting Works

After sorting, intervals appear in increasing start order:

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

Now the current interval only needs to be compared with the last merged interval.

If the current interval does not overlap, it can safely be added as a new range.

if (last[1] < current[0]) {
    merged.add(current);
}

If it overlaps, extend the end of the last merged interval.

last[1] = Math.max(last[1], current[1]);

Boundary Detail

The non-overlap condition is:

last[1] < current[0]

That means touching intervals are merged.

last    = [1,4]
current = [4,5]
merged  = [1,5]

If the problem wanted touching intervals to stay separate, the condition would change to:

last[1] <= current[0]

For LeetCode Merge Intervals, touching intervals should be merged.

Walkthrough

Input:

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

Sorted input is the same:

[[1,3], [2,6], [8,10], [15,18]]
current intervallast merged intervalaction
[1,3]noneadd [1,3]
[2,6][1,3]overlap, merge into [1,6]
[8,10][1,6]no overlap, add [8,10]
[15,18][8,10]no overlap, add [15,18]

Final result:

[[1,6], [8,10], [15,18]]

Complexity

StepTimeSpace
Sort intervalsO(n log n)depends on sort implementation
Scan and mergeO(n)O(n) for the result
TotalO(n log n)O(n)

The scan is linear, but sorting dominates the runtime.

Takeaways