Facebook Pixel

352. Data Stream as Disjoint Intervals

HardDesignBinary SearchOrdered Set
Leetcode Link

Problem Description

This problem asks you to design a data structure that can efficiently summarize a stream of integers as a collection of disjoint intervals.

You need to implement a SummaryRanges class with the following functionality:

  1. Constructor SummaryRanges(): Creates an empty object ready to receive integers from a data stream.

  2. Method addNum(int value): Takes a non-negative integer and adds it to the data structure. As numbers are added, they should be incorporated into existing intervals when possible.

  3. Method getIntervals(): Returns the current summary of all numbers seen so far as a list of disjoint intervals. Each interval is represented as [start, end] where both start and end are inclusive. The intervals must be sorted by their starting values.

The key challenge is to maintain these intervals efficiently as new numbers arrive. When a new number is added:

  • If it can extend an existing interval (adjacent to its boundaries), the interval should be extended
  • If it can merge two separate intervals (filling a gap of exactly 1), those intervals should be merged
  • If it falls within an existing interval, no change is needed
  • If it's isolated from all existing intervals, it creates a new single-point interval [value, value]

For example, if you've seen numbers 1, 3, 7, 2, 6:

  • After 1: intervals are [[1,1]]
  • After 1,3: intervals are [[1,1], [3,3]]
  • After 1,3,7: intervals are [[1,1], [3,3], [7,7]]
  • After 1,3,7,2: intervals are [[1,3], [7,7]] (1,2,3 form a continuous range)
  • After 1,3,7,2,6: intervals are [[1,3], [6,7]] (6,7 form a continuous range)
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The core insight is that we need to efficiently maintain sorted, non-overlapping intervals while handling new numbers that could affect existing intervals in various ways. This naturally suggests using a sorted data structure where we can quickly find where a new number fits relative to existing intervals.

When we add a new number val, we need to determine its relationship with existing intervals. The key observation is that a number can interact with at most two intervals - the one immediately to its left and the one immediately to its right. This is because intervals are disjoint.

We can identify four scenarios when adding a number:

  1. Bridge scenario: The number connects two adjacent intervals (when left interval ends at val-1 and right interval starts at val+1)
  2. Extend left: The number extends or falls within the left interval (when val is at most one more than the left interval's end)
  3. Extend right: The number extends or falls within the right interval (when val is at most one less than the right interval's start)
  4. Isolated: The number creates a new interval by itself

Using a SortedDict (ordered map) with interval start points as keys makes sense because:

  • We can use binary search (bisect_right) to quickly find where a new number fits
  • We maintain intervals sorted by start position automatically
  • We can efficiently access and modify adjacent intervals

The clever part is storing each interval as [start, end] with the start as the key. When we find where val fits using bisect_right, we immediately know:

  • The interval to the right (if it exists) starts at the key found by bisect_right
  • The interval to the left (if it exists) is the one just before that position

This approach gives us O(log n) time for adding numbers and O(n) space, where n is the number of disjoint intervals, making it efficient for streams where numbers tend to cluster into ranges.

Solution Approach

The solution uses a SortedDict data structure to maintain intervals sorted by their starting points. Here's how each component works:

Initialization:

def __init__(self):
    self.mp = SortedDict()

We initialize an empty SortedDict where keys are interval start points and values are [start, end] pairs.

Adding a Number:

When addNum(val) is called, we follow these steps:

  1. Find position using binary search:

    ridx = self.mp.bisect_right(val)
    lidx = n if ridx == 0 else ridx - 1
    • ridx finds the index of the first interval whose start is greater than val
    • lidx is the index of the interval immediately to the left (or n if none exists)
  2. Handle the merge case (bridging two intervals):

    if (lidx != n and ridx != n and 
        values[lidx][1] + 1 == val and 
        values[ridx][0] - 1 == val):
        self.mp[keys[lidx]][1] = self.mp[keys[ridx]][1]
        self.mp.pop(keys[ridx])

    If val perfectly bridges two intervals (left ends at val-1, right starts at val+1), merge them by extending the left interval to include the right interval's end, then remove the right interval.

  3. Handle extending or falling within left interval:

    elif lidx != n and val <= values[lidx][1] + 1:
        self.mp[keys[lidx]][1] = max(val, self.mp[keys[lidx]][1])

    If val is at most one more than the left interval's end, extend the left interval. The max ensures we don't shrink an interval if val is already within it.

  4. Handle extending or falling within right interval:

    elif ridx != n and val >= values[ridx][0] - 1:
        self.mp[keys[ridx]][0] = min(val, self.mp[keys[ridx]][0])

    If val is at most one less than the right interval's start, extend the right interval leftward. Since we're modifying the start point (which is the key), we update the value's start component.

  5. Create new isolated interval:

    else:
        self.mp[val] = [val, val]

    If val doesn't connect with any existing intervals, create a new single-point interval [val, val].

Getting Intervals:

def getIntervals(self) -> List[List[int]]:
    return list(self.mp.values())

Simply return all interval values as a list. Since SortedDict maintains order by keys (start points), the result is automatically sorted.

Time Complexity:

  • addNum: O(log n) for binary search and O(1) for updates, where n is the number of intervals
  • getIntervals: O(n) to convert values to a list

Space Complexity: O(n) where n is the maximum number of disjoint intervals.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through adding the sequence: 5, 2, 7, 6, 1

Initial state: mp = {} (empty SortedDict)

Step 1: addNum(5)

  • bisect_right(5) returns 0 (no intervals exist)
  • ridx = 0, lidx = 0 (n = 0, so both are "out of bounds")
  • No intervals to merge or extend
  • Create new interval: mp = {5: [5,5]}
  • Intervals: [[5,5]]

Step 2: addNum(2)

  • Current: mp = {5: [5,5]}
  • bisect_right(2) returns 0 (2 < 5)
  • ridx = 0 (interval at key 5), lidx = 1 (no left interval)
  • Check right interval: [5,5]
    • Is 2 >= 5-1? No (2 < 4)
  • Create new interval: mp = {2: [2,2], 5: [5,5]}
  • Intervals: [[2,2], [5,5]]

Step 3: addNum(7)

  • Current: mp = {2: [2,2], 5: [5,5]}
  • bisect_right(7) returns 2 (7 > all existing keys)
  • ridx = 2 (no right interval), lidx = 1 (interval at key 5)
  • Check left interval: [5,5]
    • Is 7 <= 5+1? No (7 > 6)
  • Create new interval: mp = {2: [2,2], 5: [5,5], 7: [7,7]}
  • Intervals: [[2,2], [5,5], [7,7]]

Step 4: addNum(6)

  • Current: mp = {2: [2,2], 5: [5,5], 7: [7,7]}
  • bisect_right(6) returns 2 (6 > 5 but < 7)
  • ridx = 2 (interval at key 7), lidx = 1 (interval at key 5)
  • Check merge condition:
    • Left interval ends at 5, right interval starts at 7
    • Is 5+1 == 6 and 7-1 == 6? YES!
  • Merge intervals: Extend [5,5] to [5,7], remove [7,7]
  • Result: mp = {2: [2,2], 5: [5,7]}
  • Intervals: [[2,2], [5,7]]

Step 5: addNum(1)

  • Current: mp = {2: [2,2], 5: [5,7]}
  • bisect_right(1) returns 0 (1 < 2)
  • ridx = 0 (interval at key 2), lidx = 2 (no left interval)
  • Check right interval: [2,2]
    • Is 1 >= 2-1? YES! (1 >= 1)
  • Extend right interval leftward: [2,2] becomes [1,2]
  • Update: mp[2][0] = 1
  • Result: mp = {2: [1,2], 5: [5,7]}
  • Intervals: [[1,2], [5,7]]

Final Summary: The stream 5, 2, 7, 6, 1 produces intervals [[1,2], [5,7]]. Notice how:

  • Individual numbers started as single-point intervals
  • Number 6 bridged the gap between [5,5] and [7,7], merging them
  • Number 1 extended the interval [2,2] leftward to become [1,2]

Solution Implementation

1from sortedcontainers import SortedDict
2from typing import List
3
4class SummaryRanges:
5    """
6    A class to maintain disjoint intervals as numbers are added to a stream.
7    When a number is added, it either creates a new interval, extends an existing one,
8    or merges two adjacent intervals.
9    """
10  
11    def __init__(self):
12        """Initialize with an empty sorted dictionary to store intervals."""
13        # SortedDict maintains intervals sorted by their start points (keys)
14        # Each key maps to [start, end] of an interval
15        self.intervals_map = SortedDict()
16
17    def addNum(self, val: int) -> None:
18        """
19        Add a number to the data stream and update intervals accordingly.
20      
21        Args:
22            val: The integer value to add to the stream
23        """
24        num_intervals = len(self.intervals_map)
25      
26        # Find the position where val would be inserted (right bisection)
27        right_idx = self.intervals_map.bisect_right(val)
28      
29        # Find the index of the interval that might contain or be adjacent to val
30        # If right_idx is 0, no interval starts before val, so set left_idx to num_intervals (invalid)
31        # Otherwise, left_idx is the interval just before where val would be inserted
32        left_idx = num_intervals if right_idx == 0 else right_idx - 1
33      
34        # Get references to keys and values for easier access
35        interval_keys = self.intervals_map.keys()
36        interval_values = self.intervals_map.values()
37      
38        # Case 1: val connects two adjacent intervals (merging case)
39        # Check if val is exactly between two intervals (end of left + 1 == val == start of right - 1)
40        if (left_idx != num_intervals and 
41            right_idx != num_intervals and 
42            interval_values[left_idx][1] + 1 == val and 
43            interval_values[right_idx][0] - 1 == val):
44            # Merge the two intervals by extending the left interval to include the right
45            self.intervals_map[interval_keys[left_idx]][1] = self.intervals_map[interval_keys[right_idx]][1]
46            # Remove the right interval as it's now merged
47            self.intervals_map.pop(interval_keys[right_idx])
48          
49        # Case 2: val extends or is within the left interval
50        elif left_idx != num_intervals and val <= interval_values[left_idx][1] + 1:
51            # Extend the end of the left interval if val is beyond it
52            # If val is already within the interval, keep the current end
53            self.intervals_map[interval_keys[left_idx]][1] = max(val, self.intervals_map[interval_keys[left_idx]][1])
54          
55        # Case 3: val extends or is at the beginning of the right interval
56        elif right_idx != num_intervals and val >= interval_values[right_idx][0] - 1:
57            # Extend the start of the right interval to include val
58            # Use min to handle case where val might already be in the interval
59            self.intervals_map[interval_keys[right_idx]][0] = min(val, self.intervals_map[interval_keys[right_idx]][0])
60          
61        # Case 4: val creates a new isolated interval
62        else:
63            # Create a new interval containing only val
64            self.intervals_map[val] = [val, val]
65
66    def getIntervals(self) -> List[List[int]]:
67        """
68        Get all current disjoint intervals in sorted order.
69      
70        Returns:
71            A list of intervals, where each interval is [start, end]
72        """
73        return list(self.intervals_map.values())
74
75
76# Your SummaryRanges object will be instantiated and called as such:
77# obj = SummaryRanges()
78# obj.addNum(val)
79# param_2 = obj.getIntervals()
80
1class SummaryRanges {
2    // TreeMap to store intervals, key is the start of interval, value is [start, end]
3    private TreeMap<Integer, int[]> intervalMap;
4
5    /**
6     * Initialize the data structure
7     */
8    public SummaryRanges() {
9        intervalMap = new TreeMap<>();
10    }
11
12    /**
13     * Add a number to the data stream and merge intervals if necessary
14     * @param val the number to be added
15     */
16    public void addNum(int val) {
17        // Find the largest key that is less than or equal to val
18        Integer leftKey = intervalMap.floorKey(val);
19        // Find the smallest key that is greater than or equal to val
20        Integer rightKey = intervalMap.ceilingKey(val);
21      
22        // Case 1: val connects two adjacent intervals - merge them
23        if (leftKey != null && rightKey != null && 
24            intervalMap.get(leftKey)[1] + 1 == val && 
25            intervalMap.get(rightKey)[0] - 1 == val) {
26          
27            // Extend left interval to include right interval's end
28            intervalMap.get(leftKey)[1] = intervalMap.get(rightKey)[1];
29            // Remove the right interval as it's now merged
30            intervalMap.remove(rightKey);
31        }
32        // Case 2: val can extend or is within the left interval
33        else if (leftKey != null && val <= intervalMap.get(leftKey)[1] + 1) {
34            // Extend the end of left interval if necessary
35            intervalMap.get(leftKey)[1] = Math.max(val, intervalMap.get(leftKey)[1]);
36        }
37        // Case 3: val can extend or is within the right interval
38        else if (rightKey != null && val >= intervalMap.get(rightKey)[0] - 1) {
39            // Extend the start of right interval if necessary
40            intervalMap.get(rightKey)[0] = Math.min(val, intervalMap.get(rightKey)[0]);
41        }
42        // Case 4: val is isolated - create a new interval
43        else {
44            intervalMap.put(val, new int[] {val, val});
45        }
46    }
47
48    /**
49     * Return the current intervals as a 2D array
50     * @return array of intervals where each interval is [start, end]
51     */
52    public int[][] getIntervals() {
53        int[][] result = new int[intervalMap.size()][2];
54        int index = 0;
55      
56        // Copy all intervals from the map to the result array
57        for (int[] interval : intervalMap.values()) {
58            result[index++] = interval;
59        }
60      
61        return result;
62    }
63}
64
65/**
66 * Your SummaryRanges object will be instantiated and called as such:
67 * SummaryRanges obj = new SummaryRanges();
68 * obj.addNum(val);
69 * int[][] param_2 = obj.getIntervals();
70 */
71
1class SummaryRanges {
2private:
3    // Map to store intervals: key is the start of interval, value is {start, end}
4    map<int, vector<int>> intervals;
5
6public:
7    SummaryRanges() {
8        // Constructor - map is automatically initialized
9    }
10
11    void addNum(int value) {
12        // Find the first interval whose start is greater than value
13        auto rightInterval = intervals.upper_bound(value);
14      
15        // Find the interval just before rightInterval (could be the one containing value)
16        auto leftInterval = (rightInterval == intervals.begin()) ? intervals.end() : prev(rightInterval);
17      
18        // Case 1: Value bridges two intervals (connects left and right intervals)
19        if (leftInterval != intervals.end() && rightInterval != intervals.end() && 
20            leftInterval->second[1] + 1 == value && rightInterval->second[0] - 1 == value) {
21            // Merge the two intervals by extending left interval to include right interval
22            leftInterval->second[1] = rightInterval->second[1];
23            intervals.erase(rightInterval);
24        }
25        // Case 2: Value extends or is within the left interval
26        else if (leftInterval != intervals.end() && value <= leftInterval->second[1] + 1) {
27            // Extend the end of left interval if necessary
28            leftInterval->second[1] = max(value, leftInterval->second[1]);
29        }
30        // Case 3: Value extends or is at the start of the right interval
31        else if (rightInterval != intervals.end() && value >= rightInterval->second[0] - 1) {
32            // Extend the start of right interval to include value
33            // Need to update the key in map, so remove old entry and add new one
34            vector<int> updatedInterval = {min(value, rightInterval->second[0]), rightInterval->second[1]};
35            intervals.erase(rightInterval);
36            intervals[updatedInterval[0]] = updatedInterval;
37        }
38        // Case 4: Value is isolated, create a new interval
39        else {
40            intervals[value] = {value, value};
41        }
42    }
43
44    vector<vector<int>> getIntervals() {
45        // Convert map values to result vector
46        vector<vector<int>> result;
47        for (const auto& intervalPair : intervals) {
48            result.push_back(intervalPair.second);
49        }
50        return result;
51    }
52};
53
54/**
55 * Your SummaryRanges object will be instantiated and called as such:
56 * SummaryRanges* obj = new SummaryRanges();
57 * obj->addNum(val);
58 * vector<vector<int>> param_2 = obj->getIntervals();
59 */
60
1// Map to store intervals: key is the start of interval, value is [start, end]
2const intervals: Map<number, number[]> = new Map();
3
4/**
5 * Adds a number to the data structure and maintains disjoint intervals
6 * @param value - The number to add to the summary ranges
7 */
8function addNum(value: number): void {
9    // Find all interval starts and sort them
10    const intervalStarts = Array.from(intervals.keys()).sort((a, b) => a - b);
11  
12    // Find the first interval whose start is greater than value
13    let rightIntervalIndex = intervalStarts.findIndex(start => start > value);
14    let rightIntervalStart = rightIntervalIndex !== -1 ? intervalStarts[rightIntervalIndex] : null;
15  
16    // Find the interval just before rightInterval (could be the one containing value)
17    let leftIntervalIndex = rightIntervalIndex === -1 ? intervalStarts.length - 1 : rightIntervalIndex - 1;
18    let leftIntervalStart = leftIntervalIndex >= 0 ? intervalStarts[leftIntervalIndex] : null;
19  
20    // Get the actual interval arrays
21    const leftInterval = leftIntervalStart !== null ? intervals.get(leftIntervalStart) : null;
22    const rightInterval = rightIntervalStart !== null ? intervals.get(rightIntervalStart) : null;
23  
24    // Case 1: Value bridges two intervals (connects left and right intervals)
25    if (leftInterval && rightInterval && 
26        leftInterval[1] + 1 === value && rightInterval[0] - 1 === value) {
27        // Merge the two intervals by extending left interval to include right interval
28        leftInterval[1] = rightInterval[1];
29        intervals.delete(rightIntervalStart!);
30    }
31    // Case 2: Value extends or is within the left interval
32    else if (leftInterval && value <= leftInterval[1] + 1) {
33        // Extend the end of left interval if necessary
34        leftInterval[1] = Math.max(value, leftInterval[1]);
35    }
36    // Case 3: Value extends or is at the start of the right interval
37    else if (rightInterval && value >= rightInterval[0] - 1) {
38        // Extend the start of right interval to include value
39        // Need to update the key in map, so remove old entry and add new one
40        const updatedInterval = [Math.min(value, rightInterval[0]), rightInterval[1]];
41        intervals.delete(rightIntervalStart!);
42        intervals.set(updatedInterval[0], updatedInterval);
43    }
44    // Case 4: Value is isolated, create a new interval
45    else {
46        intervals.set(value, [value, value]);
47    }
48}
49
50/**
51 * Returns the current disjoint intervals as a sorted array
52 * @returns Array of intervals where each interval is [start, end]
53 */
54function getIntervals(): number[][] {
55    // Convert map values to result array and sort by start value
56    const result: number[][] = [];
57    const sortedKeys = Array.from(intervals.keys()).sort((a, b) => a - b);
58  
59    for (const key of sortedKeys) {
60        result.push(intervals.get(key)!);
61    }
62  
63    return result;
64}
65

Time and Space Complexity

Time Complexity:

  • __init__(): O(1) - Simply initializes an empty SortedDict.

  • addNum(val): O(log n) where n is the number of intervals in the SortedDict.

    • bisect_right(val): O(log n) - Binary search in the sorted keys.
    • Accessing keys and values lists: O(1) - These are views in SortedDict.
    • All merge operations (checking conditions and updating intervals): O(1).
    • pop() operation in worst case (merging two intervals): O(log n).
    • Insertion of new interval: O(log n).
    • Overall: O(log n) per operation.
  • getIntervals(): O(n) where n is the number of intervals.

    • Converting the values view to a list requires iterating through all intervals.

Space Complexity:

  • Overall space: O(n) where n is the number of disjoint intervals stored.
    • In the best case (all numbers form one continuous range), space is O(1).
    • In the worst case (all numbers are non-consecutive), space is O(k) where k is the number of unique values added.
    • The SortedDict stores at most one interval per disjoint range of consecutive numbers.

Common Pitfalls

1. Incorrect Interval Merging Logic

Pitfall: One of the most common mistakes is incorrectly handling the case where a new number should merge two existing intervals. Developers often forget to check both conditions properly or make off-by-one errors.

Example of incorrect code:

# Wrong: Checking if val equals the boundaries instead of being adjacent
if (left_interval[1] == val and right_interval[0] == val):
    # This would only merge if val is exactly on both boundaries
    # Missing the case where val fills a gap of 1

Solution: Ensure you check if the value bridges the gap between two intervals:

if (interval_values[left_idx][1] + 1 == val and 
    interval_values[right_idx][0] - 1 == val):
    # Correctly checks if val fills a gap of exactly 1

2. Not Handling Already-Contained Values

Pitfall: When a value falls within an existing interval, some implementations incorrectly try to modify the interval or create a duplicate.

Example of incorrect code:

# Wrong: Always extending the interval even if val is already inside
if val <= interval_values[left_idx][1] + 1:
    self.intervals_map[interval_keys[left_idx]][1] = val  # Wrong!

Solution: Use max() to ensure the interval doesn't shrink:

self.intervals_map[interval_keys[left_idx]][1] = max(val, self.intervals_map[interval_keys[left_idx]][1])

3. Key Modification in SortedDict

Pitfall: Attempting to directly modify a key in SortedDict breaks the sorted order. When extending an interval's start point, you cannot simply change the key.

Example of incorrect approach:

# Wrong: Trying to change the key directly
if val < interval_start:
    del self.intervals_map[interval_start]
    self.intervals_map[val] = [val, interval_end]  # Inefficient and error-prone

Solution: The current implementation cleverly avoids this by only modifying the value's start component when extending leftward:

# Correct: Only modify the value, not the key
self.intervals_map[interval_keys[right_idx]][0] = min(val, self.intervals_map[interval_keys[right_idx]][0])

4. Binary Search Index Confusion

Pitfall: Misunderstanding what bisect_right returns and incorrectly calculating the left interval index.

Example of incorrect code:

# Wrong: Using bisect_left or incorrectly calculating left_idx
left_idx = self.intervals_map.bisect_left(val) - 1  # Can be -1!
if left_idx >= 0:  # Need to check bounds
    # process...

Solution: Use consistent index handling with a sentinel value:

right_idx = self.intervals_map.bisect_right(val)
left_idx = num_intervals if right_idx == 0 else right_idx - 1
# Now left_idx == num_intervals serves as "no left interval" indicator

5. Order of Condition Checking

Pitfall: Checking conditions in the wrong order can lead to incorrect behavior. For example, checking single interval extensions before checking the merge case.

Example of problematic ordering:

# Wrong order: Extending first might prevent proper merging
if extends_left_interval:
    extend_left()
elif extends_right_interval:
    extend_right()
elif merges_two_intervals:  # This might never be reached!
    merge()

Solution: Always check the merge case first, as it's the most specific condition:

if merges_two_intervals:
    merge()
elif extends_left_interval:
    extend_left()
elif extends_right_interval:
    extend_right()
else:
    create_new_interval()

6. Edge Case: Single-Point Intervals

Pitfall: Not properly handling single-point intervals where start equals end.

Solution: The implementation correctly handles this by initializing new intervals as [val, val] and using inclusive boundaries throughout the logic.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More