LeetCode 57. 插入区间:从基础实现到高效优化,一步步吃透区间合并

10 阅读8分钟

区间类题目是LeetCode中数组模块的高频考点,其中「插入区间」更是结合了“排序”与“合并”的核心思路,既考察对边界条件的把控,也考验代码的简洁性和效率。今天我们就以LeetCode 57题为例,从基础实现入手,逐步优化代码,吃透这道经典面试题。

一、题目解析(清晰理解需求)

题目给出两个关键条件,这是解题的前提,必须先明确:

  • 给定的区间列表 intervals无重叠的,且按照「区间起始端点 starti」升序排列;

  • 需要插入一个新区间 newInterval,插入后要求:列表依然升序、区间无重叠(必要时合并区间);

  • 注意:不需要原地修改原数组,可创建新数组返回。

举两个典型示例,帮大家快速get题意:

示例1:intervals = [[1,3],[6,9]], newInterval = [2,5] → 输出 [[1,5],[6,9]]

解析:新区间[2,5]与[1,3]重叠,合并为[1,5],与[6,9]无重叠,直接拼接。

示例2:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] → 输出 [[1,2],[3,10],[12,16]]

解析:新区间[4,8]与[3,5]、[6,7]、[8,10]均重叠,合并为[3,10]。

核心难点:如何高效判断“新区间与原有区间是否重叠”,以及“如何合并重叠区间”,同时避免冗余操作、提升代码效率。

二、基础实现(insert_1):先跑通,再优化

先从最直观的思路入手,写出能正确解题的代码,再分析其中的可优化点。基础思路分为3步:处理无重叠左区间、处理重叠区间、处理无重叠右区间。

2.1 基础解法代码

function insert_1(intervals: number[][], newInterval: number[]): number[][] {
  if(!intervals.length){
    return [newInterval];
  }
  const res: number[][] = [];
  let i = 0;
  // 1. 先把所有在newInterval左边、完全不重叠的区间放入结果(区间右端 < 新区间左端)
  while (intervals[i] && intervals[i][1] < newInterval[0]) {
    res.push(intervals[i]);
    i++;
  }
  // 边界处理:如果所有区间都在新区间左边,直接加入新区间返回
  if(!intervals[i]){
    res.push(newInterval);
    return res;
  }
  // 2. 处理与newInterval相干(重叠/相邻)的区间
  if (intervals[i][0] > newInterval[1]) {
    // 情况1:当前区间在新区间右边,无重叠,先加新区间,再加当前区间
    res.push(newInterval);
    res.push(intervals[i]);
  } else {
    // 情况2:有重叠,合并当前区间与新区间,加入结果
    res.push([Math.min(intervals[i][0], newInterval[0]), Math.max(intervals[i][1], newInterval[1])]);
  }
  i++;
  // 3. 处理剩余区间,判断是否与结果中最后一个区间(已合并的区间)重叠
  while (i < intervals.length) {
    const end = res[res.length - 1];
    if (intervals[i][0] > end[1]){
      // 无重叠,直接加入
      res.push(intervals[i]);
    }else{
      // 有重叠,合并,更新最后一个区间的右端
      end[1] = Math.max(end[1], intervals[i][1]);
      res.pop(); // 冗余操作:弹出再推入,其实可直接修改
      res.push(end);
    }
    i++;
  }
  return res;
};

2.2 基础解法思路总结

这段代码的逻辑是“分步处理、逐个判断”,核心是先分离无重叠的左区间,再处理重叠区间,最后处理无重叠的右区间。优点是逻辑清晰、边界考虑全面,能覆盖所有测试用例(比如空区间、新区间在最左/最右、新区间覆盖所有区间等)。

但缺点也很明显:存在冗余操作,导致时间和空间效率有优化空间,我们重点看两个可优化点:

  • 空间冗余:合并右区间时,执行 res.pop() + res.push(end),其实可以直接修改结果数组中最后一个元素,无需弹出再推入(数组的pop/push操作会涉及底层内存重分配,增加开销);

  • 逻辑冗余:重叠区间的处理拆分为“单次判断”,需要额外处理“当前区间是否存在”的边界,可简化为“循环合并所有重叠区间”;

  • 判断冗余:intervals[i] 的存在性判断可通过数组长度简化,无需重复判断。

三、优化实现(insert_2):精简逻辑,提升效率

针对基础解法的冗余问题,我们优化核心思路:合并重叠区间时,直接更新新区间的边界,而非频繁操作结果数组,同时精简边界判断,让代码更紧凑、效率更高。

3.1 优化解法代码

function insert_2(intervals: number[][], newInterval: number[]): number[][] {
  const res: number[][] = [];
  let i = 0;
  const n = intervals.length;

  // 1. 先把所有在newInterval左边且不重叠的区间加入结果(和基础版一致,但精简了判断)
  while (i < n && intervals[i][1] < newInterval[0]) {
    res.push(intervals[i]);
    i++;
  }

  // 2. 合并所有与newInterval重叠的区间(核心优化点)
  while (i < n && intervals[i][0] <= newInterval[1]) {
    // 直接更新newInterval的边界,无需操作res数组
    newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
    newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
    i++;
  }

  // 3. 将合并后的新区间加入结果(仅1次push操作)
  res.push(newInterval);

  // 4. 把剩余在newInterval右边且不重叠的区间加入结果
  while (i < n) {
    res.push(intervals[i]);
    i++;
  }

  return res;
}

3.2 核心优化点拆解(重点看这里!)

优化后的代码,核心是“减少数组操作、精简逻辑判断”,具体优化点如下,结合时间和空间效率分析:

1. 空间效率优化(关键!)

基础版中,合并重叠区间时,会频繁对结果数组执行 pop() + push() 操作——这是完全不必要的。因为结果数组的最后一个元素,本质就是“当前已合并的区间”,我们可以直接修改它的边界,无需弹出再推入。

优化版的处理方式:不操作结果数组,而是直接更新 newInterval 的左右边界,将所有重叠的区间都合并到 newInterval 中,最后只执行1次 res.push(newInterval)。这样就减少了数组增删操作的开销,空间使用更高效(虽然整体空间复杂度还是O(n),但中间过程的临时开销大幅降低)。

2. 时间效率优化

时间复杂度依然是O(n)(n为区间数量),因为我们只遍历了一次原数组,每个区间只被处理一次。但优化后:

  • 减少了循环内的条件判断次数(比如移除了 if(!intervals[i]) 的冗余判断,用 i < n 天然处理边界);

  • 移除了 pop() + push() 的冗余操作(数组增删的时间开销高于直接修改元素);

  • 合并逻辑更紧凑,减少了分支跳转的时间开销。

3. 逻辑精简,可读性提升

优化后的代码,将整个流程拆分为4个清晰、连贯的步骤,无需额外的边界判断(比如“所有区间在新区间左边”的情况,会被第1个循环和第3步自然覆盖),代码行数减少,逻辑更易理解,也更符合“高效算法”的编码习惯。

四、两种解法对比(一目了然)

对比维度基础解法(insert_1)优化解法(insert_2)
时间复杂度O(n)(但循环内操作冗余)O(n)(循环内操作精简,实际效率更高)
空间复杂度O(n)(冗余的pop/push操作增加内存开销)O(n)(无冗余操作,空间使用更高效)
逻辑复杂度较高,多分支判断、冗余边界处理较低,流程清晰,无冗余判断
适用场景新手入门,便于理解分步思路面试/实际开发,追求高效简洁

五、易错点提醒(避坑必备)

做这道题时,很多人会栽在边界条件上,结合两种解法,总结3个高频易错点:

  1. 空区间处理:当 intervals 为空时,直接返回 [newInterval](基础版和优化版都考虑到了,但容易遗漏);

  2. 重叠边界判断:重叠的条件是「当前区间左端 ≤ 新区间右端」,而非「当前区间左端 < 新区间右端」——比如区间[1,5]和[5,7]是相邻的,也需要合并;

  3. 新区间在最右/最左:新区间完全在所有区间右边时,遍历完所有左区间后,直接加入新区间即可;完全在左边时,先加入新区间,再加入所有原区间。

六、总结与延伸

LeetCode 57题的核心是「区间合并」,解题的关键思路是:利用原数组的有序性,分步骤处理“无重叠左区间、重叠区间、无重叠右区间”,优化的核心是“减少数组操作、精简逻辑判断”。

从基础版到优化版,我们能感受到:好的算法不仅要“能跑通”,还要“高效、简洁”——冗余的操作不仅会降低效率,还会增加代码的维护成本和出错概率。

延伸练习:如果这道题要求「原地修改原数组」,如何优化空间复杂度?(提示:可以利用双指针,将合并后的区间直接覆盖原数组,空间复杂度可优化到O(1),感兴趣的可以尝试实现~)

最后,区间类题目有很强的规律性,比如LeetCode 56题(合并区间)、LeetCode 986题(区间列表的交集),核心思路都是“排序+双指针/遍历+边界判断”,掌握这道题的思路,能轻松应对同类题目。