1. 合并区间问题概述
合并区间(Merge Intervals)是算法面试中的经典问题,也是实际开发中处理时间调度、资源分配等场景的常见需求。给定一组可能重叠的区间,我们需要将它们合并成不重叠的区间集合。
举个例子,假设我们有以下区间:
code复制[1,3], [2,6], [8,10], [15,18]
合并后的结果应该是:
code复制[1,6], [8,10], [15,18]
因为[1,3]和[2,6]有重叠部分,可以合并为[1,6]。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题分析与算法设计
2.1 输入输出定义
输入是一个二维数组intervals,其中每个元素intervals[i]表示一个区间,包含两个整数[starti, endi]。输出是合并后的区间数组。
2.2 核心算法思路
解决这个问题的关键在于:
- 首先将所有区间按照起始点进行排序
- 然后依次检查每个区间是否能与结果集中的最后一个区间合并
具体步骤:
- 如果输入的区间数组为空,直接返回空数组
- 将区间数组按照每个区间的起始点进行升序排序
- 初始化结果数组,将第一个区间加入结果
- 从第二个区间开始遍历:
- 如果当前区间的起始点小于等于结果数组中最后一个区间的结束点,说明有重叠,需要合并
- 合并的方式是更新结果数组中最后一个区间的结束点为两者结束点的较大值
- 如果没有重叠,直接将当前区间加入结果数组
2.3 时间复杂度分析
排序的时间复杂度是O(n log n),其中n是区间的数量。合并过程只需要线性扫描一次,时间复杂度是O(n)。因此总的时间复杂度是O(n log n)。
3. C语言实现详解
3.1 数据结构定义
首先我们需要定义区间的数据结构:
c复制typedef struct {
int start;
int end;
} Interval;
3.2 比较函数实现
为了使用qsort进行排序,我们需要实现比较函数:
c复制int compareIntervals(const void* a, const void* b) {
Interval* intervalA = (Interval*)a;
Interval* intervalB = (Interval*)b;
return intervalA->start - intervalB->start;
}
3.3 合并算法实现
下面是完整的合并算法实现:
c复制Interval* merge(Interval* intervals, int intervalsSize, int* returnSize) {
if (intervalsSize == 0) {
*returnSize = 0;
return NULL;
}
// 排序区间数组
qsort(intervals, intervalsSize, sizeof(Interval), compareIntervals);
Interval* result = (Interval*)malloc(intervalsSize * sizeof(Interval));
int resultSize = 0;
result[resultSize++] = intervals[0];
for (int i = 1; i < intervalsSize; i++) {
Interval* last = &result[resultSize - 1];
if (intervals[i].start <= last->end) {
// 有重叠,合并区间
last->end = intervals[i].end > last->end ? intervals[i].end : last->end;
} else {
// 无重叠,添加新区间
result[resultSize++] = intervals[i];
}
}
*returnSize = resultSize;
return result;
}
3.4 测试用例与验证
为了验证我们的实现是否正确,可以编写以下测试代码:
c复制void printIntervals(Interval* intervals, int size) {
for (int i = 0; i < size; i++) {
printf("[%d,%d] ", intervals[i].start, intervals[i].end);
}
printf("\n");
}
int main() {
Interval intervals1[] = {{1,3},{2,6},{8,10},{15,18}};
int size1 = sizeof(intervals1)/sizeof(intervals1[0]);
int returnSize1;
Interval* result1 = merge(intervals1, size1, &returnSize1);
printIntervals(result1, returnSize1);
free(result1);
Interval intervals2[] = {{1,4},{4,5}};
int size2 = sizeof(intervals2)/sizeof(intervals2[0]);
int returnSize2;
Interval* result2 = merge(intervals2, size2, &returnSize2);
printIntervals(result2, returnSize2);
free(result2);
return 0;
}
预期输出:
code复制[1,6] [8,10] [15,18]
[1,5]
4. 边界条件与特殊处理
4.1 空输入处理
当输入数组为空时,应该直接返回空数组:
c复制if (intervalsSize == 0) {
*returnSize = 0;
return NULL;
}
4.2 单个区间处理
如果只有一个区间,不需要任何合并操作,直接返回该区间即可。
4.3 完全包含的区间
考虑这种情况:
code复制[1,4],[2,3]
合并结果应该是[1,4],因为第二个区间完全包含在第一个区间内。我们的算法已经处理了这种情况,因为last->end不会被更小的值更新。
4.4 相邻但不重叠的区间
考虑这种情况:
code复制[1,2],[3,4]
这两个区间不重叠也不相邻,应该保持原样返回。
5. 性能优化与内存管理
5.1 内存分配优化
我们预先分配了与输入数组相同大小的内存,这在最坏情况下(所有区间都不重叠)是合适的。但在实际应用中,可以根据需要动态调整内存分配。
5.2 原地合并
如果允许修改输入数组,我们可以实现原地合并,减少内存使用:
c复制int mergeInPlace(Interval* intervals, int intervalsSize) {
if (intervalsSize <= 1) return intervalsSize;
qsort(intervals, intervalsSize, sizeof(Interval), compareIntervals);
int resultIndex = 0;
for (int i = 1; i < intervalsSize; i++) {
if (intervals[i].start <= intervals[resultIndex].end) {
intervals[resultIndex].end = intervals[i].end > intervals[resultIndex].end
? intervals[i].end : intervals[resultIndex].end;
} else {
intervals[++resultIndex] = intervals[i];
}
}
return resultIndex + 1;
}
5.3 大数组处理
对于非常大的区间数组,可以考虑使用更高效的排序算法,或者分治策略来减少内存访问开销。
6. 实际应用场景
合并区间算法在实际开发中有广泛应用:
- 会议时间安排:合并重叠的会议时间段,找出可用的时间槽
- 磁盘空间管理:合并相邻或重叠的磁盘块
- 日程管理应用:合并用户添加的重复或重叠的事件
- 图形渲染:合并重叠的图形区域以提高渲染效率
- 网络路由:合并IP地址范围以优化路由表
7. 常见错误与调试技巧
7.1 排序错误
忘记对区间进行排序是最常见的错误。未排序的区间会导致合并结果不正确。
7.2 边界条件处理
容易忽略空输入或单个区间的情况,导致程序崩溃或错误结果。
7.3 内存泄漏
在C语言实现中,必须记得释放动态分配的内存。可以使用以下辅助函数:
c复制void freeIntervals(Interval* intervals) {
free(intervals);
}
7.4 调试技巧
- 打印中间结果:在排序后和合并过程中打印区间数组,验证逻辑是否正确
- 使用断言:添加断言检查不变条件,如区间始终是合法的(start <= end)
- 单元测试:为各种边界情况编写测试用例
8. 扩展与变种问题
8.1 插入新区间
给定一组不重叠的区间和一个新区间,插入并合并重叠的区间。这是合并区间问题的变种,可以先插入再合并。
8.2 区间交集
给定两组区间,找出它们的交集。这需要双指针技巧来高效解决。
8.3 最小会议室数量
给定一组会议时间区间,计算需要的最小会议室数量。这可以通过"时间点扫描"算法解决。
8.4 删除最小区间数使剩余不重叠
这是一个典型的贪心算法问题,可以通过选择最早结束的区间来解决。
9. 与其他语言的实现对比
9.1 C++实现
C++可以利用STL的vector和sort函数简化实现:
cpp复制vector<vector<int>> merge(vector<vector<int>>& intervals) {
if (intervals.empty()) return {};
sort(intervals.begin(), intervals.end());
vector<vector<int>> merged;
for (auto interval : intervals) {
if (merged.empty() || merged.back()[1] < interval[0]) {
merged.push_back(interval);
} else {
merged.back()[1] = max(merged.back()[1], interval[1]);
}
}
return merged;
}
9.2 Python实现
Python的实现更加简洁:
python复制def merge(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
9.3 Java实现
Java的实现与C++类似:
java复制public int[][] merge(int[][] intervals) {
if (intervals.length <= 1) return intervals;
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
List<int[]> result = new ArrayList<>();
int[] current = intervals[0];
result.add(current);
for (int[] interval : intervals) {
if (interval[0] <= current[1]) {
current[1] = Math.max(current[1], interval[1]);
} else {
current = interval;
result.add(current);
}
}
return result.toArray(new int[result.size()][]);
}
10. 个人实现经验与建议
在实际项目中实现合并区间算法时,我有以下几点经验分享:
-
排序是关键:确保使用正确的排序标准(按区间起始点排序),这是算法正确性的基础。
-
边界检查:总是先处理空输入和单元素输入的边界情况,这能避免很多运行时错误。
-
内存管理:在C语言中要特别注意内存分配和释放,避免内存泄漏。可以考虑使用内存池技术来优化频繁的小内存分配。
-
测试覆盖:编写全面的测试用例,包括:
- 空输入
- 单个区间
- 完全不重叠的区间
- 完全包含的区间
- 相邻但不重叠的区间
- 需要多次合并的复杂情况
-
性能考量:对于非常大的区间集合,可以考虑并行化排序步骤,或者使用更高效的排序算法。
-
API设计:设计良好的函数接口,明确输入输出参数的含义。例如在C语言中,使用指针参数返回结果大小比返回结构体更灵活。
-
错误处理:添加适当的错误检查,如验证每个区间的start <= end,处理内存分配失败等情况。
-
文档注释:为函数添加详细的注释,说明其行为、参数含义和返回值,特别是内存所有权(调用者是否需要释放返回的指针)。
-
可重用性:将核心算法与IO操作分离,使代码更容易测试和重用。
-
性能分析:使用性能分析工具验证算法的时间复杂度是否符合预期,特别是在大数据集上的表现。
