上一章咱们讲到了栈,此次咱们来说队列。其实队列和栈有不少类似的地方,好比它们都是线性表,操做都是受限。区别也是比较明显,队列主要是先进先出,和排队同样,可是栈是先进后出。队列的先进先出这两个操做对应的是入队(enqueue)和出队(dequeue),入队是从队尾插入一个元素,出队是从队头使一个元素出队。算法
队列的概念很好理解,基本操做也很容易掌握。做为一种很是基础的数据结构,队列的应用也很是普遍,特别是一些具备某些额外特性的队列,好比循环队列、阻塞队列、并发队列。它们在不少偏底层系统、框架、中间件的开发中,起着关键性的做用。好比高性能队列 Disruptor、Linux 环形缓存,都用到了循环并发队列;Java concurrent 并发包利用 ArrayBlockingQueue 来实现公平锁等。设计模式
队列和栈同样,能够经过数组和栈来实现。若是是数组实现的就叫顺序队列,链表实现的叫链式队列。下面经过JavaScript代码来分别实现链式队列和顺序队列:数组
class Node {
constructor(element) {
this.element = element
this.next = null
}
}
class linkedList {
constructor(length) {
this.head = null
this.length = length // 声明队列长度
this.currLength = 0 // 队列实际长度
}
enqueue (item) {
let curr = this.head
let newNode = new Node(item)
if (curr == null) {
this.head = newNode
curr = this.head
}
else {
++this.currLength
if (this.currLength < this.length) {
while (curr) {
if (curr.next) {
curr = curr.next
}
else {
curr.next = newNode
break
}
}
}
else {
return false
}
}
}
dequeue () {
// 队列为空的状况下直接返回null
let curr = this.head
if (this.head != null) {
let next = this.head.next
this.head = next
--this.currLength
}
return curr
}
}复制代码
这段链式队列的代码很好理解。在入队的时候将结点不断的插入到链表的最后一个结点的next结点里,出队的时候直接将头结点拿出来,并赋值给下一个结点。而顺序链表的操做上则要有两个指针,一个头指针一个尾指针,每次出队操做都会将数组往前移一位,代码以下:缓存
class ArrayLisk {
constructor (length) {
this.queue = new Array(length)
this.head = 0
this.tail = 0
this.currLength = 0
}
enqueue (item) {
++this.currLength
if (this.currLength > this.queue.length) {
return false
}
++this.tail
this.queue[this.currLength - 1] = item
}
dequeue () {
if (this.head === this.tail) {
return null
}
else {
let current = this.queue[0]
for (let a = 0; a < this.queue.length - 1; a++) {
this.queue[a] = this.queue[a + 1]
if (a === this.queue.length - 2) {
this.queue[a + 1] = undefined
}
}
--this.currLength
this.tail = this.currLength
return current
}
}
}复制代码
上面讲到一些特殊队列有某些额外特性,如循环队列、阻塞队列、并发队列等等,下面咱们主要来看一下这三个特殊队列的具体用途。安全
首先讲的是循环队列。在上面的代码中,若是是用数组来实现一个队列的话,当进行出队操做的时候,数组里的元素都会往前移一位,元素的移动会形成须要更多的时间,这个时候咱们就能够用一个循环队列来解决这个问题。循环队列,顾名思义,它长得像一个环。本来数组是有头有尾的,是一条直线。如今咱们把首尾相连,扳成了一个环。以下图:bash
咱们能够看到,图中这个队列的大小为 10,当前 front=1,rear=·10。当有一个新的元素 a 入队时,咱们放入下标为 10 的位置。但这个时候,咱们并不把 tail 更新为 11,而是将其在环中后移一位,到下标为 1 的位置。当再有一个元素 b 入队时,咱们将 b 放入下标为 1 的位置,而后 tail 加 1 更新为 2。经过这样的方法,咱们成功避免了数据搬移操做。看起来不难理解,可是循环队列的代码实现难度要比前面讲的非循环队列难多了。要想写出没有 bug 的循环队列的实现代码,我我的以为,最关键的是,肯定好队空和队满的断定条件。数据结构
在用数组实现的非循环队列中,队满的判断条件是 tail == n,队空的判断条件是 head == tail。那针对循环队列,如何判断队空和队满呢?队列为空的判断条件仍然是 head == tail且当前head下标对应的值为空。但队列满的判断条件就稍微有点复杂了,当队满时,(tail+1)%n=head+1且当前tail下标对应的值不为空。下面用代码来实现一下:多线程
class ArrayLisk {
constructor (length) {
this.queue = new Array(length)
this.head = 0
this.tail = 0
}
enqueue (item) {
if ((this.tail + 1) % this.queue.length === this.head + 1 && this.queue[this.tail] !== undefined) {
return false
}
this.queue[this.tail] = item
if (this.tail === this.queue.length - 1) {
this.tail = 0
}
else {
++this.tail
}
}
dequeue () {
if (this.head === this.tail && this.queue[this.head] === undefined) {
return false
}
this.queue[this.head] = undefined
if (this.head === this.queue.length - 1) {
this.head = 0
}
else {
++this.head
}
}
}复制代码
前面讲的内容理论比较多,看起来很难跟实际的项目开发扯上关系。确实,队列这种数据结构很基础,平时的业务开发不大可能从零实现一个队列,甚至都不会直接用到。而一些具备特殊特性的队列应用却比较普遍,好比阻塞队列和并发队列。阻塞队列其实就是在队列基础上增长了阻塞操做。简单来讲,就是在队列为空的时候,从队头取数据会被阻塞。由于此时尚未数据可取,直到队列中有了数据才能返回;若是队列已经满了,那么插入数据的操做就会被阻塞,直到队列中有空闲位置后再插入数据,而后再返回。并发
/**
* @description 这里用JavaScript单线程同步状态来模拟了一个阻塞队列,
* 当入队请求远大于出队请求而且队列已满的状况下记录入队数据,
* 下一次出队操做的时候将从溢出的入队数据中读取。
* 当出队请求远大于入队请求而且队列为空的状况下记录出队数量,
* 下一次入队操做的时候将把这数据返回并减小出队数量。
* */
class BlockingQueue extends Array{
constructor(args, size) {
super(...args)
this.sizeLength = args.length // 数组初始长度
this.maxSize = size // 数组限定最长长度
this.pushArr = [] // 溢出的入队数据
this.popSum = 0 // 溢出的出队请求数量
}
enqueue(val) {
if (this.maxSize === this.sizeLength) {
this.pushArr.push(val)
return false
}
if (this.popSum > 0) {
--this.popSum
return val
}
++this.sizeLength
return super.push(val)
}
dequeue() {
if (this.pushArr.length > 0) {
super.push(this.pushArr.shift())
return super.shift()
}
if (this.sizeLength === 0) {
++this.popSum
return false
}
--this.sizeLength
return super.shift()
}
}复制代码
这里的代码按照上面的状况下作了一点修改,当生产者生产的数据远远多于消费者消费的数据的时候,咱们这个时候就要阻塞生产者让消费者可以及时的处理完数据。同理,反过来讲若是消费者消费的数据远远多于生产者生产的数据的时候,就阻塞消费者让生产者可以及时的生产数据。这个模式就是设计模式里生产者消费者模式。框架
上面的例子里能够改造一下,好比Java里的DelayQueue,就是加了个延时器当过了一段时间才开始生产数据或者消费数据。
前面咱们讲了阻塞队列,在多线程状况下,会有多个线程同时操做队列,这个时候就会存在线程安全问题,那如何实现一个线程安全的队列呢?
线程安全的队列咱们叫做并发队列。。最简单直接的实现方式是直接在 enqueue()、dequeue() 方法上加锁,可是锁粒度大并发度会比较低,同一时刻仅容许一个存或者取操做。实际上,基于数组的循环队列,利用 CAS 原子操做,能够实现很是高效的并发队列。这也是循环队列比链式队列应用更加普遍的缘由。因为JavaScript是单线程的,因此模拟Java的多线程的时候通常都是采用异步来实现,这里因为博主水平不够,目前暂时不会用异步的方式实现一个线程安全的并发队列,因此就不贴代码了,感兴趣的能够看Java是如何实现的。
上一篇文章:数据结构与算法的重温之旅(六)——栈
下一篇文章:数据结构与算法的重温之旅(八)——递归