🧠
Balanced Tree / Sorted Container with Merge on Insert
💡 This approach introduces a balanced tree or sorted container to maintain intervals dynamically, avoiding full re-sorting each time. It teaches efficient data structure usage for interval merging.
Intuition
Keep intervals sorted by start, and on adding a number, find where it fits, then merge with adjacent intervals if overlapping or contiguous.
Algorithm
- Maintain a balanced tree (e.g., TreeMap or SortedDict) keyed by interval start.
- On addNum(val), find intervals that might overlap or be adjacent to val.
- Merge val with these intervals or create a new interval if no overlap.
- Update the tree with the merged interval.
💡 The key insight is to only check intervals adjacent to val, not all intervals, to keep operations efficient.
from bisect import bisect_left
class SummaryRanges:
def __init__(self):
self.intervals = [] # list of [start, end]
def addNum(self, val: int) -> None:
intervals = self.intervals
if not intervals:
intervals.append([val, val])
return
# Find position to insert
i = bisect_left(intervals, [val, val])
# Check left interval
left_merge = (i > 0 and intervals[i-1][1] + 1 >= val)
# Check right interval
right_merge = (i < len(intervals) and intervals[i][0] - 1 <= val) if i < len(intervals) else False
if left_merge and right_merge:
# Merge left and right intervals with val
intervals[i-1][1] = intervals[i][1]
intervals.pop(i)
elif left_merge:
# Extend left interval
intervals[i-1][1] = max(intervals[i-1][1], val)
elif right_merge:
# Extend right interval
intervals[i][0] = min(intervals[i][0], val)
else:
# Insert new interval
intervals.insert(i, [val, val])
def getIntervals(self) -> list:
return self.intervals
# Example usage:
# obj = SummaryRanges()
# obj.addNum(1)
# print(obj.getIntervals()) # [[1,1]]
# obj.addNum(3)
# print(obj.getIntervals()) # [[1,1],[3,3]]
Line Notes
self.intervals = []Store intervals sorted by start
i = bisect_left(intervals, [val, val])Find insertion point for val
left_merge = (i > 0 and intervals[i-1][1] + 1 >= val)Check if val merges with left interval
intervals.insert(i, [val, val])Insert new interval if no merges
import java.util.*;
class SummaryRanges {
private TreeMap<Integer, Integer> intervals;
public SummaryRanges() {
intervals = new TreeMap<>();
}
public void addNum(int val) {
if (intervals.containsKey(val)) return;
Integer lowerKey = intervals.floorKey(val);
Integer higherKey = intervals.ceilingKey(val);
if (lowerKey != null && intervals.get(lowerKey) >= val) {
return; // val already covered
}
boolean leftMerge = false, rightMerge = false;
if (lowerKey != null && intervals.get(lowerKey) + 1 >= val) {
leftMerge = true;
}
if (higherKey != null && higherKey - 1 <= val) {
rightMerge = true;
}
if (leftMerge && rightMerge) {
intervals.put(lowerKey, intervals.get(higherKey));
intervals.remove(higherKey);
} else if (leftMerge) {
intervals.put(lowerKey, Math.max(intervals.get(lowerKey), val));
} else if (rightMerge) {
int end = intervals.get(higherKey);
intervals.remove(higherKey);
intervals.put(val, end);
} else {
intervals.put(val, val);
}
}
public int[][] getIntervals() {
int[][] res = new int[intervals.size()][2];
int i = 0;
for (Map.Entry<Integer, Integer> entry : intervals.entrySet()) {
res[i][0] = entry.getKey();
res[i][1] = entry.getValue();
i++;
}
return res;
}
// Example main method
public static void main(String[] args) {
SummaryRanges obj = new SummaryRanges();
obj.addNum(1);
System.out.println(Arrays.deepToString(obj.getIntervals())); // [[1,1]]
obj.addNum(3);
System.out.println(Arrays.deepToString(obj.getIntervals())); // [[1,1],[3,3]]
}
}
Line Notes
intervals = new TreeMap<>()Use TreeMap to keep intervals sorted by start
Integer lowerKey = intervals.floorKey(val)Find interval start <= val
if (leftMerge && rightMerge)Merge left and right intervals if val bridges them
intervals.put(val, val)Insert new interval if no merge possible
#include <iostream>
#include <map>
#include <vector>
using namespace std;
class SummaryRanges {
map<int, int> intervals; // key=start, value=end
public:
SummaryRanges() {}
void addNum(int val) {
if (intervals.empty()) {
intervals[val] = val;
return;
}
auto it = intervals.upper_bound(val);
int start = val, end = val;
if (it != intervals.begin()) {
auto prev = prev(it);
if (prev->second >= val) return; // already covered
if (prev->second + 1 == val) {
start = prev->first;
intervals.erase(prev);
}
}
if (it != intervals.end() && it->first - 1 == val) {
end = it->second;
intervals.erase(it);
}
intervals[start] = end;
}
vector<vector<int>> getIntervals() {
vector<vector<int>> res;
for (auto &p : intervals) {
res.push_back({p.first, p.second});
}
return res;
}
};
// Example usage
// int main() {
// SummaryRanges obj;
// obj.addNum(1);
// auto intervals = obj.getIntervals();
// for (auto &iv : intervals) cout << '[' << iv[0] << ',' << iv[1] << ']';
// cout << '\n';
// obj.addNum(3);
// intervals = obj.getIntervals();
// for (auto &iv : intervals) cout << '[' << iv[0] << ',' << iv[1] << ']';
// cout << '\n';
// return 0;
// }
Line Notes
map<int, int> intervals;Store intervals sorted by start using map
auto it = intervals.upper_bound(val);Find first interval with start > val
if (prev->second + 1 == val)Check if val extends previous interval
intervals[start] = end;Insert or update merged interval
class SummaryRanges {
constructor() {
this.intervals = new Map(); // key=start, value=end
}
addNum(val) {
if (this.intervals.size === 0) {
this.intervals.set(val, val);
return;
}
const keys = Array.from(this.intervals.keys()).sort((a,b) => a-b);
let left = 0, right = keys.length - 1;
let pos = keys.length;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (keys[mid] > val) {
pos = mid;
right = mid - 1;
} else {
left = mid + 1;
}
}
let start = val, end = val;
if (pos > 0) {
const prevStart = keys[pos - 1];
const prevEnd = this.intervals.get(prevStart);
if (prevEnd >= val) return; // already covered
if (prevEnd + 1 >= val) {
start = prevStart;
end = Math.max(prevEnd, val);
this.intervals.delete(prevStart);
}
}
if (pos < keys.length) {
const nextStart = keys[pos];
if (nextStart - 1 <= val) {
end = Math.max(end, this.intervals.get(nextStart));
this.intervals.delete(nextStart);
}
}
this.intervals.set(start, end);
}
getIntervals() {
const res = [];
const keys = Array.from(this.intervals.keys()).sort((a,b) => a-b);
for (const k of keys) {
res.push([k, this.intervals.get(k)]);
}
return res;
}
}
// Example usage:
// const obj = new SummaryRanges();
// obj.addNum(1);
// console.log(obj.getIntervals()); // [[1,1]]
// obj.addNum(3);
// console.log(obj.getIntervals()); // [[1,1],[3,3]]
Line Notes
this.intervals = new Map()Store intervals keyed by start
const keys = Array.from(this.intervals.keys()).sort(...)Get sorted interval starts for binary search
if (prevEnd + 1 >= val)Check if val merges or extends previous interval
this.intervals.set(start, end)Insert or update merged interval
TimeO(log n) per addNum, O(n) per getIntervals
SpaceO(n) to store intervals
Each addNum uses balanced tree operations to find and merge intervals efficiently. getIntervals returns stored intervals in order.
💡 For n=100000, each insertion is about log(100000) ≈ 17 operations, which is efficient for large streams.
Interview Verdict: Accepted / Efficient for large inputs
This approach balances complexity and performance, suitable for interview coding and real use.