原文链接: https://leetcode-cn.com/problems/data-stream-as-disjoint-intervals
英文原文
Given a data stream input of non-negative integers a1, a2, ..., an
, summarize the numbers seen so far as a list of disjoint intervals.
Implement the SummaryRanges
class:
SummaryRanges()
Initializes the object with an empty stream.void addNum(int val)
Adds the integerval
to the stream.int[][] getIntervals()
Returns a summary of the integers in the stream currently as a list of disjoint intervals[starti, endi]
.
Example 1:
Input ["SummaryRanges", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals"] [[], [1], [], [3], [], [7], [], [2], [], [6], []] Output [null, null, [[1, 1]], null, [[1, 1], [3, 3]], null, [[1, 1], [3, 3], [7, 7]], null, [[1, 3], [7, 7]], null, [[1, 3], [6, 7]]] Explanation SummaryRanges summaryRanges = new SummaryRanges(); summaryRanges.addNum(1); // arr = [1] summaryRanges.getIntervals(); // return [[1, 1]] summaryRanges.addNum(3); // arr = [1, 3] summaryRanges.getIntervals(); // return [[1, 1], [3, 3]] summaryRanges.addNum(7); // arr = [1, 3, 7] summaryRanges.getIntervals(); // return [[1, 1], [3, 3], [7, 7]] summaryRanges.addNum(2); // arr = [1, 2, 3, 7] summaryRanges.getIntervals(); // return [[1, 3], [7, 7]] summaryRanges.addNum(6); // arr = [1, 2, 3, 6, 7] summaryRanges.getIntervals(); // return [[1, 3], [6, 7]]
Constraints:
0 <= val <= 104
- At most
3 * 104
calls will be made toaddNum
andgetIntervals
.
Follow up: What if there are lots of merges and the number of disjoint intervals is small compared to the size of the data stream?
中文题目
给你一个由非负整数 a1, a2, ..., an
组成的数据流输入,请你将到目前为止看到的数字总结为不相交的区间列表。
实现 SummaryRanges
类:
SummaryRanges()
使用一个空数据流初始化对象。void addNum(int val)
向数据流中加入整数val
。int[][] getIntervals()
以不相交区间[starti, endi]
的列表形式返回对数据流中整数的总结。
示例:
输入: ["SummaryRanges", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals", "addNum", "getIntervals"] [[], [1], [], [3], [], [7], [], [2], [], [6], []] 输出: [null, null, [[1, 1]], null, [[1, 1], [3, 3]], null, [[1, 1], [3, 3], [7, 7]], null, [[1, 3], [7, 7]], null, [[1, 3], [6, 7]]] 解释: SummaryRanges summaryRanges = new SummaryRanges(); summaryRanges.addNum(1); // arr = [1] summaryRanges.getIntervals(); // 返回 [[1, 1]] summaryRanges.addNum(3); // arr = [1, 3] summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3]] summaryRanges.addNum(7); // arr = [1, 3, 7] summaryRanges.getIntervals(); // 返回 [[1, 1], [3, 3], [7, 7]] summaryRanges.addNum(2); // arr = [1, 2, 3, 7] summaryRanges.getIntervals(); // 返回 [[1, 3], [7, 7]] summaryRanges.addNum(6); // arr = [1, 2, 3, 6, 7] summaryRanges.getIntervals(); // 返回 [[1, 3], [6, 7]]
提示:
0 <= val <= 104
- 最多调用
addNum
和getIntervals
方法3 * 104
次
进阶:如果存在大量合并,并且与数据流的大小相比,不相交区间的数量很小,该怎么办?
通过代码
高赞题解
二分查找 + 朴素维护区间
一个朴素的做法是直接使用 ArrayList
(数组)来维护所有的不相交区间,然后调用 addNum
时先通过二分找到当前值相邻的区间,然后根据题意进行模拟即可。
同时为了简化复杂的分情况讨论,起始时我们可以先往 ArrayList
添加两个哨兵分别代表正无穷和负无穷。
代码(不使用哨兵的代码见 $P2$):
class SummaryRanges {
List<int[]> list = new ArrayList<>();
int[] head = new int[]{-10, -10}, tail = new int[]{10010, 10010};
public SummaryRanges() {
list.add(head);
list.add(tail);
}
public void addNum(int val) {
int n = list.size();
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (list.get(mid)[0] <= val) l = mid;
else r = mid - 1;
}
int[] cur = new int[]{val, val};
int[] prev = list.get(r);
int[] next = list.get(r + 1);
if ((prev[0] <= val && val <= prev[1]) || (next[0] <= val && val <= next[1])) {
// pass
} else if (prev[1] + 1 == val && val == next[0] - 1) {
prev[1] = next[1];
list.remove(next);
} else if (prev[1] + 1 == val) {
prev[1] = val;
} else if (next[0] - 1 == val) {
next[0] = val;
} else {
list.add(r + 1, cur);
}
}
public int[][] getIntervals() {
int n = list.size();
int[][] ans = new int[n - 2][2];
int idx = 0;
for (int i = 1; i < n - 1; i++) ans[idx++] = list.get(i);
return ans;
}
}
class SummaryRanges {
List<int[]> list = new ArrayList<>();
public void addNum(int val) {
int n = list.size();
if (n == 0) {
list.add(new int[]{val, val});
return ;
}
int l = 0, r = n - 1;
while (l < r) {
int mid = l + r + 1 >> 1;
if (list.get(mid)[0] <= val) l = mid;
else r = mid - 1;
}
int[] cur = list.get(r);
if (cur[0] > val) {
if (val + 1 == cur[0]) {
cur[0] = val;
} else {
list.add(r, new int[]{val, val});
}
return ;
}
if (cur[0] <= val && val <= cur[1]) {
// pass
} else if (r == n - 1) {
if (cur[1] + 1 == val) {
cur[1] = val;
} else {
list.add(new int[]{val, val});
}
} else {
int[] next = list.get(r + 1);
if (cur[1] + 1 == val && val == next[0] - 1) {
cur[1] = next[1];
list.remove(r + 1);
} else if (cur[1] + 1 == val) {
cur[1] = val;
} else if (next[0] - 1 == val) {
next[0] = val;
} else {
list.add(r + 1, new int[]{val, val});
}
}
}
public int[][] getIntervals() {
int n = list.size();
int[][] ans = new int[n][2];
for (int i = 0; i < n; i++) ans[i] = list.get(i);
return ans;
}
}
- 时间复杂度:令 $m$ 为不交区间个数,
addNum
操作中查询相邻区间的复杂度为 $O(\log{m})$,维护不相交区间时,由于需要针对 $list$ 的索引位置进行元素插入和删除,复杂度为 $O(m)$,整体复杂度为 $O(m)$;getIntervals
操作需要遍历所有的 $list$ 元素,复杂度为 $O(m)$ - 空间复杂度:$O(m)$
二分查找 + 优化维护区间
解法一中虽然在 addNum
使用到了复杂度诶 $O(\log{m})$ 的二分查找,但在维护区间时,由于要对 $list$ 进行针对索引的插入和删除,导致整体复杂度为 $O(m)$。
我们期望找到一种插入/删除操作复杂度同样为 $O(\log{m})$ 的方式来维护区间,这引导我们使用 TreeSet
(红黑树)等数据结构。
具体做法不变,仍然是先使用「二分」(TreeSet
自带的 floor/ceiling
)来找到当前值的相邻区间,
同时为了简化复杂的分情况讨论,起始时我们可以先往 TreeSet
添加两个哨兵分别代表正无穷和负无穷,以确保调用 floor/ceiling
时不会返回空。
代码:
class SummaryRanges {
TreeSet<int[]> ts = new TreeSet<>((a, b)->a[0]-b[0]);
int[] head = new int[]{-10, -10}, tail = new int[]{10010, 10010};
public SummaryRanges() {
ts.add(head);
ts.add(tail);
}
public void addNum(int val) {
int[] cur = new int[]{val, val};
int[] prev = ts.floor(cur);
int[] next = ts.ceiling(cur);
if ((prev[0] <= val && val <= prev[1]) || (next[0] <= val && val <= next[1])) {
// pass
} else if (prev[1] + 1 == val && next[0] - 1 == val) {
prev[1] = next[1];
ts.remove(next);
} else if (prev[1] + 1 == val) {
prev[1] = val;
} else if (next[0] - 1 == val) {
next[0] = val;
} else {
ts.add(cur);
}
}
public int[][] getIntervals() {
// using ceiling
// int n = ts.size();
// int[][] ans = new int[n - 2][2];
// int[] cur = head;
// for (int i = 0; i < n - 2; i++) {
// ans[i] = ts.ceiling(new int[]{cur[0] + 1, cur[1] + 1});
// cur = ans[i];
// }
// return ans;
// using iterator
int n = ts.size();
int[][] ans = new int[n - 2][2];
Iterator<int[]> iterator = ts.iterator();
iterator.next();
for (int i = 0; i < n - 2; i++) ans[i] = iterator.next();
return ans;
}
}
- 时间复杂度:令 $m$ 为不交区间个数,
addNum
操作中「查询相邻区间」和「维护不相交区间」的复杂度为 $O(\log{m})$,整体复杂度为 $O(\log{m})$;getIntervals
操作需要获取有序集合中的所有元素,使用ceiling
复杂度为 $O(m\log{m})$。使用iterator
的话,由于TreeSet
底层同步维护了一个TreeMap
,迭代器由TreeMap
所提供,获取iterator
来遍历所有元素的复杂度为 $O(m)$ - 空间复杂度:$O(m)$
进阶
如果存在大量合并,并且与数据流的大小相比,不相交区间的数量很小,该怎么办?
即需要确保 addNum
的复杂度,而对 getIntervals
复杂度要求不高,上述解法就已经是这么做的了。
最后
如果有帮助到你,请给题解点个赞和收藏,让更多的人看到 ~ (“▔□▔)/
也欢迎你 关注我(公主号后台回复「送书」即可参与长期看题解学算法送实体书活动)或 加入「组队打卡」小群 ,提供写「证明」&「思路」的高质量题解。
所有题解已经加入 刷题指南,欢迎 star 哦 ~
统计信息
通过次数 | 提交次数 | AC比率 |
---|---|---|
21361 | 31477 | 67.9% |
提交历史
提交时间 | 提交结果 | 执行时间 | 内存消耗 | 语言 |
---|
相似题目
题目 | 难度 |
---|---|
汇总区间 | 简单 |
寻找右区间 | 中等 |
Range 模块 | 困难 |