news 2026/10/4 7:03:42

LeetCode 56合并区间详解:排序策略与边界处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LeetCode 56合并区间详解:排序策略与边界处理

说实话,LeetCode 56这道题,属于那种“看着简单、一写就飘”的典型题目。合并区间这四个字,初学者会觉得不就是比大小吗?真上了面试白板,或者被扔进周赛第三题的题解里,你会发现细节全在边界和排序策略里。我这篇就把这题的完整拆解、实现细节、易错点、变体思路一次说透。

1. 先想清楚一个问题:为什么“合并”本身不是考点

很多人拿到这题,第一反应是“遍历一遍,能合并就合并”,于是直接开始写两个for循环硬搓。结果往往是把简单问题搞成O(n²)的复杂度,还容易在边界上翻车。

我先把最核心的逻辑单独拎出来说清楚。假设现在有两个区间,分别叫[a, b]和[c, d],判断它们能不能合并,条件只有一个:

  • 如果c <= b,说明这两个区间存在重叠或者相邻,可以合并成一个[min(a, c), max(b, d)]。
  • 如果c > b,说明中间有缝隙,合并不了。

注意这个“c <= b”的细节,是很多题的隐藏考点。LeetCode 56对重叠的定义是“闭合区间”,也就是说[1, 3]和[3, 5]这种头尾相接的区间,也视为可以合并,结果是[1, 5]。如果你写的是c < b,这两个区间就不会合并,答案直接错掉。

再往深一层说,区间之间还有三种关系需要区分:

  1. 完全不重叠:[1, 2]和[3, 4],两头不沾,保持原样。
  2. 部分重叠:[1, 3]和[2, 5],有交集,合并为[1, 5]。
  3. 完全包含:[1, 6]和[2, 4],后者被前者整体包住,合并结果还是[1, 6]。

很多人写代码时只处理了前两种,遇到完全包含的情况就晕了。比如维护了一个current区间,遍历到下一个区间时发现它的右端点比current的右端点还小,这时候不该更新右端点,否则会把区间拉长,得到错误结果。

所以,合并逻辑本身真的不复杂,复杂的是你用什么顺序去处理这些区间,以及你的数据结构能不能让你在处理过程中不迷路。这就要引出最关键的一步了。

2. 真正的分水岭:排序策略的选择

做这道题之前,你必须在心里确认一个事实:如果给你一组乱序的区间,你直接遍历,是没办法稳定合并的。原因很简单,合并是一个“依赖邻居”的操作,你只有知道了“谁在谁左边”,才能确定合并的先后顺序。

所以,排序是前置条件,不是可选优化项。这一步没想明白,后面全是稀里糊涂。

2.1 为什么排序后只需盯住右端点

按左端点从小到大排序之后,会有一个特别好的性质:因为后面的区间左端点一定不小于前面的左端点,所以在合并时,新合并区间的左端点永远保持为当前区间的起点的左端点,不需要比较、不需要更新,你需要关心的只有一件事——右端点是否要被拉长。

这句话值得再念一遍。排序前,合并时要同时比较左端点和右端点;排序后,左端点被锁死了,所有决策全部集中到右端点这一个变量上。

打个比方:你有一排书架,每本书的起始位置和结束位置都写在书脊上,乱序时你根本不知道哪本挨着哪本。但你把所有书按起始位置从左到右排好之后,你只需要从头往后扫一遍,看到哪本书的起始位置在当前“已合并段”的结束位置之内,就说明它是连着的,直接把它纳入进来并更新结束位置就行。这个思路爽就爽在,一次遍历、O(n)搞定。

2.2 错误排序方向的代价

我知道有相当一部分人第一直觉是“按右端点排序”。这也不是不能做,但你会发现合并逻辑马上变得棘手:排序后右端点有序,但左端点是乱的,你可能合并出[10, 20]后又遇到[1, 15],它明明应该和前面的区间合并,却因为遍历顺序问题被你当成一个新区间处理了。

不信你拿[[1, 4], [0, 2], [3, 5]]这个用例试验一下。按左端点排序输出是[[0, 2], [1, 4], [3, 5]],一路顺风合并成[0, 5]。按右端点排序输出是[[0, 2], [1, 4], [3, 5]]?不,右端点排序会变成[[0, 2], [1, 4], [3, 5]]其实都一样,因为数据太巧了。你换一组:[[2, 3], [1, 2], [0, 1]],按右端点排完是[[0, 1], [1, 2], [2, 3]],还能合并。但你再试试[[1, 10], [2, 3], [4, 5]],按右端点排序变成[[2, 3], [4, 5], [1, 10]],直接从第一个区间开始扫描,发现第二个和第三个区间的左端点都在当前右端点之外,于是错误地分成三个区间。可实际上这三个区间都能合成一个。

这就是排序策略选错的典型恶果:你不是不能做,而是需要额外的回溯逻辑来修正,复杂度直接上升。按左端点排序则天然避免了这个问题,遍历过程就像拉链一样一路咬合过去。

2.3 关于排序稳定性的一句话

如果两个区间左端点相等,那么排序时它们谁先谁后其实无所谓,因为合并结果是一样的。所以用Arrays.sort默认的归并排序(稳定)或者快速排序(不稳定),对本题结果没有任何影响。这一点不用过度纠结。

3. 手写实现:从第一版到能过所有用例

我直接给你一份能提交通过的Java版本,然后一行一行说为什么这么写。

class Solution { public int[][] merge(int[][] intervals) { if (intervals == null || intervals.length == 0) { return new int[0][2]; } // 按区间左端点升序排序 Arrays.sort(intervals, (a, b) -> a[0] - b[0]); List<int[]> merged = new ArrayList<>(); int[] current = intervals[0]; merged.add(current); for (int i = 1; i < intervals.length; i++) { int[] next = intervals[i]; if (next[0] <= current[1]) { // 重叠或相邻,更新当前合并区间的右端点 current[1] = Math.max(current[1], next[1]); } else { // 不重叠,开启新区间 current = next; merged.add(current); } } return merged.toArray(new int[merged.size()][]); } }

你自己动手写第一版的时候,最容易错的三个地方,我给你全列出来。

3.1 易错点一:更新右端点时用了min而不是max

这是初学者最常见的手误。你的current右端点如果比next的右端点大,说明next被完全包含,这时候不能把右端点改成小的,否则区间缩短,后面再来一个区间可能就被错误地判定为不重叠。所以这里必须取Math.max,宁可让区间暂时“虚胖”,也不能让它“缩水”。

3.2 易错点二:用排序后的原始二维数组直接改,造成源数据被破坏

有人在合并时喜欢直接修改intervals[i]的值,比如:

intervals[i][1] = Math.max(intervals[i][1], intervals[i - 1][1]);

这样写完会发现逻辑上能跑,但如果你后面还需要原始区间做别的事,或者测试框架里对你传入的数组做断言,数据已经被改了。更重要的是,这种写法在可读性上很差,面试官一眼就能看出你对“可变状态”的管理不够敏感。

更稳妥的做法是像我的示例代码一样,用一个merged容器来收集结果,current作为哨兵区间,不断更新它的右端点。注意我merged.add(current)添加的是引用,后续修改current[1]会同步反映到列表里,所以不需要再单独维护一个“最后结果的右端点”变量。

3.3 易错点三:toArray的用法写错

Java里List<int[]>转二维数组,正确写法是:

merged.toArray(new int[merged.size()][]);

有人会写new int[0][],也能跑,但会在内存里多分配一次。面试时可以顺手写成new int[merged.size()][],显得你考虑过容量问题。当然,toArray本身有扩容机制,写new int[0][]也不会错,只是敏感一点的人会注意到这种微小差异。

如果你平时写Python,这道题的实现会更简洁:

class Solution: def merge(self, intervals: List[List[int]]) -> List[List[int]]: if not intervals: return [] 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

注意Python版本里判断条件是merged[-1][1] < interval[0],意思是当前区间的右端点够不到下一个区间的左端点,属于“隔开”的情形,此时才新建区间;否则直接合并并更新右端点。逻辑上跟Java版本完全等价,只是表达顺序反过来。

4. 复杂度与边界:别小看这些“显然”的问题

LeetCode上这题标的是Medium,但它之所以不是Easy,不是因为合并逻辑有多绕,而是因为你要意识到排序带来的复杂度占比,以及边界用例的多样性。

4.1 时间复杂度到底看哪一部分

排序的时间复杂度是O(n log n),合并遍历是O(n)。所以整体是O(n log n),其中n是区间数量。空间复杂度方面,如果按题目要求输出一个新的二维数组,那么需要O(n)的额外空间来存结果;如果你在原数组上改,理论上是O(1)额外空间(抛开排序栈消耗),但实际工程中没人这么干,因为破坏入参不是一个好习惯。

很多人分析到这里就停了,但我建议你再想一层:这个O(n log n)里,常数大不大?其实很大,因为二维数组排序的比较器涉及数组元素访问,会比一维数组排序更慢。这也是为什么面试官有时候会问“你能不能想出不用排序的做法”,本质上是在试探你能否权衡预处理成本和后续处理成本之间的关系。

4.2 边界用例清单

我每次做这道题,都会先在脑子里过一遍这些边界:

场景示例预期输出
空数组[][]
单区间[[1, 2]][[1, 2]]
完全相同[[1, 5], [1, 5], [1, 5]][[1, 5]]
包含关系[[1, 6], [2, 4], [3, 5]][[1, 6]]
首尾相接[[1, 2], [2, 3], [3, 4]][[1, 4]]
全部分开[[1, 2], [3, 4], [5, 6]][[1, 2], [3, 4], [5, 6]]
负数区间[[-5, -1], [-3, 0], [2, 4]][[-5, 0], [2, 4]]
极端大值[[0, 0], [0, 10], [10, 100]][[0, 100]]

这些用例在本地跑一遍,基本就能确认代码的鲁棒性。我特别提到负数区间,是因为很多人写比较器时直接用a[0] - b[0],这在绝大多数场景没问题,但如果左右端点都是非常大的整数,比如Integer.MAX_VALUE和Integer.MIN_VALUE,相减会溢出,导致排序结果错误。严谨一点应该用Integer.compare(a[0], b[0])。

推荐直接改成:

Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));

别小看这一行,它能帮你规避掉一个极其隐蔽的整数溢出bug。面试时写出来还能顺手展示你对API的熟悉度。

5. 从一道题看一类题:区间问题家族

合并区间是区间类问题的基础原型。你把它吃透了,后面好几个题都能顺藤摸瓜,所以我多花点篇幅把这串题的关系理清楚。

5.1 一道题延伸出的变体对比

我把LeetCode上常见的区间类题目整理成一张表,方便你对比它们的差异:

题目核心操作与56题的关系
56 合并区间把重叠区间合并原型,注重“排序+单次扫描”
57 插入区间给定有序区间,插入一个新区间再合并排序步骤被省略/简化为二分查找,核心仍是合并
435 无重叠区间移除最少区间使剩余区间互不重叠同样需要排序,但“贪婪”策略相反
452 用最少数量的箭引爆气球实际上是“最多重叠区间数”问题排序后贪心,但判断条件和56相反
1288 删除被覆盖区间统计有多少个区间被其他区间完全覆盖依赖包含关系的判断,恰好是56排序后要处理的情况
986 区间列表的交集两个有序区间列表求交集双指针扫描,不用排序,但区间比较逻辑相通

你会发现,区间类题目最核心的就三件事:排序、比较两个区间的位置关系、用贪心或扫描维护一个“当前状态”。

5.2 从56过渡到57:插入区间的隐藏坑

57题是“给你一个已按左端点排好序的区间列表,再给你一个新区间,把新区间插入进去并合并”。很多人直接复用56的方法,先把新区间塞进列表,再整体排序、合并。这当然能过,但时间复杂度变成O(n log n),而题目本身因为输入有序,其实可以O(n)解决。

正确的思路是这样:遍历已排序的区间列表,把所有在新区间左侧且不相交的部分直接加入结果;然后开始处理重叠部分,不断用新区间和当前遍历到的区间比较,更新新区间的左右端点;处理完所有重叠者后,把新区间加入结果;最后把剩下的右侧区间直接加入结果。这个过程中用到的合并逻辑跟56完全一样,差别只在于你已经站在一个有序列表上遍历,所以少了排序这一步。

如果你能在面试时主动说出“因为输入已经有序,这里可以优化到O(n)”,这比单纯写出代码更容易加分。

5.3 从56过渡到435:为什么同样是贪心,做法完全不同

435要求移除最少的区间使剩余区间互不重叠,也常用贪婪算法,但判断逻辑是:按右端点排序,总是选择右端点最小的区间作为保留对象,然后遍历其余区间,跳过所有和它重叠的。这样能保证留下的区间最多,从而移除的最少。

对比56你会发现:56按左端点排序,合并时关注右端点能不能被拉长;435按右端点排序,保留时希望右端点尽量小,给后面的区间留空间。同样是“排序+贪心”,一个往大合并,一个往小收缩,方向正好相反。这个反差特别能考验你对问题本质的理解。

我建议刷题时把这个串烧放在一起做,做完56马上做57、435、452,你会明显感觉到“哇,原来都是一个套路的不同用法”。

6. 面试实战:几个隐含考点,不是LeetCode会告诉你的

我在面试中问过这道题,也被别人问过这道题,给你交个底,面试官真正想看的东西是什么。

6.1 千万别急着写代码:先画图

面试白板上,如果你上来就写Arrays.sort,面试官心里可能已经在扣分了。他们更希望看到你先举例、画示意、口头描述合并逻辑,然后再说“我打算先按左端点排序,再用一次扫描完成合并”。这个过程体现的是结构化思维,而不是背题能力。

我自己面试候选人时,如果对方花两分钟画了一下区间重叠的图,再说出排序策略,我基本已经给这道题的代码分了。因为后面的代码只是把这个思路翻译成语法而已。

6.2 关于排序比较器的讨论

Java里Arrays.sort(intervals, (a, b) -> a[0] - b[0])能写,但如果你用Integer.compare,并解释一句“防止整数溢出”,这就是一个主动展示工程经验的moment。这两个写法在LeetCode的测试数据里可能都AC,但在面试环境下,你展现出的细致程度会直接拉开差距。

6.3 能不能原地合并?多数人答不好

面试官可能会追问:“能不能不借助额外容器,直接原地合并?”答案是可以,但需要你用一个指针idx指向合并结果填到哪里:

class Solution { public int[][] merge(int[][] intervals) { if (intervals == null || intervals.length == 0) { return new int[0][2]; } Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0])); int idx = 0; for (int i = 1; i < intervals.length; i++) { if (intervals[idx][1] >= intervals[i][0]) { intervals[idx][1] = Math.max(intervals[idx][1], intervals[i][1]); } else { idx++; intervals[idx] = intervals[i]; } } return Arrays.copyOf(intervals, idx + 1); } }

这里的关键是,被合并掉的区间所占的位置会被跳过,我们不断把“新的不重叠区间”搬到数组前部,最后用Arrays.copyOf截断多余部分。这种写法空间上更省,但破坏了入参数组的顺序结构。面试时如果你能主动说出“我可以原地合并,但代价是修改了入参,工程上一般不推荐”,这一句话就包含了几层意思:你会优化、你懂取舍、你了解工程习惯。

6.4 和输入流结合:如果区间是流式到达呢

有一个进阶讨论可能不在LeetCode范围内,但偶尔会被问到:如果区间不是一次性给你,而是一个一个到达的,怎么维护合并结果?答案是维护一个有序结构(比如TreeMap,键是左端点,值是右端点),每次插入新区间后找到可能的重叠区间并合并。插入和合并都是O(log n)级别,整体可以接受。这个思路本质上是把静态排序变成了动态插入排序。能说出这一层,说明你举一反三的能力到位了。

7. 复盘与最终建议

我不知道你刷这道题处于什么阶段。如果是刚开始接触区间类问题,我建议你至少手写三遍:第一遍看着题解写,第二遍合上书自己写,第三遍用不同的语言或不同写法(比如原地合并)再写一遍。三遍下来,你对排序加扫描这个模板基本就形成肌肉记忆了。

如果是准备面试,我建议你在写完这道题之后,把57、435、452这三个题连着刷,并且每做一题都回头问自己:“如果输入已经有序,我会怎么改?如果数据是流式的,我又会怎么改?”能把这两个问题的答案脱口而出,你在这类题上的掌握程度就已经超过绝大多数候选人了。

我在实际项目部里面代码评审时见过有人把这道题的逻辑用得很巧——处理时间区间、处理IP段合并、处理日程表冲突检测,都是一个套路:先排序、再扫描、维护当前边界。所谓“合并区间”看起来是在解决一道算法题,实际上是在解决现实里最普通不过的归并问题。想通这一层,LeetCode 56就不仅仅是一道题了,它会沉淀成你处理连续数据问题的底层思考方式。

最后分享一个小技巧:在你写任何区间类题目之前,先在注释里画三个区间,标上重叠、包含、分离三种关系,然后再动笔。这个习惯帮我省掉的debug时间,比我写任何代码都快。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/4 7:03:29

STM32与MRAM的工业存储改造:告别Flash扇区磨损

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 7:01:46

园区AI巡检告警闭环5大坑:从IM推送到工单联动的工程实践

1. 园区巡检告警闭环&#xff0c;为什么“发出去”不等于“处理完”做园区 AI 巡检系统的团队&#xff0c;十有八九会把注意力压在算法侧&#xff1a;摄像头选型、模型精度、误报率、边缘盒子算力。这些当然重要&#xff0c;但真正让一线运维骂娘的&#xff0c;往往不是“没检测…

作者头像 李华
网站建设 2026/10/4 7:01:27

台积电技术研发实力解析:从先进制程到良率闭环

在半导体行业待久了&#xff0c;你会发现一个很有意思的现象&#xff1a;几乎每家芯片公司都在强调“先进制程”&#xff0c;但真正能把先进制程从流片演示变成大规模出货产品的&#xff0c;绕来绕去总绕不开台积电。手机上用的应用处理器、PC里的GPU、AI服务器上那颗又贵又难买…

作者头像 李华
网站建设 2026/10/4 6:58:22

哈夫曼编码原理与Java实现:从优先队列到文件压缩实战

1. 项目概述与核心思路拆解1.1 哈夫曼编码到底是什么&#xff0c;为什么能压缩这东西说穿了不复杂&#xff0c;本质就是一句话&#xff1a;让出现频率高的字符用更短的二进制编码&#xff0c;让出现频率低的字符用更长的二进制编码&#xff0c;整体算下来总位数变小了&#xff…

作者头像 李华
网站建设 2026/10/4 6:53:06

Unity网络编程面经:从TCP/UDP选型到同步方案与弱网优化

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 6:51:58

Open-Shell:一键把 Windows 11 开始菜单改回经典高效布局

说实话&#xff0c;这两年我帮人装电脑&#xff0c;系统装完干的第一件事不是激活&#xff0c;不是装驱动&#xff0c;而是把开始菜单换掉。Windows 11 那个新的开始菜单&#xff0c;很多人真的用不惯&#xff0c;找程序要点开“所有应用”&#xff0c;最近文件的位置还被推荐内…

作者头像 李华