可能的面试问题:如何找到所有重叠的区间

77

这不是一个典型的面试问题,因为我在项目中遇到了这个问题,但我认为这可能是一个不错的面试问题。

给定N对区间,每个区间由整数表示。你需要在O(N)时间内找出所有有重叠的区间。例如,如果给出以下区间:

{1, 3} {12, 14} {2, 4} {13, 15} {5, 10}

答案是 {1, 3}, {12, 14}, {2, 4}, {13, 15}。请注意,结果无需按顺序分组。

我之所以提到O(N)时间复杂度,是因为KMP算法在字符串搜索时需要O(N)时间复杂度。:D

目前我能想到的最好的解决方法是O(N^2)。这很低效,但没有人抱怨,所以我不会重新设计它。:P 不过,我还是很好奇是否有更优雅的解决方案。


8
有两点不太清楚:(1)你说“N对间隔”,但我相当确定你实际上指的是“N个间隔”,因为如果只有N,所有重叠可以在O(N)中被轻易地找到 :-P 假设N = 区间数:(2)无法在O(N)时间内报告所有重叠对,因为可能有O(N^2)个!然而,要求O(N)大小的集合中的所有间隔至少与一个其他间隔重叠是合理的。这是你要求的吗? - j_random_hacker
3
gbenison的回答(https://dev59.com/sG855IYBdhLWcg3wGQJM#9775727)是目前这9个答案中唯一以O(nlog n)时间复杂度回答问题的。请考虑将该答案标记为正确答案。 - j_random_hacker
2
很有趣,因为我曾经参加了亚马逊的面试,他们问了我一个类似的问题... - AJ Meyghani
1
@stackoverflowuser2010:主要问题在于问题表述非常糟糕,正如我在第一条评论中写的那样。如果按字面意思解释,它没有解决方案,因此回答者已经(合理地)寻找了具有类似解决方案的问题。如果我们将marcog的说法“我们可以找到哪些区间与哪些区间重叠…”解释为列出所有重叠区间对,则这与他后来声称的“这是O(N logN)解决方案”相矛盾——可能存在O(n^2)对,没有算法能在O(n log n)时间内列出所有这些对。 - j_random_hacker
@stackoverflowuser2010:对问题的这种解释是gbenison首先明确并解决的。而marcog的答案则忽略了澄清事情,然后说通过一些额外的簿记,你可以“找到哪些区间与哪些区间重叠”,这对我来说显然意味着所有区间对——然后声称需要O(n log n)时间。但是找到这些对需要O(n^2)时间。 - j_random_hacker
显示剩余3条评论
10个回答

104

将区间的端点放入数组中,并标记它们是起点还是终点。通过按照以下方式排序来打破平局:如果区间是闭合的,则先放置终点,然后放置起点;如果区间是半开的,则先放置起点,然后放置终点。

1S, 2S, 3E, 4E, 5S, 10E, 12S, 13S, 14E, 15E

然后遍历该列表,记录我们目前所处的区间数(这等同于处理的起始点数量减去结束点数量)。每当我们在已经在一个区间内时遇到一个起始点时,这意味着我们必须有重叠的区间。

1S, 2S, 3E, 4E, 5S, 10E, 12S, 13S, 14E, 15E
    ^                          ^
   overlap                    overlap

我们可以通过将数据存储在端点旁边并跟踪我们所处的区间来找到哪些区间重叠。

这是一个O(N logN)的解决方案,其中排序是主要因素。


8
根据区间的定义,通过将终点放在起点之前来打破联系。如果它们是半开放的,则{1,2}和{2,3}不重叠。如果它们是闭合区间,则存在重叠。问题没有指定具体是哪种情况。 - Steve Jessop
6
不确定,但是这个算法真的是O(nlogn)吗?如果你需要返回哪些区间相互重叠,它似乎更像是O(n^2)。当所有区间重叠时(例如{1,8},{2,7},{3,6},{4,5}),解决方案中有O(n^2)个区间。 - Gruber
@Gruber:我认为你是对的。不过,如果我们只想要与某些其他区间重叠的O(N_intervals)大小的区间集,我们可以通过再次运行算法,但从末尾向后运行,并将其与第一次运行的结果取并集来实现。我们还必须在进行操作时检查顶级区间。为什么?如果一个区间X与另一个区间Y重叠,则至少有以下一种情况成立:Y的起点在X之前(X在第一阶段被捕获);Y的结束点在X之后(X在第二阶段被捕获);Y完全包含在X中且X处于顶层 - j_random_hacker
@Gruber:看看gbenison的可爱回答,有一个不同的方法。 - j_random_hacker
2
@faizan 原帖提问不够清晰,导致了混淆。正如 j_random_hacker 在他的评论中所说:“在 O(N) 时间内报告所有重叠对是不可能的,因为可能有 O(N^2) 个!然而,要求所有与至少一个其他区间重叠的区间的 O(N) 大小集合是合理的。” marcog 和 gcbenison 都用 O(N log N) 回答了第二个问题。 - antoine
显示剩余6条评论

34

按起点对区间进行排序。然后折叠这个列表,如果相邻的两个区间重叠(即 (1,4),(2,6) -> (1,6)),则将它们合并。最终结果列表中包含那些没有重叠项的区间。从原始列表中筛选掉这些区间。

在初始排序操作之后,该算法的时间复杂度是线性的,可以使用任何 (n log n) 算法进行排序。不确定如何规避这一问题。即使是最后的“筛选重复项”操作,如果利用了输入和第一次操作的已排序顺序,则复杂度也是线性的。

以下是 Haskell 实现:

-- Given a list of intervals, select those which overlap with at least one other inteval in the set.
import Data.List

type Interval = (Integer, Integer)

overlap (a1,b1)(a2,b2) | b1 < a2 = False
                       | b2 < a1 = False
                       | otherwise = True
                                     
mergeIntervals (a1,b1)(a2,b2) = (min a1 a2, max b1 b2)
                                     
sortIntervals::[Interval]->[Interval]
sortIntervals = sortBy (\(a1,b1)(a2,b2)->(compare a1 a2))

sortedDifference::[Interval]->[Interval]->[Interval]
sortedDifference [] _ = []
sortedDifference x [] = x
sortedDifference (x:xs)(y:ys) | x == y = sortedDifference xs ys
                              | x < y  = x:(sortedDifference xs (y:ys))
                              | y < x  = sortedDifference (x:xs) ys

groupIntervals::[Interval]->[Interval]
groupIntervals = foldr couldCombine []
  where couldCombine next [] = [next]
        couldCombine next (x:xs) | overlap next x = (mergeIntervals x next):xs
                                 | otherwise = next:x:xs
                                               
findOverlapped::[Interval]->[Interval]
findOverlapped intervals = sortedDifference sorted (groupIntervals sorted)
  where sorted = sortIntervals intervals
                                     
sample = [(1,3),(12,14),(2,4),(13,15),(5,10)]

这实际上是我在这里看到的唯一一个能够确实找到所有与其他区间重叠的区间,并在O(nlog n)时间内完成的答案。(marcog的算法是一个开始,但实际上是O(n ^ 2)。)我喜欢“减去”组合区间(其中包括所有不与任何其他东西重叠的区间),以找到重叠的区间的想法。 - j_random_hacker
1
我得说,我通常有点慢,但我认为我没有完全理解这个解决方案。 您确定这个解决方案能够找到所有可能的重叠区间对吗?我还注意到在您的解决方案标题中,有以下评论:
  • 给定一组区间列表,选择与集合中至少一个其他区间重叠的区间。 这与所提出的问题不同。我在这里错过了什么吗?
- Pa_
我认为你的“overlap”条件可以简化为overlap (_, b1) (a2,_) | b1 > a2 = True | otherwise = False或者只是:overlap (_, b1) (a2,_) = b1 > a2,假设a1已经排序。否则,只需overlap (_, b1) (a2,_) = (b1>a2) && (a1<a2) - ssm

10

针对线段问题的标准方法是按起点排序,然后从第一个开始走到最后。 O(n*logn)(如果已经排序则为O(n)

end = 0;
for (current in intervals) {
    if current.start < end {
        // there's an intersection!
        // 'current' intersects with some interval before it
        ...
    }
    end = max(end, current.end)
}

这只是解决方案的一小部分。你忘记了可能存在独立的重叠区间集合。例如:{0, 5},{1, 6},{40, 45},{41, 46}。 - Ilya Kogan
1
@Nikita,所以你只提供了部分解决方案,剩下的就留给想象力了吗? :) - jbx
@Ilya 那又怎样?两个集合都将被检测到。 - Nikita Rybak
@Nikita 如果每次你检查“current”是否与其之前的某个区间相交,那么复杂度将为O(N^2),因为对于每个区间,你都要检查它之前的所有区间。 - Ilya Kogan
1
在伪代码注释中,它并没有说“让我们检查语句X”,而是说“语句X为真”。 - Nikita Rybak
显示剩余4条评论

5
不确定O(N)的情况,但如果我们首先按每个元组中第一个数字对它们进行排序,然后依次查找那些元组中第一个数字大于先前元组中最大数字的元组,并且与下一个元组不重叠。
所以您首先会得到:
{1,3},{2,4},{5,10},{12,14},{13,15}
因为4(最大值)<5并且10<12,因此{5,10}是孤立的。
这意味着我们要跟踪遇到的最大数字,并且每次我们找到起始数字更大的元组时,我们检查它是否与下一个元组重叠。
这取决于排序算法的效率,因为后续过程将是O(N)。

不是很简单。你的算法会说这里的最后两个区间是“孤立”的:{1,10} {2,3} {4,5} {6,7}。 - Nikita Rybak
你说得对...我们需要跟踪最大的第二个数字。 - jbx

2
假设起点和终点之间的差距很小,比如说小于32,例如1..32。那么每个区间都可以被写成一个32位单词中的一位模式。例如:[1,2] → 001;[2,3] → 010;[1,3] → 011;[2,3,4] → 110。两个区间(或区间的组合)重叠,如果它们的按位AND非零。例如:[1,2] 与 [1,3] 重叠,因为001&011 == 001,非零。O(n)算法是保持到目前为止看到的区间的运行按位OR,并且对每个新区间进行AND操作。
bitsSoFar = 0
for (n=0; n < bitPatterns.length; n++)
    if (bitPatterns[n] & bitsSoFar != 0)
        // overlap of bitPatterns[n] with an earlier pattern
        bitsSoFar |= bitPatterns[n]

作为一项练习:

  • 修改算法以识别位模式与后面的模式重叠

  • 在O(1)时间内找出区间的比特模式


2
以下是一个在Java中的O(N lg N)实现,它扩展了@Nikita Rybak提供的答案。
我的解决方案找到每个与至少另一个间隔重叠的间隔,并将它们都视为重叠间隔。例如,OP原始问题中的两个间隔(1,3)和(2,4)互相重叠,因此在这种情况下有2个重叠的间隔。换句话说,如果间隔A与间隔B重叠,则我将A和B都添加到重叠间隔的结果集中。
现在考虑间隔(1,100),(10,20)和(30,50)。我的代码将发现:
[ 10,  20] overlaps with [  1, 100]
[ 30,  50] overlaps with [  1, 100]

Resulting intervals that overlap with at least one other interval:
[  1, 100]
[ 30,  50]
[ 10,  20]

为了防止 (1, 100) 被计算两次,我使用了一个 Java 的 Set 来仅保留唯一的区间对象。
我的解决方案如下:
  1. 按起始点对所有区间进行排序。此步骤为 O(N lg N)
  2. 跟踪 intervalWithLatestEnd,即具有最新结束点的区间。
  3. 迭代排序列表中的所有区间。如果一个区间与 intervalWithLatestEnd 重叠,则将它们都添加到 Set 中。必要时更新 intervalWithLatestEnd。此步骤为 O(N)
  4. 返回 Set(如果需要,转换为 List)。
总运行时间为 O(N lg N)。它需要一个大小为 O(N) 的输出 Set。

实现

为了将区间添加到 Set 中,我创建了一个自定义 Interval 类,覆盖了 equals() 方法,如预期所示。
class Interval {
    int start;
    int end;
    Interval(int s, int e) { 
        start = s; end = e; 
    }

    @Override
    public String toString() {
        return String.format("[%3d, %3d]", start, end);
    }
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + start;
        result = prime * result + end;
        return result;
    }
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final Interval other = (Interval) obj;
        if (start != other.start)
            return false;
        if (end != other.end)
            return false;
        return true;
    }
}

以下是运行该算法的代码:

private static List<Interval> findIntervalsThatOverlap(List<Interval> intervals) {

    // Keeps unique intervals.
    Set<Interval> set = new HashSet<Interval>();

    // Sort the intervals by starting time.
    Collections.sort(intervals, (x, y) -> Integer.compare(x.start, y.start));

    // Keep track of the interval that has the latest end time.
    Interval intervalWithLatestEnd = null;

    for (Interval interval : intervals) {

        if (intervalWithLatestEnd != null &&
            interval.start < intervalWithLatestEnd.end) {

            // Overlap occurred.
            // Add the current interval and the interval it overlapped with.
            set.add(interval); 
            set.add(intervalWithLatestEnd);

            System.out.println(interval + " overlaps with " +
                               intervalWithLatestEnd);
        }

        // Update the interval with latest end.
        if (intervalWithLatestEnd == null ||
            intervalWithLatestEnd.end < interval.end) {

            intervalWithLatestEnd = interval;
        }
    }
    // Convert the Set to a List.
    return new ArrayList<Interval>(set);
}

测试用例

以下是一个测试用例,运行原始的间隔:

public static void testcase() {

    List<Interval> intervals = null;
    List<Interval> result = null;

    intervals = new ArrayList<Interval>();

    intervals.add(new Interval(1, 3));
    intervals.add(new Interval(12, 14));
    intervals.add(new Interval(2, 4));
    intervals.add(new Interval(13, 15));
    intervals.add(new Interval(5, 10));


    result = findIntervalsThatOverlap(intervals);
    System.out.println("Intervals that overlap with at least one other interval:");
    for (Interval interval : result) {
        System.out.println(interval);
    }
}

得到以下结果:

[  2,   4] overlaps with [  1,   3]
[ 13,  15] overlaps with [ 12,  14]
Intervals that overlap with at least one other interval:
[  2,   4]
[  1,   3]
[ 13,  15]
[ 12,  14]

最后,这是一个更高级的测试案例:

public static void testcase() {

    List<Interval> intervals = null;
    List<Interval> result = null;

    intervals = new ArrayList<Interval>();

    intervals.add(new Interval(1, 4));
    intervals.add(new Interval(2, 3));
    intervals.add(new Interval(5, 7));
    intervals.add(new Interval(10, 20));
    intervals.add(new Interval(15, 22));
    intervals.add(new Interval(9, 11));
    intervals.add(new Interval(8, 25));
    intervals.add(new Interval(50, 100));
    intervals.add(new Interval(60, 70));
    intervals.add(new Interval(80, 90));


    result = findIntervalsThatOverlap(intervals);
    System.out.println("Intervals that overlap with at least one other interval:");
    for (Interval interval : result) {
        System.out.println(interval);
    }
}

结果如下:

[  2,   3] overlaps with [  1,   4]
[  9,  11] overlaps with [  8,  25]
[ 10,  20] overlaps with [  8,  25]
[ 15,  22] overlaps with [  8,  25]
[ 60,  70] overlaps with [ 50, 100]
[ 80,  90] overlaps with [ 50, 100]
Intervals that overlap with at least one other interval:
[  2,   3]
[  8,  25]
[  9,  11]
[ 50, 100]
[  1,   4]
[ 15,  22]
[ 10,  20]
[ 60,  70]
[ 80,  90]

2
如果N对间隔是整数,则可以在O(n)内获得它们。
按照一对中的第一个数字,然后是第二个数字进行排序。如果都是整数,我们可以使用桶排序或基数排序在O(n)内获取它们。
{1, 3},{2,4},{5, 10},{12, 14},{13, 15}
然后逐一组合,
{1,3}
{1,4}与重叠{1,3}和{2,4}
{1,4},{5,10}
{1,4},{5,10},{12,14}
{1,4},{5,10},{12,15}与重叠{12,14}和{13,15}
组合将花费O(N)时间

0

我已经有一段时间没有使用过它了,但我使用的解决方案是《算法导论》中描述的红黑树的一个变体,叫做区间树。它是按区间开始排序的树,因此您可以快速(二分搜索)找到第一个符合条件的节点。如果我没记错的话,这些节点是按某个属性排序的,当候选节点无法匹配您的区间时,该属性让您停止“遍历”整棵树。所以我认为它是O(m)搜索,其中m是匹配区间的数量。

我在搜索中发现了这个实现

Brett

[编辑] 重新阅读问题,这不是你要求的。我认为当您已经有一份已排定会议列表(添加到树中)并且希望查找哪些会议室仍然可用于新的开始和持续时间的会议(搜索条件)时,这是最佳实现。希望这个解决方案还有一些相关性。


0

这个问题可以简化为元素唯一性问题。

元素唯一性具有Ω(n log n)的下限(计算比较次数),因此你无法做得比这更好。


7
回答问题时,您应该清晰明确。我不确定您删除的帖子是什么样子,但在这篇帖子中,您至少可以告诉人们如何将元素唯一性降低到区间重叠。 - miushock

-1

你可以遍历列表一次,并保留一个哈希表来存储到目前为止遇到的所有区间。如果输入区间是哈希表中某个区间的一部分,则将其合并到哈希表区间中。标记非基本区间(由多个区间组成的合并区间)。

然后,您再次遍历列表,并针对每个区间在哈希表中检查它是否包含在合并的区间中。

我不知道它是否是O(N),但它比O(N^2)要好得多。


4
唯一的问题是,哈希表不支持“区间交集”操作 :) - Nikita Rybak
你是在指某个哈希表的具体实现吗?因为我在谈论概念。你总是可以自己实现这个操作。 - Ilya Kogan
@IlyaKogan 我认为Nikita试图表达的观点是,没有明显的方法可以快速找到与给定查询区间相交的区间。朴素的方法每个查询需要O(n)时间,这将是一个O(n^2)算法。你可以使用一个区间树,但那并不真正涉及哈希表。 - John Kurlak
@JohnKurlak 同意。看起来我错过了这部分。 - Ilya Kogan

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