8.重叠区间
重叠区间模式用于合并或处理数组中的重叠区间 。
在按开始时间排序的区间数组中,如果两个区间[a,b]和[c,d]重叠,则b>=c(即,第一个区间的结束时间大于或等于第二个区间的开始时间)。
核心:先排序,再遍历合并,通用解题步骤:
步骤 1:按区间起始点排序
先将所有区间按左端点(start)升序排序,这是处理重叠问题的前提。
排序后,我们只需要和已合并列表的最后一个区间比较,就能判断是否重叠。
步骤 2:初始化结果列表创建一个空列表 merged,用来存放最终合并后的区间。
若输入区间列表为空,直接返回空。
步骤 3:遍历并合并区间遍历排序后的每个区间 curr:
若 merged 为空,直接将 curr 加入 merged。
取 merged 中最后一个区间 last:
重叠判断 :curr.start <= last.end(因为已按 start 排序,只要当前区间的 start ≤ 上一个区间的 end,就一定重叠)。
重叠时合并 :更新 last.end 为 max(last.end, curr.end)(保留最大的结束值,覆盖重叠部分)。
不重叠时添加:将 curr 直接加入 merged。
| 场景 | 判断条件 | 操作 |
|---|---|---|
| 两个区间重叠 | curr.start ≤ last.end |
更新 last.end = max(last.end, curr.end) |
| 两个区间不重叠 | curr.start > last.end |
将 curr 追加到结果列表 |
合并区间(LeetCode #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 <= 104
intervals[i].length == 2
0 <= starti <= endi <= 104
题目求解
python
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort(key=lambda x: x[0])
merged = []
for cur in intervals:
if not merged:
merged.append(cur)
else:
item = merged[-1]
if item[0] <= cur[1] and cur[0] <= item[1]:
merged.pop(-1)
merged.append([min(item[0], cur[0]), max(item[1], cur[1])])
else:
merged.append(cur)
return merged
稍微优化下代码,把if...else...语句缩一下,当merged为空 或者 cur区间 和 item 区间 不重合,直接append;否则,合并,并且区间开始已经排好序了,所以肯定是最小值,不需要[min(item[0], cur[0])判断。代码如下:
python
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
intervals.sort(key=lambda x:x[0])
merged = []
for num in intervals:
# 为空,或者 当前遍历的区间 和 merged最后一个区间 不重叠
if not merged or merged[-1][1] < num[0]:
merged.append(num)
else:
# 有重叠
merged[-1][1] = max(merged[-1][1], num[1])
return merged
插入区间(LeetCode#57)
题目描述
给你一个 无重叠的 ,按照区间起始端点排序的区间列表 intervals,其中 intervals[i] = [starti, endi] 表示第 i 个区间的开始和结束,并且 intervals 按照 starti 升序排列。同样给定一个区间 newInterval = [start, end] 表示另一个区间的开始和结束。
在 intervals 中插入区间 newInterval,使得 intervals 依然按照 starti 升序排列,且区间之间不重叠(如果有必要的话,可以合并区间)。
返回插入之后的 intervals。
注意 你不需要原地修改 intervals。你可以创建一个新数组然后返回它。
示例 1:
输入:intervals = [[1,3],[6,9]], newInterval = [2,5]
输出:[[1,5],[6,9]]
示例 2:
输入: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] 重叠。
提示:
0 <= intervals.length <= 104
intervals[i].length == 2
0 <= starti <= endi <= 105
intervals 根据 starti 按 升序 排列
newInterval.length == 2
0 <= start <= end <= 105
题目求解
把新的区间 append 到 区间列表中,其实就转为第一个题了
python
class Solution:
def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
merged = intervals
merged.append(newInterval)
merged.sort(key=lambda x: x[0])
ans = []
for num in merged:
if not ans or ans[-1][1] < num[0]:
ans.append(num)
else:
ans[-1][1] = max(ans[-1][1], num[1])
return ans
模拟------直接合并,但是需要考虑新的区间 的位置,思路:
- 阶段 1:遍历所有「右端点 < 新区间左端点」的区间 → 直接加入结果(这些区间和新区间完全不重叠,且在新区间左侧)。
- 阶段 2:遍历所有「左端点 ≤ 新区间右端点」的区间 → 合并这些区间和新区间(更新新区间的左右端点为合并后的极值)。
- 阶段 3:将合并后的新区间加入结果,再遍历剩余所有区间 → 直接加入结果(这些区间在新区间右侧,完全不重叠)。
python
# class Solution:
# def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
# merged = intervals
# merged.append(newInterval)
# merged.sort(key=lambda x: x[0])
# ans = []
# for num in merged:
# if not ans or ans[-1][1] < num[0]:
# ans.append(num)
# else:
# ans[-1][1] = max(ans[-1][1], num[1])
# return ans
class Solution:
def insert(self, intervals: List[List[int]], newInterval: List[int]) -> List[List[int]]:
i = 0
n = len(intervals)
ans = []
# 右端点 < 新区间左端点 ->直接加入结果
while i < n and intervals[i][1] < newInterval[0]:
ans.append(intervals[i])
i += 1
# 看是否需要合并
while i < n and intervals[i][0] <= newInterval[1]:
newInterval[0] = min(intervals[i][0], newInterval[0])
newInterval[1] = max(intervals[i][1], newInterval[1])
i += 1
ans.append(newInterval)
# 添加剩余元素
while i < n:
ans.append(intervals[i])
i += 1
return ans
纯模拟,但很多小问题,不过还是很考虑细节的。
不重叠区间(LeetCode#435)
题目描述
给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠 。
注意 只在一点上接触的区间是 不重叠的。例如 [1, 2] 和 [2, 3] 是不重叠的。
示例 1:
输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。
示例 2:
输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。
示例 3:
输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。
提示:
1 <= intervals.length <= 105
intervals[i].length == 2
-5 * 104 <= starti < endi <= 5 * 104
题目求解
贪心策略:选结束早的,能给后面留出更多的空间。
生活类比(最易理解)
假设你是一个时间管理大师,要在一天内安排尽可能多的「不重叠会议」:
- 会议 A:9:00-9:30(右端点 9:30)
- 会议 B:9:15-10:00(右端点 10:00)
- 会议 C:9:30-10:00(右端点 10:00)
你会优先选哪个?→ 选 A(9:00-9:30)!因为 A 结束得最早,选完 A 后还能选 C(9:30-10:00),总共能安排 2 个会议;→ 但如果选 B(9:15-10:00),就只能安排 1 个会议。
结论:选结束早(右端点小)的区间,能给后面留出更多「空闲时间」,从而容纳更多不重叠的区间。
优先选右端点最小的区间,能得到最多的不重叠区间数。(反证法 证得)
python
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
if not intervals:
return 0
intervals.sort(key=lambda x: x[1])
ans = 1 # 至少能保留的区间数
n = len(intervals)
last_end = intervals[0][1]
for i in range(1, n):
if intervals[i][0] >= last_end:
ans += 1
last_end = intervals[i][1]
return n - ans
Python 中的排序 相关练习
python
# list.sort()
# 基础排序
# 列表原地排序
nums = [3, 1, 4, 2, 7]
nums.sort()
print(nums)
# 降序
nums.sort(reverse=True)
print(nums)
# 返回新列表排序
nums_sorted = [6, 4, 2, 7, 1, 9]
sorted_nums = sorted(nums_sorted)
print(sorted_nums)
# 降序
sorted_nums_desc = sorted(nums_sorted, reverse=True)
print(sorted_nums_desc)
# 进阶排序
# 按元素某个属性
intervals = [[2, 3], [1, 4], [5, 6]]
intervals.sort(key=lambda x: x[0])
print(intervals)
intervals.sort(key=lambda x: x[1])
print(intervals)
# 排序 字典/对象
d = {"a": 3, "b": 1, "c": 2}
sorted_d = sorted(d.items(), key=lambda x: x[1])
print(sorted_d)
# 自定义 对象的属性排序
class Student:
def __init__(self, name, score):
self.name = name
self.score = score
student = [Student('A', 88), Student('B', 71), Student('C', 98)]
# 按照分数升序
student.sort(key=lambda s: s.score)
print([s.name for s in student])
# 按照字符串长度排序
words = ['apple', 'banana', 'cat', 'dog']
words.sort(key=lambda w: len(w))
print(words)
# 多条件 排序
# 先按分数降序,分数相同按名字升序
students = [
Student('Alice', 90),
Student('Bob', 85),
Student('Charlie', 90),
Student('David', 85)
]
# key=(分数(负号实现降序),名字)
students.sort(key=lambda s: (-s.score, s.name))
print([(s.name, s.score) for s in students])
# 排序不可变对象
s = 'python'
sorted_s = sorted(s)
print(sorted_s) # ['h', 'n', 'o', 'p', 't', 'y']
# 排序元组
tup = (3, 1, 2, 7, 4, 5)
sorted_t = sorted(tup)
print(sorted_t) # 返回的是排序后的列表
# python中的 排序是稳定排序
students = [
Student('Alice', 90),
Student('Bob', 85),
Student('Charlie', 90),
]
students.sort(key=lambda s: s.score)
print([s.name for s in students])