第二十一章:数组的区间合并——合并重叠区间与插入新区间

区间合并,说白了就是处理一堆有重叠的“线段”。

我刚开始接触这类问题时,总觉得不就是比大小嘛,有什么难的?直到有一次在项目中处理用户的时间段数据,才发现坑比想象的多。嗯,今天咱们就来好好聊聊这个。

21.1 什么是区间合并?

先看一个场景:你有一组区间,比如 [1,3]、[2,6]、[8,10]、[15,18]。这些区间有的互相重叠([1,3] 和 [2,6] 重叠了),有的没有。合并重叠区间的意思就是:把重叠的部分“捏”成一个更大的区间。

比如上面这组,合并后变成 [1,6]、[8,10]、[15,18]。

为什么会这样?因为 [1,3] 和 [2,6] 重叠了,所以合并成 [1,6]。而 [8,10] 和 [15,18] 跟谁都不重叠,保持原样。

核心思路:先按区间起点排序,然后逐个检查相邻区间是否重叠。如果重叠,就合并;如果不重叠,就保留。

21.2 合并重叠区间的标准做法

我个人习惯用二维数组来表示区间集合。每个区间是一个长度为2的数组,比如 intervals[0] = {1, 3}。

合并的步骤其实就三步:

  1. 排序:按每个区间的起点升序排列。
  2. 遍历:从第一个区间开始,逐个往后看。
  3. 判断:当前区间的终点是否 >= 下一个区间的起点?如果是,就合并;否则,把当前区间存起来,换下一个。

来看代码:

#include <stdio.h>
#include <stdlib.h>

// 排序用的比较函数
int cmp(const void* a, const void* b) {
    int* intervalA = *(int**)a;
    int* intervalB = *(int**)b;
    return intervalA[0] - intervalB[0];
}

// 合并重叠区间
int** merge(int** intervals, int intervalsSize, int* intervalsColSize, int* returnSize, int** returnColumnSizes) {
    if (intervalsSize == 0) {
        *returnSize = 0;
        return NULL;
    }

    // 第一步:排序
    qsort(intervals, intervalsSize, sizeof(int*), cmp);

    // 分配结果数组
    int** result = (int**)malloc(intervalsSize * sizeof(int*));
    *returnColumnSizes = (int*)malloc(intervalsSize * sizeof(int));

    int count = 0;
    result[count] = (int*)malloc(2 * sizeof(int));
    result[count][0] = intervals[0][0];
    result[count][1] = intervals[0][1];

    // 第二步:遍历合并
    for (int i = 1; i < intervalsSize; i++) {
        // 如果当前区间的起点 <= 上一个区间的终点,说明重叠
        if (intervals[i][0] <= result[count][1]) {
            // 合并:终点取较大值
            if (intervals[i][1] > result[count][1]) {
                result[count][1] = intervals[i][1];
            }
        } else {
            // 不重叠,新建一个区间
            count++;
            result[count] = (int*)malloc(2 * sizeof(int));
            result[count][0] = intervals[i][0];
            result[count][1] = intervals[i][1];
        }
    }

    *returnSize = count + 1;
    for (int i = 0; i < *returnSize; i++) {
        (*returnColumnSizes)[i] = 2;
    }

    return result;
}

小技巧:合并时终点取两个区间终点的最大值,而不是简单用第二个区间的终点。我曾经在这里吃过亏——以为重叠了就直接用第二个区间的终点,结果 [1,5] 和 [2,4] 合并后变成了 [1,4],把 5 给丢了。

21.3 插入新区间

插入新区间是合并问题的变种。给你一组已经不重叠的区间,再给你一个新区间,让你插进去,同时合并可能产生的重叠。

我遇到这个需求是在做排课系统的时候。课程表里已经排好了课,现在要加一节临时课,得看看跟哪些课冲突了,然后合并。

做法其实不复杂:

  1. 先把新区间左边不重叠的区间全部加入结果。
  2. 然后处理所有跟新区间重叠的区间,合并成一个。
  3. 最后把右边不重叠的区间全部加入结果。

看代码:

int** insert(int** intervals, int intervalsSize, int* intervalsColSize,
             int* newInterval, int newIntervalSize,
             int* returnSize, int** returnColumnSizes) {

    int** result = (int**)malloc((intervalsSize + 1) * sizeof(int*));
    *returnColumnSizes = (int*)malloc((intervalsSize + 1) * sizeof(int));

    int idx = 0;
    int i = 0;

    // 第一步:加入所有在新区间左边的区间(终点 < 新区间起点)
    while (i < intervalsSize && intervals[i][1] < newInterval[0]) {
        result[idx] = (int*)malloc(2 * sizeof(int));
        result[idx][0] = intervals[i][0];
        result[idx][1] = intervals[i][1];
        (*returnColumnSizes)[idx] = 2;
        idx++;
        i++;
    }

    // 第二步:合并所有与新区间重叠的区间
    while (i < intervalsSize && intervals[i][0] <= newInterval[1]) {
        if (intervals[i][0] < newInterval[0]) {
            newInterval[0] = intervals[i][0];
        }
        if (intervals[i][1] > newInterval[1]) {
            newInterval[1] = intervals[i][1];
        }
        i++;
    }
    result[idx] = (int*)malloc(2 * sizeof(int));
    result[idx][0] = newInterval[0];
    result[idx][1] = newInterval[1];
    (*returnColumnSizes)[idx] = 2;
    idx++;

    // 第三步:加入右边剩余的区间
    while (i < intervalsSize) {
        result[idx] = (int*)malloc(2 * sizeof(int));
        result[idx][0] = intervals[i][0];
        result[idx][1] = intervals[i][1];
        (*returnColumnSizes)[idx] = 2;
        idx++;
        i++;
    }

    *returnSize = idx;
    return result;
}

注意:插入新区间时,原区间集合必须是已经按起点排序且不重叠的。如果原集合本身就有重叠,那得先合并再插入。我见过有人直接拿乱序的区间来插,结果插完一团糟。

21.4 两种操作的对比

操作 输入条件 核心逻辑 时间复杂度
合并重叠区间 任意区间集合 先排序,再逐个合并 O(n log n)
插入新区间 已排序且不重叠的区间集合 分三段处理:左边、重叠、右边 O(n)

你想想看,合并操作因为要排序,所以是 O(n log n)。而插入操作假设原集合已经有序,所以只需要 O(n) 遍历一遍就行。

21.5 避坑指南

我来说几个自己踩过的坑:

  • 边界条件:区间 [1,2] 和 [2,3] 算不算重叠?这取决于你的定义。通常我们认为 [1,2] 和 [2,3] 是重叠的(因为 2 是公共点)。但有些场景下不算。我建议统一用「终点 >= 起点」作为重叠判断。
  • 空集合:如果传入的区间数组是空的,直接返回空结果。别傻乎乎地去 malloc。
  • 内存泄漏:每次 malloc 都要记得 free。我在做 LeetCode 时经常因为忘记释放内存而报错。
  • 整数溢出:如果区间端点很大,比如接近 INT_MAX,做加法或比较时要小心。不过一般题目不会这么变态。

21.6 知识结构图

下面这张图帮你理清区间合并的核心逻辑:

区间合并核心逻辑 输入区间集合 是否已排序? 先排序 O(n log n) 遍历并合并 O(n) 是否重叠? 合并区间 保留原区间 继续

21.7 总结

区间合并和插入,说白了就是「排序 + 遍历 + 判断重叠」这三板斧。

我个人觉得,这类问题的难点不在于算法本身,而在于边界条件的处理。你想想看,空数组、单个区间、全部重叠、完全不重叠……每种情况都得考虑到。

我在项目中用过很多次这个技巧。除了排课系统,还有日志时间段的合并、IP地址段的合并等等。掌握了这个,很多看似复杂的问题都能迎刃而解。

一句话记住:先排序,再遍历,重叠就合并,不重叠就保留。插入新区间时,分三段处理——左边、重叠、右边。

公众号:蓝海资料掘金营,微信 deep3321