加载中...
352-将数据流变为多个不相交区间(Data Stream as Disjoint Intervals)
发表于:2021-12-03 | 分类: 困难
字数统计: 2.2k | 阅读时长: 11分钟 | 阅读量:

原文链接: 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 integer val 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 to addNum and getIntervals.

 

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
  • 最多调用 addNumgetIntervals 方法 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 模块 困难
上一篇:
350-两个数组的交集 II(Intersection of Two Arrays II)
下一篇:
354-俄罗斯套娃信封问题(Russian Doll Envelopes)
本文目录
本文目录