携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情
题目描述
实现一个 MyCalendar 类来存放你的日程安排。如果要添加的日程安排不会造成 重复预订 ,则可以存储这个新的日程安排。
当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生 重复预订。
日程可以用一对整数 start 和 end 表示,这里的时间是半开区间,即 [start, end), 实数 x 的范围为, start <= x < end 。
实现 MyCalendar 类:
MyCalendar()初始化日历对象。boolean book(int start, int end)如果可以将日程安排成功添加到日历中而不会导致重复预订,返回true。否则,返回false并且不要将该日程安排添加到日历中。
解题思路——遍历数组
这题有点像合并区间的题了。
手摸手提桶跑路——LeetCode56. 合并区间 - 掘金 (juejin.cn)
我们需要一个数据结构来保存当前已经添加进来的区间。这里我使用的是数组,后续查找区间是否重复的时候,需要 遍历查找,还是比较费时的。具体过程如下:
- 如果当前存放已添加区间的数组
schedule长度为 0,说明一个预定也没有,可以直接安排,push进schedule。 - 如果当前
schedule中存在区间(已有日程安排),那么新进来的区间要判断是否重复预定。重复预定分为三种情况,分别是:① 新增区间 完全重复 于已预定区间;② 新增区间 后半部分重复 于已预定区间;③ 新增区间 前半部分重复 于已预定区间。通过以下三张图我们可以得出如果区间重复,那么必然有bStart < aEnd且bEnd > aStart。 - 如果新添加的区间 没有 和已添加的区间 重叠 的话,说明没有重复预定,返回
true。
全部重叠的情况
前半部分重叠的情况
后半部分重叠的情况
题解
var MyCalendar = function () {
this.schedule = [];
};
/**
* @param {number} start
* @param {number} end
* @return {boolean}
*/
MyCalendar.prototype.book = function (start, end) {
for(let i=0; i<this.schedule.length; ++i) {
if(start < this.schedule[i][1] && end > this.schedule[i][0]) return false;
}
this.schedule.push([start, end]);
return true;
};
解题思路——二叉搜索树
二叉搜索树介绍
二叉搜索树从字面上理解就是专注于搜索用的二叉树,相比于无序数组的遍历来说,二叉搜索树的一个节点,对于节点 n 来说,n 的左节点 n.left 的 val 值永远比 n 的 val 值小,n 的右节点 n.right 的 val 值永远比 n 的 val 值大。
如果我们要搜索一个节点值 target 时,从根节点 root 开始判断,如果 target < root,说明 target 在 root 的左子树上,则继续判断 target 和 root.left 节点的 val 值的大小,一直到找到 target 为止,说明 target 存在该树上,否则一直找到叶子节点,则说明当前 target 不在该树上。
观察这个过程就能发现,对于数组的遍历搜索来说,效率快了很多。
关于二叉搜索树的相关概念,请参考 JavaScript 二叉搜索树以及实现翻转二叉树
构建节点
那么对于这题来说需要怎么做呢?
我们可以知道构建的树节点必然是要保存左右节点的指向的,除此之外,通过分析题目,可以知道每个节点还需要存储 start 和 end,用于标识当前节点所表示的区间范围。
当查找一个区间时,通过新增区间的 start 以及 end 在二叉搜索树中的关系,来判断该区间是否具有被插入的条件。
题解
class TreeNode {
constructor(start, end) {
this.left = null;
this.right = null;
this.start = start;
this.end = end;
}
}
var MyCalendar = function () {
this.root = null;
};
/**
* @param {number} start
* @param {number} end
* @return {boolean}
*/
MyCalendar.prototype.book = function (start, end) {
if(this.root === null) {
this.root = new TreeNode(start, end);
return true;
}
let tHead = this.root;
while(true) {
if(end <= tHead.start) {
if(tHead.left === null) {
tHead.left = new TreeNode(start, end);
return true;
}
tHead = tHead.left;
} else if(start >= tHead.end) {
if(tHead.right === null) {
tHead.right = new TreeNode(start, end);
return true;
}
tHead = tHead.right;
} else {
return false;
}
}
};
解题思路——数组二分查找法
二分查找就是在数组遍历法的基础上进行的优化,因为数组遍历法需要遍历查找无序数组,比较费时,考虑到如果是个有序数组,通过二分查找来搜索的话,效率会快很多。
和前面说的一样,二分本质上还是对数组遍历的优化手段,其实逻辑上是有共同之处的。
二分查找,到底查的啥?关于结束条件是什么这件事困扰了我好久。所以二分查找法到底是找什么呢?
拿 [[10,20], [24, 26], [30, 32], [40, 50]] 区间来说,这个时候有个新的日程安排 B=[22, 25]。
我们可以肉眼看出来,这个新日程安排 B,是和 A=[24, 26] 区间有重合的,为啥我们知道它有重合,因为 B 的 start=22 小于 A 的 end=26, 且 B 的 end=25 大于 A 的 start=24。那么我们就知道了,嗷!原来 bStart < aEnd && bEnd > aStart,就可以插入了。那么我们二分查找找到的第一个符合 bStart < aEnd 的下标,就是我们需要的!
注意这里的查找条件只有半个,所以最后还要判断 bEnd > aStart,成立则说明重合,不插入,否则就插入到 left 的位置,返回 true。
题解
var MyCalendar = function () {
this.schdedule = [];
};
/**
* @param {number} start
* @param {number} end
* @return {boolean}
*/
MyCalendar.prototype.book = function (start, end) {
const lens = this.schdedule.length;
if (lens === 0) {
this.schdedule.push([start, end]);
return true;
}
// 二分查找——落在中间的情况
let left = 0, right = lens, mid;
while (left < right) {
mid = left + Math.floor(((right - left) / 2));
if (start < this.schdedule[mid][1]) { // 落在左区间
right = mid;
} else {
left = mid + 1;
}
}
// 至此找到可能重复预定的区间
if (left >= this.schdedule.length) {
this.schdedule.push([start, end]);
return true;
}
if (end > this.schdedule[left][0]) {
return false;
}
this.schdedule.splice(left, 0, [start, end]);
return true;
};
解题思路——插旗法(公交车上下车)
我们把一段区域的开始 和 结束的位置都插上一根旗子,用来标记领土范围。
假设开始旗子用 1 表示,结束旗子用 -1 表示。
可以看到,正常来说开始和结束之间是没有别人的领土的(没有别的1在里头)。
那么如果别人的领土也在里头是怎么样的呢?
可以看到从 领土A 的 开始旗子 到领土A的 结束旗子 之间,多了个 领土B 的 开始旗子,那么当 AStart 到 AEnd 的 和大于1 时,说明当前领土被侵犯了,这是决不允许的。
题解
var MyCalendar = function () {
this.schedule = Object.create(null);
};
/**
* @param {number} start
* @param {number} end
* @return {boolean}
*/
MyCalendar.prototype.book = function (start, end) {
this.schedule[start]++ || (this.schedule[start] = 1);
this.schedule[end]-- || (this.schedule[end] = -1);
let cnt = 0;
for(let k in this.schedule) {
cnt += this.schedule[k];
if(cnt > 1) {
this.schedule[start]--;
this.schedule[end]++;
return false;
}
}
return true;
};
这么慢还用个der啊。
js中,对象属性如果是数字的话,会转成字符串,并且会把数字属性从小到大去排序,也成为“排序属性”。如果是换成c++的map<int,int>的话,会快不少的。这里我们就当了解一下了。