[Leetcode 729] 日程安排 1

实现一个 MyCalendar 类来存放你的日程安排。
如果要添加的日程安排不会造成 重复预订 ,则可以存储这个新的日程安排。
当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生 重复预订 。
日程可以用一对整数 startTime 和 endTime 表示,这里的时间是半开区间,即 [startTime, endTime), 实数 x 的范围为,  startTime <= x < endTime 。
实现 MyCalendar 类:
  • MyCalendar() 初始化日历对象。
  • boolean book(int startTime, int endTime) 
    • 如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true 。
      • 否则,返回 false ,并且不要将该日程安排添加到日历中。
示例:
 
这道题的本质是动态维护一组区间,并在每次插入新区间时判断它是否和已有区间重叠。
这里区间是半开区间:[start, end),这意味着 end 这个时间点不属于事件本身。所以[10, 20)[20, 30) 这两个事件不重叠,可以同时存在。
两个半开区间 [s1, e1)[s2, e2) 不重叠,只有两种情况:
  • e1 <= s2 # 第一个完全在第二个左边
  • e2 <= s1 # 第二个完全在第一个左边
反过来,如果两种情况都不成立,就说明它们有交集。

解法一:线性扫描 —— 每次和所有已有事件比较

最直观的做法是维护一个列表 calendar,里面存所有已经成功预订的事件。每次来了一个新区间 [start, end),我们就遍历已有事件,检查是否重叠。
如果存在某个已有区间 [s, e),满足 start < e and s < end,就说明两个区间重叠,不能插入。
为什么这个条件成立?因为它等价于“新区间不是完全在旧区间左边,也不是完全在旧区间右边”。换句话说,只要两个区间互相越过了对方的起点,就产生了交集。
这个解法在日历规模不大时,直接拿新区间和所有旧区间比一遍就够了。它不依赖额外复杂数据结构,
Walk Through Example
假设当前已经有 [10, 20),现在尝试插入 [15, 25) 。因为 15 < 20,并且 10 < 25,两个区间发生重叠,所以返回 False
如果尝试插入[20, 30), 由于新区间的 start == 20,刚好等于旧区间的 end,两个半开区间不重叠,因此可以插入,返回 True
复杂度分析
设当前已经成功预订了 n 个事件。
  • 时间复杂度:
    • 每次 book 的时间复杂度是 O(n),因为最坏情况下需要检查所有已有事件。
      如果总共有 n 次预订操作,那么整体时间复杂度是 O(n^2)
  • 空间复杂度是 O(n),用于保存所有成功预订的事件。

解法二:排序列表 + 二分 —— 只检查相邻区间

线性扫描的问题是每次都要检查所有已有事件。但如果我们始终让事件按开始时间排序,就不需要看所有事件。
原因是:对于一个新区间 [start, end),如果它会和某个已有区间重叠,那么最有可能发生冲突的只有两个邻居:
  1. 插入位置右边的第一个区间;
  1. 插入位置左边的最后一个区间。
为什么只看邻居就够了?
因为区间已经按开始时间排序,并且已有区间之间本来互不重叠。新区间插入后,如果它不和左邻居、右邻居重叠,那它也不可能越过这些邻居去和更远的区间重叠。
所以流程是:
  1. 先用二分找到新区间按 start 应该插入的位置 idx
  1. 然后检查:
      • 右邻居是否满足 end > right_start
      • 左邻居是否满足 left_end > start
      只要有一个成立,就说明重叠。
复杂度分析
  • 时间复杂度
    • 每次查找插入位置是 O(log n)
      但 Python list 在中间插入是 O(n),所以每次 book 的总时间复杂度是 O(n)
      如果语言里有真正的平衡树结构,比如 Java 的 TreeMap、C++ 的 set,或者 Python 第三方库 SortedList / SortedDict,则查找和插入都可以做到 O(log n)
  • 空间复杂度是 O(n)

解法三:有序集合 / SortedDict —— 用平衡树维护区间

如果使用有序集合,我们可以让插入和查找都保持 O(log n)
一种常见设计是按事件的结束时间 end 排序,存储:
当要插入新区间 [start, end) 时,我们只需要找第一个:
这表示这个已有事件不是完全在新区间左边,它是第一个可能和新区间重叠的事件。
如果这个事件的 start < end,那么它和新区间有重叠,返回 False。否则,新区间可以插入。
这个解法的 intuition 是:按结束时间排序后,所有 end <= start 的事件都安全地在新区间左侧,不可能冲突。第一个 end > start 的事件,是唯一需要检查的候选冲突。
复杂度分析
设当前已经成功预订了 n 个事件。
  • 时间复杂度:每次 book 的时间复杂度是 O(log n),因为有序集合支持对数级查找和插入。
  • 空间复杂度: O(n),用于保存所有事件。
注意:这份代码依赖 sortedcontainers,LeetCode Python 环境通常支持。

解法四:手写二叉搜索树 —— 把区间放进 BST

如果没有现成的有序集合,也可以自己维护一棵二叉搜索树。每个节点存一个区间 [start, end)
插入新区间时,和当前节点比较:
  • 如果 start >= cur.end,说明新区间完全在当前区间右边,去右子树;
  • 如果 end <= cur.start,说明新区间完全在当前区间左边,去左子树;
  • 否则,说明两个区间重叠,插入失败。
这个逻辑和普通 BST 插入很像,只不过比较依据不是单个值,而是区间相对位置。
树里的每个节点都代表一个已经预订成功的事件。对于某个节点 [s, e)
  • 左子树中的所有事件都应该完全在它左边;
  • 右子树中的所有事件都应该完全在它右边。
这样一来,插入新区间时,只要不断和当前节点比较,就能决定应该往左走、往右走,还是发现重叠直接失败。
复杂度分析
  • 时间复杂度:
    • 每次插入的时间复杂度取决于树高。
      如果树比较平衡,每次 book 平均是 O(log n)
      但这棵树不是自平衡 BST,最坏情况下会退化成链表,比如事件按递增顺序插入时,时间复杂度会变成 O(n)
  • 空间复杂度是 O(n),每个成功预订的事件对应一个树节点。

总结

方法
每次 book 时间
空间
适合场景
线性扫描
O(n)
O(n)
最简单,约束小,最容易写对
排序列表 + 二分
查找 O(log n),插入 O(n)
O(n)
Python list 写法简洁,但插入仍是线性
有序集合 / 平衡树
O(log n)
O(n)
语言支持 TreeMap / set / SortedDict 时最优雅
手写 BST
平均 O(log n),最坏 O(n)
O(n)
不依赖库,但不是严格平衡树
这题本质是维护一组互不重叠的半开区间 [start, end)。判断两个区间是否重叠,可以用条件 start < old_end and old_start < end。最简单的做法是每次遍历所有已有事件,如果有重叠就返回 False,否则插入新区间,时间复杂度是 O(n)
如果想优化,可以保持事件有序。按开始时间排序后,新区间只可能和插入位置左右两个邻居冲突,所以可以二分找到插入位置并检查相邻区间。在 Python list 里,二分查找是 O(log n),但插入仍然是 O(n);如果用真正的有序集合或平衡树,就可以做到每次 O(log n)
还有一种不依赖库的写法是手写二叉搜索树。每个节点存一个区间。插入新区间时,如果它完全在当前区间右边,就去右子树;如果完全在当前区间左边,就去左子树;否则说明重叠,返回 False。这个方法平均表现接近 O(log n),但因为不是自平衡树,最坏会退化到 O(n)
Buy Me a Coffee
上一篇
[Leetcode 560] 和为 K 的子数组
下一篇
[Leetcode 1169] 无效交易