mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
2355 字
6 分钟
合并区间
2026-07-01

题目描述#

题目链接

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [start_i, end_i]。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

输入#

二维整数数组 intervals,每个元素是 [start_i, end_i] 表示一个区间

输出#

合并重叠后的区间数组

约束条件#

  • 1 <= intervals.length <= 10^4
  • intervals[i].length == 2
  • 0 <= start_i <= end_i <= 10^4

示例1#

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]

解释:区间 [1,3][2,6] 重叠,将它们合并为 [1,6]

示例2#

输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]

解释:区间 [1,4][4,5] 在端点 4 处相接,视为重叠区间,合并为 [1,5]

解题思路#

这道题的核心是判断两个区间是否重叠,以及如何高效地将重叠的区间合并。两个区间 [a,b][a, b][c,d][c, d] 重叠的充要条件是:

adcba \le d \quad \text{且} \quad c \le b

换句话说——只要一个区间的起点不超过另一个区间的终点,它们就有交集。合并后的区间就是 [min(a,c), max(b,d)][\min(a,c),\ \max(b,d)]

💡 一句话概括:把区间按起点排序,然后从左到右扫描——当前区间能和上一个合并就合并,不能就”封存”上一个,开始一个新的。

从暴力枚举出发#

最朴素的想法是:反复扫描所有区间对,发现重叠就合并,合并后重新扫描,直到没有任何区间可以合并为止:

class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
if not intervals:
return []
res = []
used = [False] * len(intervals)
for i in range(len(intervals)):
if used[i]:
continue
l, r = intervals[i]
# 反复扫描,不断扩展当前区间,直到无法再扩展
changed = True
while changed:
changed = False
for j in range(len(intervals)):
if used[j]:
continue
a, b = intervals[j]
if a <= r and l <= b: # 发现重叠
l = min(l, a)
r = max(r, b)
used[j] = True
changed = True
res.append([l, r])
return res

以每个未合并区间为起点,反复扫描剩余区间并不断吞并重叠者。最坏情况下(区间呈”链式重叠”,每次只合并一个),复杂度达到 O(n2)O(n^2)

瓶颈分析:暴力法花了大量时间在”寻找与当前区间重叠的下一个区间”上——区间是无序的,每次都要全量扫描。如果能让区间按某种顺序排列,使得可能重叠的区间彼此相邻,一次从左到右的扫描就能完成所有合并。

🧠 直觉理解:想象你面前有一堆纸条,每张纸条写着起止时间,散乱地摊在桌上。暴力法是你随手拿起一张,然后翻遍全桌找能和它接上的——找到一张接一张,接不上了再换下一张。如果先把纸条按起始时间从左到右排好,你会发现——能合并的纸条一定彼此相邻,从左到右一趟就能全部处理完。

排序 + 贪心扫描#

将所有区间按起点升序排序后,一个关键性质浮出水面:

性质:排序后,如果当前区间 [x,y][x, y] 不能与前面正在合并的区间 [l,r][l, r] 合并(即 x>rx > r),那么后面所有的区间也不可能与 [l,r][l, r] 合并——因为后面的起点只会更大。

这意味着我们可以用”甩手掌柜”的方式处理:维护一个”当前正在合并”的区间 [l,r][l, r],从左到右扫描每个区间——

  • 如果 xrx \le r(重叠),就扩展右边界:r=max(r,y)r = \max(r, y)
  • 如果 x>rx > r(不重叠),就把 [l,r][l, r] 存入结果,然后开启一个新的合并区间 [l,r]=[x,y][l, r] = [x, y]

扫描结束后,别忘了把最后一个 [l,r][l, r] 也存进去。

class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort() # 按起点排序
res = []
l, r = intervals[0] # 当前正在合并的区间
for x, y in intervals:
if x <= r: # 重叠:扩展右边界
r = max(r, y)
else: # 不重叠:封存当前区间,开启新区间
res.append([l, r])
l, r = x, y
res.append([l, r]) # 封存最后一个区间
return res

⚠️ 注意intervals.sort() 对二元列表默认按第一个元素排序,第一个相同时按第二个元素排序。这里我们只需要按起点排序,intervals.sort() 刚好满足。如果面试官要求显式表达,可以写成 intervals.sort(key=lambda x: x[0])

执行过程追踪#

intervals = [[1,3],[2,6],[8,10],[15,18]] 为例(排序后不变):

步骤当前 [x,y]lr判断操作res
初始13[]
1[1,3]131 ≤ 3r = max(3, 3) = 3(无变化)[]
2[2,6]132 ≤ 3r = max(3, 6) = 6[]
3[8,10]168 > 6保存 [1, 6],新 l=8, r=10[[1,6]]
4[15,18]81015 > 10保存 [8,10],新 l=15, r=18[[1,6],[8,10]]
结束1518保存 [15,18][[1,6],[8,10],[15,18]]

💡 小贴士:注意步骤 1——l, rintervals[0] 初始化,然后 for 循环再次遍历到了 intervals[0]。此时 x = ly = r,所以 x ≤ r 恒成立,r = max(r, y) 不改变任何值,属于一次”空操作”。这一步看似多余,但它让循环逻辑保持统一——所有区间(包括第一个)都经过同一套判断逻辑,代码更简洁,且 O(1)O(1) 的额外开销完全可以忽略。

再来看示例 2 [[1,4],[4,5]],端点相接的情况:

步骤当前 [x,y]lr判断操作
初始14
1[1,4]141 ≤ 4r = max(4, 4) = 4
2[4,5]144 ≤ 4r = max(4, 5) = 5
结束15保存 [1,5]

关键在步骤 2:判断条件 x ≤ r4 ≤ 4 成立,因此 [4,5] 被视为与 [1,4] 重叠。这正是题目要求的——端点相接也算重叠。

为什么贪心是对的?#

排序后从左到右扫描,每次遇到不重叠的区间就”封存”当前——为什么这样不会错过更优的合并方式?

🧠 直觉理解:排序保证了区间的起点是单调递增的。假设当前合并区间 [l,r][l, r] 与某个后面的区间 [x,y][x, y] 不重叠(即 x>rx > r),那 [x,y][x, y] 之后的所有区间起点 x>r\ge x > r,更不可能与 [l,r][l, r] 重叠。所以此时”封存”[l,r][l, r] 一定是安全的——它已经”长到最大了”,不会再被任何后续区间扩展。

复杂度#

  • 时间复杂度O(nlogn)O(n \log n),其中 nn 是区间个数。排序占 O(nlogn)O(n \log n),扫描占 O(n)O(n)
  • 空间复杂度O(1)O(1)(不计输出数组 res),或 O(n)O(n)(计入输出)。仅使用了 lr 两个额外变量。

O(nlogn)O(n \log n) 已是理论最优——因为区间合并问题至少需要排序,而比较排序的下界就是 O(nlogn)O(n \log n)。如果输入本身已按起点有序(如某些变体题目的设定),则可做到 O(n)O(n)

⚠️ 注意:Python 的 list.sort() 是原地排序,空间复杂度为 O(1)O(1)(TimSort 的临时空间为 O(n)O(n) 但通常不计入)。如果你不希望修改输入数组,可以 sorted(intervals) 代替,但空间变为 O(n)O(n)

代码优化#

写法对比:l, r vs res[-1]#

上面使用了 l, r 两个变量追踪正在合并的区间。另一种常见写法是直接在结果数组的最后一个区间上操作:

class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort()
res = [intervals[0]]
for x, y in intervals[1:]:
if x <= res[-1][1]:
res[-1][1] = max(res[-1][1], y)
else:
res.append([x, y])
return res

两种写法的对比:

对比项l, r 变量res[-1] 操作
可读性⭐⭐⭐⭐ 变量名直观⭐⭐⭐ [-1] 需要一点适应
简洁度⭐⭐⭐ 需要尾部 append⭐⭐⭐⭐⭐ 循环结束即完成
避免浅拷贝问题✅ 直接用 l, r⚠️ 修改 res[-1][1] 是原地修改,恰好安全
适用场景适合讲解、教学适合生产、竞赛

l, r 版本的好处是思路更清晰——“我正在合并一个区间,还没想好要不要放进结果”。res[-1] 版本的好处是代码更紧凑——边界处理被 res 初始化吃掉,循环从 intervals[1:] 开始,省去了尾部的那句 res.append([l, r])

💡 小贴士:在面试中建议先写出 l, r 版本,把思路讲清楚;如果面试官要求更简洁,再优化到 res[-1] 版本。一上来就甩出 res[-1] 可能会让面试官觉得你在背模板。

intervals.sort() vs intervals.sort(key=lambda x: x[0])#

两种写法在本题中完全等价——因为 Python 的列表比较是按元素顺序逐个比较的:

  • [1, 3] < [2, 6] 先比第一个元素 1 < 2,结果为 True,不再比第二个
  • [1, 3] < [1, 4] 第一个相等 1 == 1,比第二个 3 < 4

这意味着 intervals.sort() 等价于 “按起点排序,起点相同时按终点排序”。而题目只需要按起点排序——起点相同时按终点排不排都不影响正确性,但恰好也能正常工作。

显式写成 key=lambda x: x[0] 的好处是意图表达更明确,但性能上反而略慢(多了 lambda 调用开销)。两种写法都可以,看个人偏好。

处理空输入#

虽然约束条件保证 n >= 1,但在工程实践中防御式编程不会错:

class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
if not intervals:
return []
# ...

如果约束真的可能为 0,intervals[0] 会在初始化 l, r 时抛出 IndexError

总结#

「合并区间」是贪心算法的经典入门题。它的核心技巧只有三步:

  1. 排序——让区间按起点有序
  2. 扫描——从左到右逐个判断是否重叠
  3. 合并——重叠就扩展,不重叠就封存

这个”排序 + 一次扫描”的模式广泛适用于一大类区间问题。掌握了它,下面这些题目你会觉得似曾相识:

  • 57. 插入区间:给定一个已排序的不重叠区间列表,插入新区间后合并——相当于”手动的排序 + 合并”中的合并步骤
  • 435. 无重叠区间:移除最少的区间使剩余区间不重叠——同样是排序 + 贪心,但贪心策略不同
  • 452. 用最少数量的箭引爆气球:气球 = 区间,箭 = 公共交点——排序 + 贪心扫描的变体

🧠 核心收获:区间问题八成从排序入手,排序后重叠的区间会”挤在一起”,剩下的就是设计贪心扫描逻辑。别被题面吓到,先问自己——“如果区间已经排好序了,怎么做?”

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

合并区间
https://www.hygen.red/posts/hot100/05-普通数组/014合并区间/
作者
Hygen
发布于
2026-07-01
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录