常见编程模式之合并区间

时间:2022-07-24
本文章向大家介绍常见编程模式之合并区间,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

4. 合并区间(Merge Intervals)

基本原理及应用场景

合并区间模式是一种处理重叠区间的有效手段。在很多包含区间的问题中,我们可能需要去找出重叠的部分或将重叠部分合并。给定两个区间,其关联方式有如下六种:

在以下场景中,我们可能会用到合并区间:

  • 题目涉及生成只包含互斥区间的列表
  • 题目涉及重叠区间

经典例题

56. 合并区间(Medium)

给出一个区间的集合,请合并所有重叠的区间。

「示例」

输入: intervals = [[1,3],[2,6],[8,10],[15,18]]
输出: [[1,6],[8,10],[15,18]]
解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

先基于左边界对区间集合进行排序,这样将区间的关联关系限定在前三种,我们只需要对下面两种重叠情况进行合并即可:

基于上述思想,代码实现如下:

class Solution:
    def merge(self, intervals: List[List[int]]) -> List[List[int]]:
        if len(intervals) < 2: return intervals
        intervals.sort(key = lambda x: x[0]) # 对集合排序(按左边界)
        
        merged = []
        start = intervals[0][0]
        end = intervals[0][1]

        for i in range(1, len(intervals)):
            interval = intervals[i] # 从第二个区间开始,逐个比较
            if interval[0] <= end: # 当前区间的左边界小于上一个区间的右边界,说明有重叠
                end = max(interval[1], end) # 合并后的右边界为两个区间右边界的最大值,左边界为上一个区间的(因为已排序)
                # 合并后继续遍历,直到不重叠再添加到结果中
            else:
                merged.append([start, end]) # 不存在重叠,将上一个区间添加到结果中
                start = interval[0]
                end = interval[1]
        
        merged.append([start, end]) # 将最后一组区间添加到结果中
        return merged

986. 区间列表的交集(Medium)

给定两个由一些「闭区间」组成的列表,每个区间列表都是成对不相交的,并且已经排序。返回这两个区间列表的交集。 (形式上,闭区间 [a, b](其中 a <= b)表示实数 x 的集合,而 a <= x <= b。两个闭区间的交集是一组实数,要么为空集,要么为闭区间。例如,[1, 3] 和 [2, 4] 的交集为 [2, 3]。)

「示例」

输入:A = [[0,2],[5,10],[13,23],[24,25]], B = [[1,5],[8,12],[15,24],[25,26]]
输出:[[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]

我们可以用两个指针分别指向 A 和 B,比较其当前指向的区间。由于 A 和 B 内部的区间均已排序且不相交,所以对于存在重叠的两个区间,较小的末端点只可能与一个区间相交,否则会在列表内部出现两个相交的区间,与题意不符。所以我们只需要按顺序比较区间,存在重叠则合并区间,并移动末端点较小的区间所在列表的指针即可。基于上述思想,可以写出如下代码:

class Solution:
    def intervalIntersection(self, A: List[List[int]], B: List[List[int]]) -> List[List[int]]:
        ans = []
        i, j = 0, 0

        while i < len(A) and j < len(B):
            lo = max(A[i][0], B[j][0])
            hi = min(A[i][1], B[j][1])
            if lo <= hi: # 存在重叠
                ans.append([lo, hi])
            
            # 移动较小末端点所在列表的指针,可能存在同时移动的情况
            if hi == A[i][1]:
                i += 1
            if hi == B[j][1]:
                j += 1
        
        return ans

57. 插入区间(Hard)

给出一个无重叠的,按照区间起始端点排序的区间列表。 在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。

「示例」

输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8]
输出:[[1,2],[3,10],[12,16]]
解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。

这道题的一种简单做法是参考 56 题,先把新的区间添加到列表中,然后执行 56 题的代码即可。不过由于本题中给定的是无重叠已排序区间列表,所以再次进行排序是没有必要的,可以仅遍历一次合并即可,具体算法如下:

  • newInterval 之前开始的区间添加到输出
  • 添加 newInterval 到输出,若 newInterval 与输出中的最后一个区间重合则合并他们
  • 一个个添加区间到输出,若有重叠部分则合并他们

对应的代码实现如下:

class Solution:
    def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
        new_start, new_end = newInterval
        idx, n = 0, len(intervals)
        output = []

        # 将所有新区间之前的区间添加到结果中
        while idx < n and new_start > intervals[idx][0]:
            output.append(intervals[idx])
            idx += 1
        
        # 如果输出为空或最后一个区间和新区间不重合
        if not output or output[-1][1] < new_start:
            output.append(newInterval)
        # 如果存在重合,则进行合并
        else: 
            output[-1][1] = max(output[-1][1], new_end)
        
        # 添加之后的区间,存在重合则合并
        while idx < n:
            interval = intervals[idx]
            start, end = interval
            idx += 1
            if output[-1][1] < start:
                output.append(interval)
            else:
                output[-1][1] = max(output[-1][1], end)
        return output