1. 问题背景与核心挑战
合并区间是算法面试中的经典问题,也是实际业务场景中处理时间窗口、资源分配等需求的基础操作。题目给出一个区间集合,要求合并所有重叠的区间,最终返回不重叠的区间列表。
以示例 [[1,3],[2,6],[8,10],[15,18]] 为例,经过合并后应得到 [[1,6],[8,10],[15,18]]。这个看似简单的问题背后隐藏着几个关键挑战:
- 边界条件处理:当输入为空数组或单个区间时如何返回
- 完全包含场景:如
[1,4]和[2,3]的合并 - 多级重叠:连续多个区间存在交叉时的合并逻辑
- 性能考量:如何用最优时间复杂度解决问题
提示:在实际工程中,类似逻辑常用于合并日志时间段、优化会议安排等场景,是开发者的必备技能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算法思路与贪心思想解析
2.1 贪心算法的适用性分析
合并区间问题具有典型的贪心算法特征:
- 最优子结构:每个区间的合并决策不影响后续操作
- 无后效性:当前合并结果只与前面已处理区间相关
- 局部最优可推导全局最优:保证相邻区间有序后,只需单次遍历即可完成合并
具体实现步骤:
- 将所有区间按起始点升序排序
- 初始化结果集,放入第一个区间
- 遍历后续区间,比较当前区间与结果集最后一个区间:
- 若重叠则合并右边界
- 否则直接加入结果集
2.2 时间复杂度对比
| 方法 | 时间复杂度 | 空间复杂度 | 适用场景 |
|---|---|---|---|
| 暴力法 | O(n^2) | O(n) | 小数据量 |
| 排序+贪心 | O(nlogn) | O(logn) | 通用场景(推荐) |
| 区间树 | O(nlogn) | O(n) | 动态区间查询 |
实测数据(Go 1.18,10000个随机区间):
- 暴力法:~450ms
- 贪心法:~2ms(包含排序时间)
3. Go语言实现详解
3.1 自定义排序实现
Go的sort包要求实现Len/Less/Swap三个方法。对于二维切片,我们需要特殊处理:
go复制type Intervals [][]int
func (itv Intervals) Len() int { return len(itv) }
func (itv Intervals) Swap(i, j int) { itv[i], itv[j] = itv[j], itv[i] }
func (itv Intervals) Less(i, j int) bool {
return itv[i][0] < itv[j][0] // 按起始点升序
}
注意:当起始点相同时,建议按结束点降序排列,可以提前合并更多区间。这在处理类似
[[1,4],[1,3]]的情况时更高效。
3.2 合并逻辑的核心代码
go复制func merge(intervals [][]int) [][]int {
if len(intervals) == 0 {
return nil
}
sort.Sort(Intervals(intervals))
res := [][]int{intervals[0]}
for i := 1; i < len(intervals); i++ {
last := res[len(res)-1]
curr := intervals[i]
if curr[0] <= last[1] { // 有重叠
last[1] = max(last[1], curr[1]) // 合并右边界
} else {
res = append(res, curr)
}
}
return res
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
3.3 边界情况处理技巧
- 空输入检查:函数开头直接返回
nil - 单元素优化:排序后若只有一个区间直接返回
- 整数溢出:题目限定
10^4范围内,实际工程中需考虑math.MaxInt32 - 非法区间校验:实际应用中应检查
start <= end
4. 测试用例设计与调试技巧
4.1 必须覆盖的测试场景
go复制func TestMerge(t *testing.T) {
tests := []struct {
name string
input [][]int
expected [][]int
}{
{"空输入", nil, nil},
{"单区间", [][]int{{1,3}}, [][]int{{1,3}}},
{"完全包含", [][]int{{1,4},{2,3}}, [][]int{{1,4}}},
{"多级重叠", [][]int{{1,4},{4,5},{5,8}}, [][]int{{1,8}}},
{"不相邻", [][]int{{1,2},{3,4}}, [][]int{{1,2},{3,4}}},
{"负数值", [][]int{{-2,-1},{1,2}}, [][]int{{-2,-1},{1,2}}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := merge(tt.input); !reflect.DeepEqual(got, tt.expected) {
t.Errorf("merge() = %v, want %v", got, tt.expected)
}
})
}
}
4.2 常见错误排查
- 切片越界:未检查空输入直接访问
intervals[0] - 修改原数组:排序操作会影响输入参数(必要时深拷贝)
- 边界相等:
[1,2]和[2,3]应该合并为[1,3] - 性能陷阱:在循环中频繁
append可能导致多次内存分配
5. 工程实践中的扩展应用
5.1 实际业务场景变种
-
带权重的区间合并:每个区间附加优先级,合并时保留最大值
go复制type Interval struct { Start, End int Weight float64 } -
多维度合并:同时处理开始时间和结束时间
go复制sort.Slice(intervals, func(i, j int) bool { if intervals[i].Start != intervals[j].Start { return intervals[i].Start < intervals[j].Start } return intervals[i].End < intervals[j].End }) -
流式处理:适用于无法一次性加载所有区间的场景(使用堆维护当前边界)
5.2 性能优化进阶
- 并行排序:对于超大规模数据(1M+区间),可使用
sort.Slice配合go routines - 内存复用:预分配结果切片避免扩容
go复制res := make([][]int, 0, len(intervals)) - SIMD优化:使用
gorgonia等库加速排序操作
6. 与其他语言实现的对比
6.1 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
优势:
- 代码更简洁(得益于lambda表达式)
- 列表操作更灵活
劣势:
- 性能较差(CPython解释器执行效率低)
- 类型安全性弱
6.2 Java实现差异
java复制public int[][] merge(int[][] intervals) {
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
LinkedList<int[]> merged = new LinkedList<>();
for (int[] interval : intervals) {
if (merged.isEmpty() || merged.getLast()[1] < interval[0]) {
merged.add(interval);
} else {
merged.getLast()[1] = Math.max(merged.getLast()[1], interval[1]);
}
}
return merged.toArray(new int[merged.size()][]);
}
特点:
- 使用
LinkedList便于获取最后一个元素 - 需要处理二维数组到集合的转换
- 类型系统更严格
7. 刷题与面试技巧
7.1 白板编程要点
-
先沟通再写码:
- 明确输入输出格式
- 讨论边界条件
- 确认是否需要原地修改
-
手写排序逻辑:
go复制// 面试官可能要求手写快排 func quickSort(intervals [][]int, low, high int) { if low < high { pi := partition(intervals, low, high) quickSort(intervals, low, pi-1) quickSort(intervals, pi+1, high) } } -
可视化演示:画出区间重叠的几种情况
7.2 高频变种问题
- 插入新区间:在已排序的区间列表中插入并合并
- 求交集:找出多个区间集合的共同空闲时间
- 会议室II:计算需要的最少会议室数量(扫描线算法)
- 删除最小区间数:使剩余区间互不重叠(类似贪心)
8. Go语言特性深度利用
8.1 使用sort.Slice简化代码
Go 1.8+ 支持更简洁的排序方式:
go复制sort.Slice(intervals, func(i, j int) bool {
return intervals[i][0] < intervals[j][0]
})
8.2 内存优化技巧
-
复用输入切片:修改原数组节省内存
go复制func merge(intervals [][]int) [][]int { sort.Slice(intervals, func(i, j int) bool { return intervals[i][0] < intervals[j][0] }) idx := 0 for i := 1; i < len(intervals); i++ { if intervals[idx][1] >= intervals[i][0] { intervals[idx][1] = max(intervals[idx][1], intervals[i][1]) } else { idx++ intervals[idx] = intervals[i] } } return intervals[:idx+1] } -
使用固定长度数组:当区间长度已知时
go复制type Interval [2]int
8.3 错误处理最佳实践
go复制func validateIntervals(intervals [][]int) error {
for _, itv := range intervals {
if len(itv) != 2 {
return fmt.Errorf("invalid interval length: %v", itv)
}
if itv[0] > itv[1] {
return fmt.Errorf("start > end in interval: %v", itv)
}
}
return nil
}
