手摸手提桶跑路——LeetCode729.我的日程安排Ⅰ

157 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第18天,点击查看活动详情

题目描述

实现一个 MyCalendar 类来存放你的日程安排。如果要添加的日程安排不会造成 重复预订 ,则可以存储这个新的日程安排。

当两个日程安排有一些时间上的交叉时(例如两个日程安排都在同一时间内),就会产生 重复预订

日程可以用一对整数 startend 表示,这里的时间是半开区间,即 [start, end), 实数 x 的范围为,  start <= x < end 。

实现 MyCalendar 类:

  • MyCalendar() 初始化日历对象。
  • boolean book(int start, int end) 如果可以将日程安排成功添加到日历中而不会导致重复预订,返回 true 。否则,返回 false 并且不要将该日程安排添加到日历中。

解题思路——遍历数组

这题有点像合并区间的题了。

手摸手提桶跑路——LeetCode56. 合并区间 - 掘金 (juejin.cn)

我们需要一个数据结构来保存当前已经添加进来的区间。这里我使用的是数组,后续查找区间是否重复的时候,需要 遍历查找,还是比较费时的。具体过程如下:

  1. 如果当前存放已添加区间的数组 schedule 长度为 0,说明一个预定也没有,可以直接安排, pushschedule
  2. 如果当前 schedule 中存在区间(已有日程安排),那么新进来的区间要判断是否重复预定。重复预定分为三种情况,分别是:① 新增区间 完全重复 于已预定区间;② 新增区间 后半部分重复 于已预定区间;③ 新增区间 前半部分重复 于已预定区间。通过以下三张图我们可以得出如果区间重复,那么必然有 bStart < aEndbEnd > aStart
  3. 如果新添加的区间 没有 和已添加的区间 重叠 的话,说明没有重复预定,返回 true

QQ截图20220811210904.png

全部重叠的情况

前半部分重叠.png

前半部分重叠的情况

后半部分重叠.png

后半部分重叠的情况

题解

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;
};

21.png

解题思路——二叉搜索树

二叉搜索树介绍

二叉搜索树从字面上理解就是专注于搜索用的二叉树,相比于无序数组的遍历来说,二叉搜索树的一个节点,对于节点 n 来说,n 的左节点 n.leftval 值永远比 nval 值小,n 的右节点 n.rightval 值永远比 nval 值大。

如果我们要搜索一个节点值 target 时,从根节点 root 开始判断,如果 target < root,说明 targetroot 的左子树上,则继续判断 targetroot.left 节点的 val 值的大小,一直到找到 target 为止,说明 target 存在该树上,否则一直找到叶子节点,则说明当前 target 不在该树上。

观察这个过程就能发现,对于数组的遍历搜索来说,效率快了很多。

关于二叉搜索树的相关概念,请参考 JavaScript 二叉搜索树以及实现翻转二叉树

构建节点

那么对于这题来说需要怎么做呢?

我们可以知道构建的树节点必然是要保存左右节点的指向的,除此之外,通过分析题目,可以知道每个节点还需要存储 startend用于标识当前节点所表示的区间范围

当查找一个区间时,通过新增区间的 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;
        }
    }
};

333.png

解题思路——数组二分查找法

二分查找就是在数组遍历法的基础上进行的优化,因为数组遍历法需要遍历查找无序数组,比较费时,考虑到如果是个有序数组,通过二分查找来搜索的话,效率会快很多。

微信图片_20220727184749.jpg

和前面说的一样,二分本质上还是对数组遍历的优化手段,其实逻辑上是有共同之处的。

二分查找,到底查的啥?关于结束条件是什么这件事困扰了我好久。所以二分查找法到底是找什么呢?

[[10,20], [24, 26], [30, 32], [40, 50]] 区间来说,这个时候有个新的日程安排 B=[22, 25]

我们可以肉眼看出来,这个新日程安排 B,是和 A=[24, 26] 区间有重合的,为啥我们知道它有重合,因为 Bstart=22 小于 Aend=26, 且 Bend=25 大于 Astart=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;
};

8.png

解题思路——插旗法(公交车上下车)

我们把一段区域的开始 结束的位置都插上一根旗子,用来标记领土范围。

假设开始旗子用 1 表示,结束旗子用 -1 表示。

QQ截图20220814155859.png

可以看到,正常来说开始和结束之间是没有别人的领土的(没有别的1在里头)。

那么如果别人的领土也在里头是怎么样的呢?

QQ截图20220814155910.png

可以看到从 领土A开始旗子 到领土A的 结束旗子 之间,多了个 领土B开始旗子,那么当 AStartAEnd和大于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;
};

QQ截图20220814160617.png

这么慢还用个der啊。

js中,对象属性如果是数字的话,会转成字符串,并且会把数字属性从小到大去排序,也成为“排序属性”。如果是换成c++的map<int,int>的话,会快不少的。这里我们就当了解一下了。

微信图片_20220727220812.jpg