LeetCode LCR 058. 我的日程安排表 I
题目描述


题意分析
book(start, end)尝试加入半开区间[start, end)。与任何已接受日程重叠时返回false,且不改变日程表;没有重叠则记录区间并返回true。右端点不包含在区间内,所以两个日程首尾相接是允许的。已接受区间始终两两不重叠,可以按起点排序,只检查新日程插入位置附近的区间。
解法:TreeMap / 有序映射维护区间
核心思路
[!blue]
数据结构保持两个性质:已接受区间按起点有序,而且彼此不重叠。对于新区间,只需查看紧邻的前驱和后继,因为其他区间在时间轴上更远。
前驱的起点不晚于
start,只需检查它是否结束得太晚:prev.end > start表示重叠。后继的起点不早于start,只需检查新区间是否越过它:end > next.start表示重叠。等号都允许,因为区间右端不包含在内。两项检查通过后,前驱及更早区间都结束在
start之前或恰好结束于它;后继及更晚区间都开始于end之后或恰好开始于它。因此其他区间也不可能冲突,可以安全插入。这个结论依赖既有区间不重叠,不能直接用于任意一组相互重叠的区间。Java 用
TreeMap保存起点到终点的映射,floorKey(start)找前驱,ceilingKey(start)找后继。同起点的已有区间会被检查并拒绝,不会被后面的put覆盖。Go 用有序切片,二分找到第一个起点不小于
start的位置l,于是l是后继,l-1是前驱。同起点区间归入后继检查;两边都通过后,扩展切片、把后面的区间右移,再在l写入新区间。两份实现都先检查再写入,失败分支直接返回即可保持旧状态。空日程表、插在最前或最后的位置,只需在访问前驱后继前判断它是否存在。
解题步骤
- 按起点有序定位新日程附近的前驱和后继;Go 的二分直接得到插入位置。
- 前驱存在且其终点大于
start时,返回false。- 后继存在且其起点小于
end时,返回false。- 两项检查都通过后记录新区间;Go 先右移后续元素,以维持切片有序。
- 返回
true,之后的调用继续以不重叠、有序的已接受区间为基础。
代码实现
class MyCalendar {
private final TreeMap<Integer, Integer> map = new TreeMap<>();
public boolean book(int start, int end) {
Integer prevStart = map.floorKey(start);
if (prevStart != null && map.get(prevStart) > start) {
return false;
}
Integer nextStart = map.ceilingKey(start);
if (nextStart != null && end > nextStart) {
return false;
}
map.put(start, end);
return true;
}
}
type MyCalendar struct {
intervals [][2]int
}
func Constructor() MyCalendar {
return MyCalendar{intervals: make([][2]int, 0)}
}
func (c *MyCalendar) Book(start int, end int) bool {
l, r := 0, len(c.intervals)
for l < r {
mid := (l + r) / 2
if c.intervals[mid][0] < start {
l = mid + 1
} else {
r = mid
}
}
if l > 0 && c.intervals[l-1][1] > start {
return false
}
if l < len(c.intervals) && end > c.intervals[l][0] {
return false
}
c.intervals = append(c.intervals, [2]int{})
copy(c.intervals[l+1:], c.intervals[l:])
c.intervals[l] = [2]int{
start,
end,
}
return true
}
复杂度分析
设已经接受的日程数量为
m。
- 时间复杂度:Java 单次
book为 $O(\log(m+2))$,执行常数次有序映射查找与插入。Go 二分查询为 $O(\log(m+2))$,成功插入最坏需要移动 $O(m)$ 个区间,因此单次最坏为 $O(m+1)$。- 空间复杂度:$O(m)$,保存已接受的日程;被拒绝的预定不进入容器。
关键点总结
[!green]
- 只检查相邻区间的依据是既有日程有序且两两不重叠。
- 半开区间允许端点相接,冲突条件使用严格不等号。
- 检查通过后才插入,让失败预定自然保持原状态。
- Go 二分只加快定位,不能消除有序切片插入时的搬移成本。
易错点总结
[!yellow]
- 用大于等于判断端点冲突:会把首尾相接的合法日程拒绝。
- 只检查前驱或只检查后继:新区间可能向另一个方向延伸并发生重叠。
- 先写入同起点区间再检查:可能覆盖旧日程;当前实现应在全部检查后写入。
- 访问不存在的邻居:空表和边界位置可能没有前驱或后继,要先判断。
- Go 定位后直接追加到末尾:会破坏按起点排序的前提,应在二分得到的位置插入。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 731. 我的日程安排表 II | 中等 | 本题禁止任何重叠,原题允许双重预订但禁止三重预订,需要追踪更高层重叠。 |
| 732. 我的日程安排表 III | 困难 | 同样处理半开时间区间,原题直接维护最大同时预订数,而不是接受或拒绝新日程。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!