【问题标题】:how to efficiently merge int ranges in a stream?如何有效地合并流中的 int 范围?
【发布时间】:2011-01-20 14:16:41
【问题描述】:

给定一个连续的整数范围流,如 [1,3]、[5,10]、[2,6]、... 当每个新范围出现时,我们需要检查现有范围,看看它是否与任何现有范围重叠,如果发现任何重叠,则删除所有重叠范围并插入合并范围。为此,我们需要一个有效的算法。请注意,范围一次出现一个,形成一个流......

在一次采访中被问到这个问题。想法?

目的是将重叠的范围合并为一个。例如,如果我们有 3 个按以下顺序排列的范围:[1,3]、[2,6]、[5,10]。然后我们先把前两个合并成[1,6],再和第三个合并成[1,10]。

【问题讨论】:

  • 您的问题不清楚。您希望输出是像 [1,2,3,5,6,7,8,9,10,2,3,4,5,6] 这样的单个流吗?
  • @Jim Mischel:我假设每个元组都提供了一个范围的开始和结束,因此,在第一个元组之后,结果范围将包括 [1,2,3]。在第二个元组之后,结果范围将是 [1,2,3,5,6,7,8,9,10](没有 4)。
  • 在我看来,可能的逻辑输出应该是合并重叠范围的(有序?)范围列表
  • 目的是将重叠的范围合并为一个。例如,如果我们有 3 个按以下顺序排列的范围:[1,3]、[2,6]、[5,10]。然后我们先把前两个合并成[1,6],再和第三个合并成[1,10]。

标签: algorithm merge stream


【解决方案1】:

这个问题的标准算法是interval tree

【讨论】:

  • 谢谢。我只是看了一下区间树,虽然在查找新范围的重叠条目时只需要 O(logn) 时间,但插入新范围可能会与树中的许多条目重叠,导致它们都被删除。删除这些重叠条目并将树旋转到平衡状态的成本将是不平凡的。还是我错过了什么?
  • 是的,插入新范围可能会导致删除许多其他范围,但您可以先将该成本与插入这些其他范围的成本分摊。我没有考虑过保持树平衡,但我想有一种方法可以做到这一点,比如 AVL 或红黑树。
【解决方案2】:

当一个新的元组出现在(newstart,newend) 中时,在现有的结束元素列表中执行newstart-1 的二分查找,同样在现有的开始元素列表中查找newend+1

与任何匹配范围合并。

如果没有范围匹配,则在两个最接近的范围之间插入。

更新:暂且不提,我正在解决错误的问题:合并接触范围。但重叠范围解不会太不相似。

  1. 二进制搜索最大的现有起始元素,其中start(n) <= newstart
  2. 二进制搜索最小的现有结束元素,其中end(n) >= newstart
  3. 如果两者返回相同的索引,则您的元组开始可与第 n 个条目合并,请将 newstart 替换为 start(n)
  4. 二进制搜索最大的现有起始元素,其中start(m) <= newend
  5. 二进制搜索最小的现有结束元素,其中end(m) >= newend
  6. 如果两者返回相同的索引,则您的元组结尾可与第 m 个条目合并,请将 newend 替换为 end(m)
  7. 将第 n 个和第 m 个索引之间的所有条目替换为 (newstart,newend) 元组。

【讨论】:

  • 这和我面试时的回答差不多。这样做的问题是它会在第 7 步逐渐创建重复条目,随着时间的推移会导致空间浪费。
  • @Robin 重复条目?在哪里?
  • 二分查找是log(n),但是合并区间是什么数据结构。删除间隔的数组需要 O(n)。链表不能用于二分搜索。
【解决方案3】:

您可以将范围表示为交替的“开”和“关”点的单个序列。每当新范围到达时,就会搜索其起点和终点,并采取适当的合并或插入操作。

为了获得所需操作(搜索、插入和删除)的良好性能,可能会在此处使用 B 树之类的东西。

【讨论】:

    【解决方案4】:

    间隔树服务器的目的很好,但这是我用来添加范围的另一种方法。

    假设要添加新元组(a rane)的列表为空或没有重叠范围。其次,输入的形式为 a, b 其中 a

    令 rangeList 为当前范围列表。例如。 [1, 4, 6, 10, 12, 14] 表示 [(1, 4), (6, 10), (12, 14)] 的范围列表

    1. 如果列表为空,只需将元组的元素插入列表中即可完成
    2. 使用二分搜索查找列表中 a、b 元素的位置。 (如果该元素不存在,则返回大于搜索数字的最小元素的位置)
    3. 让返回的位置分别为元组元素 a、b 的 pos_a、pos_b。
    4. 如果 pos_a 是偶数,则 list_A = [a]
    5. 如果 pos_b 是偶数,则 list_B = [b]
    6. 新列表 = rangeList[0:pos_a] + list_A + list_B + rangeList[b:end] 就完成了。

    通过将元组列表转换为单个列表,我们无需将新元素与 2 个不同的列表进行比较。因此很容易找到它的位置并通过检查奇数或偶数来告诉我们它是否位于现有范围之间

    def subroutine(rangeList, currTuple)
    """
    rangeList: Existing list of non overlapping tuples
    currTuple: The range tuple to be added
    """
        if not rangeList:
            rangeList.extend(currTuple)
        else:
            a, b = binSearch(currTuple, rangeList)
            list_changed = rangeList[0:a]
            if a%2 == 0:
                list_changed.append(currTuple[0])
            if b%2 == 0:
                list_changed.append(currTuple[1])
            list_changed.extend(rangeList[b:])
        return list_changed
    

    【讨论】:

      【解决方案5】:

      由于间隔是流式传输的,它们必须保存在某种有效的集合中

      • 插入新间隔
      • 寻找重叠区间
      • 在添加新区间时合并重叠区间

      合并操作需要适应以下情况

      • 无重叠(7, 9)插入[(1, 5), (10, 15)]得到[(1,5), (7, 9), (10,15)]

      • 一些重叠(4, 6)插入[(1, 5), (10, 15)]得到[(1,6), (10,15)]

      • 桥重叠(4, 11)插入[(1, 5), (10, 15), (20, 30)]得到[(1, 15), (20, 30)]

      • 最高重叠(1, 20)插入[(2, 5), (7, 9), (13, 15)]得到[(1, 20)]

      • 完全重叠(5, 6)插入[(1, 7), (10, 19)]得到[(1, 7), (10, 19)]

      • 以及许多中间情况和上述情况的变体

      在插入操作结束时,此集合必须仅包含非重叠间隔。很明显,保持这些非重叠区间在此集合中排序对于有效地查找和合并重叠区间是必要的。任何未排序的解决方案都需要在插入任何内容时搜索每个间隔,然后在每次发生合并时重复该过程。记住元素必须保持排序,任何类型的散列或任何无序的数据结构也对我们没有帮助。我们可以在这里做的最好的事情是O(nlgn)

      使用排序数组(或其数组列表或向量变体),一种方法是对新插入的区间的开始进行二分搜索,然后左右搜索并在必要时合并。这将导致O(nlgn) 搜索放置元素的位置,但是在数组中间插入并从数组中删除元素需要在插入或合并发生时重新索引数组中的每个元素。这种操作成本太高,不能很好地扩展,也违背了保持元素排序的目的,降低了天真的未排序解决方案的性能。

      解决插入/删除问题的一种方法是使用 排序的链表,其中插入和删除的成本很低,但是如果不是不可能的话,这会使二分查找效率非常低,并且还会破坏目的是保持元素排序,再次降低天真的未排序解决方案的性能。

      值得研究维基百科中定义的interval trees,但是这个数据结构不合并区间,它只能查询它们,所以这也不支持我们的用例。

      最好的方法是使用二叉树来存储区间,其中区间有一个定义的比较器方法,如果有任何重叠,则返回元素相等。这使得在O(lgn) 时间查找重叠间隔非常有效。

      一个示例比较器类如下所示

      public class Interval<T extends Comparable<T>> implements Comparable<Interval<T>>
      {
          private final T start_;
          private final T end_;
      
          public Interval(final T start, final T end)
          {
              this.start_ = start;
              this.end_ = end;
          }
      
          @Override 
          public int compareTo(Interval other) {
              if (other.start_.compareTo(this.end_) == -1) { 
                  return -1;
              } else if (other.start_.compareTo(this.end_) == 1) { 
                  return 1; 
              } else { 
                  return 0;
              }
          }
      }
      

      可以扩展或组合树集(或集合或其任何变体)以适应插入操作。请注意,下面我使用的是 treemap 变体,因为 java 不允许返回集合中的元素。

      public class IntervalCollection
      {
          private Map<Interval, Interval> intervalCollection_ = new TreeMap<>();
      
          public void insert(Interval interval)
          {
              while (intervalCollection_.containsKey(interval)) {
      
                  Interval other = intervalCollection_.get(interval);
                  intervalCollection_.remove(interval);
      
                  interval = new Interval(Math.min(interval.start_, other.start_),
                      Math.max(interval.end_, other.end_));
              }
      
              intervalCollection_.put(interval, interval);
          }
      }
      

      此集合使用手头语言的力量为您完成大部分繁重的工作,并为流间隔提供有效的插入和合并。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2016-02-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-12-09
        • 1970-01-01
        相关资源
        最近更新 更多