
1. 区间合并到底在解决什么问题1.1 一堆时间轴糊在一起就是区间的日常先把话说白区间合并Merge Intervals这件事本质上是把一堆互相重叠、互相包含、互相挨着的范围揉成尽量少的几个大范围。输入是若干对数字[l, r]输出是一组互不重叠、按顺序排好的区间并且要求覆盖的范围和原来一模一样——一个点不能多一个点也不能少。我第一次接触这玩意儿是在排会议室的场景里手上有一张表几十个部门分别报了我们想用 9:30 到 11:00我们想用 10:00 到 12:00你作为行政要回答一个非常朴素的问题——今天这间会议室到底有几个不可打断的占用大段如果答案是 3 段那就能塞进去两段空闲时间做设备巡检如果答案是 8 段那今天别想干别的了。这就是最典型的区间合并需求。它看起来简单到不像一道算法题但恰恰是这种看着简单的题在面试和工程里翻车率极高。因为坑都藏在边界上左端点相等怎么办、右端点相等算不算重叠、[1,2]和[2,3]到底该不该并、坐标到了 10 的 9 次方会不会溢出、输入是闭区间还是半开区间。这些东西题目里往往一句话带过但写错一个符号就是全错。1.2 四个真实场景说明它不只是刷题很多人觉得区间合并是LeetCode 专属玩具其实它趴在很多工程的角落里。日程与资源排布。会议室、工位、充电桩、客服坐席凡是同一时刻只能被一个任务占用的资源都要先把碎片化的预约合并成连续占用段再据此计算空闲窗口。空闲窗口的计算其实就是合并结果的补集这一步后面会专门讲。内存与磁盘分配。内存分配器维护一张空闲块表释放内存时会产生大量相邻的空闲块如果不合并就会碎片化到再也分配不出大块。这一块的实现方式和我们写的区间合并几乎是同一个骨架只不过它们用的是平衡树或者空闲链表而不是数组排序。网络地址段与权限范围。访问控制列表里经常出现多条互相包含的地址段比如10.1.0.0/16和10.1.2.0/24前者已经把后者吞掉了。规则匹配前先做一次合并既能减少规则条目也能避免规则顺序不同导致结果不同这种诡异 bug。数据清洗与统计口径。埋点日志里同一次会话被切成了很多段统计会话时长时要把间隔小于 30 秒的段并起来基因组学里合并测序得到的区间剪辑时间轴上去掉冻结帧后合并剩余片段。这些都是同一个套路。1.3 合并到底合的是什么三种重叠形态判断两个区间能不能合并只看一件事它们的交集是否非空或者按题目要求是否相邻即算连续。具体有三种形态形态示例合并结果说明部分重叠[1,4]、[3,7][1,7]最常见左右端点各取极值完全包含[1,10]、[3,5][1,10]必须用max保住大右端点这里最容易翻车端点相接[1,2]、[2,3]看题目口径闭区间通常算连续可并半开区间则本来就不重叠第三种形态是分水岭。如果题目说区间是实数的闭区间[l, r]那[1,2]和[2,3]在点 2 处共享一个点可以合并如果题目规定是半开区间[l, r)那[1,2)和[2,3)完全不相交强行合并就错了。所以拿到题目第一件事是确认区间口径而不是急着敲代码。提示判断条件写成l curR还是l curR唯一依据就是区间口径。这个符号选错样例可能还过一交就 WA 一半。2. 核心思路拆解排序加一次扫描2.1 为什么必须先排序无序扫描会漏假设不排序直接两两比较能不能并思路是有的——拿第一个区间去和后面所有区间比能并就并并完再回头重新扫一遍直到某一轮没有任何合并发生。这个做法能出正确答案但复杂度是 O(n²) 甚至 O(n³)几千个区间就卡住了。排序的价值在于把两两比较降维成只看相邻。只要把所有区间按左端点从左到右排好那么任意两个可能重叠的区间在排序后的序列里一定是挨着出现或者在更早的位置就已经并掉了。于是你只需要维护当前正在生长的这个合并区间从左往右滑一遍每一步只做一次判断。这就是排序带来的全部收益也是它能从 O(n²) 降到 O(n log n) 的根本原因。排序键选左端点就够了不需要同时排右端点。理由很直白左端点决定了谁先被处理而右端点的大小在处理过程中会用max动态修正。排右端点除了浪费比较开销还会破坏左端点的自然顺序反而添乱。2.2 贪心扫描的不变量用一句话说清扫描过程中始终维护两个变量curL和curR代表目前已经吃完的这一坨的最终左端点和已知最大右端点。这个不变量的含义是[curL, curR]是当前这一坨从最左端出发能覆盖到的完整范围左边不可能再有东西了右边还可能有待扩展。对每个新区间[l, r]只有两种命运断开l curR说明它和当前这一坨中间真的有空隙没有任何点重叠。此时把[curL, curR]结算进结果数组然后让curL l、curR r开启新的一坨。粘连l curR说明有交集或者直接接上了。此时curL一定不需要改因为排序保证l curL只需要curR max(curR, r)。那个max就是用来对付完全包含这种形态的——[1,10]后面跟一个[3,5]如果你写成curR r答案立刻变成[1,5]范围凭空少了一大截。最后一步是几乎所有手写实现都会忘的循环结束后最后一坨[curL, curR]还没有被结算进结果数组必须手动补上。这个 bug 的典型症状是输入一个区间时输出空数组或者输入全部重叠时只少了一段。2.3 边界相等算不算重叠一个符号定生死我见过最多的翻车点就在这个符号上。把两种常见口径列清楚整数闭区间[l, r]即包含所有整数点。此时[1,2]和[2,3]共享整数点 2应当合并。判断条件写l curR才算断开等价于l curR合并。实数闭区间[l, r]即包含中间所有实数。同样在点 2 处相交处理方式和上面一致。半开区间[l, r)。[1,2)和[2,3)没有公共点不能合并。判断条件要改成l curR才算断开即l curR才合并。还有一种更刁钻的变体是相邻即算连续比如题目说间隔小于等于 0 的段合并这是标准重叠而有些业务说间隔小于等于 5 分钟的段合并。后者其实是在原区间上做了膨胀把每个右端点加 5 再合并然后再把右端点减回去。这招我用过好几次比在扫描逻辑里塞一个阈值分支干净得多。2.4 复杂度、数据结构与不变量校验时间复杂度由排序主导O(n log n)扫描本身是O(n)。空间复杂度取决于实现如果新建结果数组是O(n)如果允许原地覆盖输入数组像 LeetCode 上那样把输入当草稿纸可以做到O(1)额外空间。数据结构的选择取决于使用模式一次性批处理数组加排序最简单也最快缓存友好。持续动态插入/删除用有序映射C 的std::map、Java 的TreeMap、Python 的sortedcontainers.SortedList每次插入后在前后邻域做局部合并单次操作O(log n)。高频查询最大重叠深度扫描线加差分数组或者线段树维护区间加与全局最大值。我自己写业务代码时的默认选择是数组排序。除非插入频率明显高于查询频率否则有序容器的常数因子和内存开销都不划算。心得写完后一定要在心里跑三个样例——全部分离、全部重叠、单区间。这三个能扛住绝大多数边界就稳了。3. 手把手写参考代码四种语言3.1 C 版本从暴力到 O(n log n)先给一个最容易理解但会超时的暴力版本目的是让你看清排序到底省了什么// 暴力版反复扫描直到一轮没有合并发生 vectorvectorint mergeBrute(vectorvectorint a) { bool changed true; while (changed) { changed false; for (size_t i 0; i a.size() !changed; i) for (size_t j i 1; j a.size(); j) { if (a[i][0] a[j][1] a[j][0] a[i][1]) { a[i][0] min(a[i][0], a[j][0]); a[i][1] max(a[i][1], a[j][1]); a.erase(a.begin() j); changed true; break; } } } sort(a.begin(), a.end()); return a; }正解版本把上面的循环彻底删掉只留一趟扫描vectorvectorint merge(vectorvectorint intervals) { if (intervals.empty()) return {}; sort(intervals.begin(), intervals.end(), [](const vectorint a, const vectorint b) { return a[0] b[0]; // 只按左端点右端点不参与 }); vectorvectorint res; int curL intervals[0][0], curR intervals[0][1]; for (size_t i 1; i intervals.size(); i) { int l intervals[i][0], r intervals[i][1]; if (l curR) { // 严格大于才断开 res.push_back({curL, curR}); curL l; curR r; } else { curR max(curR, r); // 这里必须取 max } } res.push_back({curL, curR}); // 补上最后一坨 return res; }三个细节值得单独说。第一比较器里绝对不要写a[0] b[0]std::sort要求严格弱序用在某些实现下会直接越界崩溃这个坑我在早期踩过一次调试了半天才发现是比较器的锅。第二res.push_back({curL, curR})里用花括号初始化比先造一个临时vector再 push 少一次拷贝。第三如果题目允许修改输入可以把结果直接写回intervals[write]把额外空间压到O(1)。3.2 Python 版本简洁写法与性能陷阱Python 写起来最短但有几个陷阱def merge(intervals): if not intervals: return [] intervals.sort(keylambda x: x[0]) res [] cur_l, cur_r intervals[0] for l, r in intervals[1:]: if l cur_r: res.append([cur_l, cur_r]) cur_l, cur_r l, r elif r cur_r: cur_r r res.append([cur_l, cur_r]) return res第一个陷阱是排序方式。intervals.sort()不加key会按元组的字典序排也就是左端点相同时右端点也参与比较。这在纯合并题里不影响结果但在删除被覆盖区间那类题里会直接改变答案因为那道题需要左端点相同的区间按右端点降序排。养成显式写key的习惯能避免大量隐性错误。第二个陷阱是intervals[1:]这个切片。它复制了一份列表区间数量到百万级时会明显吃内存。写成索引循环for i in range(1, len(intervals))就没这个问题代价是代码长一点。第三个陷阱是元组解包的误用。cur_l, cur_r intervals[0]要求每个元素恰好两个值如果输入偶尔混进了长度不对的列表会抛ValueError。做数据清洗的时候我一般会在前面加一道校验把长度不为 2 的条目先剔出去比在循环里做 try 干净。3.3 Java 与 Go比较器和切片的两点差异Java 的坑集中在排序。int[][]不能用Arrays.sort(intervals)直接按第一列排那样是按行字典序必须传比较器。而且比较器里要用Integer.compare(a[0], b[0])而不是a[0] - b[0]因为相减在极端值下会溢出导致排序结果错乱——这个 bug 特别隐蔽只有坐标接近Integer.MIN_VALUE或MAX_VALUE时才暴露。public int[][] merge(int[][] intervals) { if (intervals null || intervals.length 0) return new int[0][]; Arrays.sort(intervals, (a, b) - Integer.compare(a[0], b[0])); Listint[] res new ArrayList(); int curL intervals[0][0], curR intervals[0][1]; for (int i 1; i intervals.length; i) { if (intervals[i][0] curR) { res.add(new int[]{curL, curR}); curL intervals[i][0]; curR intervals[i][1]; } else { curR Math.max(curR, intervals[i][1]); } } res.add(new int[]{curL, curR}); return res.toArray(new int[res.size()][]); }Go 这边sort.Slice用闭包逻辑很直观但要注意append的扩容语义如果结果切片是从已有的切片派生出来的比如res : intervals[:0]后续 append 会覆盖原数组内容。做原地合并时这是优点做纯函数时就是隐患得用make单独分配。func merge(intervals [][]int) [][]int { if len(intervals) 0 { return [][]int{} } sort.Slice(intervals, func(i, j int) bool { return intervals[i][0] intervals[j][0] }) res : make([][]int, 0, len(intervals)) curL, curR : intervals[0][0], intervals[0][1] for i : 1; i len(intervals); i { l, r : intervals[i][0], intervals[i][1] if l curR { res append(res, []int{curL, curR}) curL, curR l, r } else if r curR { curR r } } return append(res, []int{curL, curR}) }3.4 用测试用例自检边界样例清单写完代码别急着提交先把下面这组用例过一遍。我把它们按能杀死哪类 bug归类用例期望输出专门用来抓什么[][]空输入未处理[[1,4]][[1,4]]忘记补最后一坨[[1,4],[4,5]][[1,5]]端点相接的判断符号[[1,10],[2,3]][[1,10]]忘记取max[[1,2],[3,4]]原样无重叠时误合并[[5,6],[1,2],[3,4]]三个独立区间排序缺失或排序键错误[[1,4],[0,4]][[0,4]]左端点被覆盖时curL的处理其中[[1,4],[0,4]]这条特别值得一提。合并后左端点应该是 0 而不是 1。因为我们按左端点排序[0,4]会排在前面所以标准算法里curL天然是 0不需要额外取 min。但如果你用了原地去重的写法或者在动态插入场景里没排序就很容易忘记更新左端点。这也是我建议批处理一律先排序的另一个理由排序帮你把curL的更新问题消掉了。4. 六个高频变体与例题思路分析4.1 变体一插入区间三段式最稳插入区间LeetCode 57是合并的直接扩展给一个已排好序且互不重叠的区间列表再插入一个新区间要求保持有序不重叠。标准解法是把整个过程切成三段左边干净段所有右端点小于新区间左端点的区间它们和新区间完全无关原样输出。中间合并段所有左端点小于等于新区间右端点的区间它们和新区间有交集不断把新区间左右端点扩张。右边干净段剩下的全部原样输出。vectorvectorint insert(vectorvectorint a, vectorint t) { vectorvectorint res; int i 0, n a.size(); while (i n a[i][1] t[0]) res.push_back(a[i]); // 左边 while (i n a[i][0] t[1]) { // 中间 t[0] min(t[0], a[i][0]); t[1] max(t[1], a[i][1]); i; } res.push_back(t); while (i n) res.push_back(a[i]); // 右边 return res; }为什么不用插入再整体合并因为那样是O(n log n)而三段式利用了输入本身有序的特性做到O(n)。数据量大时差距明显。4.2 变体二无重叠区间与最少箭数换个排序键无重叠区间LeetCode 435问的是最少删掉几个区间让剩下的互不重叠。最少删 总数 − 最多能留下几个不重叠的。这里的关键是贪心要按右端点排序而不是左端点。每次选右端点最小的、且和上一个选择不冲突的区间这样给后面留的空间最大。最少箭数LeetCode 452的骨架几乎一模一样一支箭能穿过一组互相重叠的区间问最少几支箭。答案就是最大不重叠区间数——注意这里的判断条件是start lastEnd才需要新箭因为[1,2]和[2,3]可以被同一支箭在点 2 处穿过和闭区间的合并口径保持一致。这两个题放在一起讲是因为它们共享同一个思维按右端点排序 贪心选择。什么时候用左端点排序什么时候用右端点排序我的判断标准是问题要求输出合并后的区间列表用左端点问题要求最大化留下的数量/最小化资源数用右端点。4.3 变体三合并总长度与最大重叠深度扫描线上场有时候不需要具体区间只要一个数。比如所有区间合并后的总长度和同时存在的最大区间数量。总长度可以在合并过程中直接累加每结算一坨就把curR - curL加进答案整数闭区间的长度是curR - curL 1实数区间是curR - curL别搞混。最大重叠深度用扫描线更合适。把每个区间拆成两个事件左端点1右端点-1排序后扫一遍累加过程中出现的最大值就是深度。闭区间和半开区间在这里的差别体现在同坐标事件的先后顺序上def max_overlap(intervals): events [] for l, r in intervals: events.append((l, 1)) events.append((r, -1)) # 闭区间同一坐标先处理 1让相接的两个区间也算重叠 events.sort(keylambda e: (e[0], -e[1])) cur best 0 for _, delta in events: cur delta best max(best, cur) return best半开区间的写法只要把排序键改成(e[0], e[1])——同一坐标先处理-1让[1,2)结束后[2,3)才开始。这个顺序差别看起来只有一行却是扫描线题最常见的失分点。4.4 变体四区间交集与区间列表交集单对区间求交集很直接lo max(l1, l2)、hi min(r1, r2)lo hi则有交集。但两个区间列表的交集LeetCode 986就有意思了因为两个列表各自的区间都是有序不重叠的可以用双指针线性扫。思路是每次比较两个当前区间先算出交集放进结果然后把右端点较小的那个指针往前推。这一步的道理是右端点小的那个区间已经不可能再和后面任何区间产生交集的新内容了因为后面所有区间的右端点都更大。这个腾挪逻辑和归并排序的 merge 步骤神似。vectorvectorint intervalIntersection(vectorvectorint A, vectorvectorint B) { vectorvectorint res; int i 0, j 0; while (i A.size() j B.size()) { int lo max(A[i][0], B[j][0]); int hi min(A[i][1], B[j][1]); if (lo hi) res.push_back({lo, hi}); if (A[i][1] B[j][1]) i; else j; } return res; }4.5 变体五区间补集与区间删除补集就是合并结果的反面计算空闲窗口、可预约时段都要用。做法是先合并再从域的左端点开始把合并结果之间的缝隙抠出来。def complement(intervals, domain_l, domain_r): merged merge(intervals) res [] cur domain_l for l, r in merged: if l cur: res.append([cur, l - 1]) # 整数闭区间缝隙到 l-1 cur max(cur, r 1) if cur domain_r: res.append([cur, domain_r]) return res两个坑。第一l - 1和r 1只适用于整数闭区间如果是实数区间缝隙就是[cur, l]不需要加减一。第二merged里可能出现在域外的大区间所以cur max(cur, r 1)里的max不能省否则cur会被一个更小的r 1拽回去产生错误的重复缝隙。区间删除是补集的对偶操作把要删的区间补齐然后和大区间求交、取差。批量删除时先合并所有删除项性能会好不少。4.6 变体六删除被覆盖区间与划分字母区间删除被覆盖区间LeetCode 1288有个漂亮的排序技巧左端点升序右端点降序。为什么因为左端点相同的区间里右端点最大的那个一定覆盖其他所有同左端点的区间。排完之后从左往右扫维护已见的maxR只要当前右端点 maxR就说明它没被覆盖计数加一否则就是被覆盖跳过。int removeCoveredIntervals(vectorvectorint a) { sort(a.begin(), a.end(), [](const vectorint x, const vectorint y) { if (x[0] ! y[0]) return x[0] y[0]; return x[1] y[1]; // 同左端点右端点降序 }); int cnt 0, maxR -1; for (auto iv : a) { if (iv[1] maxR) { cnt; maxR iv[1]; } } return cnt; }划分字母区间LeetCode 763看起来毫不相干其实是每个字母的首次出现到末次出现构成一个区间然后对这些区间做合并合并后有几坨就分几段。这就是我特别喜欢这道题的原因——它把构造区间和合并区间两个步骤串起来了非常适合作为检验理解的综合练习。5. 常见错误与排查技巧实录5.1 排序比较函数的三个暗雷比较器看着最不起眼出问题却最难查因为错误往往是偶尔错一次。暗雷一非严格弱序。写return a[0] b[0]当两个区间左端点相同时比较器返回 true而反过来的比较也返回 true破坏了相等元素互不大于的约定。std::sort在数据量大时会走内省排序一旦触发就可能在数组边界外乱跳表现为随机崩溃或者结果错乱。修法就是老老实实用。暗雷二相减溢出。return a[0] - b[0];在坐标接近整型极值时结果溢出符号翻转排序结果完全错。用Integer.compare或显式的比较分支。暗雷三排序键不完整。1288 那道题如果只按左端点排同左端点的顺序不确定导致maxR的判断随机出错。凡是题目对同键元素有额外要求排序键里必须显式带上第二个维度。心得写完比较器心里默念一遍如果两个元素完全相同它返回什么。答案必须是 false。5.2 最后一个区间被吃掉一个高频低级错误这个 bug 的形态极其固定循环里只处理了[0, n-2]到[1, n-1]的转移最后一坨忘了push。测试用例用单个区间结果是空数组用全部重叠的区间结果少一段。修法也简单循环结束后补一行。但还有更隐蔽的变体。如果你用的是当发现断开时结算的写法那么最后一坨永远不会触发断开分支所以必须手动补。如果你用的是每轮先扩张再结算的写法就要检查结算是否重复。我自己的习惯是固定用第一种写法因为逻辑最好验证。5.3 原地修改与引用共享的隐蔽 bug为了压空间很多人会原地把合并结果写回输入数组。这本身没问题但要小心两件事。一是在写回之前不能再用旧数据比如你一边读intervals[i]一边往intervals[write]写而write i恒成立所以读的位置永远在写的位置之后是安全的但如果你的循环顺序是倒着来的就完蛋了。二是调用方可能还在用这个数组Python 和 Go 里的切片/引用共享语义会让被调用函数悄悄改了入参这种 bug 在跨模块调用时很难定位。我的做法是除非题面或性能明确要求原地一律返回新数组。可读性和安全性远比那点内存值钱。5.4 数据类型、溢出与精度坐标范围到 10 的 9 次方、数量到 10 的 5 次方时长度累加可能突破 32 位整数上限。求总长度、求面积覆盖这类题累加变量一定要用 64 位整型。C 用long longJava 用longGo 用int64Python 天然任意精度不用管但要注意它慢。浮点坐标是另一个雷区。用double判断区间是否重叠端点相等时容易因为精度问题判错。稳妥做法是把浮点坐标放大成整数比如时间精确到毫秒就乘 1000 转成整数全程用整数运算需要输出时再除回去。5.5 排查速查表症状最可能的原因快速验证方法单区间输入返回空漏补最后一坨用[[1,4]]跑包含关系的结果变短忘了curR max(curR, r)用[[1,10],[2,3]]跑端点相接的没合并判断条件用了用[[1,4],[4,5]]跑结果顺序乱排序键写错或没排序检查比较器大规模随机崩溃比较器非严格弱序加assert或换长度累加为负数32 位整型溢出换 64 位整型最大深度偶尔少 1扫描线同坐标事件顺序反了检查排序键的第二项6. 工程化落地与性能调优6.1 流式与超大规模区间怎么处理数据量大到内存放不下时数组排序就行不通了。可行的路子有三条。外部排序。把区间按左端点分成若干块每块内部排好序写临时文件然后做 k 路归并归并的同时做合并一次只持有 k 个区间在内存里。这套做法和数据库的排序归并连接是一个思路。分桶后局部合并。如果坐标范围已知且不算太大可以按坐标分桶每个桶内独立合并再处理桶边界的粘连。坐标范围到 10 的 9 次方就不适用了适合坐标比较密集的场景。预合并 有序结构。如果数据是分批到达的可以维护一个有序容器每次插入后在邻居附近做局部合并。Python 里没有内置的有序容器bisect加列表的插入是O(n)数据大时改用sortedcontainers或者干脆自己写跳表。6.2 线段树、有序集合的取舍需求一旦变成动态增删区间 随时查询被覆盖的总长度单纯的合并就力不从心了。这时候要上带懒标记的线段树支持区间赋值、区间加和全局求和。坐标范围大时先做离散化把端点压缩到2n个刻度上线段树的规模就降到可接受范围。有序集合适合插入区间时自动合并重叠邻居的场景典型实现是维护一个start - end的映射插入[l, r]时先找到所有左端点落在[l, r]范围内的条目把它们和[l, r]一起并成一个大的再删旧插新。这套逻辑在内存分配器和区间调度器里非常常见。我的建议是能排序解决的绝不上线段树。线段树的代码量和调试成本是排序版本的五到十倍只有确实需要在线查询时才值得。6.3 一份可复用的合并模板最后给你一份我自己用了很久的 C 模板把常见需求都做成可选项改一个枚举就切换行为struct Interval { long long l, r; }; enum class Touch { Strict, Adjacent }; // 相接是否算连接 vectorInterval mergeIntervals(vectorInterval a, Touch mode Touch::Adjacent) { if (a.empty()) return {}; sort(a.begin(), a.end(), [](const Interval x, const Interval y) { return x.l y.l; }); vectorInterval res; long long curL a[0].l, curR a[0].r; for (size_t i 1; i a.size(); i) { bool connected (mode Touch::Adjacent) ? (a[i].l curR) : (a[i].l curR); if (!connected) { res.push_back({curL, curR}); curL a[i].l; curR a[i].r; } else { curR max(curR, a[i].r); } } res.push_back({curL, curR}); return res; }把它包一层还能顺手算出总长度、区间个数和最大深度struct MergeStat { vectorInterval merged; long long totalLen; // 实数长度整数长度请自行 1 int segments; }; MergeStat analyze(vectorInterval a, Touch mode Touch::Adjacent) { auto m mergeIntervals(std::move(a), mode); long long len 0; for (auto iv : m) len iv.r - iv.l; return {m, len, (int)m.size()}; }这套封装上了生产环境之后我发现最大的收益不是性能而是口径统一所有调用方都通过Touch明确表达自己的相接语义再也不会出现A 模块认为相接算连接、B 模块认为不算导致统计口径对不上的问题。这类问题在多人协作的项目里比算法本身的 bug 更难查——因为每一段代码单独看都是对的。最后分享一个我自己的小习惯凡是要写合并逻辑的地方先花两分钟用纸把测试用例画成数轴上的线段图把端点的开闭、相接的处理、域外的过滤在图上标一遍。画完再动手比写完再调省的时间多得多。区间合并这类题的难点从来不在代码长度而在你脑子里那张图是否清晰。