Kiner算法刷题记(九):有趣的排序思想(手撕算法篇)

系列文章导引

开源项目

本系列所有文章都将会收录到GitHub中统一收藏与管理,欢迎ISSUEStar

GitHub传送门:Kiner算法算题记

1122. 数组的相对排序

解题思路

这题是典型使用计数排序的题目,我们只需要根据第二个数组中的元素对第一个数组中每一个元素统计数量,然后按照计数排序的规则先将第二个数组中有的数放入结果数组,然后再将第二个数组中没有的元素依次加入到结果数组即可。

代码演示
/*
 * @lc app=leetcode.cn id=1122 lang=typescript
 *
 * [1122] 数组的相对排序
 *
 * https://leetcode-cn.com/problems/relative-sort-array/description/
 *
 * algorithms
 * Easy (70.81%)
 * Likes:    175
 * Dislikes: 0
 * Total Accepted:    59.6K
 * Total Submissions: 84.1K
 * Testcase Example:  '[2,3,1,3,2,4,6,7,9,2,19]\n[2,1,4,3,9,6]'
 *
 * 给你两个数组,arr1 和 arr2,
 * 
 * 
 * arr2 中的元素各不相同
 * arr2 中的每个元素都出现在 arr1 中
 * 
 * 
 * 对 arr1 中的元素进行排序,使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。未在 arr2 中出现过的元素需要按照升序放在 arr1
 * 的末尾。
 * 
 * 
 * 
 * 示例:
 * 
 * 
 * 输入:arr1 = [2,3,1,3,2,4,6,7,9,2,19], arr2 = [2,1,4,3,9,6]
 * 输出:[2,2,2,1,4,3,3,9,6,7,19]
 * 
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 
 * 0 
 * arr2 中的元素 arr2[i] 各不相同
 * arr2 中的每个元素 arr2[i] 都出现在 arr1 中
 * 
 * 
 */

// @lc code=start
function relativeSortArray(arr1: number[], arr2: number[]): number[] {
    // 统计第一个数组中每个元素出现的次数,依据题意,我们值域最大值是1000,因此,用于计数用的数组大小设置为1001,
    // 确保能够放下所有满足值域的数字
    const count: number[] = new Array<number>(1001);
    const temp: number[] = [];
    count.fill(0);
    for(let num of arr1) count[num] += 1;
    // 用来标记当前已经替换的元素数量
    let p = 0;
    // 然后将arr2出现的数字先放入到arr1的前半段
    for(let num of arr2) {
         线面几行代码其实做的工作就是将arr2中存在的数字按照数量替换到arr1中,替换完就将计数器置为0,因此,我们可以简化一下代码:
        // // 需要往arr1数组中塞count[num]个元素
        // for(let i=0;i<count[num];i++) {
        //     arr1[p++] = num;
        // }
        // // 塞进去之后,将当前数据的计数器设置为0,代表已经处理过了
        // count[num] = 0;

        // 这行代码实际上跟上面的for的代码用处是一样的,但是更加精简一些
        while(count[num]--) arr1[p++] = num;
    }

    // 经过上面的步骤,我们已经完成了arr2中存在元素的排序了,接下来就对arr2中不存在元素进行排序
    for(let i=0;i<count.length;i++) {
        // 如果计数为0代表不存在该数字或者该数字在第二个数组中已经存在并在上面的步骤已经处理完了,无需处理
        // 那么,为什么会有小于0的情况呢?是因为我们进行count[i]--时,后可能出现0--为-1的情况,因此兼容一下
        // if(count[i] === 0) continue;
        // for(let j=0;j<count[i];j++) arr1[p++] = i;
        // 上面两行代码实际上跟下面的代码是等效的,只是代码量会稍微精简一点
        if(count[i] <= 0) continue;
        // 否则就将目标数字加到数组某位,不过要记住,此处不应该取count中的元素,而应该获取count的索引,
        // 因为count的元素代表的是元素的数量,他的索引才是真正的值
        while(count[i]--) arr1[p++] = i;
    }
    return arr1;

};
// @lc code=end


164. 最大间距

解题思路

依题意,要求我们要用线性的复杂度解决,由于快速排序和归并排序都是nlogn的,是非线性的,而又因为我们值域的范围过于庞大,我们也没办法使用计数排序,因此,这道题最适合使用基数排序实现。

代码演示

PS: 受限于语言特性和LeetCode判题逻辑,下面代码有一定概率会超出内存限制,但基数排序的思想是没错的

/*
 * @lc app=leetcode.cn id=164 lang=typescript
 *
 * [164] 最大间距
 *
 * https://leetcode-cn.com/problems/maximum-gap/description/
 *
 * algorithms
 * Hard (60.96%)
 * Likes:    383
 * Dislikes: 0
 * Total Accepted:    52.9K
 * Total Submissions: 86.6K
 * Testcase Example:  '[3,6,9,1]'
 *
 * 给定一个无序的数组,找出数组在排序之后,相邻元素之间最大的差值。
 * 
 * 如果数组元素个数小于 2,则返回 0。
 * 
 * 示例 1:
 * 
 * 输入: [3,6,9,1]
 * 输出: 3
 * 解释: 排序后的数组是 [1,3,6,9], 其中相邻元素 (3,6) 和 (6,9) 之间都存在最大差值 3。
 * 
 * 示例 2:
 * 
 * 输入: [10]
 * 输出: 0
 * 解释: 数组元素个数小于 2,因此返回 0。
 * 
 * 说明:
 * 
 * 
 * 你可以假设数组中所有元素都是非负整数,且数值在 32 位有符号整数范围内。
 * 请尝试在线性时间复杂度和空间复杂度的条件下解决此问题。
 * 
 * 
 */

// @lc code=start

const halfBit32 = Math.pow(2, 16);
function getLow16(num: number): number {
    // 依据题意,不需要考虑负数情况,可以使用这种方式
    // return Math.floor(num % halfBit32);
    // 或使用位运算
    return num & 0xffff;
}

function getHeigh16(num: number): number {
    // return Math.floor(num / halfBit32);
    // 或使用位运算
    return (num & 0xffff0000) >> 16
}

function maximumGap(nums: number[]): number {
    const count: number[] = new Array<number>(halfBit32);
    const tmp: number[] = [];
    count.fill(0);
    // 对低16位进行处理
    // 首先进行计数
    for(let num of nums) count[num] += 1;
    // 求前缀和
    for(let i=1;i<count.length;i++) count[i] += count[i-1] 
    // 归位
    for(let i=nums.length - 1; i>=0;--i) tmp[--count[getLow16(nums[i])]] = nums[i];
    // 复位count
    count.fill(0);
    // 处理高16位
    for(let num of tmp) count[num] += 1;
    for(let i=1;i<count.length;i++) count[i] += count[i-1];
    for(let i=tmp.length - 1;i>=0;--i) nums[--count[getHeigh16(tmp[i])]] = tmp[i];

    // 至此,数组就已经用线性复杂度排好序了,然后就是计算差值
    let res = 0;
    for(let i=1;i<nums.length;i++) {
        res = Math.max(res, nums[i] - nums[i-1]);
    }
    return res;
};
// @lc code=end


207. 课程表

解题思路

这道题因为涉及到学习课程与期对应先修课的关系,其实就可以建立起一个以课程为节点,以先修关系为有向边的图,我们只需要对这个图进行拓扑排序,如果我们的课程关系中存在环,即ai的先修课为bi,bi的先修课为ci,ci的先修课为ai,那么这几节课就形成了环,每个节点的入度都不可能为0,此时使用拓扑排序,这三门课就不会被加入到结果中,因此,我们只需要通过拓扑排序之后,判断一下节点数量是否等于课程数量便可判断是否能够完成所有课程的学习了。

代码演示
/*
 * @lc app=leetcode.cn id=207 lang=typescript
 *
 * [207] 课程表
 *
 * https://leetcode-cn.com/problems/course-schedule/description/
 *
 * algorithms
 * Medium (54.64%)
 * Likes:    816
 * Dislikes: 0
 * Total Accepted:    112.3K
 * Total Submissions: 205.6K
 * Testcase Example:  '2\n[[1,0]]'
 *
 * 你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
 * 
 * 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi]
 * ,表示如果要学习课程 ai 则 必须 先学习课程  bi 。
 * 
 * 
 * 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
 * 
 * 
 * 请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
 * 
 * 
 * 
 * 示例 1:
 * 
 * 
 * 输入:numCourses = 2, prerequisites = [[1,0]]
 * 输出:true
 * 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。
 * 
 * 示例 2:
 * 
 * 
 * 输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
 * 输出:false
 * 解释:总共有 2 门课程。学习课程 1 之前,你需要先完成​课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 
 * 0 
 * prerequisites[i].length == 2
 * 0 i, bi < numCourses
 * prerequisites[i] 中的所有课程对 互不相同
 * 
 * 
 */

// @lc code=start
function canFinish(numCourses: number, prerequisites: number[][]): boolean {
    // 用于存储每个节点的入度(即指向该节点的节点数量)是多少,默认都为0
    const indeg: number[] = new Array<number>(numCourses);
    indeg.fill(0);
    // 定义一个队列用来辅助拓扑排序
    const queue: number[] = [];
    // 定义一个二维数组用于存储每个先修课程学完之后要学的是哪一门或几门课程
    const dep: number[][] = [];

    // 首先初始化入度和依赖数组
    for(let num of prerequisites) {
        // num数组第一个元素代表课程,第二个代表先修课程
        // 因此如果prerequisites包含了num[0]课程,说明这个课程有依赖课程,所以入度加1
        indeg[num[0]] += 1;
        // 将依赖关系存入二维数组中
        (dep[num[1]]||(dep[num[1]] = [])).push(num[0])
    }

    // 按照拓扑排序的规则,只有入度为0的节点才能进入队列,初始时,我们将所有入度为0的节点
    // 都先放入队列
    for(let i=0;i<numCourses;i++) indeg[i] === 0 && queue.push(i);

    // 初始化准备完毕,我们就可以在进行拓扑排序的过程中计算能够学习完的课程的数量了
    let res = 0;

    // 如果队列不为空则继续执行
    while(queue.length !== 0) {
        // 从队首出元素
        const node = queue.shift();
        // 每弹出一个元素,计数器加1
        res++;
        // 然后将依赖弹出元素的课程的入度减一,如果减一后发现该课程入度为0,则加入队列
        if(dep[node]) {
            for(let num of dep[node]) {
                indeg[num]--;
                indeg[num] === 0 && queue.push(num);
            }
        }
    }
    // console.log(res, numCourses);
    // 只需判断拓扑序的节点树是否与总课程数相等即可
    return res === numCourses;

};
// @lc code=end


210. 课程表 II

解题思路

这题其实跟上一题的解题思路是一样的,我们只需要把计算数量改成将拓扑排序的结果加入到数组中,如果最终数组的长度与课程数量相等则说明能修完全部课程,直接返回改排序后的数组,否则不能修完全部课程,直接返回空数组

代码演示
/*
 * @lc app=leetcode.cn id=210 lang=typescript
 *
 * [210] 课程表 II
 *
 * https://leetcode-cn.com/problems/course-schedule-ii/description/
 *
 * algorithms
 * Medium (53.28%)
 * Likes:    413
 * Dislikes: 0
 * Total Accepted:    71.1K
 * Total Submissions: 133.3K
 * Testcase Example:  '2\n[[1,0]]'
 *
 * 现在你总共有 n 门课需要选,记为 0 到 n-1。
 * 
 * 在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
 * 
 * 给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
 * 
 * 可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
 * 
 * 示例 1:
 * 
 * 输入: 2, [[1,0]] 
 * 输出: [0,1]
 * 解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
 * 
 * 示例 2:
 * 
 * 输入: 4, [[1,0],[2,0],[3,1],[3,2]]
 * 输出: [0,1,2,3] or [0,2,1,3]
 * 解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
 * 因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
 * 
 * 
 * 说明:
 * 
 * 
 * 输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
 * 你可以假定输入的先决条件中没有重复的边。
 * 
 * 
 * 提示:
 * 
 * 
 * 这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
 * 通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
 * 
 * 拓扑排序也可以通过 BFS 完成。
 * 
 * 
 * 
 */

// @lc code=start
function findOrder(numCourses: number, prerequisites: number[][]): number[] {
    let indeg: number[] = new Array<number>(numCourses);
    indeg.fill(0);
    let queue: number[] = [];
    let dep: number[][] = [];

    // 先初始化入度和依赖关系
    for(let course of prerequisites) {
        indeg[course[0]] += 1;
        (dep[course[1]] || (dep[course[1]] = [])).push(course[0]);
    }

    // 将所有入度为0的节点放入队列当中
    for(let i=0;i<indeg.length;i++) {
        if(indeg[i] === 0) queue.push(i);
    }

    // 进行拓扑排序
    let res: number[] = [];
    while(queue.length!==0) {
        const top = queue.shift();
        res.push(top);
        indeg[top] = 0;
        if(dep[top]) {
            for(let num of dep[top]) {
                indeg[num]--;
                indeg[num] === 0 && queue.push(num);
            }
        }
    }

    // 如果拓扑排序后的节点数量等于课程数量,说明可以修完全部课程,直接返回排序后的数组
    if(res.length === numCourses) return res;
    // 否则不能修满全部课程,返回空数组
    return [];
};
// @lc code=end


56. 合并区间

解题思路

这道题的难点其实就在于如何判断两个区间重合可以进行合并。其实我们可以借助类似于之前栈的一个思想。我们之前学习栈的时候有说过,栈不仅仅是一个数据结构,还代表的一类思想,就是遇到开始标记则加1,遇到结束标记则减1,当计数为0时说明已经抵消掉一对起始标记了。在这里也同样适用。我们拿例题举一个例子:

[[1,3],[2,6],[8,10],[15,18]]
# 上面的二维数组代表的是一个区间,但是区间可能出现重合。我们要做的就是合并重合区间,那么,怎么合并呢?
# 我们可以这样,当遇到区间起始点时+1,遇到区间结束点时-1,当计数为0时,从第一个区间起始点到当前区间的终点就是可以我们合并后的区间
# 为了实现上述的操作,我们先处理一下这个数组
[[1,1], [3,-1], [2,1],[6,-1],[8,1],[10,-1],[15,1],[16,-1]]
# 如上操作,将二位数组根据起始点和结束点拆分并打上+1和-1的标记
# 然后我们现根据起始点进行从小到大排序,如果起始点相同,就根据+1和-1从大到小排序
[[1,1],[2,1], [3,-1],[6,-1],[8,1],[10,-1],[15,1],[16,-1]]
# 当我们循环这个二位数组时,刚好到了6为其实点时,我们的计数器为0,此时,我们合并后区间就是[1,6],剩下的也以此类推,遍历结束后,我们就已经得到了最终的答案了
[[1,6],[8,10],[15,18]]

代码演示
/*
 * @lc app=leetcode.cn id=56 lang=typescript
 *
 * [56] 合并区间
 *
 * https://leetcode-cn.com/problems/merge-intervals/description/
 *
 * algorithms
 * Medium (45.39%)
 * Likes:    945
 * Dislikes: 0
 * Total Accepted:    237.9K
 * Total Submissions: 521.2K
 * Testcase Example:  '[[1,3],[2,6],[8,10],[15,18]]'
 *
 * 以数组 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] 可被视为重叠区间。
 * 
 * 
 * 
 * 提示:
 * 
 * 
 * 1 
 * intervals[i].length == 2
 * 0 i i 
 * 
 * 
 */

// @lc code=start
function merge(intervals: number[][]): number[][] {
    let tmp: number[] = [];
    let res: number[][] = [];
    // 将原本区间范围改造成[[起点,1],[终点,-1],....]的数组
    for(let num of intervals) {
        tmp[0] = num[0];
        tmp[1] = 1;
        res.push([...tmp]);
        tmp[0] = num[1];
        tmp[1] = -1;
        res.push([...tmp]);
    }
    // 将改造后的数组先按每个元素的第一个从小到大排序,如果两个相同,则按第二个数从大到小排
    res.sort((a, b) => {
        if(a[0] === b[0]) return b[1] - a[1];

        return a[0]-b[0];
    });
    
    // pre用于记录区间初始位置,count用来辅助判断是否已经左右断点相互抵消
    let pre = -1, count = 0, ret: number[][] = [];

    for(let num of res) {
        // 当pre为-1时,说明是初始状态,让pre指向区间头部元素
        if(pre === -1) pre = num[0];
        // 计数器累加
        count += num[1];
        // 当计数器为0时,说明左右断点已经销户抵消了,那么,此时pre所指向的就应该是合并后区间的起始值,而当前num的
        // 第一个元素则是区间的终止值
        if(count === 0) {
            tmp[0] = pre;
            tmp[1] = num[0];
            ret.push([...tmp]);
            // 将合并后的区间压入结果数组之后,需要重置pre,以便进入下一轮的合并
            pre = -1;
        }
    }

    return ret;
};
// @lc code=end


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星河阅卷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值