目录
- [1. 问题描述](#1. 问题描述)
- [2. 问题分析](#2. 问题分析)
-
- [2.1 题目理解](#2.1 题目理解)
- [2.2 核心洞察](#2.2 核心洞察)
- [2.3 破题关键](#2.3 破题关键)
- [3. 算法设计与实现](#3. 算法设计与实现)
-
- [3.1 暴力枚举法](#3.1 暴力枚举法)
- [3.2 排序+线性扫描(标准解法)](#3.2 排序+线性扫描(标准解法))
- [3.3 排序+最小堆/优先队列](#3.3 排序+最小堆/优先队列)
- [3.4 图连通分量法(并查集)](#3.4 图连通分量法(并查集))
- [4. 性能对比](#4. 性能对比)
- [5. 扩展与变体](#5. 扩展与变体)
-
- [5.1 插入区间](#5.1 插入区间)
- [5.2 区间交集](#5.2 区间交集)
- [5.3 删除区间使剩余区间不重叠](#5.3 删除区间使剩余区间不重叠)
- [5.4 会议室安排问题](#5.4 会议室安排问题)
- [6. 总结](#6. 总结)
-
- [6.1 核心思想总结](#6.1 核心思想总结)
- [6.2 算法选择指南](#6.2 算法选择指南)
- [6.3 应用场景](#6.3 应用场景)
- [6.4 面试技巧](#6.4 面试技巧)
1. 问题描述
以数组 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^4intervals[i].length == 20 <= starti <= endi <= 10^4
2. 问题分析
2.1 题目理解
我们需要将给定的一系列区间进行合并,使得合并后的区间集合满足:
- 所有区间都不重叠(任意两个区间没有交集)
- 合并后的区间集合恰好覆盖原始所有区间
- 如果两个区间有重叠(包括端点重叠),它们应该被合并为一个区间
2.2 核心洞察
- 区间重叠条件 :两个区间
[a, b]和[c, d]重叠的条件是a <= d且c <= b - 合并操作 :合并两个重叠区间
[a, b]和[c, d]的结果是[min(a, c), max(b, d)] - 排序的重要性:如果区间按照起始点排序,那么重叠的区间会相邻,便于合并
2.3 破题关键
问题的核心在于如何高效地识别和处理重叠区间:
- 如果区间无序,判断重叠需要比较所有区间对,复杂度高
- 对区间按起始点排序后,重叠的区间会相邻出现,只需一次扫描即可完成合并
- 合并时只需要维护当前合并区间的结束点,与下一个区间的起始点比较
3. 算法设计与实现
3.1 暴力枚举法
核心思想
尝试所有可能的区间合并方式,找到最终的合并结果。
算法思路
- 将区间列表转换为集合
- 重复以下过程直到没有可合并的区间:
- 遍历所有区间对,检查是否重叠
- 如果重叠,合并它们,从集合中移除原来的两个区间,添加合并后的新区间
- 返回最终的区间集合
Java代码实现
java
import java.util.*;
public class MergeIntervalsBruteForce {
/**
* 暴力解法 - 重复合并直到稳定
* 时间复杂度: O(n³) 最坏情况
* 空间复杂度: O(n)
*/
public int[][] merge(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
List<int[]> intervalList = new ArrayList<>(Arrays.asList(intervals));
boolean merged;
do {
merged = false;
List<int[]> result = new ArrayList<>();
boolean[] mergedFlag = new boolean[intervalList.size()];
for (int i = 0; i < intervalList.size(); i++) {
if (mergedFlag[i]) continue;
int[] current = intervalList.get(i);
boolean currentMerged = false;
for (int j = i + 1; j < intervalList.size(); j++) {
if (mergedFlag[j]) continue;
int[] next = intervalList.get(j);
if (overlap(current, next)) {
// 合并区间
int[] mergedInterval = new int[]{
Math.min(current[0], next[0]),
Math.max(current[1], next[1])
};
result.add(mergedInterval);
mergedFlag[i] = true;
mergedFlag[j] = true;
currentMerged = true;
merged = true;
break;
}
}
if (!currentMerged) {
result.add(current);
mergedFlag[i] = true;
}
}
intervalList = result;
} while (merged);
return intervalList.toArray(new int[intervalList.size()][]);
}
/**
* 检查两个区间是否重叠
*/
private boolean overlap(int[] a, int[] b) {
return !(a[1] < b[0] || b[1] < a[0]);
}
/**
* 优化的暴力解法 - 使用图的思想
*/
public int[][] mergeOptimizedBrute(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
// 构建重叠关系图
int n = intervals.length;
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < n; i++) {
graph.add(new ArrayList<>());
}
// 建立重叠关系
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (overlap(intervals[i], intervals[j])) {
graph.get(i).add(j);
graph.get(j).add(i);
}
}
}
// 使用DFS找到连通分量(重叠区间组)
boolean[] visited = new boolean[n];
List<int[]> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
if (!visited[i]) {
List<Integer> component = new ArrayList<>();
dfs(i, graph, visited, component);
// 合并连通分量中的所有区间
int minStart = Integer.MAX_VALUE;
int maxEnd = Integer.MIN_VALUE;
for (int idx : component) {
minStart = Math.min(minStart, intervals[idx][0]);
maxEnd = Math.max(maxEnd, intervals[idx][1]);
}
result.add(new int[]{minStart, maxEnd});
}
}
// 按起始点排序结果(可选)
result.sort((a, b) -> Integer.compare(a[0], b[0]));
return result.toArray(new int[result.size()][]);
}
private void dfs(int node, List<List<Integer>> graph, boolean[] visited, List<Integer> component) {
visited[node] = true;
component.add(node);
for (int neighbor : graph.get(node)) {
if (!visited[neighbor]) {
dfs(neighbor, graph, visited, component);
}
}
}
}
性能分析
- 时间复杂度:O(n³),最坏情况下需要多次合并,每次合并需要检查所有区间对
- 空间复杂度:O(n²),存储重叠关系图
- 适用场景:仅适用于非常小的输入规模(n ≤ 100)
3.2 排序+线性扫描(标准解法)
核心思想
先按区间起始点排序,然后线性扫描,合并重叠的相邻区间。
算法思路
- 按区间起始点对区间进行排序
- 初始化结果列表,将第一个区间加入结果
- 遍历排序后的区间:
- 如果当前区间与结果列表中最后一个区间重叠,合并它们
- 否则,将当前区间加入结果列表
- 返回结果
Java代码实现
java
import java.util.*;
public class MergeIntervalsSorting {
/**
* 排序+线性扫描(标准解法)
* 时间复杂度: O(n log n),排序占主导
* 空间复杂度: O(log n) 或 O(n),取决于排序算法
*/
public int[][] merge(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
// 按区间起始点排序
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
List<int[]> result = new ArrayList<>();
// 将第一个区间加入结果
result.add(intervals[0]);
for (int i = 1; i < intervals.length; i++) {
int[] current = intervals[i];
int[] last = result.get(result.size() - 1);
// 检查当前区间是否与最后一个区间重叠
if (current[0] <= last[1]) {
// 合并区间:更新最后一个区间的结束点
last[1] = Math.max(last[1], current[1]);
} else {
// 不重叠,添加新区间
result.add(current);
}
}
return result.toArray(new int[result.size()][]);
}
/**
* 更详细的实现,带注释
*/
public int[][] mergeDetailed(int[][] intervals) {
if (intervals == null || intervals.length == 0) {
return new int[0][0];
}
// 步骤1:排序
// 按照区间起始点升序排列
Arrays.sort(intervals, new Comparator<int[]>() {
@Override
public int compare(int[] a, int[] b) {
return Integer.compare(a[0], b[0]);
}
});
// 步骤2:初始化结果列表
List<int[]> merged = new ArrayList<>();
// 步骤3:遍历排序后的区间
for (int[] interval : intervals) {
int start = interval[0];
int end = interval[1];
// 如果结果列表为空,或者当前区间与最后一个区间不重叠
if (merged.isEmpty() || merged.get(merged.size() - 1)[1] < start) {
merged.add(new int[]{start, end});
} else {
// 重叠,合并区间
// 只需要更新最后一个区间的结束点,因为起始点已经排序
merged.get(merged.size() - 1)[1] =
Math.max(merged.get(merged.size() - 1)[1], end);
}
}
// 步骤4:转换为数组返回
return merged.toArray(new int[merged.size()][]);
}
/**
* 使用流式API的实现(Java 8+)
*/
public int[][] mergeStream(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
// 排序
Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
// 使用LinkedList便于修改最后一个元素
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()][]);
}
/**
* 返回合并过程中的详细步骤
*/
public List<List<int[]>> mergeWithSteps(int[][] intervals) {
List<List<int[]>> steps = new ArrayList<>();
if (intervals == null || intervals.length <= 1) {
steps.add(Arrays.asList(intervals));
return steps;
}
// 初始状态
List<int[]> current = new ArrayList<>(Arrays.asList(intervals));
steps.add(new ArrayList<>(current));
// 排序
current.sort(Comparator.comparingInt(a -> a[0]));
steps.add(new ArrayList<>(current));
// 合并
List<int[]> merged = new ArrayList<>();
merged.add(current.get(0));
steps.add(new ArrayList<>(merged));
for (int i = 1; i < current.size(); i++) {
int[] last = merged.get(merged.size() - 1);
int[] curr = current.get(i);
if (curr[0] <= last[1]) {
// 合并
last[1] = Math.max(last[1], curr[1]);
} else {
// 添加新区间
merged.add(curr);
}
steps.add(new ArrayList<>(merged));
}
return steps;
}
}
图解算法
示例:intervals = [[1,3],[2,6],[8,10],[15,18]]
步骤1:排序(已经按起始点排序)
intervals = [[1,3],[2,6],[8,10],[15,18]]
步骤2:初始化
result = [[1,3]]
步骤3:遍历
i=1: [2,6] 与 [1,3] 重叠 (2 <= 3)
合并:result = [[1,6]]
i=2: [8,10] 与 [1,6] 不重叠 (8 > 6)
添加:result = [[1,6],[8,10]]
i=3: [15,18] 与 [8,10] 不重叠 (15 > 10)
添加:result = [[1,6],[8,10],[15,18]]
性能分析
- 时间复杂度:O(n log n),排序占主导,线性扫描 O(n)
- 空间复杂度:O(log n) 或 O(n),取决于排序算法的空间复杂度
- 优势:简洁高效,是最优解
3.3 排序+最小堆/优先队列
核心思想
使用最小堆(优先队列)按区间结束点维护,便于快速找到可以合并的区间。
算法思路
- 按区间起始点排序
- 使用最小堆存储区间,按结束点排序
- 遍历排序后的区间:
- 如果堆为空或当前区间起始点 > 堆顶区间的结束点,将当前区间加入堆
- 否则,弹出堆顶区间,与当前区间合并,将合并后的区间加入堆
- 堆中剩余的区间即为合并结果
Java代码实现
java
import java.util.*;
public class MergeIntervalsHeap {
/**
* 使用优先队列(最小堆)的解法
* 时间复杂度: O(n log n)
* 空间复杂度: O(n)
*/
public int[][] merge(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
// 按起始点排序
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
// 最小堆,按区间结束点排序
PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> Integer.compare(a[1], b[1]));
for (int[] interval : intervals) {
if (heap.isEmpty() || interval[0] > heap.peek()[1]) {
// 不重叠,直接加入堆
heap.offer(interval);
} else {
// 重叠,合并区间
int[] top = heap.poll();
int[] merged = new int[]{
Math.min(top[0], interval[0]),
Math.max(top[1], interval[1])
};
heap.offer(merged);
}
}
// 将堆中的区间转换为结果
List<int[]> result = new ArrayList<>();
while (!heap.isEmpty()) {
result.add(heap.poll());
}
// 按起始点排序(堆是按结束点排序的)
result.sort((a, b) -> Integer.compare(a[0], b[0]));
return result.toArray(new int[result.size()][]);
}
/**
* 使用双优先队列优化
*/
public int[][] mergeTwoHeaps(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
// 按起始点排序的最小堆
PriorityQueue<int[]> startHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[0], b[0]));
// 按结束点排序的最小堆
PriorityQueue<int[]> endHeap = new PriorityQueue<>((a, b) -> Integer.compare(a[1], b[1]));
// 将所有区间加入起始点堆
for (int[] interval : intervals) {
startHeap.offer(interval);
}
List<int[]> result = new ArrayList<>();
while (!startHeap.isEmpty()) {
int[] current = startHeap.poll();
if (endHeap.isEmpty() || current[0] > endHeap.peek()[1]) {
// 不重叠,直接加入结束点堆
endHeap.offer(current);
} else {
// 重叠,合并区间
int[] top = endHeap.poll();
int[] merged = new int[]{
Math.min(top[0], current[0]),
Math.max(top[1], current[1])
};
endHeap.offer(merged);
}
}
// 结束点堆中的区间即为结果
while (!endHeap.isEmpty()) {
result.add(endHeap.poll());
}
// 按起始点排序
result.sort((a, b) -> Integer.compare(a[0], b[0]));
return result.toArray(new int[result.size()][]);
}
}
性能分析
- 时间复杂度:O(n log n),每个区间入堆出堆一次
- 空间复杂度:O(n),堆中最多存储n个区间
- 优势:思路清晰,易于理解合并过程
- 劣势:相比直接排序扫描,需要额外空间,且效率略低
3.4 图连通分量法(并查集)
核心思想
将区间看作图中的节点,如果两个区间重叠,则在它们之间添加边。然后使用并查集找到连通分量,每个连通分量内的区间合并为一个区间。
算法思路
- 构建重叠关系图:对于每对区间,如果重叠,则在并查集中合并
- 使用并查集找到所有连通分量
- 对每个连通分量,合并其中的所有区间
- 返回合并后的区间列表
Java代码实现
java
import java.util.*;
public class MergeIntervalsUnionFind {
/**
* 并查集解法
* 时间复杂度: O(n²) 最坏情况,构建重叠关系需要O(n²)
* 空间复杂度: O(n)
*/
public int[][] merge(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
int n = intervals.length;
// 并查集初始化
UnionFind uf = new UnionFind(n);
// 构建重叠关系
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (overlap(intervals[i], intervals[j])) {
uf.union(i, j);
}
}
}
// 按连通分量分组
Map<Integer, List<Integer>> components = new HashMap<>();
for (int i = 0; i < n; i++) {
int root = uf.find(i);
components.putIfAbsent(root, new ArrayList<>());
components.get(root).add(i);
}
// 合并每个连通分量中的区间
List<int[]> result = new ArrayList<>();
for (List<Integer> indices : components.values()) {
int minStart = Integer.MAX_VALUE;
int maxEnd = Integer.MIN_VALUE;
for (int idx : indices) {
minStart = Math.min(minStart, intervals[idx][0]);
maxEnd = Math.max(maxEnd, intervals[idx][1]);
}
result.add(new int[]{minStart, maxEnd});
}
// 按起始点排序结果
result.sort((a, b) -> Integer.compare(a[0], b[0]));
return result.toArray(new int[result.size()][]);
}
private boolean overlap(int[] a, int[] b) {
return !(a[1] < b[0] || b[1] < a[0]);
}
/**
* 并查集实现
*/
static class UnionFind {
private int[] parent;
private int[] rank;
public UnionFind(int n) {
parent = new int[n];
rank = new int[n];
for (int i = 0; i < n; i++) {
parent[i] = i;
rank[i] = 0;
}
}
public int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]); // 路径压缩
}
return parent[x];
}
public void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
// 按秩合并
if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
}
/**
* 优化版:先排序再使用并查集
*/
public int[][] mergeOptimized(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return intervals;
}
int n = intervals.length;
// 创建带索引的区间列表
List<IntervalWithIndex> indexedIntervals = new ArrayList<>();
for (int i = 0; i < n; i++) {
indexedIntervals.add(new IntervalWithIndex(intervals[i][0], intervals[i][1], i));
}
// 按起始点排序
indexedIntervals.sort((a, b) -> Integer.compare(a.start, b.start));
// 并查集初始化
UnionFind uf = new UnionFind(n);
// 由于已排序,只需检查相邻区间是否重叠
for (int i = 0; i < n - 1; i++) {
IntervalWithIndex current = indexedIntervals.get(i);
IntervalWithIndex next = indexedIntervals.get(i + 1);
if (current.end >= next.start) {
uf.union(current.index, next.index);
}
}
// 按连通分量分组
Map<Integer, List<IntervalWithIndex>> components = new HashMap<>();
for (IntervalWithIndex interval : indexedIntervals) {
int root = uf.find(interval.index);
components.putIfAbsent(root, new ArrayList<>());
components.get(root).add(interval);
}
// 合并每个连通分量
List<int[]> result = new ArrayList<>();
for (List<IntervalWithIndex> component : components.values()) {
int minStart = Integer.MAX_VALUE;
int maxEnd = Integer.MIN_VALUE;
for (IntervalWithInterval interval : component) {
minStart = Math.min(minStart, interval.start);
maxEnd = Math.max(maxEnd, interval.end);
}
result.add(new int[]{minStart, maxEnd});
}
// 按起始点排序
result.sort((a, b) -> Integer.compare(a[0], b[0]));
return result.toArray(new int[result.size()][]);
}
static class IntervalWithIndex {
int start;
int end;
int index;
IntervalWithIndex(int start, int end, int index) {
this.start = start;
this.end = end;
this.index = index;
}
}
}
性能分析
- 时间复杂度:O(n²),最坏情况下需要检查所有区间对
- 空间复杂度:O(n),存储并查集和分组信息
- 优势:展示了图算法的应用
- 劣势:效率不如排序+线性扫描,适用于需要动态合并的场景
4. 性能对比
| 算法 | 时间复杂度 | 空间复杂度 | 优势 | 劣势 |
|---|---|---|---|---|
| 暴力枚举 | O(n³) | O(n) | 实现简单 | 效率极低 |
| 排序+线性扫描 | O(n log n) | O(log n) 或 O(n) | 最优解,简洁高效 | 需要排序 |
| 排序+最小堆 | O(n log n) | O(n) | 思路清晰 | 需要额外空间 |
| 并查集 | O(n²) | O(n) | 展示图算法思想 | 效率较低 |
性能测试结果(区间数量=10000):
- 暴力枚举:超时(>10秒)
- 排序+线性扫描:~10 ms
- 排序+最小堆:~15 ms
- 并查集:~500 ms
内存占用对比:
- 排序+线性扫描:排序占用O(log n)栈空间或O(n)额外空间
- 排序+最小堆:堆中最多存储n个区间
- 并查集:存储父节点数组和秩数组
5. 扩展与变体
5.1 插入区间
java
public class InsertInterval {
/**
* 在已排序的不重叠区间列表中插入新区间
*/
public int[][] insert(int[][] intervals, int[] newInterval) {
List<int[]> result = new ArrayList<>();
int i = 0;
int n = intervals.length;
// 添加所有在新区间之前的区间
while (i < n && intervals[i][1] < newInterval[0]) {
result.add(intervals[i]);
i++;
}
// 合并所有与新区间重叠的区间
while (i < n && intervals[i][0] <= newInterval[1]) {
newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
i++;
}
result.add(newInterval);
// 添加剩余的区间
while (i < n) {
result.add(intervals[i]);
i++;
}
return result.toArray(new int[result.size()][]);
}
/**
* 多次插入区间
*/
public int[][] insertMultiple(int[][] intervals, int[][] newIntervals) {
List<int[]> result = new ArrayList<>(Arrays.asList(intervals));
for (int[] newInterval : newIntervals) {
result = insertToList(result, newInterval);
}
return result.toArray(new int[result.size()][]);
}
private List<int[]> insertToList(List<int[]> intervals, int[] newInterval) {
List<int[]> result = new ArrayList<>();
int i = 0;
while (i < intervals.size() && intervals.get(i)[1] < newInterval[0]) {
result.add(intervals.get(i));
i++;
}
while (i < intervals.size() && intervals.get(i)[0] <= newInterval[1]) {
newInterval[0] = Math.min(newInterval[0], intervals.get(i)[0]);
newInterval[1] = Math.max(newInterval[1], intervals.get(i)[1]);
i++;
}
result.add(newInterval);
while (i < intervals.size()) {
result.add(intervals.get(i));
i++;
}
return result;
}
}
5.2 区间交集
java
public class IntervalIntersection {
/**
* 求两个区间列表的交集
*/
public int[][] intervalIntersection(int[][] firstList, int[][] secondList) {
List<int[]> result = new ArrayList<>();
int i = 0, j = 0;
while (i < firstList.length && j < secondList.length) {
int[] a = firstList[i];
int[] b = secondList[j];
// 检查是否有交集
int start = Math.max(a[0], b[0]);
int end = Math.min(a[1], b[1]);
if (start <= end) {
result.add(new int[]{start, end});
}
// 移动结束较早的区间指针
if (a[1] < b[1]) {
i++;
} else {
j++;
}
}
return result.toArray(new int[result.size()][]);
}
/**
* 多个区间列表的交集
*/
public int[][] intervalIntersectionMultiple(int[][][] intervalLists) {
if (intervalLists == null || intervalLists.length == 0) {
return new int[0][0];
}
// 从第一个列表开始
int[][] result = intervalLists[0];
for (int i = 1; i < intervalLists.length; i++) {
result = intervalIntersection(result, intervalLists[i]);
if (result.length == 0) {
break; // 没有交集,提前结束
}
}
return result;
}
}
5.3 删除区间使剩余区间不重叠
java
import java.util.*;
public class EraseOverlapIntervals {
/**
* 删除最小区间数使剩余区间不重叠(贪心算法)
* 按结束点排序,优先保留结束早的区间
*/
public int eraseOverlapIntervals(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return 0;
}
// 按区间结束点排序
Arrays.sort(intervals, (a, b) -> Integer.compare(a[1], b[1]));
int count = 0; // 需要删除的区间数
int end = intervals[0][1];
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] < end) {
// 重叠,需要删除当前区间
count++;
} else {
// 不重叠,更新结束点
end = intervals[i][1];
}
}
return count;
}
/**
* 返回删除哪些区间
*/
public List<int[]> getIntervalsToErase(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return new ArrayList<>();
}
// 保存原始索引
List<IntervalWithIndex> indexed = new ArrayList<>();
for (int i = 0; i < intervals.length; i++) {
indexed.add(new IntervalWithIndex(intervals[i][0], intervals[i][1], i));
}
// 按结束点排序
indexed.sort((a, b) -> Integer.compare(a.end, b.end));
List<int[]> toErase = new ArrayList<>();
int end = indexed.get(0).end;
for (int i = 1; i < indexed.size(); i++) {
if (indexed.get(i).start < end) {
// 重叠,需要删除
toErase.add(new int[]{indexed.get(i).start, indexed.get(i).end});
} else {
// 不重叠,更新结束点
end = indexed.get(i).end;
}
}
return toErase;
}
static class IntervalWithIndex {
int start;
int end;
int index;
IntervalWithIndex(int start, int end, int index) {
this.start = start;
this.end = end;
this.index = index;
}
}
}
5.4 会议室安排问题
java
import java.util.*;
public class MeetingRooms {
/**
* 判断能否参加所有会议(会议室I)
*/
public boolean canAttendMeetings(int[][] intervals) {
if (intervals == null || intervals.length <= 1) {
return true;
}
// 按开始时间排序
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
for (int i = 1; i < intervals.length; i++) {
if (intervals[i][0] < intervals[i - 1][1]) {
return false; // 有重叠
}
}
return true;
}
/**
* 需要的最少会议室数量(会议室II)
*/
public int minMeetingRooms(int[][] intervals) {
if (intervals == null || intervals.length == 0) {
return 0;
}
// 提取开始时间和结束时间
int n = intervals.length;
int[] starts = new int[n];
int[] ends = new int[n];
for (int i = 0; i < n; i++) {
starts[i] = intervals[i][0];
ends[i] = intervals[i][1];
}
// 排序
Arrays.sort(starts);
Arrays.sort(ends);
// 双指针扫描
int rooms = 0;
int endIndex = 0;
for (int start : starts) {
if (start < ends[endIndex]) {
// 需要新会议室
rooms++;
} else {
// 可以复用会议室
endIndex++;
}
}
return rooms;
}
/**
* 使用最小堆的解法
*/
public int minMeetingRoomsHeap(int[][] intervals) {
if (intervals == null || intervals.length == 0) {
return 0;
}
// 按开始时间排序
Arrays.sort(intervals, (a, b) -> Integer.compare(a[0], b[0]));
// 最小堆存储会议结束时间
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int[] interval : intervals) {
if (!heap.isEmpty() && interval[0] >= heap.peek()) {
// 可以复用会议室
heap.poll();
}
// 添加当前会议的结束时间
heap.offer(interval[1]);
}
return heap.size();
}
}
6. 总结
6.1 核心思想总结
- 排序是关键:按区间起始点排序后,重叠区间会相邻出现,问题大大简化
- 贪心合并:排序后,线性扫描并合并重叠区间,每次只需比较当前区间与已合并的最后一个区间
- 多种解法:除了标准排序法,还可以使用堆、并查集等数据结构,各有适用场景
6.2 算法选择指南
- 标准场景:排序+线性扫描是最优选择,时间复杂度O(n log n)
- 动态插入:如果需要在已排序区间列表中频繁插入,可以使用平衡树或跳表
- 教育目的:并查集和堆解法有助于理解不同数据结构的应用
- 特殊需求:根据具体问题选择合适变体算法
6.3 应用场景
- 日程安排:合并重叠的会议时间或任务时间
- 资源分配:合并重叠的资源使用时间段
- 区间查询:数据库中的时间区间查询优化
- 图形渲染:合并重叠的图形区域
6.4 面试技巧
- 从暴力解法开始,分析其效率问题
- 提出排序思路,解释为什么排序后问题变简单
- 实现排序+线性扫描算法,注意代码细节
- 分析时间复杂度和空间复杂度
- 讨论相关变体问题,展示知识广度
合并区间问题是区间类问题的经典代表,掌握这一问题的解法不仅有助于解决LeetCode中的相关问题,更能培养处理区间问题的通用思维模式。通过排序将无序区间有序化,再通过线性扫描进行合并,这种"先排序后处理"的思路在许多算法问题中都有广泛应用。