找到包含给定时间点的时间间隔。

6

我正在尝试解决以下问题:给定N个时间区间,每个区间都指定为(开始时间,结束时间),不重叠,并按照开始时间排序 - 找到包含给定日期的区间。例如,给定:

[1,4] [5,8] [9,10][11,20]

3落入第一个区间,15落入第四个区间等。

到目前为止我有以下基本想法:

  1. 我们可以使用二分查找来找到相应的区间(Log N)
  2. 由于可能只有少数几个区间很大,其余小,因此按照它们的持续时间对区间进行排序可能是值得的。然后,从统计学上讲,大多数情况下我们会“命中”最长的区间(O(1)),只有有时会导致最坏情况复杂度为N。

我在考虑是否有结合这两种方法的空间。另一个想法是根据持续时间排序并将所有区间插入树中,比较开始日期。在最坏的情况下,当最长的持续时间按时间顺序排列时,这种方法和2的性能相同。

我想象中的理想解决方案是拥有一个树(或类似的数据结构),其中包含最长的区间,然后两个分支就会有接下来的两个最长的区间等等。然而,我看不到在该树中分支的方法,即由于我们根据长度进行显式假设,因此我们无法真正丢弃树的左侧或右侧。

任何评论都将不胜感激。


2
我会坚持第一种方法。 - John Dvorak
1
如果您认为您的分布是正确的,请查看Splay树。 - John Dvorak
所以我描述的综合方法 - Bober02
我在考虑这种方法:从这棵树开始,然后将其转换为AVL树。 - John Dvorak
我可能可以这样做,但那样我就只能变成简单的值数组,而且没有“长度”信息。 - Bober02
显示剩余2条评论
6个回答

4
一个天真的方法是将两种方法结合起来,这在大多数情况下提供了 O(1),最坏情况下为 O(logN),但是在大 O 表示法中隐藏的常数将会翻倍:
  1. 让原始数组为 intervals
  2. 创建第二个数组,按建议按区间长度降序排序。让它成为 lengths
  3. 对于每个查询,使用二分搜索在 intervals 和线性搜索在 lengths 中进行搜索。搜索将同时进行1,第一个完成的 - 也会导致另一个停止。

因为在步骤 3 中,我们可能在 intervals 中做高达 c*log(n) 步的二分查找,因此我们总共最多会做高达 2*c*log(n) 步。如果我们在 lengths 中更快地找到元素,它将导致二分搜索在中途结束,并且我们将获得减少的操作次数(但是常数会翻倍)。


(1) 不需要并行计算机,可以通过在每个步骤中搜索来实现,在单个线程上模拟并行计算,直到找到答案。(一般信息,不需要理解答案:S.Even、A.Itai和A.Shamir在他们的文章On the Complexity of TimeTable and Multi Commodity Flow Problems中介绍了“并行搜索”的概念)


1
不错的方法;+1 但我认为我的答案也值得关注。 - John Dvorak
你如何“停止”并行运行的两种方法?您只需调用Thread.interrupt吗? - Bober02
@Bober02 不行。你需要通过手动交替执行两个搜索步骤来模拟多线程。 - John Dvorak

3
你可以对二叉搜索树中的某些节点进行优先处理,同时保持其平衡性:
首先采用综合方法(将较长的区间放置在靠近顶部的位置),并使用一系列旋转来使二叉搜索树保持平衡(虽然会破坏某些优先级,但如果它们不破坏平衡,则应保留高优先级节点靠近根)。
为了平衡一棵树,请尝试以下策略(未经测试):
- 平衡左半部分(根的子树) - 平衡右半部分 - 如果右半部分比左半部分高出1以上(AVL平衡条件) - 当这个条件成立时 - 如果右-左四分之一(右半部分的左半部分)高于右-右四分之一,则将右子树向右旋转(开始一个AVL双旋转) - 将树向左旋转(完成一个AVL旋转) - 平衡左半部分(两个左四分之一已经平衡-从#3开始) - 否则,如果左半部分比右半部分高出1以上 - 进行其他情况的相反操作
这应该会产生一个AVL平衡树,尽可能地遵守原始优先级。

嗯,这很有趣,但是 splay 树的想法也很诱人:在大多数情况下,如果我将某个间隔优先级提升到树中,那么许多后续日期就会落入此间隔。因此,splay 树旋转将被摊销,并且平均而言,它应该为O(1)。 - Bober02
@Bober02 我认为伸展树有点像多动症 :-) - John Dvorak
1
这是一个非常好的方法,但是由于缓存不佳,我通常不喜欢将树用于常量数据。它是否可以以数组的形式实现(类似于我们如何实现二叉堆)?(无论如何,感谢这个好方法) - amit
@amit 你可以在普通列表的基础上构建一棵树(通过存储索引而不是内存指针)。这样,你就完全控制分配和缓存。请注意,能够重新排序堆上的元素的语言很可能在树节点的内存组织方面胜过你。 - John Dvorak
@amit 你可以使用隐式索引,但是树旋转最终会变得代价高昂 - 或者你可以在编译期间使用显式索引,然后将其复制到具有隐式索引的数组中(防止(有效)进一步修改,但启用更紧凑的表示)。请注意,我不认为数组堆(具有2n + 1; 2n + 2索引)具有高缓存效率。 - John Dvorak
显示剩余2条评论

0

我使用JodaTime和TimeSlots实现了类似的功能(几乎等同于Joda Period)。 Period是我的自定义类,具有DateTime类型的开始和结束时间。 我持有一个按结束日期排序的Periods TreeSet。

TimeSlot类:

public class TimeSlot<T> {
    private DateTime start;
    private DateTime end;

    // accessors
    ...
}

实用方法:

public TimeSlot<T> getTimeSlot(DateTime pointInTime) {
    for (TimeSlot<T> ts : getCounterTimeSlotSet()) {
        if (ts.getPeriod().isDateInRange(pointInTime)) {
            return ts;
        }
    }
    return null;
}


public boolean isDateInRange(DateTime date) {
    if (date == null) {
        return false;
    }

    return date.isAfter(this.start.minusMillis(1)) 
        && date.isBefore(this.end.plusMillis(1));
}

最近我发现了一个实现艾伦区间代数, 你可能会觉得它很有用。


这确实是一个非常简单和直观的东西(在我看来)。 - aviad

0

你可能是对的,二分查找在这种奇怪的分布下不是最优的。尽管如此,当N = 10亿时,log_2(N)只有大约30。

如果要搜索的列表中的每个元素具有相等的可能性,则元素的二分查找是最优的。

因此,在这里使用多级二分查找可能是理想的,其中每个级别中的间隔大小大致相同。使用给定的示例:

First Level: [1-10] [11-20] # Size = 10
Second Level: [1-10] = [1,4] [5,8] [9,10] # Size = 4

如果在11到20之间,则完成,否则扩展[1-10]并检查间隔[1,4] [5,8] [9,10]。

设置搜索结构的额外成本考虑O(N log(N))以上,并且对于小间隔来说更加昂贵,但是如果最大间隔大小和最小间隔之间有足够大的差距,则平均搜索时间应该相当不错。

在最坏的情况下,您将拥有log(N)级别。


0

可能可以将两个结合在一起。这是我的想法,

根据范围长度构建二叉树,假设我们有以下范围,

a:[0,1),b:[1,2),c:[3,10),d:[11,12),e:[13,14)

我们从底部构建树,根据范围大小组合叶子节点,因此第一轮我们可以得到内部叶子节点,

(a,b),c,(d,e)

然后,

(a,b,c),(d,e)

根将是,

(a,b,c,d,e)

在每一轮中,我们组合范围长度最小的节点,记住树的深度。

每个节点指向左、右子节点,并保留子节点的最小值和最大值。

如果有任何错误,请指出。


这是错误的 - 区间不能合并,否则就没有问题了,我只需找到第一个区间的开始和最后一个区间的结束,并构建新的区间(开始,结束)。不需要任何树。 - Bober02
不是真的将它们合并,一个节点只是指示叶子的最小和最大值,您需要遍历树到叶子以找到所需的区间。 - Qiang Jin
这种情况不会发生,首先需要对原始节点进行排序,这是我忘记提到的。 - Qiang Jin
树的形状将会是((((1, 2), (5, 6)), (100, 102)), (1000, 2000)。优点在于大范围更靠近根部,这意味着可以更快地找到它。 - Qiang Jin
没关系,我完全不理解你的回答,而且我认为仅仅按长度排序并插入到TreeSet中不会带来任何额外的东西。如果您愿意,可以尝试用具体、复杂的示例重新表述您的答案,展示复杂度是什么。 - Bober02
显示剩余2条评论

0
我会这样做: 使用第一个和最后一个间隔的数据(平均持续时间和次数),估计在统计上你期望时间所处的间隔。如果该间隔不包含目标,则使用估计的间隔重新执行估计,其中一个端点将成为数组的结束(另一个端点将是最接近的实际结束)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接