56. 合并区间(Merge Intervals)

难度:中等 | 主题:数组 / 排序 / 贪心

题目

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

示例

 
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
 
输出:[[1,6],[8,10],[15,18]]
 
解释:区间 [1,3] 和 [2,6] 重叠,合并为 [1,6]
 

思路

先讲个故事:拼乐高

你有一堆积木,每块积木从位置 start 铺到位置 end。你想知道这些积木铺完后,桌面被覆盖成了几段。

有的积木叠在一起——比如 [1,3][2,6]——它们就合成一块更大的 [1,6]。有的积木离得很远——[8,10][15,18]——各自独立。

怎么拼?先把积木按左端点排好,然后从头往后一块一块看:新积木的起点比上一块积木的终点靠左(或相接),就合并;否则新开一段。


引导式推导:排序为什么能贪心

不排序会怎样?

 
[1,3], [8,10], [2,6], [15,18]
 

遍历到 [2,6] 时需要回看前面有没有重叠。每来一个新区间,都要和之前所有已合并的区间比较——O(n²)。

排序后呢?

按左端点升序排列:

 
[1,3], [2,6], [8,10], [15,18]
 

相邻的一定是候选——因为左端点递增,新区间 x 只可能和紧挨着的上一个区间 y 重叠。

证明:如果 x 的左端点 > y 的右端点,那 x 不可能和 y 之前的任何区间重叠(那些区间的右端点 ≤ y 的右端点 < x 的左端点)。这就是贪心可以成立的关键性质


graph TD

    subgraph 排序前

        A1["[1,3]"] --> A2["[8,10]"]

        A2 --> A3["[2,6]"]

        A3 --> A4["[15,18]"]

    end

    subgraph 排序后

        B1["[1,3]"] --> B2["[2,6]"]

        B2 --> B3["[8,10]"]

        B3 --> B4["[15,18]"]

    end

合并的两种情况:


graph LR

    subgraph 不重叠

        C1["[1,3]"] --- C2["[4,6]"]

        C3["3 < 4  → 直接加入"]

    end

    subgraph 重叠

        D1["[1,4]"] --- D2["[3,6]"]

        D3["4 >= 3  → 合并为 [1, max(4,6)] = [1,6]"]

    end


四层递进:从暴力到最优


graph LR

    A["暴力 O(n²)<br/>每来一个回看所有"] -->|排序后相邻即候选| B["排序+贪心<br/>O(n log n)"]

    B -->|已排序则跳过排序| C["O(n) 原地合并<br/>(进阶)"]


代码

 
def merge(self, intervals):                 # 排序+贪心:合并重叠区间
 
    intervals.sort(key=lambda x: x[0])          # 按左端点排序,使可合并的区间相邻
 
    merged = []                                 # 结果列表
 
    for interval in intervals:                  # 遍历每个区间
 
        if not merged or merged[-1][1] < interval[0]:  # 不重叠
 
            merged.append(interval)                      # 直接加入
 
        else:                                   # 重叠
 
            merged[-1][1] = max(merged[-1][1], interval[1])  # 扩展右端点
 
    return merged
 

为什么判断不重叠用 < 而不是 <=

因为 [1,3][3,6] 虽然端点相接,但题目里这类情况也需要合并(结果应该是 [1,6])。所以重叠条件是 interval[0] <= merged[-1][1],反推不重叠就是 <


复杂度

指标解释
时间O(n log n)排序是瓶颈
空间O(log n)排序的递归栈(Python Timsort)
免排序O(n)如果输入已按左端点排序

实战考量

频率分析

出现在:频率极高的区间问题入门题。约 60% 的同类题会考区间类问题(合并、插入、覆盖、交集),本题目是这整个系列的基础。

延伸思考

Q:为什么排序后贪心不会漏解?

A:反证法。假设最优解中相邻两个合并块 AB 在排序后的列表里不连续 → 中间夹着一个不重叠的区间 C → 那 A 的右 < C 的左,C 的右 < B 的左 → A 和 B 不可能重叠。所以可合并的区间在排序后一定是连续的。

Q:「插入一个新区间到已有的合并列表中」怎么做?

A:找到插入位置后向左向右合并前后可能的区间。具体看 57插入区间。这题是 56 的直接变体。

Q:区间调度问题(最多能安排多少个不重叠区间)?

A:按右端点排序 + 贪心选择最早结束的。这就是 435无重叠区间

Q:给定若干线段,求它们被覆盖的总长度?

A:排序 + 合并后对每个合并块 (l, r) 累加 r - l。或者用扫描线 + 差分数组。

Q:间隔重复的日程安排系统怎么实现?

A:会议室预定(252会议室)就是合并区间的直接应用——合并后看密度。

易错点

  • 判断不重叠用 < 而非 <=(端点相接要合并)

  • 排序后 merged[-1][1] 是当前合并块的右端点,需要不断取 max 扩展

  • intervals.sort(key=lambda x: x[0]) 是原地排序,sorted() 返回新列表但实践中 sort() 就够


生活类比

合并区间 → 拼乐高

先把积木按起点排好,然后从头拼到尾。新积木和上一块连上了就融进去(取最大终点),没连上就另起一堆。

排好队了就不需要回头看——排好序的集合,相邻即全部。


相关题目

题目关系
57插入区间直接变体,插入一个新区间到已合并列表
435无重叠区间区间调度,按右端点排序 + 贪心
252会议室合并区间的应用,判断能否参加所有会议
56合并区间本题

→ 返回题单:LeetCode学习路线图 > 一、数组与哈希(含双指针、矩阵)

速记卡(面试闪卡)

Q1:一句话讲清「56. 合并区间(Merge Intervals)」到底是什么?

A:把所有重叠的区间合并成一个,返回不重叠的区间数组。

Q2:题目 —— 怎么理解?

A:像拼积木:给你一堆区间 [start,end],把叠在一起(重叠)的拼成大区间,互不搭界的各自留着。输入 intervals,输出合并后的不重叠数组,如 [1,3] 和 [2,6] 合成 [1,6]。

Q3:思路 —— 怎么理解?

A:像先排队再拼:先把区间按左端点排序,这样可能重叠的一定挨着。从头扫,新区间起点 ≤ 上一块终点就合并(取最大终点),否则另起一块。排序后’相邻即候选’,不用回头看——这就是贪心成立的底气。

Q4:代码 —— 怎么理解?

A:排序后遍历:merged 空或 merged[-1][1] < interval[0](不重叠)就直接加;否则 merged[-1][1]=max(…,interval[1]) 扩展右端点。注意不重叠用 ’<’ 不是 ’<=‘,因为端点相接 [1,3] 和 [3,6] 也要合并成 [1,6]。

Q5:复杂度 —— 怎么理解?

A:像排序是瓶颈:时间 O(n log n)(排序主导);空间 O(log n)(Timsort 递归栈);若输入已按左端点排好可降到 O(n)。

Q6:核心速记主线有哪些?

  • 先按左端点排序,让可合并的区间相邻

  • 重叠判定 interval[0] <= merged[-1][1],反推不重叠用 ’<’

  • 合并时不断对右端点取 max 扩展

  • 时间 O(n log n),是区间类问题(插入/覆盖/交集)的基础

口诀

A:合并区间先排序,

左端排好邻可并;

相接也算要合并,

右端取大不漏掉。

相关链接