← Back to DevBytes

Solving Insert Interval in JavaScript: Step-by-Step Guide

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:

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:

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:

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

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles