LeetCode算法题详解 56:合并区间

目录

  • [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^4
  • intervals[i].length == 2
  • 0 <= starti <= endi <= 10^4

2. 问题分析

2.1 题目理解

我们需要将给定的一系列区间进行合并,使得合并后的区间集合满足:

  1. 所有区间都不重叠(任意两个区间没有交集)
  2. 合并后的区间集合恰好覆盖原始所有区间
  3. 如果两个区间有重叠(包括端点重叠),它们应该被合并为一个区间

2.2 核心洞察

  • 区间重叠条件 :两个区间 [a, b][c, d] 重叠的条件是 a <= dc <= b
  • 合并操作 :合并两个重叠区间 [a, b][c, d] 的结果是 [min(a, c), max(b, d)]
  • 排序的重要性:如果区间按照起始点排序,那么重叠的区间会相邻,便于合并

2.3 破题关键

问题的核心在于如何高效地识别和处理重叠区间

  1. 如果区间无序,判断重叠需要比较所有区间对,复杂度高
  2. 对区间按起始点排序后,重叠的区间会相邻出现,只需一次扫描即可完成合并
  3. 合并时只需要维护当前合并区间的结束点,与下一个区间的起始点比较

3. 算法设计与实现

3.1 暴力枚举法

核心思想

尝试所有可能的区间合并方式,找到最终的合并结果。

算法思路

  1. 将区间列表转换为集合
  2. 重复以下过程直到没有可合并的区间:
    • 遍历所有区间对,检查是否重叠
    • 如果重叠,合并它们,从集合中移除原来的两个区间,添加合并后的新区间
  3. 返回最终的区间集合

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 排序+线性扫描(标准解法)

核心思想

先按区间起始点排序,然后线性扫描,合并重叠的相邻区间。

算法思路

  1. 按区间起始点对区间进行排序
  2. 初始化结果列表,将第一个区间加入结果
  3. 遍历排序后的区间:
    • 如果当前区间与结果列表中最后一个区间重叠,合并它们
    • 否则,将当前区间加入结果列表
  4. 返回结果

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 排序+最小堆/优先队列

核心思想

使用最小堆(优先队列)按区间结束点维护,便于快速找到可以合并的区间。

算法思路

  1. 按区间起始点排序
  2. 使用最小堆存储区间,按结束点排序
  3. 遍历排序后的区间:
    • 如果堆为空或当前区间起始点 > 堆顶区间的结束点,将当前区间加入堆
    • 否则,弹出堆顶区间,与当前区间合并,将合并后的区间加入堆
  4. 堆中剩余的区间即为合并结果

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 图连通分量法(并查集)

核心思想

将区间看作图中的节点,如果两个区间重叠,则在它们之间添加边。然后使用并查集找到连通分量,每个连通分量内的区间合并为一个区间。

算法思路

  1. 构建重叠关系图:对于每对区间,如果重叠,则在并查集中合并
  2. 使用并查集找到所有连通分量
  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 核心思想总结

  1. 排序是关键:按区间起始点排序后,重叠区间会相邻出现,问题大大简化
  2. 贪心合并:排序后,线性扫描并合并重叠区间,每次只需比较当前区间与已合并的最后一个区间
  3. 多种解法:除了标准排序法,还可以使用堆、并查集等数据结构,各有适用场景

6.2 算法选择指南

  • 标准场景:排序+线性扫描是最优选择,时间复杂度O(n log n)
  • 动态插入:如果需要在已排序区间列表中频繁插入,可以使用平衡树或跳表
  • 教育目的:并查集和堆解法有助于理解不同数据结构的应用
  • 特殊需求:根据具体问题选择合适变体算法

6.3 应用场景

  • 日程安排:合并重叠的会议时间或任务时间
  • 资源分配:合并重叠的资源使用时间段
  • 区间查询:数据库中的时间区间查询优化
  • 图形渲染:合并重叠的图形区域

6.4 面试技巧

  1. 从暴力解法开始,分析其效率问题
  2. 提出排序思路,解释为什么排序后问题变简单
  3. 实现排序+线性扫描算法,注意代码细节
  4. 分析时间复杂度和空间复杂度
  5. 讨论相关变体问题,展示知识广度

合并区间问题是区间类问题的经典代表,掌握这一问题的解法不仅有助于解决LeetCode中的相关问题,更能培养处理区间问题的通用思维模式。通过排序将无序区间有序化,再通过线性扫描进行合并,这种"先排序后处理"的思路在许多算法问题中都有广泛应用。

相关推荐
鱼跃鹰飞2 小时前
Leetcode尊享面试100题:252. 会议室
算法·leetcode·面试
程序员-King.3 小时前
day131—链表—反转链表Ⅱ(区域反转)(LeetCode-92)
leetcode·链表·贪心算法
圣保罗的大教堂3 小时前
leetcode 2943. 最大化网格图中正方形空洞的面积 中等
leetcode
独自破碎E3 小时前
包含min函数的栈
android·java·开发语言·leetcode
Tisfy4 小时前
LeetCode 2943.最大化网格图中正方形空洞的面积:小小思维
算法·leetcode·题解·数组·思维·排序·连续
平生不喜凡桃李4 小时前
LeetCode: 基本计算器详解
算法·leetcode·计算器·逆波兰表达式
Swift社区4 小时前
LeetCode 375 - 猜数字大小 II
算法·leetcode·swift
漫随流水4 小时前
leetcode算法(257.二叉树的所有路径)
数据结构·算法·leetcode·二叉树
有一个好名字4 小时前
力扣-二叉树的最大深度
算法·leetcode·深度优先