352. Data Stream as Disjoint Intervals - Explanation
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
- Initialize an empty list to store all added numbers.
- For
addNum(value), append the value to the list. - For
getIntervals():- If the list is empty, return an empty result.
- Sort the list.
- Initialize
startwith 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.
- 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 resTime & Space Complexity
- Time complexity:
- time for initialization.
- time for each function call.
- time for each function call.
- Space complexity:
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
- Initialize a hash set to store unique numbers.
- For
addNum(value), add the value to the set (duplicates are ignored). - For
getIntervals():- If the set is empty, return an empty result.
- Convert the set to a sorted list.
- Initialize
startwith 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.
- 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 resTime & Space Complexity
- Time complexity:
- time for initialization.
- time for each function call.
- time for each function call.
- Space complexity:
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
- Initialize an ordered map (TreeMap/SortedDict).
- For
addNum(value), insert the value into the map. - 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].
- 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 resTime & Space Complexity
- Time complexity:
- time for initialization.
- time for each function call.
- time for each function call.
- Space complexity:
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
- Initialize an ordered set (TreeSet/SortedSet).
- For
addNum(value), insert the value into the set. - 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].
- 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 resTime & Space Complexity
- Time complexity:
- time for initialization.
- time for each function call.
- time for each function call.
- Space complexity:
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 resIncorrect 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]