Introduction to the Insert Interval Problem
The Insert Interval problem is a classic algorithmic challenge frequently encountered in coding interviews and real-world scheduling applications. Given a list of non-overlapping intervals sorted by their start times, and a new interval, your task is to insert the new interval into the list while merging any overlapping intervals. The result should remain a list of non-overlapping intervals sorted by start time.
This problem tests your understanding of array manipulation, interval merging logic, and edge-case handling. In this tutorial, we'll walk through the problem step by step, build a robust JavaScript solution, and discuss best practices to keep your code clean and efficient.
Problem Statement
You are given an array of intervals where intervals[i] = [start_i, end_i], and the intervals are sorted in ascending order by their start times. You are also given a new interval newInterval = [start, end]. Insert newInterval into intervals such that intervals remains sorted and non-overlapping, merging overlapping intervals as needed. Return the resulting array of intervals.
Why the Insert Interval Problem Matters
Understanding how to manipulate intervals is essential for many practical applications:
- Calendar and scheduling systems: Detecting conflicts when booking meetings or events.
- Resource allocation: Managing time slots for servers, rooms, or equipment.
- Genomic data analysis: Merging overlapping gene regions in bioinformatics.
- Game development: Handling collision detection across time-based ranges.
- Financial applications: Aggregating trading windows or interest accrual periods.
Beyond its practical uses, the problem is a favorite among interviewers because it forces candidates to reason about ordering, boundary conditions, and in-place versus out-of-place array construction. Mastering it builds a foundation for more complex interval problems such as Merge Intervals, Meeting Rooms II, and Employee Free Time.
Understanding the Logic
Before writing code, it's crucial to understand the three phases the algorithm must handle:
Phase 1: Add All Intervals That Come Before the New Interval
Since the input is already sorted, we iterate through the existing intervals and add every interval whose end time is strictly less than the new interval's start time. These intervals cannot overlap with the new interval, so they pass through unchanged.
Phase 2: Merge Overlapping Intervals
Once we encounter an interval that overlaps with the new interval, we merge them. Two intervals [a, b] and [c, d] overlap if c <= b (assuming a <= c). To merge, we take the minimum of the start times and the maximum of the end times. We continue merging as long as the next interval's start is less than or equal to the merged interval's end.
Phase 3: Add All Remaining Intervals
After merging is complete, the remaining intervals in the original list come strictly after the new (merged) interval. We simply append them to the result.
Step-by-Step JavaScript Implementation
Let's translate the three phases into JavaScript code. We'll build the solution incrementally and explain each part.
The Complete Solution
/**
* Inserts a new interval into a sorted, non-overlapping list of intervals.
* @param {number[][]} intervals - Sorted, non-overlapping intervals.
* @param {number[]} newInterval - The new interval to insert.
* @returns {number[][]} The merged list of intervals.
*/
function insert(intervals, newInterval) {
const result = [];
let i = 0;
const n = intervals.length;
// Phase 1: Add all intervals that end before the new interval starts.
while (i < n && intervals[i][1] < newInterval[0]) {
result.push(intervals[i]);
i++;
}
// Phase 2: Merge all overlapping intervals with the new interval.
while (i < n && intervals[i][0] <= newInterval[1]) {
newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
i++;
}
result.push(newInterval);
// Phase 3: Add the remaining intervals.
while (i < n) {
result.push(intervals[i]);
i++;
}
return result;
}
Walking Through an Example
Let's trace the function with a concrete example to verify our logic:
const intervals = [[1, 3], [6, 9]];
const newInterval = [2, 5];
console.log(insert(intervals, newInterval));
// Output: [[1, 5], [6, 9]]
Here's what happens step by step:
- Phase 1: The first interval
[1, 3]has end3, which is not less thannewInterval[0](which is2). So we skip Phase 1 entirely. - Phase 2:
[1, 3]overlaps because1 <= 5. We merge:newInterval = [min(2,1), max(5,3)] = [1, 5]. Next,[6, 9]has start6, which is greater than5, so we stop merging. We push[1, 5]to the result. - Phase 3: We push the remaining interval
[6, 9].
The final result is [[1, 5], [6, 9]], which is correct.
Testing Edge Cases
A robust solution must handle edge cases gracefully. Let's test several scenarios:
// Case 1: Empty intervals array
console.log(insert([], [5, 7]));
// Output: [[5, 7]]
// Case 2: New interval goes at the beginning
console.log(insert([[1, 5]], [0, 0]));
// Output: [[0, 0], [1, 5]]
// Case 3: New interval goes at the end
console.log(insert([[1, 5]], [6, 8]));
// Output: [[1, 5], [6, 8]]
// Case 4: New interval engulfs all existing intervals
console.log(insert([[1, 2], [3, 4], [5, 6]], [0, 10]));
// Output: [[0, 10]]
// Case 5: No overlaps at all
console.log(insert([[1, 2], [5, 6]], [3, 4]));
// Output: [[1, 2], [3, 4], [5, 6]]
// Case 6: Multiple merges required
console.log(insert([[1, 2], [3, 5], [6, 7], [8, 10], [12, 16]], [4, 8]));
// Output: [[1, 2], [3, 10], [12, 16]]
All cases produce the expected output, confirming the solution handles boundaries, total overlaps, and multiple merges correctly.
Complexity Analysis
Understanding the performance characteristics of your solution is critical, especially in interview settings:
- Time Complexity: O(n) — We traverse the input array exactly once. Each interval is visited a single time across the three while loops.
- Space Complexity: O(n) — In the worst case, no intervals merge, and we store all
noriginal intervals plus the new one in the result array. This is the output space, which is typically not counted toward auxiliary space, but it's worth noting.
This linear complexity is optimal because we must examine every interval at least once to determine whether it overlaps with the new interval.
Best Practices
Avoid Mutating the Input
In the solution above, we mutate newInterval during merging. If the caller expects the original newInterval to remain unchanged, this could introduce subtle bugs. To be safe, create a local copy:
function insert(intervals, newInterval) {
const result = [];
let [newStart, newEnd] = newInterval; // Destructure to avoid mutation
let i = 0;
const n = intervals.length;
while (i < n && intervals[i][1] < newStart) {
result.push(intervals[i]);
i++;
}
while (i < n && intervals[i][0] <= newEnd) {
newStart = Math.min(newStart, intervals[i][0]);
newEnd = Math.max(newEnd, intervals[i][1]);
i++;
}
result.push([newStart, newEnd]);
while (i < n) {
result.push(intervals[i]);
i++;
}
return result;
}
Use Clear Variable Names
While i is acceptable for a loop counter, descriptive names like newStart and newEnd make the merging logic immediately understandable. Avoid single-letter variable names for anything beyond simple iteration.
Validate Inputs
In production code, you should validate that inputs conform to expectations. Here's a defensive version with validation:
function insert(intervals, newInterval) {
if (!Array.isArray(intervals) || !Array.isArray(newInterval)) {
throw new TypeError('Both arguments must be arrays');
}
if (newInterval.length !== 2 || newInterval.some(v => typeof v !== 'number')) {
throw new TypeError('newInterval must be a pair of numbers');
}
if (newInterval[0] > newInterval[1]) {
throw new RangeError('newInterval start must not exceed end');
}
const result = [];
let [newStart, newEnd] = newInterval;
let i = 0;
const n = intervals.length;
while (i < n && intervals[i][1] < newStart) {
result.push(intervals[i]);
i++;
}
while (i < n && intervals[i][0] <= newEnd) {
newStart = Math.min(newStart, intervals[i][0]);
newEnd = Math.max(newEnd, intervals[i][1]);
i++;
}
result.push([newStart, newEnd]);
while (i < n) {
result.push(intervals[i]);
i++;
}
return result;
}
Leverage Modern JavaScript Features
Using ES6 features like destructuring, const/let, and arrow functions makes your code more concise and readable. However, avoid over-engineering with unnecessary abstractions for a problem this straightforward.
Write Tests
Always accompany your solution with tests covering normal cases, edge cases, and boundary conditions. A simple test suite using console.assert or a framework like Jest ensures your solution remains correct as it evolves:
function testInsert() {
const assert = (actual, expected, message) => {
const a = JSON.stringify(actual);
const e = JSON.stringify(expected);
console.assert(a === e, `${message}: expected ${e}, got ${a}`);
};
assert(insert([], [5, 7]), [[5, 7]], 'Empty input');
assert(insert([[1, 3], [6, 9]], [2, 5]), [[1, 5], [6, 9]], 'Basic merge');
assert(insert([[1, 5]], [0, 0]), [[0, 0], [1, 5]], 'Insert at start');
assert(insert([[1, 5]], [6, 8]), [[1, 5], [6, 8]], 'Insert at end');
assert(insert([[1, 2], [3, 4], [5, 6]], [0, 10]), [[0, 10]], 'Total overlap');
assert(insert([[1, 2], [5, 6]], [3, 4]), [[1, 2], [3, 4], [5, 6]], 'No overlap');
console.log('All tests completed.');
}
testInsert();
Common Pitfalls to Avoid
- Using the wrong comparison operator: Remember that intervals overlap when
intervals[i][0] <= newEnd, not<. Using strict inequality can miss intervals that touch at a single point, such as[1, 4]and[4, 6]. - Forgetting to push the merged interval: After the merging loop, you must explicitly push the final
newIntervalto the result. It's easy to overlook this step. - Mutating the input array: Pushing into the original
intervalsarray instead of a newresultarray can cause unexpected side effects for the caller. - Ignoring the sorted precondition: This algorithm relies on the input being sorted. If your input might not be sorted, sort it first — but note that sorting changes the time complexity to O(n log n).
Conclusion
The Insert Interval problem is a deceptively simple challenge that rewards careful reasoning about ordering, overlap detection, and array construction. By breaking the problem into three clear phases — adding non-overlapping intervals before the new one, merging overlapping intervals, and appending the rest — you can build a clean, efficient O(n) solution in JavaScript. Remember to handle edge cases, avoid mutating inputs, validate your data in production code, and write tests to verify correctness. With these practices in place, you'll be well-equipped to tackle not only this problem but the broader family of interval-based challenges that appear throughout software development and technical interviews.