1. 概述

在实际开发中,我们经常会遇到一个经典问题:如何检测多个区间之间是否存在重叠。这类问题在日程安排、资源分配、时间轴展示等场景中非常常见。

本文将从定义开始,逐步讲解如何识别重叠区间,并提供两种常见解法:暴力法和扫描线法(Sweep Line),帮助你在面对此类问题时能快速找到最优解。

2. 基本概念

2.1 什么是重叠区间

我们假设有一条从 1 到 ∞ 的直线,上面覆盖了多个区间。如果两个区间 至少有一个点是重合的,那么我们认为它们是重叠的(Overlapping)。

数学定义如下:

给定两个区间 [L₁, R₁] 和 [L₂, R₂]:

✅ 重叠条件:max(L₁, L₂) ≤ min(R₁, R₂)

❌ 不重叠条件:max(L₁, L₂) > min(R₁, R₂)

举个例子:

这两个区间 [1, 4] 和 [2, 5] 是重叠的,因为 max(1, 2) = 2 ≤ min(4, 5) = 4。

反之,如果两个区间没有任何交集,如下图所示:

它们就是非重叠区间(Non-overlapping)。

2.2 问题描述

我们有 n 个区间 [Li, Ri],需要:

找出所有与至少一个其他区间存在重叠的区间

返回这些区间的集合

例如,给定以下区间:

输出应为:{1, 2, 3, 5, 6},因为这些区间至少与另一个区间有交集。

3. 暴力解法(Naive Approach)

✅ 思路

对每个区间,遍历所有其他区间,检查是否重叠。一旦发现重叠,就将当前区间加入结果集。

⚠️ 注意

一旦发现当前区间与其他区间重叠,就可以立即加入结果集并 break

不需要重复添加同一个区间

Java 示例代码

List findOverlappingIntervals(List intervals) {

List result = new ArrayList<>();

for (int i = 0; i < intervals.size(); i++) {

Interval a = intervals.get(i);

for (int j = 0; j < intervals.size(); j++) {

if (i == j) continue;

Interval b = intervals.get(j);

if (Math.max(a.start, b.start) <= Math.min(a.end, b.end)) {

result.add(a);

break;

}

}

}

return result;

}

时间复杂度

**O(n²)**,其中 n 是区间数量

踩坑提醒

当 n 较大时(如 10⁴ 以上),性能急剧下降,容易超时

不适合大规模数据集

4. 扫描线法(Sweep Line Approach)

4.1 核心思想

将每个区间的起始点和结束点视为事件,按顺序扫描这些事件,并维护一个当前“打开”的区间(即当前正在处理的区间)。

事件类型:

start:区间开始

end:区间结束

扫描过程中:

遇到 start:

如果当前没有打开的区间,将其设为当前打开区间

否则说明当前区间与之前打开的区间重叠,将两者加入结果集

遇到 end:

如果是当前打开的区间,则关闭它

4.2 实现步骤

构造事件列表:每个区间有两个事件(start 和 end)

按照位置排序事件列表,若位置相同,start 事件优先

遍历事件,维护当前打开区间,并判断是否重叠

Java 示例代码

class Event {

int position;

char type; // 'S' for start, 'E' for end

int index;

public Event(int position, char type, int index) {

this.position = position;

this.type = type;

this.index = index;

}

}

List sweepLine(List intervals) {

List events = new ArrayList<>();

for (int i = 0; i < intervals.size(); i++) {

Interval interval = intervals.get(i);

events.add(new Event(interval.start, 'S', i));

events.add(new Event(interval.end, 'E', i));

}

// 排序:先按位置,再按类型(start 在 end 前)

events.sort((a, b) -> {

if (a.position != b.position) return Integer.compare(a.position, b.position);

return a.type == 'S' ? -1 : 1;

});

Set result = new HashSet<>();

int currentOpen = -1;

for (Event e : events) {

if (e.type == 'S') {

if (currentOpen != -1) {

result.add(e.index);

result.add(currentOpen);

} else {

currentOpen = e.index;

}

} else {

if (e.index == currentOpen) {

currentOpen = -1;

}

}

}

return result.stream().map(intervals::get).collect(Collectors.toList());

}

时间复杂度

**O(n log n)**,主要来自排序操作

踩坑提醒

排序逻辑必须正确处理相同位置的 start 和 end 事件顺序

需要避免重复添加同一个区间(使用 Set)

4.3 示例执行过程

以如下区间为例:

事件列表如下(按排序后):

position

type

index

1

S

0

2

S

2

3

E

0

4

S

1

5

E

2

6

E

1

7

S

3

8

E

3

9

S

4

10

S

5

11

E

4

12

E

5

执行过程中,重叠区间会被逐步识别出来,最终结果为 {0, 1, 2, 4, 5},对应原图中的 {1, 2, 3, 5, 6}。

5. 总结

方法

时间复杂度

是否推荐

适用场景

暴力解法

O(n²)

小数据量,n < 1000

扫描线法

O(n log n)

大数据量,通用场景

✅ 最佳实践建议

尽量避免暴力解法,尤其是在数据量较大的场景

扫描线法是处理区间重叠问题的通用解法,建议熟练掌握

注意事件排序的细节处理,避免逻辑错误

如果你在实际项目中遇到类似问题,不妨尝试扫描线法,它不仅能解决重叠检测,还能扩展用于统计重叠次数、找出最多重叠区域等问题。