352. Data Stream as Disjoint Intervals - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Sorting - Sorting a collection of numbers to process them in order for interval construction
  • Hash Sets - Using sets to store unique values and eliminate duplicates efficiently
  • Ordered Sets / Maps (TreeMap, SortedDict) - Maintaining elements in sorted order for efficient insertion and traversal
  • Interval Merging - Identifying consecutive numbers and combining them into contiguous intervals

1. Brute Force (Sorting)

Intuition

The simplest approach is to store all incoming numbers in a list and compute the intervals on demand. When getIntervals() is called, we sort the list and scan through it to identify consecutive sequences. Two numbers belong to the same interval if they differ by exactly 1.

Algorithm

  1. Initialize an empty list to store all added numbers.
  2. For addNum(value), append the value to the list.
  3. For getIntervals():
    • If the list is empty, return an empty result.
    • Sort the list.
    • Initialize start with the first element.
    • Iterate through the sorted list. When the current number differs from the previous by more than 1, close the current interval and start a new one.
    • After the loop, add the final interval.
  4. Return the list of intervals.
class SummaryRanges:

    def __init__(self):
        self.arr = []

    def addNum(self, value: int) -> None:
        self.arr.append(value)

    def getIntervals(self) -> List[List[int]]:
        if not self.arr:
            return []

        self.arr.sort()
        n = len(self.arr)
        start = self.arr[0]
        res = []
        for i in range(1, n):
            if self.arr[i] - self.arr[i - 1] > 1:
                res.append([start, self.arr[i - 1]])
                start = self.arr[i]

        res.append([start, self.arr[n - 1]])
        return res

Time & Space Complexity

  • Time complexity:
    • O(1)O(1) time for initialization.
    • O(1)O(1) time for each addNum()addNum() function call.
    • O(nlogn)O(n \log n) time for each getIntervals()getIntervals() function call.
  • Space complexity: O(n)O(n)

2. Hash Set + Sorting

Intuition

The brute force approach stores duplicates, which wastes space and processing time during sorting. By using a hash set instead of a list, we automatically eliminate duplicates. This makes the interval computation cleaner since we only deal with unique values.

Algorithm

  1. Initialize a hash set to store unique numbers.
  2. For addNum(value), add the value to the set (duplicates are ignored).
  3. For getIntervals():
    • If the set is empty, return an empty result.
    • Convert the set to a sorted list.
    • Initialize start with the first element.
    • Iterate through the sorted list. When consecutive elements differ by more than 1, close the current interval and start a new one.
    • Add the final interval after the loop.
  4. Return the list of intervals.
class SummaryRanges:

    def __init__(self):
        self.arr = set()

    def addNum(self, value: int) -> None:
        self.arr.add(value)

    def getIntervals(self) -> List[List[int]]:
        if not self.arr:
            return []

        lst = sorted(list(self.arr))
        n = len(lst)
        start = lst[0]
        res = []
        for i in range(1, n):
            if lst[i] - lst[i - 1] > 1:
                res.append([start, lst[i - 1]])
                start = lst[i]

        res.append([start, lst[n - 1]])
        return res

Time & Space Complexity

  • Time complexity:
    • O(1)O(1) time for initialization.
    • O(1)O(1) time for each addNum()addNum() function call.
    • O(nlogn)O(n \log n) time for each getIntervals()getIntervals() function call.
  • Space complexity: O(n)O(n)

3. Ordered Map

Intuition

Using an ordered map (like TreeMap or SortedDict), we can maintain elements in sorted order as they are inserted. This eliminates the need to sort during getIntervals(). We simply iterate through the keys in order and merge consecutive numbers into intervals on the fly.

Algorithm

  1. Initialize an ordered map (TreeMap/SortedDict).
  2. For addNum(value), insert the value into the map.
  3. For getIntervals():
    • Initialize an empty result list.
    • Iterate through the keys of the ordered map in sorted order.
    • If the result is non-empty and the current number is exactly one more than the end of the last interval, extend that interval.
    • Otherwise, start a new interval [n, n].
  4. Return the list of intervals.
from sortedcontainers import SortedDict

class SummaryRanges:
    def __init__(self):
        self.treeMap = SortedDict()

    def addNum(self, value: int) -> None:
        self.treeMap[value] = True

    def getIntervals(self) -> List[List[int]]:
        res = []
        for n in self.treeMap:
            if res and res[-1][1] + 1 == n:
                res[-1][1] = n
            else:
                res.append([n, n])
        return res

Time & Space Complexity

  • Time complexity:
    • O(1)O(1) time for initialization.
    • O(logn)O(\log n) time for each addNum()addNum() function call.
    • O(n)O(n) time for each getIntervals()getIntervals() function call.
  • Space complexity: O(n)O(n)

4. Ordered Set

Intuition

An ordered set provides the same benefits as an ordered map but with simpler semantics when we only need to track the presence of numbers. Elements are kept sorted automatically, and duplicates are ignored. The interval construction logic remains the same: iterate in order and merge consecutive numbers.

Algorithm

  1. Initialize an ordered set (TreeSet/SortedSet).
  2. For addNum(value), insert the value into the set.
  3. For getIntervals():
    • Initialize an empty result list.
    • Iterate through the set in sorted order.
    • If the result is non-empty and the current number equals the last interval's end plus one, extend that interval.
    • Otherwise, create a new interval [n, n].
  4. Return the list of intervals.
from sortedcontainers import SortedSet

class SummaryRanges:
    def __init__(self):
        self.orderedSet = SortedSet()

    def addNum(self, value: int) -> None:
        self.orderedSet.add(value)

    def getIntervals(self) -> List[List[int]]:
        res = []
        for n in self.orderedSet:
            if res and res[-1][1] + 1 == n:
                res[-1][1] = n
            else:
                res.append([n, n])
        return res

Time & Space Complexity

  • Time complexity:
    • O(1)O(1) time for initialization.
    • O(logn)O(\log n) time for each addNum()addNum() function call.
    • O(n)O(n) time for each getIntervals()getIntervals() function call.
  • Space complexity: O(n)O(n)

Common Pitfalls

Not Handling Duplicates

When adding numbers to the data stream, duplicates may appear. If using a list-based approach, duplicates cause intervals like [1, 1] to appear multiple times or create incorrect interval boundaries. Using a set automatically handles this.

# Wrong: allows duplicates
self.arr.append(value)

# Correct: prevents duplicates
self.arr.add(value)

Off-by-One in Consecutive Check

When merging intervals, two numbers are consecutive if they differ by exactly 1. A common mistake is using >= instead of > when checking for gaps, which incorrectly merges non-consecutive numbers.

# Wrong: merges [1,2] and [2,3] into one interval incorrectly
if arr[i] - arr[i - 1] >= 1:
    # start new interval

# Correct: only starts new interval when gap > 1
if arr[i] - arr[i - 1] > 1:
    res.append([start, arr[i - 1]])
    start = arr[i]

Forgetting the Final Interval

When iterating through sorted numbers to build intervals, the loop only closes intervals when a gap is found. The last interval is never closed inside the loop and must be added after.

# Wrong: missing final interval
for i in range(1, n):
    if lst[i] - lst[i - 1] > 1:
        res.append([start, lst[i - 1]])
        start = lst[i]
return res  # Missing the last interval!

# Correct: add final interval after loop
res.append([start, lst[n - 1]])
return res

Incorrect Interval Extension Logic

When using ordered structures and extending intervals on the fly, the condition to extend must check if the current number is exactly one more than the interval end, not just greater.

# Wrong: extends interval incorrectly
if res and res[-1][1] < n:
    res[-1][1] = n

# Correct: only extend if consecutive
if res and res[-1][1] + 1 == n:
    res[-1][1] = n
else:
    res.append([n, n])

Empty Input Edge Case

When getIntervals() is called before any numbers are added, or when all numbers have been processed, the function should return an empty list. Forgetting this check causes index errors.

# Wrong: crashes on empty input
start = self.arr[0]  # IndexError if empty

# Correct: handle empty case first
if not self.arr:
    return []
start = self.arr[0]