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中的相关问题,更能培养处理区间问题的通用思维模式。通过排序将无序区间有序化,再通过线性扫描进行合并,这种"先排序后处理"的思路在许多算法问题中都有广泛应用。

相关推荐
smj2302_796826524 小时前
解决leetcode第3911题.移除子数组元素后第k小偶数
数据结构·python·算法·leetcode
_深海凉_8 小时前
LeetCode热题100-寻找两个正序数组的中位数
算法·leetcode·职场和发展
踩坑记录9 小时前
leetcode hot100 寻找两个正序数组的中位数 hard 二分查找 双指针
leetcode
superior tigre12 小时前
78 子集
算法·leetcode·深度优先·回溯
superior tigre13 小时前
739 每日温度
算法·leetcode·职场和发展
6Hzlia14 小时前
【Hot 100 刷题计划】 LeetCode 15. 三数之和 | C++ 排序+双指针
c++·算法·leetcode
北顾笙98015 小时前
day37-数据结构力扣
数据结构·算法·leetcode
6Hzlia17 小时前
【Hot 100 刷题计划】 LeetCode 189. 轮转数组 | C++ 三次反转经典魔法 (O(1) 空间)
c++·算法·leetcode
m0_6294947318 小时前
LeetCode 热题 100-----13.最大子数组和
数据结构·算法·leetcode
田梓燊18 小时前
力扣:94.二叉树的中序遍历
数据结构·算法·leetcode