摘要
在日常开发中,我们经常会遇到这样的需求:从一串不断到来的数据中,实时维护一组“合并好的区间”。比如,日志的连续时间段、用户连续签到天数、实时数据的连续区间等。
LeetCode 第 352 题 Data Stream as Disjoint Intervals 就是这种场景的典型抽象。它要求我们在数据流中动态添加数字,并随时返回当前数字集合的“不重叠有序区间”表示。
本文将用 Swift 从零实现一个高效的 SummaryRanges 类,逐步分析设计思路,并提供一个可直接运行的 Demo。
描述
题目要求我们实现这样一个类:
- SummaryRanges():初始化对象,数据流初始为空。
- addNum(val):添加一个数字到数据流中。
- getIntervals():返回当前数据流中所有数字的合并区间列表,且区间必须有序且不重叠。
例子
假设我们依次调用:
addNum(1) → getIntervals() → [[1, 1]]
addNum(3) → getIntervals() → [[1, 1], [3, 3]]
addNum(7) → getIntervals() → [[1, 1], [3, 3], [7, 7]]
addNum(2) → getIntervals() → [[1, 3], [7, 7]]
addNum(6) → getIntervals() → [[1, 3], [6, 7]]
你可以看到,数字进来后如果能与已有区间相连,就会被合并;否则,它会自己成为一个新区间。
题解答案
最直接的解法是:
- 用一个 有序集合 或 有序数组 存储所有出现过的数字;
- 每次
getIntervals()
遍历这些数字,按连续性分组形成区间。
但是这样有两个问题:
- 每次获取区间都要 O(n) 遍历,调用频繁时性能差;
- 插入数字时需要保持有序,普通数组插入 O(n) 会慢。
更好的方法是:
- 使用 有序字典(SortedDictionary) 或 TreeMap 思路 存储每个区间;
- 在
addNum
时直接合并到现有区间,避免重复遍历; - 这样
getIntervals
可以直接 O(k) 返回区间列表(k 是区间数)。
Swift 没有内置 TreeMap,但可以用 SortedDictionary 的思路,或直接用普通字典 + 手动合并(配合有序 keys)。
题解代码分析
下面是 Swift 代码实现,并且是可运行的 Demo 模块。
import Foundation
class SummaryRanges {
private var intervals: [(Int, Int)] = []
init() {}
func addNum(_ val: Int) {
// 如果 intervals 为空,直接加
if intervals.isEmpty {
intervals.append((val, val))
return
}
var newStart = val
var newEnd = val
var merged: [(Int, Int)] = []
var inserted = false
for (start, end) in intervals {
if end + 1 < newStart {
// 当前区间完全在新数左边
merged.append((start, end))
} else if newEnd + 1 < start {
// 当前区间完全在新数右边
if !inserted {
merged.append((newStart, newEnd))
inserted = true
}
merged.append((start, end))
} else {
// 有交集或相邻,合并区间
newStart = min(newStart, start)
newEnd = max(newEnd, end)
}
}
if !inserted {
merged.append((newStart, newEnd))
}
intervals = merged
}
func getIntervals() -> [[Int]] {
return intervals.map { [$0.0, $0.1] }
}
}
// Demo
let summaryRanges = SummaryRanges()
summaryRanges.addNum(1)
print(summaryRanges.getIntervals()) // [[1, 1]]
summaryRanges.addNum(3)
print(summaryRanges.getIntervals()) // [[1, 1], [3, 3]]
summaryRanges.addNum(7)
print(summaryRanges.getIntervals()) // [[1, 1], [3, 3], [7, 7]]
summaryRanges.addNum(2)
print(summaryRanges.getIntervals()) // [[1, 3], [7, 7]]
summaryRanges.addNum(6)
print(summaryRanges.getIntervals()) // [[1, 3], [6, 7]]
代码解析
-
数据结构选择
用一个有序的[(start, end)]
元组数组intervals
存储区间,每次插入新数字时,按顺序扫描并决定合并还是插入。 -
插入逻辑
- 如果新区间完全在某个区间左侧并且不相连,直接放进合并结果;
- 如果完全在右侧并且不相连,先放入新数字区间再放现有区间;
- 如果有交集或相邻,更新
newStart
和newEnd
进行合并。
-
返回结果
getIntervals()
直接 O(k) 遍历区间数组并转成二维数组返回。
示例测试及结果
运行上面的 Demo,会输出:
[[1, 1]]
[[1, 1], [3, 3]]
[[1, 1], [3, 3], [7, 7]]
[[1, 3], [7, 7]]
[[1, 3], [6, 7]]
这与题目中的预期完全一致。
时间复杂度
addNum
:O(k),k 为区间数(最坏情况下 k≈n,但通常 k << n);getIntervals
:O(k);- 对于数据流比较稀疏的情况,性能非常好。
空间复杂度
- 额外存储
intervals
数组,最多存储 n 个不相交区间,空间复杂度 O(n)。
总结
这道题虽然名字看起来很抽象,但在很多实际场景里都有用武之地,比如:
- 实时统计用户的连续签到区间;
- 日志中连续时间片段的合并;
- 实时视频或音频帧缺失检测。
用 Swift 实现时,我们不需要复杂的平衡树结构,也能通过有序数组+一次扫描的方式做到高效插入和查询。在数据流中区间合并的问题,关键是保持区间有序性和及时合并,这样才能保证后续查询快速、结构清晰。