数据结构与算法——队列

74 阅读4分钟

一、🍊 队列的理论基础

1、概念

队列是先进先出,后进后出,就像排队结账,先排队的先结账,后面的人排到队列尾部。

2、操作特性

队列也是一种“操作受限”的线性表。最基本的操作也是两个:入队 enqueue(),放一个数据到队列尾部;出队 dequeue(),从队列头部取一个元素。

3、使用场景

队列的应用也非常广泛,特别是一些具有某些额外特性的队列,比如循环队列、阻塞队列、并发队列。它们在很多偏底层系统、框架、中间件的开发中,起着关键性的作用。

二、🍊 如何实现一个队列

  • 跟栈一样,队列可以用数组来实现,也可以用链表来实现。
  • 用数组实现的队列叫作顺序队列,用链表实现的队列叫作链式队列。 js代码如下:
class ArrayQueue {  
  // 数组:items,数组大小:capacity  
  constructor(capacity) {  
    this.items = new Array(capacity);  
    this.capacity = capacity;  
    // head表示队头下标,tail表示队尾下标  
    this.head = 0;  
    this.tail = 0;  
  }  
  
  // 入队  
  enqueue(item) {  
    // 如果tail == capacity 表示队列已经满了  
    if (this.tail === this.capacity) {  
      console.error('Queue is full');  
      return false;  
    }  
    this.items[this.tail] = item;  
    this.tail = (this.tail + 1) % this.capacity; // 循环队列的处理  
    return true;  
  }  
  
  // 出队  
  dequeue() {  
    // 如果head == tail 表示队列为空  
    if (this.head === this.tail) {  
      return null;  
    }  
    const ret = this.items[this.head];  
    this.head = (this.head + 1) % this.capacity; // 循环队列的处理  
    return ret;  
  }  
}  
  
// 使用示例  
const queue = new ArrayQueue(3); // 创建一个容量为3的队列  
  
console.log(queue.enqueue('item1')); // 输出: true  
console.log(queue.enqueue('item2')); // 输出: true  
console.log(queue.enqueue('item3')); // 输出: true  
console.log(queue.enqueue('item4')); // 输出: false,队列已满  
  
console.log(queue.dequeue()); // 输出: 'item1'  
console.log(queue.dequeue()); // 输出: 'item2'  
console.log(queue.dequeue()); // 输出: 'item3'  
console.log(queue.dequeue()); // 输出: null,队列已空
  • 使用 class 关键字来定义 ArrayQueue 类,并使用 constructor 方法来初始化队列;
  • enqueue 方法用于向队列中添加元素,如果队列已满,则返回 false 并输出错误信息。
  • dequeue 方法用于从队列中移除元素,如果队列为空,则返回 null
  • 第19行和39行代码使用了取模运算符 % 来处理循环队列的情况,确保当 head 或 tail 到达数组末尾时能够回到数组的起始位置。

注意:上面的实现方式,随着不停地进行入队、出队操作,head 和 tail 都会持续往后移动。当 tail 移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据了。如果想要继续添加,就必须移动数组,当队列的 tail 指针移动到数组的最右边后,如果有新的数据入队,我们可以将 head 到 tail 之间的数据,整体搬移到数组中 0 到 tail-head 的位置。上面入队函数的代码(11-21行代码修改如下):

// 入队操作,将item放入队尾  
  enqueue(item) {  
    // 如果队列已满,进行特殊处理  
    if (this.size === this.capacity) {  
      // 如果队列满且head为0,则无法继续入队  
      if (this.head === 0) {  
        return false;  
      }  
        
      // 数据搬移  
      for (let i = this.head; i < this.tail; i++) {  
        // 这里可以理解成数据搬移时head就是偏移量
        this.items[i - this.head] = this.items[i];  
      }  
        
      // 更新head和tail  
      this.tail -= this.head;  
      this.head = 0;  
    }  
      
    // 在tail位置添加新元素  
    this.items[this.tail] = item;  
    // 更新tail和size  
    this.tail++;  
    this.size++;  
      
    return true;  
  }  

三、🍊 循环队列

循环队列的存在,主要是为了解决上面代码中在 tail==n 时,会有数据搬移操作,这样入队操作性能就会受到影响。如下图:

image.png 写循环队列需要注意的点主要是确定队空和队满的条件。

  • 队列为空的判断条件仍然是 head == tail
  • 队满时,(tail+1)%n=head

注意:当队列满时,tail 指向的位置实际上是没有存储数据的。所以,循环队列会浪费一个数组的存储空间。

四、🍊 阻塞队列和并发队列

  • 阻塞队列其实就是在队列基础上增加了阻塞操作。简单来说,就是在队列为空的时候,从队头取数据会被阻塞。因为此时还没有数据可取,直到队列中有了数据才能返回;如果队列已经满了,那么插入数据的操作就会被阻塞,直到队列中有空闲位置后再插入数据,然后再返回。
  • 阻塞队列,在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,那如何实现一个线程安全的队列呢?线程安全的队列我们叫作并发队列