【前端算法系列】队列queue

本文探讨了队列在信息技术领域的多种应用,如最近请求计数、滑动窗口最大值、任务调度等。通过具体示例展示了如何使用队列优化算法,如循环队列在内存管理中的优势,以及如何处理任务执行的冷却时间。同时,文章提到了设计高效队列结构如循环队列的方法,以及它们在实际问题中的实现。
摘要由CSDN通过智能技术生成

933.最近的请求次数

  • 有新请求就入队,3000ms前发出的请求出队
  • 队列的长度就是最近请求次数
/** 时间复杂度:O(n) while循环体 n为踢出对的请求个数
 *  空间复杂度:O(n) 定义了一个数组,n为队列的长度
 */
var RecentCounter = function() {
    this.q = []
};
RecentCounter.prototype.ping = function(t) {
    // 新请求入队
    this.q.push(t)
    // 把不在时间范围内的老请求出队
    while(this.q[0]<t-3000){
        this.q.shift()
    }
    return this.q.length
};

数据流中的第K大元素

int k = 3;
int[] arr = [4,5,8,2];
KthLargest kthLargest = new KthLargest(3, arr);
kthLargest.add(3); // returns 4
kthLargest.add(5); // returns 5

思路:
方法1:保存前K个最大的值;全部排序
方法2:优先队列,每次把最大的放在最上面,或把最小的放在最上面
小顶堆:把最小的放根结点,所以要保证元素个数等于K(意思是这个堆的个数为K个,最顶的是最小的,遍历数组的元素,如果有比它小的就不用进来排队了,如果比它大,就把最小的踢掉,剩下的元素重新调整log2^k)

239.滑动窗口最大值

输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
输出: [3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
--------------- -----
[1 3 -1] -3 5 3 6 7 ------> 3
1 [3 -1 -3] 5 3 6 7 ------> 3
1 3 [-1 -3 5] 3 6 7 ------> 5
1 3 -1 [-3 5 3] 6 7 ------> 5
1 3 -1 -3 [5 3 6] 7 ------> 6
1 3 -1 -3 5 [3 6 7] ------> 7

所有的滑动窗口题用队列去处理
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EpM7Tc9e-1610632729099)(../images/queue/queue_img_03.png)]

  • 方法1:暴力求解,枚举窗口的起点位置,起点开始到len-k的最大值,写两个嵌套循环O(n*K)
const maxSlidingWindow = function (nums, k) {
    const len = nums.length
    const res = []
    let left = 0, right = k-1
    // 当数组没有被遍历完时,执行循环体内的逻辑
    while(right < len){
        const max = calMax(nums, left, right)
        res.push(max)
        // 左右指针前进一步
        left++
        right++
    }
    return res
}
function calMax(arr, left, right){
    if (!arr || !arr.length) return
    // 初始化为第一个元素
    let maxNum = arr[left] 
    for(let i = left; i<=right; i++){
        // 谁比我大,我就变成谁
        if(arr[i] > maxNum) maxNum = arr[i]
    }
    return maxNum
}
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3))

上述通过遍历来更新最大值产生了K

  • 方法2:线性队列,O(n+k)即O(n)一次解决
    1)设置一个双端队列,存储nums的下标
    2)遍历每个元素,拿元素跟双端队列里的队尾做比较(双端队列:比我小的都踢出去)
    当前元素小于队尾,就直接入队,否则,将队尾元素逐个出队,直到队尾元素大于等于当前元素为止(比如,窗口移到2 3 4位置,5进来的时候,要跟窗口其他数对比,只要比5小,都可以从这个栈弹出)
    3)检查队头元素,看队头元素是否已经被排除在滑动窗口的范围之外了(即队头小于i-k)。如果是,则将队头元素出队
    4)判断滑动窗口状态,被遍历的元素个数小于K,表示还不能动结果数组,只能继续更新队列;如果大于等于k,表示最大值已经出现,队头元素就是最大值
const maxSlidingWindow = function (nums, k) {
  // 缓存数组的长度
  const len = nums.length;
  // 初始化结果数组
  const res = [];
  // 初始化双端队列
  const deque = [];
  // 开始遍历数组
  for (let i = 0; i < len; i++) {
    // 当队尾元素小于当前元素时
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      // 将队尾元素(索引)不断出队,直至队尾元素大于等于当前元素
      deque.pop();
    }
    // 入队当前元素索引(注意是索引)
    deque.push(i);
    // 当队头元素的索引已经被排除在滑动窗口之外时
    while (deque.length && deque[0] <= i - k) {
      // 将队头元素索引出队
      deque.shift();
    }
    // 判断滑动窗口的状态,只有在被遍历的元素个数大于 k 的时候,才更新结果数组
    if (i >= k - 1) {
      res.push(nums[deque[0]]);
    }
  }
  // 返回结果数组
  return res;
}

设计循环队列

循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则并且队尾被连接在队首之后以形成一个循环。它也被称为“环形缓冲器”
循环队列的一个好处是我们可以利用这个队列之前用过的空间。在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
循环队列可以高效利用存储空间,普通队列需要很大的存储空间,存储空间会往上跑,循环队列可以避免这个问题。

你的实现应该支持如下操作:
MyCircularQueue(k): 构造器,设置队列长度为 k 。
Front: 从队首获取元素。如果队列为空,返回 -1 。
Rear: 获取队尾元素。如果队列为空,返回 -1 。
enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
isEmpty(): 检查循环队列是否为空。
isFull(): 检查循环队列是否已满。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yGYtlFVa-1610632729104)(../images/queue/queue_img_01.png)]

class MyCircularQueue{
    constructor(k) {
        // 用来保存数据长度为k的数据结构
        this.list = Array(k)
        // 队首指针
        this.front = 0
        // 队尾的指针
        this.rear = 0
        // 队列的长度
        this.max = k
    }
    // 入队:rear指针指向哪里,哪里就可以入队,且rear指针+1
    enQueue(num) {
        if (this.isFull()) {
            return false
        } else {
            this.list[this.rear] = num
            // 指针向后移动
            this.rear = (this.rear + 1) % this.max
            return true
        }
    }
    // 出队:front指针指向谁,谁就出队,且front指针+1
    deQueue() {
        let v = this.list[this.front]
        this.list[this.front] = ''
        console.log('front----->'+this.front)
        // front指针往后移动
        this.front = (this.front + 1) % this.max
        console.log('front222----->'+this.front)
        return v
    }
    isEmpty() {
        return this.front === this.rear && !this.list[this.front]
    }
    isFull() {
        return this.front === this.rear && !!this.list[this.front]
    }
    Front() {
        return this.list[this.front]
    }
    rear() {
        let rear = this.rear - 1
        return this.list[rear < 0 ? this.max - 1 : rear]
    }
}
const cQueue = new MyCircularQueue(5); // 设置长度为 3
cQueue.enQueue(1);  // 返回 true
cQueue.enQueue(2);  // 返回 true
cQueue.enQueue(3);  // 返回 true
cQueue.enQueue(4);  // 返回 false,队列已满
// cQueue.rear();  // 返回 3
// cQueue.isFull();  // 返回 true
cQueue.deQueue();  // 返回 true
cQueue.deQueue();  // 返回 true
cQueue.deQueue();  // 返回 true
// cQueue.enQueue(4);  // 返回 true
// cQueue.rear();  // 返回 4
console.log(cQueue)

任务调度器

给定一个用字符数组表示的 CPU 需要执行的任务列表。其中包含使用大写的 A - Z 字母表示的26 种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间内执行完。CPU 在任何一个单位时间内都可以执行一个任务,或者在待命状态。

然而,两个相同种类的任务之间必须有长度为 n 的冷却时间,因此至少有连续 n 个单位时间内 CPU 在执行不同的任务,或者在待命状态。

你需要计算完成所有任务所需要的最短时间。
示例 :

输入:tasks = [“A”,“A”,“A”,“B”,“B”,“B”], n = 2
输出:8
解释:A -> B -> (待命) -> A -> B -> (待命) -> A -> B.
在本示例中,两个相同类型任务之间必须间隔长度为 n = 2 的冷却时间,而执行一个任务只需要一个单位时间,所以中间出现了(待命)状态。

思路:任务清单keys [‘A’,‘B’] 是用来遍历循环,拿到当前数量最多的任务,然后个push到临时队列tmp中,临时队列只能放到n+1个(3个)任务,不够放的就用’-'来补齐(做到两个相同类型任务之间必须长度为n)。执行完一个临时队列tmp后,再循环判断还有没有任务清单keys,有的话继续前面的动作,直到keys[0]=undefined为止

步骤:
1)先记录每类任务的数量 Q:{A:3, B:3}
2)max记录最多数量的先执行,先被push到临时任务队列tmp中,这个tmp就是AB-AB-AB(不够n+1个的往里面补"-")

  • 获取任务清单keys([‘A’,‘B’]),遍历任务清单找到数量最多的key,拿到数量最多的A先push到临时队列tmp中,表示执行这个队列,所以得把Q中A的数量减一,从任务清单keys清除A
  • 这个临时队列容量为n+1,所以能被循环小于n次,执行完一个临时队列后,把临时队列累加给p, 再去循环执行下一个临时队列
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mFbE2OKn-1610632729105)(../images/queue/queue_img_02.png)]
const leastInterval = (tasks, n) => {
    // 表示最终队列执行的结果
    let q = ''
    // 对归类进行存储
    let Q = {}
    tasks.forEach(item => {
        if (Q[item]) {
            Q[item]++
        } else {
            Q[item] = 1
        }
    });
    // 队列中还有任务就要循环,为什么为1,是因为效率问题,不要一直判断对象Q是否为空,会重复计算
    while (1) {
        // 处理边界问题
        // 任务清单:只要有key值,表示任务队列中还有,key值没有就表示已经都处理掉了,就不用每次去判断Q是否为空
        let keys = Object.keys(Q)
        if (!keys[0]) {
            break
        }
        // 处理任务:找到哪个数量最多,数量最多的就优先处理
        // 声明一个队列用来存储1+n任务单元
        let tmp = []
        for (let i = 0; i <= n; i++) {
            // 记录最大值
            let max=0
            // 最大值名称
            let key
            // 最大值索引位置
            let pos
            // 任务清单中找当前任务还剩下多少
            keys.forEach((item, idx)=>{
                // 找最大值
                if(Q[item]>max){
                    max = Q[item]
                    key = item
                    pos = idx
                    // console.log(item)
                    // console.log(idx)
                }
            })
            // 判断是否找到最大值key,如果是就push到临时队列
            if(key){
                // console.log('11--->'+key)
                tmp.push(key)
                // 在任务清单中清除
                keys.splice(pos, 1) //  ["A", "B"] ===> ["B"]
                Q[key]--
                if(Q[key]<1){
                    delete Q[key]
                }
                // console.log('222--->'+keys)
            }else{
                break
            }
        }
        // 如果不够,后面补什么
        q +=tmp.join('').padEnd(n+1,'-')
    }
    // 边界处理,最后不要出现冷却时间
    q = q.replace(/-+$/g, '')
    return q.length
}
const tasks = ["A", "A", "A", "B", "B", "B"], n = 2
console.log(leastInterval(tasks, n)) 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值