算法随笔 — 线性表基础 — 队列

本文介绍了队列的基本概念、操作,包括一般队列和循环队列,并探讨了它们在缓冲区、CPU多线程和线程池中的应用。详细解析了LeetCode相关题目,如设计循环队列、双向循环队列、前中后队列等,通过实例展示了队列在算法解题中的重要作用。
摘要由CSDN通过智能技术生成

队列是什么

一般链表
一般队列就如上图所示,这是一种数据结构,通常情况下只能由一边进另一边出

一般队列的组成元素通常有长度头指针尾指针数据类型。这里需要特别注意的是头指针指向的是队列中的第一位,而尾指针指向的是最后一个元素的下一位,也就是数学上的“左闭右开”。这种“左闭右开”的形式的好处是 队列长度 = 尾指针 - 头指针

队列可以通过数组和链表两种方式实现,它们在逻辑上都是连续存储的

队列最重要的两个操作是 入队出队。出队操作只要使头指针后移即可;入队操作是首先将入队元素放入尾指针指向的区域,然后尾指针后移

在一些高级语言中,数组是一段连续的内存地址。按照上述的入队出队操作,可能会造成“假溢出”。所谓“假溢出”就是客观上队列还未满,但是操作的结果却是队列已满

假溢出
假溢出 的解决办法主要有两种,一种是动态扩容、另一种是循环队列

动态扩容可以通过块状链表和哈希链表实现,在这里先不展开

循环队列是一般队列的一种拓展,顾名思义,尾指针和头指针走到尾之后会从头开始
循环队列
为了方便操作,在循环队列的实现中通常用属性cnt来进行队列元素的计数


队列操作

一般队列

  • 入队
  1. 判满,如果满了中止操作
  2. 元素填充至尾指针所指位置
  3. 尾指针后移
  • 出队
  1. 判空,如果队列为空则中止操作
  2. 头指针后移
  • 判满
    tail == arr.length
  • 判空
    头尾指针重合时队列为空
  • 清空队列
    tail = head = 0
  • 队列长度
    tail - head
  • 返回队首
    arr[head]
  • 返回队尾
    arr[tail - 1]

循环队列

循环队列大部分操作和一般队列同理

通常会定义一个属性cnt来记录队列长度

头指针和尾指针后移的计算公式类似,如 h e a d = ( h e a d + 1 ) % a r r . l e n g t h head = (head + 1)\%arr.length head=(head+1)%arr.length

获取最后元素 a r r [ ( t a i l − 1 + a r r . l e n g t h ) % a r r . l e n g t h ] arr[(tail - 1 + arr.length)\%arr.length] arr[(tail1+arr.length)%arr.length]

注意,在出队和入队的时候对 cnt 进行更新

相应的,判空操作就是 c n t = = = 0 cnt === 0 cnt===0,判满是 c n t = = = a r r . l e n g t h cnt === arr.length cnt===arr.length


队列应用

  • 缓冲区
  • CPU多线程
    真双核
    虚拟四核
  • 线程池的任务队列
    线程的申请和销毁都会消耗一定的性能,如果频繁操作则会造成不必要的性能浪费
    因此可以使用“线程池”进行优化
    所谓线程池就是固定申请几个线程(不销毁),有任务就往里推
    线程池

习题

leetCode 622 设计循环队列

具体原理参考上文,这里直接给代码

class MyCircularQueue {
  queue: number[]
  frontPointer: number
  rearPointer: number
  cnt: number
  constructor(k: number) {
    this.queue = new Array(k)
    this.frontPointer = 0
    this.rearPointer = 0
    this.cnt = 0
  }
  
  enQueue(value: number): boolean {
    if (this.isFull()) return false
    this.queue[this.rearPointer] = value
    this.rearPointer = (this.rearPointer + 1) % this.queue.length
    this.cnt++
    return true
  }
  
  deQueue(): boolean {
    if (this.isEmpty()) return false
    this.frontPointer = (this.frontPointer + 1) % this.queue.length
    this.cnt--
    return true
  }
  
  Front(): number {
    if (this.isEmpty()) return -1
    return this.queue[this.frontPointer]
  }
  
  Rear(): number {
    if (this.isEmpty()) return -1
    return this.queue[(this.rearPointer - 1 + this.queue.length) % this.queue.length]
  }
  
  isEmpty(): boolean {
    return this.cnt === 0
  }
  
  isFull(): boolean {
    return this.cnt === this.queue.length
  }
}

循环队列结果


leetCode 641 设计双向循环队列

双向循环队列(double end queue)

双向循环队列原理
该队列同样使用属性 cnt 来计算队列元素数量会比较方便
insertLast()

  1. 判满,若队列已满中止操作
  2. tail 指向区域赋值
  3. tail = (tail + 1) % arr.length
  4. cnt++

insertFront()

  1. 判满,若队列已满中止操作
  2. head 向前一步 h e a d = ( h e a d − 1 + a r r . l e n g t h ) % a r r . l e n g t h head=(head-1+arr.length)\%arr.length head=(head1+arr.length)%arr.length
  3. head 指向区域赋值
  4. cnt++

删除操作类似

class MyCircularDeque {
  queue: number[]
  frontPointer: number
  rearPointer: number
  cnt: number
  
  constructor(k: number) {
    this.queue = new Array(k)
    this.frontPointer = 0
    this.rearPointer = 0
    this.cnt = 0
  }
  
  insertFront(value: number): boolean {
    if (this.isFull()) return false
    this.frontPointer = (this.frontPointer - 1 + this.queue.length) % this.queue.length
    this.queue[this.frontPointer] = value
    this.cnt++
    return true
  }
  
  insertLast(value: number): boolean {
    if (this.isFull()) return false
    this.queue[this.rearPointer] = value
    this.rearPointer = (this.rearPointer + 1) % this.queue.length
    this.cnt++
    return true
  }
  
  deleteFront(): boolean {
    if (this.isEmpty()) return false
    this.frontPointer = (this.frontPointer + 1) % this.queue.length
    this.cnt--
    return true
  }
  
  deleteLast(): boolean {
    if (this.isEmpty()) return false
    this.rearPointer = (this.rearPointer - 1 + this.queue.length) % this.queue.length
    this.cnt--
    return true
  }
  
  getFront(): number {
    if (this.isEmpty()) return -1
    return this.queue[this.frontPointer]
  }
  
  getRear(): number {
    if (this.isEmpty()) return -1
    return this.queue[(this.rearPointer - 1 + this.queue.length) % this.queue.length]
  }
  
  isEmpty(): boolean {
    return this.cnt === 0
  }
  
  isFull(): boolean {
    return this.cnt === this.queue.length
  }
}

双向循环队列结果


leetCode 1670 设计前中后队列

前中后队列原理
这题的前中后队列通过两个双端队列实现

前后的操作和上题类似,不过每次操作后都要平衡一下 q1 和 q2 的长度

这里通过双向链表实现双端队列可以方便扩容,而JS 的数组天然具有链表的特性,因此在具体的实现中会通过数组实现

中间的操作需要先判断 q1 和 q2 数量。从中间插入元素永远向 q1 中插,插入前如果 q1.length > q2.length,则 q2.push(q1.pop())

校正操作

if (q1.length < q2.length) q2.unshift(q1.pop())
if (q1.length === q2.length + 2) q1.push(q2.shift())
class FrontMiddleBackQueue {
  leftQueue: number[]
  rightQueue: number[]
  
  constructor() {
    this.leftQueue = []
    this.rightQueue = []
  }
  
  pushFront(val: number): void {
    this.update()
    this.leftQueue.unshift(val)
  }
  
  pushMiddle(val: number): void {
    if (this.leftQueue.length > this.rightQueue.length) this.rightQueue.unshift(this.leftQueue.pop())
    this.leftQueue.push(val)
    this.update()
  }
  
  pushBack(val: number): void {
    this.update()
    this.rightQueue.push(val)
  }
  
  popFront(): number {
    if (this.isEmpty()) return -1
    this.update()
    return this.leftQueue.shift()
  }
  
  popMiddle(): number {
    if (this.isEmpty()) return -1
    this.update()
    return this.leftQueue.pop()
  }
  
  popBack(): number {
    if (this.isEmpty()) return -1
    this.update()
    if (!this.rightQueue.length && this.leftQueue.length) return this.leftQueue.pop()
    return this.rightQueue.pop()
  }
  
  update(): void {
    if (this.leftQueue.length < this.rightQueue.length) {
      this.leftQueue.push(this.rightQueue.shift())
    } else if (this.leftQueue.length === this.rightQueue.length + 2) {
      this.rightQueue.unshift(this.leftQueue.pop())
    }
  }
  
  isEmpty(): boolean {
    return this.leftQueue.length === 0 && this.rightQueue.length === 0
  }
}

前中后队列结果
如果弄懂了双端循环队列的设计,这道题不难,只是有点麻烦而已


leetCode 933 最近请求次数

这道题非常简单,首先设计一个队列,每次 push 操作之后进行判断

while (t - front() > 3000) 出队
class RecentCounter {
  queue: number[]
  constructor() {
    this.queue = []
  }
  
  ping(t: number): number {
    this.queue.push(t)
    while (t - 3000 > this.queue[0]) this.queue.shift()
    return this.queue.length
  }
}

最近请求次数结果


leetCode 17.09 第k个数

这里找的是仅包含素因子的数 1、3、5、7、9、15、21、…
第k个数原理
该题的解题思路如上图所示,定义一个原始队列和三个指针 p3 p5 p7,原始队列初始有一个1

每次操作让三个指针代表的数字和当前指针指向队列的位置中的元素相乘,将最小值推入队列中,然后将相应指针后移,如果超过一个指针的运算结果均是最小值,则同时后移

function getKthMagicNumber(k: number): number {
  const arr = [1]
  let p3 = 0, p5 = 0, p7 = 0, min = 0
  while (arr.length < k) {
    min = Math.min(arr[p3] * 3, arr[p5] * 5, arr[p7] * 7)
    arr.push(min)
    if (arr[p3] * 3 === min) p3++
    if (arr[p5] * 5 === min) p5++
    if (arr[p7] * 7 === min) p7++
  }
  return arr[k - 1]
}

第k个数结果


leetCode 859 亲密字符串

亲密字符串原理
若两个字符串中的一个可以通过交换2字符后等同于另一个字符串的话就认为这两个字符串为亲密字符串

程序逻辑如下:

首先判断两个字符串长度,若长度不相等即非亲密字符串

再判断两个字符串是否相等,如果相等则判断是否有重复字符,若有重复字符则为亲密字符串,相反则不是

最后再遍历其中一个字符串,如果字符不同的数量不是2个则不是亲密字符串,若这两个字符交换位置后和另一个的不同也不是亲密字符串

只有两个字符串当且仅当有两个字符不同,且其中一个的字符串的那两个不同字符交换后形成的字符串和另一个的相同才算这两个字符串为亲密字符串

function buddyStrings(a: string, b: string): boolean {
  if (a.length !== b.length) return false
  if (a === b) {
    // 如果有两个重复字符即为亲密字符串
    const counter: {[key: string]: number} = {}
    for (let c of a) counter[c] = counter[c] ? ++counter[c] : 1
    return Math.max(...Object.values(counter)) > 1
  }
  let a1 = '', b1 = ''
  for (let i = 0; i < a.length; i++) {
    if (a[i] !== b[i]) {
      a1 = a[i] + a1
      b1 += b[i]
      if (a1.length > 2) return false
    }
  }
  return a1.length === 2 && a1 === b1
}

亲密字符串结果
注意上述提交截图中的70行和71行,这里用了一个巧妙的编码技巧进行字符的交换对比


leetCode 860 柠檬水找零

一杯柠檬水卖5块钱,顾客给钱可能会给5、10、20

这题的策略很简单,顾客给5块的时候不用找钱

顾客给10块的时候找一张5块

顾客给20块的时候可以给1张10块和1张5块,或者给3张5块

为了能尽可能地找钱,如果能用10块就不用5块

当然,从以上策略也可以看出没有找20块的情况,所以我们只需要记录5块和10块的数量即可

function lemonadeChange(bills: number[]): boolean {
  let five = 0, ten = 0
  for (let bill of bills) {
    //* 找钱
    if (bill === 5) {
      five++
      continue
    }
    if (bill === 10) {
      ten++
      five--
    }
    if (bill === 20) {
      if (ten) {
        five--
        ten--
      } else {
        five -= 3
      }
    }
    //* 判断是否没零钱
    if (five < 0) return false
  }
  return true
}

柠檬水找零结果


leetCode 969 煎饼排序

翻转数组前k位直到数组有序递增,最后输出k的组合

首先我们要对该数组进行循环操作,循环的中止条件是数组的长度为1

在循环中,首先要找到最大值

然后再把最大值弄到数组头

reverse(arr, k)

接着把该最大值弄到数组尾部

reverse(arr, arr.length - 1)

最后抛出数组元素

在上述的调换操作中把k推入一个队列中,arr.length - 1 也属于k值

当然,只有k不是指向数组第一个位置的时候才推入队列,如果最大值就在数组头部就没必要翻转数组的前一个了

//* 翻转数组前k项
function reverse(arr: number[], k: number) {
  for (let i = 0, j = k; i < j; i++, j--) {
    [arr[i], arr[j]] = [arr[j], arr[i]]
  }
}

function pancakeSort(arr: number[]): number[] {
  const ans: number[] = [], arrClone = [...arr]
  let max, index
  while (arrClone.length > 1) {
    //* 1.找出最大值
    max = Math.max(...arrClone)
    index = arrClone.findIndex(v => v === max)
    //* 2.先把它弄到数组头,再弄到数组尾,再抛出
    if (index > 0) ans.push(index + 1)
    reverse(arrClone, index)
    reverse(arrClone, arrClone.length - 1)
    ans.push(arrClone.length)
    arrClone.pop()
  }
  return ans
}

煎饼排序结果


leetCode 621 任务调度

同类任务有步长为n的cd,计算最短时间

在cd中可待命或安排其他任务

任务调度1
如上图所示,如果所有冷却时间都能填满,则最短时长就是任务总数量

任务调度2
若填不满冷却时间,令最多数量的任务出现n次,有m种任务出现n次,冷却时间为k,那么矩形的面积为
S = ( n − 1 ) ⋅ ( k + 1 ) + m S=(n-1)·(k+1)+m S=(n1)(k+1)+m
这道题的结果就是 m a x ( S , 任 务 数 量 ) max(S, 任务数量) max(S,)

function leastInterval(tasks: string[], n: number): number {
  const counter: {[key: string]: number} = {}
  tasks.forEach(t => counter[t] = counter[t] ? ++counter[t] : 1)
  const max = Math.max(...Object.values(counter))
  let maxTasks = 0
  Object.values(counter).forEach(task => (task === max) && maxTasks++)
  return Math.max(tasks.length, (max - 1) * (n + 1) + maxTasks)
}

任务调度结果


结语

有任何问题欢迎评论区或私信讨论~

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值