kiner算法刷题记(三):线程池与任务队列

系列文章导引

开源项目

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

GitHub传送门:Kiner算法算题记

概念

有一个连续的存储区存储任意结构,有头指针和尾指针,尾指针一般指向最后一个元素的下一位

先入先出(FIFO)

基本操作

一个最简单的队列结构至少要支持以下两种操作:

入队(push)

尾指针向后移动一步,并插入元素

出队(pop)
  • 逻辑出栈:头指针向后移动一步
  • 真实出栈:如果使用数组模拟队列的话,就是调用数组的shift方法,将数组第一个元素弹出

队列的常见变种

循环队列

由于在大部分的语言中,都是采用头尾指针的方式对队列进行操作的,那么这样就可能导致一个问题:

# 加入有一个长度为10的空队列,下面队列中的*代表该为为空
[*,*,*,*,*,*,*,*,*,*]
# 先后执行多次入队和出队操作
push 1
push 2
pop
push 3
# 经过上述的操作后,我们的队列长成这样,其中头指针指向的位置是2所在的位置,尾指针指向的位置是3的下一位,也就是3后面的*
# 由于队列是先进先出的结构,上述执行了一次pop操作,因此1被弹出了队列,用<1>标识1倍逻辑删除了。
[<1>,2,3,*,*,*,*,*,*,*]
# 从上面的操作我们不难联想到,我们这个队列的大小是有限的,仅有10位,而每当我们执行pop操作时,就会出现很多个被逻辑删除的元素,
# 虽然我们已经用不上这个元素了,但是因为是逻辑删除,并没有真正的删除这个元素,所以还是会占了一个坑,就像上面的<1>,这样,就有可能会出现一个“假溢出”的情况,如:
[<1>,<2>,<3>,4,5,6,7,8,9,10]
# 如上面的这个队列,看起来这个队列好像是满了,没办法在放置下一个元素了,但是,我们可以发现,前面的<1>,<2>,<3>都是已经被逻辑删除的,对我们来说是没有用的元素,其实我们这个队列并没有真正溢出,仅仅是因为这几个家伙占着茅坑不拉屎导致的假溢出“假溢出”。

那么,如果要解决队列假溢出的问题,我们就引申出来一个队列的变种,叫做循环队列循环队列就是为了有效的利用队列的空间,当队列的尾指针到了最后的时候,如果还要插入元素,那么尾指针会回到队列的第一位,也就是上面<1>所在的位置,只要头尾指针不相遇,我们就可以继续网队列里面插入元素,如上述队列,如果使用循环队列实现,最终可能是这样:

# 以下队列便是真正的队满队列,其中队首是4,队尾是13
[11,12,13,4,5,6,7,8,9,10]
循环队列的Typescript版本实现
// leetcode [622] 设计循环队列
// 这里为了模拟大部分语言中的实现,因此除了使用数组用来存储数据外,没有使用数户组的一些方法,如pop何push等,直接使用头尾指针实现
class MyCircularQueue {
    // 用于记录队列中实际存储了多少个元素
    private count: number = 0;
    // js中使用数组模拟队列
    private queue: number[];
    // 头指针
    private head: number = 0;
    // 尾指针
    private tail: number = 0;
    // 初始化时初始化一个长度为k的数组控件
    constructor(private k: number) {
        this.queue = new Array<number>(k);
    }

    // 入队
    enQueue(value: number): boolean {
        // 若队列满了则直接返回false
        if(this.isFull()) return false;
        // console.log(this.isFull(), this.count, this.k);
        // 将元素赋值给尾指针指向的位置
        this.queue[this.tail] = value;
        // 入队操作,需要将元素数量加一
        this.count++;
        // 尾指针向后移动一位,由于当前队列是循环队列,如果刚好向后移动一位超出了数组的长度,就会出现异常
        // 这里可以使用一个技巧,尾指针向后移动一位之后,再跟初始化时的数组长度取余就可以获得真实的尾指针位置了
        // 如:k=10,当前的tail指针指向9,那么,尾指针向后移动一位就是:9+1%10=0,尾指针应该指向我们数组的第一位元素了
        this.tail = (this.tail + 1) % this.k;
        
        return true;
    }

    // 出队操作
    deQueue(): boolean {
        // 当队列为空时,返回false
        if(this.isEmpty()) return false;
        // 出队操作,元素数量减一
        this.count--;
        // 头指针向后移动一位,为了防止超过数组长度,因此进行取余操作,具体想看入队操作的解释
        this.head = (this.head + 1) % this.k;
        return true;
    }

    // 返回队首袁术
    Front(): number {
        if(this.isEmpty()) return -1;
        return this.queue[this.head];
    }

    // 返回队尾元素
    Rear(): number {
        if(this.isEmpty()) return -1;
        // 因为尾指针始终指向的是队列最后一个元素的下一位,如果tail刚好为0时,tail-1就会出现负数的情况
        // 为了解决这种情况,我们可以尾指针减一后,先加上一个数组的长度,然后再对接过与数组长度取余,
        // 如:k=10, (0 - 1 + 10) % 10 = 9,因此,最后一个元素在数组里面的索引就是9
        const idx = (this.tail - 1 + this.k) % this.k;
        return this.queue[idx];
    }

    // 判断队列是否为空
    isEmpty(): boolean {
        return this.count === 0;
    }

    // 判断队列是否已满
    isFull(): boolean {
        return this.count === this.k;
    }
}

双向循环队列

双向循环队列就是在循环队列的基础上支持既可以在头部或尾部入队,也可以在头部或尾部出队的特殊队列

双向循环队列的Typescript版本
// leetcode [641] 设计循环双端队列
class MyCircularDeque {
    private head: number = 0;
    private tail: number = 0;
    private count: number = 0;
    private queue: number[];
    constructor(private k: number) {
        this.queue = new Array<number>(k);
    }

    // 在队首插入元素
    insertFront(value: number): boolean {
        if(this.isFull()) return false;

        // 由于队首可能是有元素的,而队尾是没有元素的,所以
        // 如果要在队首插入元素的话,需要让head左移一位(注意head为0的情况)
        this.head = (this.head - 1 + this.k) % this.k;
        this.queue[this.head] = value;
        this.count++;

        return true;
    }

    // 在队尾插入元素
    insertLast(value: number): boolean {
        if(this.isFull()) return false;

        this.queue[this.tail] = value;
        this.tail = (this.tail + 1) % this.k;
        this.count++;

        return true;
    }

    // 在队首删除元素
    deleteFront(): boolean {
        if(this.isEmpty()) return false;
        this.head = (this.head + 1) % this.k;
        this.count--;
        return true;
    }

    // 在队尾删除元素
    deleteLast(): boolean {
        if(this.isEmpty()) return false;
        this.tail = (this.tail - 1 + this.k) % this.k;
        this.count--;
        return true;
    }

    // 获取队首元素
    getFront(): number {
        if(this.isEmpty()) return -1;
        return this.queue[this.head];
    }

    // 获取队尾与安娜苏
    getRear(): number {
        if(this.isEmpty()) return -1;
        const idx = (this.tail - 1 + this.k) % this.k;
        return this.queue[idx];
    }

    // 判断队列是否为空
    isEmpty(): boolean {
        return this.count === 0;
    }

    // 判断队列是否已满
    isFull(): boolean {
        return this.count === this.k;
    }
}
前中后队列

前中后循环队列双向循环队列的基础上,再加了一个,可以从队列中间入队和出队

前中后队列的Typescript版本
// leetcode: [1670] 设计前中后队列
//  使用双向链表的形式实现前中后队列
// 如:1->2->3->4
// 可以看成是:1->2 --->  3->4
// 这两个链表串在一起,这样,我们想要往中间插入时,就只需要考虑到底是在链表1后面插入还是在链表2前面插入即可

// 双向链表链表节点对象
class Node {
    constructor(public val=0,public prev: Node=null,  public next: Node=null){}
    // 在当前节点之前插入一个节点
    insertPrev(node: Node) {
        node.prev = this.prev;
        node.next = this;
        this.prev && (this.prev.next = node);
        this.prev = node;
    }

    // 在当前节点后插入一个节点
    insertNext(node: Node) {
        node.next = this.next;
        this.next && (this.next.prev = node)
        this.next = node;
        node.prev = this;
    }

    // 弹出当前节点的上一个节点
    popPrev(): void {
        if(!this.prev) return;
        let p = this.prev;
        this.prev = p.prev;
        this.prev && (this.prev.next = this);
    }

    // 弹出当前节点的下一个节点
    popNext(): void {
        if(!this.next) return;
        let p = this.next;
        this.next = p.next;
        this.next && (this.next.prev = this);
    }
}

// 使用双向链表实现一个循环双端队列
class MyQueue {
    // 因为是双端队列,可以从头部添加和删除元素,也可以从尾部添加或删除元素
    // 因此需要定义头尾两个虚拟节点辅助我们操作这个链表
    private head: Node = new Node();
    private tail: Node = new Node();
    private count: number = 0;// 用于记录队列中实际的元素数量,循环队列的关键
    constructor(){
        // 初始时,我们让头尾虚拟头相连即可
        // head -> tail
        // 我们需要从前面插入元素是,只需要在head节点后面插入元素
        // 我们需要从后面插入元素时,只需要从tail节点前面插入元素
        this.head.next = this.tail;
        this.head.prev = null;
        this.tail.next = null;
        this.tail.prev = this.head;
    }
    // 在队列尾部插入元素
    public pushBack(val: number) {
        this.tail.insertPrev(new Node(val));
        this.count++;
    }
    // 在队列头部插入元素
    public pushFront(val: number) {
        this.head.insertNext(new Node(val));
        this.count++;
    }
    // 在队列的尾部删除元素
    public popBack(): number {
        if(this.isEmpty()) return -1;
        let res = this.tail.prev.val;
        this.tail.popPrev();
        this.count--;
        return res;
    }
    // 在队列的首部删除元素
    public popFront(): number {
        if(this.isEmpty()) return -1;
        let res = this.head.next.val;
        this.head.popNext();
        this.count--;
        return res;
    }
    // 获取队首元素
    public front(): number{
        return this.head.next.val;
    }
    // 获取对队尾元素
    public back(): number{
        return this.tail.prev.val;
    }
    // 队列元素珊瑚粮
    public size(): number{
        return this.count;
    }
    // 队列是否为空
    public isEmpty(): boolean{
        return this.head.next === this.tail;
    }
}

class FrontMiddleBackQueue {
    private q1: MyQueue;
    private q2: MyQueue;
    constructor() {
        this.q1 = new MyQueue();
        this.q2 = new MyQueue();
    }
    // 每一次添加或删除元素操作后,为了始终保持q1的元素数量始终大于或等于q2的元素数量,调用此方法进行修正
    update(): void {
        // 1 -> 2 -> 3 -> 4
        // 始终确保q1的长度大于或等于q2,并且两者节点数量的差值最大为1
        // 当q1数量小于q2时,从q2头部取出一个节点放在q1尾部
        if(this.q1.size() < this.q2.size()) {
            this.q1.pushBack(this.q2.popFront());
        }
        // 如果q2的数量比q1数量少两个时,从q1尾部拿一个出来放在q2头部
        if(this.q2.size() === this.q1.size() - 2) {
            this.q2.pushFront(this.q1.popBack());
        }
    }

    // 在队首添加元素,就直接在q1上添加元素,然后修正两个队列即可
    pushFront(val: number): void {
        this.q1.pushFront(val);
        this.update();
    }

    // 在队列中间添加元素,首先判断如果q1内的元素数量大于q2的话,就把元素放在q2头部,否则放在q1尾部
    pushMiddle(val: number): void {
        if(this.q1.size() > this.q2.size()) {
            this.q2.pushFront(this.q1.popBack());
        }
        this.q1.pushBack(val);
        this.update();
    }

    // 在队尾添加元素,直接在q2尾部添加元素即可,然后修正两个队列
    pushBack(val: number): void {
        this.q2.pushBack(val);
        this.update();
    }

    // 从队首删除元素,在q1头部删除并修正两个队列接口
    popFront(): number {
        if(this.isEmpty()) return -1;
        let res = this.q1.popFront();
        this.update();
        return res;
    }
    // 从队列中间删除元素,由于q1数量永远大于或等于q2数量,因此,我们只需要把q1的队尾元素删除即可
    popMiddle(): number {
        if(this.isEmpty()) return -1;
        let res = this.q1.popBack();
        this.update();
        return res;
    }

    // 从队尾删除元素,由于有可能长出现q2为空的情况,因此,如果q2为空,则从q1的队首删除元素,否则从q2的队首删除
    popBack(): number {
        if(this.isEmpty()) return -1;
        let res;
        if(this.q2.isEmpty()){
            res = this.q1.popBack();
        } else{
            res = this.q2.popBack();
        }
        this.update();
        return res;
    }

    // 判断队列是否为空
    isEmpty(): boolean {
        return this.q1.size() === 0;
    }
}
优先队列

我们都知道,普通队列是一个严格遵循先进先出(FIFO)原则的数据结构,但是,在某些特殊场景,比如说我们的任务队列中,有一个优先级相当高的任务需要被优先执行,那么,这个时候就要插队了,而支持这种插队操作的队列,我们把它叫做优先队列,即:支持优先级的队列

队列的典型应用场景

CPU的超线程技术

CPU通过指令队列不断的处理输入的指令

虚拟四核本质上只有两个核心,只是增加了两个指令队列

1个CPU包含多个计算核心

线程池的任务队列

相当于任务的缓冲区,一般当前没空处理的时候,先放在队列里面等一会,有空了再从队列里面取

进程可以理解为是一个人

线程则是这个人要做的一些事

一个人可以同时做多件事,所以一个进程可以包含若干个线程

LeetCode刷题

LeetCode 933 最近的请求次数
解题思路

这题就是利用了队列的先进先出的原理实现的,这题比较简单,我们直接来看具体实现吧。

代码实现
/*
 * @lc app=leetcode.cn id=933 lang=typescript
 *
 * [933] 最近的请求次数
 */

// @lc code=start
class RecentCounter {
    // 使用一个数组模拟栈
    private queue: number[];
    constructor() {
        // 初始化数组
        this.queue = new Array<number>();
    }

    ping(t: number): number {
        // 每次请求时将t加入队列
        this.queue.push(t);
        // 将所有时间大于3000的元素弹出队列
        while(t - this.queue[0] > 3000) this.queue.shift();
        // 最后剩下的数组的长度就是我们最近的请求次数
        return this.queue.length;
    }
}

/**
 * Your RecentCounter object will be instantiated and called as such:
 * var obj = new RecentCounter()
 * var param_1 = obj.ping(t)
 */
// @lc code=end


leetcode 622 设计循环队列

代码解析请参考上面队列常见变种中的代码详解

leetcode 641 设计循环双端队列

代码解析请参考上面队列常见变种中的代码详解

leetcode: 1670 设计前中后队列

代码解析请参考上面队列常见变种中的代码详解

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

星河阅卷

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

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

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

打赏作者

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

抵扣说明:

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

余额充值