说明
【数据结构与算法之美】专栏学习笔记
什么是队列?
队列是一种操作受限的线性表数据结构,特点是先进先出,最基本的操作有:入队 enqueue()
,放一个数据到队列尾部;出队 dequeue()
,从队列头部取一个元素。
顺序队列和链式队列
- 用数组实现的队列叫作顺序队列
- 用链表实现的队列叫作链式队列
基于数组的队列实现方法
队列需要两个指针:
- 一个是 head 指针,指向队头;
- 一个是 tail 指针,指向队尾。
用 Java 语言实现:
// 用数组实现的队列 public class ArrayQueue { // 数组:items,数组大小:n private String[] items; private int n = 0; // head表示队头下标,tail表示队尾下标 private int head = 0; private int tail = 0; // 申请一个大小为capacity的数组 public ArrayQueue(int capacity) { items = new String[capacity]; n = capacity; } // 入队操作,将item放入队尾 public boolean enqueue(String item) { // tail == n表示队列末尾没有空间了 if (tail == n) { // tail ==n && head==0,表示整个队列都占满了 if (head == 0) return false; // 数据搬移 for (int i = head; i < tail; ++i) { items[i-head] = items[i]; } // 搬移完之后重新更新head和tail tail -= head; head = 0; } items[tail] = item; ++tail; return true; } // 出队 public String dequeue() { // 如果head == tail 表示队列为空 if (head == tail) return null; // 为了让其他语言的同学看的更加明确,把--操作放到单独一行来写了 String ret = items[head]; ++head; return ret; } }
当 tail 移动到最右边,即使数组中还有空闲空间,也无法继续往队列中添加数据,针对这种情况,只需要在入队时,再集中触发一次数据的搬移操作。示意图如下:
基于链表的队列实现方法
- 入队时:
tail->next= new_node
;tail = tail->next
- 出队时:
head = head->next
入队出队示意图:
/** * 基于链表实现的队列。 * * Author: nameczz */ class Node { constructor(element) { this.element = element this.next = null } } export class QueueBasedOnLinkedList { constructor() { this.head = null this.tail = null } enqueue(value) { if (this.head === null) { this.head = new Node(value) this.tail = this.head } else { this.tail.next = new Node(value) this.tail = this.tail.next } } dequeue() { if (this.head !== null) { const value = this.head.element this.head = this.head.next return value } else { return -1 } } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>07.基于链表实现的队列</title> </head> <body> <script type="module"> import { QueueBasedOnLinkedList } from './js/07/QueueBasedOnLinkedList.js'; const newQueue = new QueueBasedOnLinkedList(); // 元素入队 newQueue.enqueue(1); newQueue.enqueue(2); newQueue.enqueue(3); console.log('入队--->', newQueue); // 获取元素 let res = 0; console.log("-------获取dequeue元素------"); while (res !== -1) { res = newQueue.dequeue(); console.log(res); } </script> </body> </html>
循环队列
循环队列就是普通队列首尾相连形成了一个环。
比如:下面队列的大小为 8,当前 head=4,tail=7。
当有一个新的元素 a 入队时,放入下标为 7 的位置。将 tail 更新为 0 ,而不是 8。
如何判断循环队列队空和队满呢?
- 队空:队列为空的判断条件是
head == tail
- 队满:当队满时,
(tail + 1)%n = head
基于数组的循环队列实现方式
public class CircularQueue { // 数组:items,数组大小:n private String[] items; private int n = 0; // head表示队头下标,tail表示队尾下标 private int head = 0; private int tail = 0; // 申请一个大小为capacity的数组 public CircularQueue(int capacity) { items = new String[capacity]; n = capacity; } // 入队 public boolean enqueue(String item) { // 队列满了 if ((tail + 1) % n == head) return false; items[tail] = item; // 取余运算保证,数组队列的循环插入效果 tail = (tail + 1) % n; return true; } // 出队 public String dequeue() { // 如果head == tail 表示队列为空 if (head == tail) return null; String ret = items[head]; // 因为要保持一个环状,必须通过取余运算才能得到保障! head = (head + 1) % n; return ret; } }
基于链表实现的循环队列
/** * 基于链表实现的循环队列。 * * Author: nameczz */ class Node { constructor(element) { this.element = element this.next = null } } export class CircularQueue { constructor() { this.head = null this.tail = null } // 入队 enqueue(value) { if (this.head === null) { this.head = new Node(value) this.head.next = this.head this.tail = this.head } else { const flag = this.head === this.tail this.tail.next = new Node(value) this.tail.next.next = this.head this.tail = this.tail.next if (flag) { this.head.next = this.tail } } } // 出队 dequeue() { if(this.head == null) return -1 if (this.head === this.tail) { const value = this.head.element this.head = null return value } else { const value = this.head.element this.head = this.head.next this.tail.next = this.head return value } } display() { let res = 0 console.log('-------获取dequeue元素------') while (res !== -1) { res = this.dequeue() console.log(res) } } }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>07.基于链表实现的循环队列</title> </head> <body> <script type="module"> import { CircularQueue } from "./js/07/CircularQueue.js"; const newCircularQueue = new CircularQueue(); // 插入元素 newCircularQueue.enqueue(1); newCircularQueue.enqueue(2); newCircularQueue.enqueue(3); console.log(newCircularQueue); // 获取元素 newCircularQueue.display(); newCircularQueue.enqueue(1); newCircularQueue.display(); </script> </body> </html>
阻塞队列
阻塞队列就是在队列基础上增加了阻塞操作。
- 在队列为空的时候,从队头取数据会被阻塞。直到队列中有了数据才能返回
- 如果队列已经满了,那么插入数据的操作就会被阻塞。直到队列中有空闲位置后再插入数据,然后再返回
可以使用阻塞队列实现一个生产者 - 消费者模型
,有效地协调生产和消费的速度。
如何实现一个线程安全的队列呢?
线程安全的队列叫作并发队列。在多线程情况下,会有多个线程同时操作队列,这个时候就会存在线程安全问题,最简单的解决方式就是直接在 enqueue()、dequeue() 方法上加锁,但是锁粒度大并发度会比较低,同一时刻仅允许一个存或者取操作。
如何实现一个高效的并发队列:
基于数组的循环队列:避免数据搬移
CAS原子操作:避免真正的去OS底层申请锁资源
队列的应用
基于链表的实现方式,可以实现一个支持无限排队的无界队列(unbounded queue),但是可能会导致过多的请求排队等待,请求处理的响应时间过长。所以,针对响应时间比较敏感的系统,基于链表实现的无限排队的线程池是不合适的。而基于数组实现的有界队列(bounded queue),队列的大小有限,所以线程池中排队的请求超过队列大小时,接下来的请求就会被拒绝,这种方式对响应时间敏感的系统来说,就相对更加合理。
队列可以应用在任何有限资源池中,用于排队请求,比如数据库连接池等。对于大部分资源有限的场景,当没有空闲资源时,基本上都可以通过队列这种数据结构来实现请求排队。