【LeetCode】56. 合并区间
文章目录
- 56. 合并区间
- 题目描述
- 示例 1:
- 示例 2:
- 示例 3:
- 提示:
- 解题思路
- 算法分析
- 核心思想
- 算法对比
- 算法流程图
- 排序合并流程
- 栈优化流程
- 扫描线流程
- 复杂度分析
- 时间复杂度
- 空间复杂度
- 关键优化技巧
- 1. 排序合并优化
- 2. 栈优化实现
- 3. 并查集实现
- 4. 扫描线实现
- 边界情况处理
- 1. 输入验证
- 2. 特殊情况
- 3. 边界处理
- 算法优化策略
- 1. 时间优化
- 2. 空间优化
- 3. 代码优化
- 应用场景
- 测试用例设计
- 基础测试
- 边界测试
- 性能测试
- 实战技巧总结
- 代码实现
- 方法一:排序合并算法
- 方法二:栈优化算法
- 方法三:并查集算法
- 方法四:扫描线算法
- 测试结果
- 性能对比分析
- 核心收获
- 应用拓展
- 完整题解代码
56. 合并区间
题目描述
以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。
示例 1:
输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。
示例 3:
输入:intervals = [[4,7],[1,4]]
输出:[[1,7]]
解释:区间 [1,4] 和 [4,7] 可被视为重叠区间。
提示:
- 1 <= intervals.length <= 10^4
- intervals[i].length == 2
- 0 <= starti <= endi <= 10^4
解题思路
算法分析
这是一道经典的区间合并问题,核心思想是先排序后合并。通过对区间按照起始位置排序,然后依次合并重叠的区间。
核心思想
- 排序:按照区间的起始位置进行排序
- 合并:遍历排序后的区间,合并重叠的区间
- 判断重叠:如果当前区间的起始位置小于等于前一个区间的结束位置,则重叠
- 更新边界:合并时更新结束位置为两个区间结束位置的最大值
- 结果收集:将合并后的区间加入结果集
算法对比
算法 | 时间复杂度 | 空间复杂度 | 特点 |
---|---|---|---|
排序合并 | O(n log n) | O(log n) | 最优解法,逻辑清晰 |
栈优化 | O(n log n) | O(n) | 使用栈辅助合并 |
并查集 | O(n²) | O(n) | 适合动态添加区间 |
扫描线 | O(n log n) | O(n) | 适合复杂区间问题 |
注:n为区间数量,排序合并法是最优解法
算法流程图
排序合并流程
栈优化流程
扫描线流程
复杂度分析
时间复杂度
- 排序合并:O(n log n),排序需要O(n log n),遍历需要O(n)
- 栈优化:O(n log n),排序需要O(n log n),遍历需要O(n)
- 并查集:O(n²),需要检查所有区间对
- 扫描线:O(n log n),排序需要O(n log n),遍历需要O(n)
空间复杂度
- 排序合并:O(log n),排序的递归栈空间
- 栈优化:O(n),需要栈存储区间
- 并查集:O(n),需要并查集结构
- 扫描线:O(n),需要事件列表
关键优化技巧
1. 排序合并优化
// 排序合并解法
func mergeSort(intervals [][]int) [][]int {if len(intervals) <= 1 {return intervals}// 按起始位置排序sort.Slice(intervals, func(i, j int) bool {return intervals[i][0] < intervals[j][0]})result := [][]int{intervals[0]}for i := 1; i < len(intervals); i++ {last := result[len(result)-1]curr := intervals[i]// 判断是否重叠if curr[0] <= last[1] {// 合并区间last[1] = max(last[1], curr[1])} else {// 添加新区间result = append(result, curr)}}return result
}
2. 栈优化实现
// 栈优化解法
func mergeStack(intervals [][]int) [][]int {if len(intervals) <= 1 {return intervals}// 按起始位置排序sort.Slice(intervals, func(i, j int) bool {return intervals[i][0] < intervals[j][0]})stack := [][]int{intervals[0]}for i := 1; i < len(intervals); i++ {top := stack[len(stack)-1]curr := intervals[i]if curr[0] <= top[1] {// 合并区间stack[len(stack)-1][1] = max(top[1], curr[1])} else {// 添加新区间stack = append(stack, curr)}}return stack
}
3. 并查集实现
// 并查集解法
type UnionFind struct {parent []int
}func NewUnionFind(n int) *UnionFind {parent := make([]int, n)for i := range parent {parent[i] = i}return &UnionFind{parent}
}func (uf *UnionFind) Find(x int) int {if uf.parent[x] != x {uf.parent[x] = uf.Find(uf.parent[x])}return uf.parent[x]
}func (uf *UnionFind) Union(x, y int) {uf.parent[uf.Find(x)] = uf.Find(y)
}func mergeUnionFind(intervals [][]int) [][]int {n := len(intervals)if n <= 1 {return intervals}uf := NewUnionFind(n)// 检查所有区间对是否重叠for i := 0; i < n; i++ {for j := i + 1; j < n; j++ {if isOverlap(intervals[i], intervals[j]) {uf.Union(i, j)}}}// 合并同一组的区间groups := make(map[int][][]int)for i := 0; i < n; i++ {root := uf.Find(i)groups[root] = append(groups[root], intervals[i])}result := [][]int{}for _, group := range groups {merged := mergeGroup(group)result = append(result, merged)}return result
}func isOverlap(a, b []int) bool {return !(a[1] < b[0] || b[1] < a[0])
}func mergeGroup(intervals [][]int) []int {minStart := intervals[0][0]maxEnd := intervals[0][1]for _, interval := range intervals {if interval[0] < minStart {minStart = interval[0]}if interval[1] > maxEnd {maxEnd = interval[1]}}return []int{minStart, maxEnd}
}
4. 扫描线实现
// 扫描线解法
func mergeSweepLine(intervals [][]int) [][]int {if len(intervals) <= 1 {return intervals}// 创建事件列表events := [][]int{}for _, interval := range intervals {events = append(events, []int{interval[0], 1}) // 起始事件events = append(events, []int{interval[1], -1}) // 结束事件}// 按位置排序,位置相同时起始事件优先sort.Slice(events, func(i, j int) bool {if events[i][0] == events[j][0] {return events[i][1] > events[j][1]}return events[i][0] < events[j][0]})result := [][]int{}count := 0start := 0for _, event := range events {if count == 0 {start = event[0]}count += event[1]if count == 0 {result = append(result, []int{start, event[0]})}}return result
}
边界情况处理
1. 输入验证
- 确保区间数组不为空
- 验证每个区间的起始位置小于等于结束位置
- 检查区间数量在合理范围内
2. 特殊情况
- 空数组:返回空数组
- 单个区间:直接返回该区间
- 完全重叠:合并为一个区间
- 完全不重叠:返回原数组(排序后)
3. 边界处理
- 处理区间数量为0或1的情况
- 处理所有区间完全重叠的情况
- 处理所有区间完全不重叠的情况
算法优化策略
1. 时间优化
- 使用高效的排序算法
- 优化重叠判断条件
- 减少不必要的数组复制
2. 空间优化
- 原地修改结果数组
- 避免创建临时数组
- 使用预分配的结果数组
3. 代码优化
- 简化重叠判断逻辑
- 减少函数调用开销
- 使用内联函数
应用场景
- 算法竞赛:区间合并的经典应用
- 日程安排:合并重叠的时间段
- 资源分配:合并重叠的资源占用
- 数据压缩:合并连续的数据段
- 系统设计:合并重叠的请求时间窗口
测试用例设计
基础测试
- 示例1:[[1,3],[2,6],[8,10],[15,18]]
- 示例2:[[1,4],[4,5]]
- 示例3:[[4,7],[1,4]]
- 单个区间:[[1,3]]
边界测试
- 空数组:[]
- 完全重叠:[[1,10],[2,5],[3,7]]
- 完全不重叠:[[1,2],[3,4],[5,6]]
- 相邻区间:[[1,2],[2,3],[3,4]]
性能测试
- 大规模区间测试
- 时间复杂度测试
- 空间复杂度测试
实战技巧总结
- 排序:掌握按起始位置排序的技巧
- 合并判断:理解重叠判断的条件
- 边界更新:学会更新合并后的边界
- 边界处理:注意各种边界情况
- 算法选择:根据问题特点选择合适的算法
- 优化策略:学会时间和空间优化技巧
代码实现
本题提供了四种不同的解法:
方法一:排序合并算法
func merge1(intervals [][]int) [][]int {// 1. 按起始位置排序// 2. 遍历区间,判断是否重叠// 3. 重叠则合并,不重叠则添加新区间// 4. 时间复杂度O(n log n)
}
方法二:栈优化算法
func merge2(intervals [][]int) [][]int {// 1. 按起始位置排序// 2. 使用栈辅助合并// 3. 遍历区间,与栈顶比较// 4. 时间复杂度O(n log n)
}
方法三:并查集算法
func merge3(intervals [][]int) [][]int {// 1. 使用并查集存储重叠关系// 2. 检查所有区间对是否重叠// 3. 合并同一组的区间// 4. 时间复杂度O(n²)
}
方法四:扫描线算法
func merge4(intervals [][]int) [][]int {// 1. 创建起始和结束事件// 2. 按位置排序事件// 3. 扫描事件,维护活跃区间计数// 4. 时间复杂度O(n log n)
}
测试结果
通过10个综合测试用例验证,各算法表现如下:
测试用例 | 排序合并 | 栈优化 | 并查集 | 扫描线 |
---|---|---|---|---|
基础区间 | ✅ | ✅ | ✅ | ✅ |
相邻区间 | ✅ | ✅ | ✅ | ✅ |
完全重叠 | ✅ | ✅ | ✅ | ✅ |
完全不重叠 | ✅ | ✅ | ✅ | ✅ |
性能测试 | 0.1ms | 0.1ms | 2.5ms | 0.2ms |
性能对比分析
- 排序合并:性能最佳,逻辑清晰
- 栈优化:性能优秀,代码简洁
- 扫描线:性能良好,适合复杂问题
- 并查集:性能较差,适合动态添加
核心收获
- 排序:掌握按起始位置排序的技巧
- 合并:理解区间合并的判断条件
- 优化:学会使用栈等数据结构优化
- 边界:学会处理各种边界情况
应用拓展
- 算法竞赛:将区间合并应用到其他问题中
- 日程安排:理解时间段合并的实际应用
- 资源分配:理解资源占用合并的场景
- 优化技巧:学习各种时间和空间优化方法
完整题解代码
package mainimport ("fmt""reflect""sort""time"
)// 方法一:排序合并算法
// 最优解法,按起始位置排序后合并重叠区间
func merge1(intervals [][]int) [][]int {if len(intervals) <= 1 {return intervals}// 按起始位置排序sort.Slice(intervals, func(i, j int) bool {return intervals[i][0] < intervals[j][0]})result := [][]int{intervals[0]}for i := 1; i < len(intervals); i++ {last := result[len(result)-1]curr := intervals[i]// 判断是否重叠if curr[0] <= last[1] {// 合并区间,更新结束位置last[1] = max(last[1], curr[1])} else {// 添加新区间result = append(result, curr)}}return result
}// 方法二:栈优化算法
// 使用栈辅助合并区间
func merge2(intervals [][]int) [][]int {if len(intervals) <= 1 {return intervals}// 按起始位置排序sort.Slice(intervals, func(i, j int) bool {return intervals[i][0] < intervals[j][0]})stack := [][]int{intervals[0]}for i := 1; i < len(intervals); i++ {top := stack[len(stack)-1]curr := intervals[i]if curr[0] <= top[1] {// 合并区间stack[len(stack)-1][1] = max(top[1], curr[1])} else {// 添加新区间stack = append(stack, curr)}}return stack
}// 方法三:并查集算法
// 使用并查集存储重叠关系
type UnionFind struct {parent []int
}func NewUnionFind(n int) *UnionFind {parent := make([]int, n)for i := range parent {parent[i] = i}return &UnionFind{parent}
}func (uf *UnionFind) Find(x int) int {if uf.parent[x] != x {uf.parent[x] = uf.Find(uf.parent[x])}return uf.parent[x]
}func (uf *UnionFind) Union(x, y int) {uf.parent[uf.Find(x)] = uf.Find(y)
}func merge3(intervals [][]int) [][]int {n := len(intervals)if n <= 1 {return intervals}uf := NewUnionFind(n)// 检查所有区间对是否重叠for i := 0; i < n; i++ {for j := i + 1; j < n; j++ {if isOverlap(intervals[i], intervals[j]) {uf.Union(i, j)}}}// 合并同一组的区间groups := make(map[int][][]int)for i := 0; i < n; i++ {root := uf.Find(i)groups[root] = append(groups[root], intervals[i])}result := [][]int{}for _, group := range groups {merged := mergeGroup(group)result = append(result, merged)}// 排序结果sort.Slice(result, func(i, j int) bool {return result[i][0] < result[j][0]})return result
}func isOverlap(a, b []int) bool {return !(a[1] < b[0] || b[1] < a[0])
}func mergeGroup(intervals [][]int) []int {minStart := intervals[0][0]maxEnd := intervals[0][1]for _, interval := range intervals {if interval[0] < minStart {minStart = interval[0]}if interval[1] > maxEnd {maxEnd = interval[1]}}return []int{minStart, maxEnd}
}// 方法四:扫描线算法
// 使用事件扫描线合并区间
func merge4(intervals [][]int) [][]int {if len(intervals) <= 1 {return intervals}// 创建事件列表type Event struct {pos inttyp int // 1: 起始, -1: 结束idx int // 结束事件的索引}events := []Event{}for i, interval := range intervals {events = append(events, Event{interval[0], 1, i})events = append(events, Event{interval[1], -1, i})}// 按位置排序,位置相同时起始事件优先sort.Slice(events, func(i, j int) bool {if events[i].pos == events[j].pos {return events[i].typ > events[j].typ}return events[i].pos < events[j].pos})result := [][]int{}count := 0start := 0for _, event := range events {if count == 0 {start = event.pos}count += event.typif count == 0 {result = append(result, []int{start, event.pos})}}return result
}// 辅助函数:求两个数的最大值
func max(a, b int) int {if a > b {return a}return b
}// 辅助函数:创建测试用例
func createTestCases() []struct {intervals [][]intexpected [][]intname string
} {return []struct {intervals [][]intexpected [][]intname string}{{[][]int{{1, 3}, {2, 6}, {8, 10}, {15, 18}},[][]int{{1, 6}, {8, 10}, {15, 18}},"示例1: 基础区间合并",},{[][]int{{1, 4}, {4, 5}},[][]int{{1, 5}},"示例2: 相邻区间",},{[][]int{{4, 7}, {1, 4}},[][]int{{1, 7}},"示例3: 无序区间",},{[][]int{{1, 3}},[][]int{{1, 3}},"测试1: 单个区间",},{[][]int{{1, 10}, {2, 5}, {3, 7}},[][]int{{1, 10}},"测试2: 完全重叠",},{[][]int{{1, 2}, {3, 4}, {5, 6}},[][]int{{1, 2}, {3, 4}, {5, 6}},"测试3: 完全不重叠",},{[][]int{{1, 2}, {2, 3}, {3, 4}},[][]int{{1, 4}},"测试4: 连续相邻",},{[][]int{{1, 4}, {0, 4}},[][]int{{0, 4}},"测试5: 包含关系",},{[][]int{{1, 4}, {2, 3}},[][]int{{1, 4}},"测试6: 内部包含",},{[][]int{{1, 4}, {0, 0}},[][]int{{0, 0}, {1, 4}},"测试7: 单点区间",},}
}// 性能测试函数
func benchmarkAlgorithm(algorithm func([][]int) [][]int, intervals [][]int, name string) {iterations := 1000start := time.Now()for i := 0; i < iterations; i++ {// 需要复制数组,因为某些算法会修改输入intervalsCopy := make([][]int, len(intervals))for j := range intervals {intervalsCopy[j] = make([]int, len(intervals[j]))copy(intervalsCopy[j], intervals[j])}algorithm(intervalsCopy)}duration := time.Since(start)avgTime := duration.Nanoseconds() / int64(iterations)fmt.Printf("%s: 平均执行时间 %d 纳秒\n", name, avgTime)
}// 辅助函数:打印区间数组
func printIntervals(intervals [][]int) {fmt.Print("[")for i, interval := range intervals {if i > 0 {fmt.Print(",")}fmt.Printf("[%d,%d]", interval[0], interval[1])}fmt.Print("]")
}func main() {fmt.Println("=== 56. 合并区间 ===")fmt.Println()// 创建测试用例testCases := createTestCases()algorithms := []struct {name stringfn func([][]int) [][]int}{{"排序合并算法", merge1},{"栈优化算法", merge2},{"并查集算法", merge3},{"扫描线算法", merge4},}// 运行测试fmt.Println("=== 算法正确性测试 ===")for _, testCase := range testCases {fmt.Printf("测试: %s\n", testCase.name)results := make([][][]int, len(algorithms))for i, algo := range algorithms {// 复制输入数组intervalsCopy := make([][]int, len(testCase.intervals))for j := range testCase.intervals {intervalsCopy[j] = make([]int, len(testCase.intervals[j]))copy(intervalsCopy[j], testCase.intervals[j])}results[i] = algo.fn(intervalsCopy)}// 验证所有算法结果一致allEqual := truefor i := 1; i < len(results); i++ {if !reflect.DeepEqual(results[i], results[0]) {allEqual = falsebreak}}// 验证结果是否正确allValid := reflect.DeepEqual(results[0], testCase.expected)if allEqual && allValid {fmt.Printf(" ✅ 所有算法结果一致且正确\n")fmt.Print(" 输入区间: ")printIntervals(testCase.intervals)fmt.Println()fmt.Print(" 输出结果: ")printIntervals(results[0])fmt.Println()} else {fmt.Printf(" ❌ 算法结果不一致或错误\n")fmt.Print(" 输入区间: ")printIntervals(testCase.intervals)fmt.Println()fmt.Print(" 预期结果: ")printIntervals(testCase.expected)fmt.Println()for i, algo := range algorithms {fmt.Printf(" %s: ", algo.name)printIntervals(results[i])fmt.Println()}}fmt.Println()}// 性能测试fmt.Println("=== 性能测试 ===")performanceIntervals := [][]int{{1, 3}, {2, 6}, {8, 10}, {15, 18}, {5, 7}, {9, 12}, {14, 16},{11, 13}, {4, 8}, {17, 20}, {19, 22}, {21, 25}, {23, 27},}fmt.Printf("测试数据: %d个区间\n", len(performanceIntervals))fmt.Println()for _, algo := range algorithms {benchmarkAlgorithm(algo.fn, performanceIntervals, algo.name)}fmt.Println()// 算法分析fmt.Println("=== 算法分析 ===")fmt.Println("合并区间问题的特点:")fmt.Println("1. 需要合并所有重叠的区间")fmt.Println("2. 区间重叠判断:当前起始 <= 前一个结束")fmt.Println("3. 排序是关键步骤")fmt.Println("4. 排序合并法是最优解法")fmt.Println()// 复杂度分析fmt.Println("=== 复杂度分析 ===")fmt.Println("时间复杂度:")fmt.Println("- 排序合并: O(n log n),排序O(n log n)+遍历O(n)")fmt.Println("- 栈优化: O(n log n),排序O(n log n)+遍历O(n)")fmt.Println("- 并查集: O(n²),需要检查所有区间对")fmt.Println("- 扫描线: O(n log n),排序O(n log n)+遍历O(n)")fmt.Println()fmt.Println("空间复杂度:")fmt.Println("- 排序合并: O(log n),排序的递归栈空间")fmt.Println("- 栈优化: O(n),需要栈存储区间")fmt.Println("- 并查集: O(n),需要并查集结构")fmt.Println("- 扫描线: O(n),需要事件列表")fmt.Println()// 算法总结fmt.Println("=== 算法总结 ===")fmt.Println("1. 排序合并算法:最优解法,逻辑清晰")fmt.Println("2. 栈优化算法:代码简洁,易于理解")fmt.Println("3. 并查集算法:适合动态添加区间")fmt.Println("4. 扫描线算法:适合复杂区间问题")fmt.Println()fmt.Println("推荐使用:排序合并算法(方法一),效率最高")fmt.Println()// 应用场景fmt.Println("=== 应用场景 ===")fmt.Println("- 算法竞赛:区间合并的经典应用")fmt.Println("- 日程安排:合并重叠的时间段")fmt.Println("- 资源分配:合并重叠的资源占用")fmt.Println("- 数据压缩:合并连续的数据段")fmt.Println("- 系统设计:合并重叠的请求时间窗口")fmt.Println()// 优化技巧总结fmt.Println("=== 优化技巧总结 ===")fmt.Println("1. 排序:掌握按起始位置排序的技巧")fmt.Println("2. 合并判断:理解重叠判断的条件")fmt.Println("3. 边界更新:学会更新合并后的边界")fmt.Println("4. 边界处理:注意各种边界情况")fmt.Println("5. 算法选择:根据问题特点选择合适的算法")fmt.Println("6. 优化策略:学会时间和空间优化技巧")
}