前言
- 文章内容是本人之前学习数据结构时所做的零散笔记的汇总及补充,整理过程难免有瑕疵,如果小伙伴发现错误请及时不吝指出,在此谢过!
第一篇---->基本概念和认识
第二篇---->算法
第三篇---->算法概述补充
第四篇---->数据结构概述补充
第五篇---->Java线性表详细实现
第六篇---->Java栈详细实现
第七篇---->Java队列详细实现
队列(Queue)
4.1 情景
- 使用电脑时,机器有时会处于疑似死机状态,鼠标点什么都没有反应,当失去耐心打算关机重启的时候,突然清醒一般,把刚刚点击的所有操作都按顺序执行了一遍。这是因为操作系统的多个程序需要通过一个通道输出,而按先后次序排队等待造成的。
- 还有就是一些系统中的客服人员,客服人员有限,当咨询人数大于客服人员总数后,就需要排队,当哪个客服人员有空闲时,会把最早排队的人进行安排。
4.2 什么是队列结构
-
队列结构从逻辑上来看是线性结构,从存储结构上来看可以划分为两类
- 顺序队列:使用一组地址连续的内存单元依次保存队列中的数据。可以定义一个指定大小的结构数组作为队列。
- 链式队列:使用链表形式保存队列中各元素的值。
-
队列结构,是一种先进先出(FIFO)的线性表,允许插入的一端
-
队列的常见方法
- initQueue
- 初始化操作,建立一个空队列
- destroyQueue
- 若队列存在,则销毁
- clearQueue
- 清空队列
- queueEmpty
- 判断队列是否为空
- getHead
- 获取队头元素
- enQueue
- 若队列存在,插入元素入队
- deQueue
- 删除队头元素,并返回值
- queueLength
- 获取队列实际元素个数
- initQueue
-
顺序队列
-
Java
package com.fc.queue; /** * @ClassName SequentialQueue 顺序队列 * @Description 保持队头始终在索引为0的位置 * @Author Fclever * @Date 2021/7/2 15:59 **/ public class SequentialQueue<T> { /** * 队列默认长度10 */ private static final int MAXLEN = 10; /** * 存储数据数组 */ Object[] queueData; /** * 队尾索引 * 队列为空,指向-1,否则,始终指向队尾元素n */ int tail; public SequentialQueue() { } /** * 1. 初始化队列 */ public void initQueue() { // 初始化存储数组 this.queueData = new Object[MAXLEN]; // 设置队尾 this.tail = -1; } /** * 2. 销毁队列 */ // public void destroyQueue() { // // } /** * 3. 清空队列 */ public void clearQueue() { for (int i = 0; i<=this.tail;i++){ this.queueData[i] = null; } this.tail = -1; } /** * 4. 判断队列是否为空 * @return */ public boolean queueEmpty() { return this.tail == -1; } /** * 5. 获取队头元素 * @return */ public T getHead() { return (T) this.queueData[0]; } /** * 6. 入队 * @param data 入队元素 */ public void enQueue(T data) { // 判断队列是否满 if (this.tail + 1 == this.MAXLEN) { throw new OutOfMemoryError(); } // 插入元素 this.queueData[++this.tail] = data; } /** * 7. 出队 * @return 返回队头元素 */ public T deQueue() { if (this.queueEmpty()) { return null; } // 返回值 T data = (T) this.queueData[0]; // 其他往前移动 System.arraycopy(this.queueData, 1, this.queueData, 0, this.tail); this.queueData[this.tail--] = null; return data; } /** * 8. 获取队列实际元素个数 * @return */ public int queueLength() { return this.tail+1; } /** * 9. 遍历元素 */ public void getAll() { for (int i=0;i<=this.tail;i++){ System.out.printf("第%d个元素为:%s\n",i,this.queueData[i]); } } }
-
测试
package com.fc.queue; import org.junit.Test; import static org.junit.Assert.*; /** * @ClassName SequentialQueueTest * @Description * @Author Fclever * @Date 2021/7/5 13:20 **/ public class SequentialQueueTest { @Test public void testSequentialQueueTest() { SequentialQueue<String> sequentialQueue = new SequentialQueue<>(); sequentialQueue.initQueue(); sequentialQueue.enQueue("1"); sequentialQueue.enQueue("2"); sequentialQueue.enQueue("3"); sequentialQueue.enQueue("4"); sequentialQueue.enQueue("5"); System.out.println(sequentialQueue.queueLength()); sequentialQueue.deQueue(); sequentialQueue.deQueue(); sequentialQueue.getAll(); } }
-
4.3 循环队列
4.3.1 顺序存储的不足之处
- 假设一个队列有n个元素,数组下标为0的一端是队头,对于入队操作,就是在队尾追加元素,不需要移动元素,时间复杂度为O(1);但是在进行出队操作时,会把队头元素(下标为0)移除,后面元素向前移动,事件复杂度为O(n)
- 但是这种出队方式会使得出队性能降低,如果不限制队列的元素必须存储在数组前n个单元时,出队的性能就可以进行优化了,也就是可以不把队头一定设置在下标为0的位置。
- 引入两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置,空队列情况下,两个指针都指向下标为0的位置
4.3.2 假溢出
- 一个队列,使用长度为5的数组来保存数据,初始情况front=rear=0。入队三个元素,front=0,rear=3,再出队一个元素,front=1,rear=3,再入队两个元素,front=1,rear=5?数组下标最大为4,rear不可能是5,会导致数组越界问题。但是此时看一下数组的情况,还是有一个位置是空缺的,下标为0的位置,这种情况就可以被称为假溢出。
4.3.3 循环队列定义
- 解决假溢出可以是数组后面满了,再从头开始。这种队列头尾相接的顺序存储结构称为循环队列。
- 根据上面假溢出问题,使用循环队列,就可以将新的入队元素插入到下标为0的位置即可,此时rear=1
4.3.4 循环队列判空和判满的条件
- 当空队列时,front=rear,那么当队列满的时候,按上面的情况,条件也变成了front=rear了,那该如何区分空和满呢?
- 方式一:设置标志变量flag,当front=rear&&flag=0时队列为空,当front=rear&&flag=1时队列满。(flag值自定义)
- 方式二:当队列为空,front=rear;当队列满时,约定数组保留一个元素的位置不使用,数组中还存在一个空闲单元。下图这种情况就表示队列已经满了
- 方式二更加常用,但是需要进行分析
- front和rear相差一个位置的情况分好多情况:rear可能大于front、rear也可能小于front。
- 假设队列最大容量为MAXLEN,那么队列满的条件为:(rear+1)%MAXLEN==front。
- 借此,分析一下循环队列实际数据个数怎么计算
- 当rear > front时,队列长度为rear-front
- 当rear <= front时,队列长度为rear-front+MAXLE
- 也可以推导出计算队列实际数据个数的公式:(rear-front+MAXLEN)%MAXLEN
4.3.5 顺序循环队列
-
代码
package com.fc.queue; /** * @ClassName CircularQueue 循环队列 * @Description * @Author Fclever * @Date 2021/7/13 11:29 **/ public class CircularQueue<T> { private final int MAXLEN = 10; Object[] queueData; private int front; private int rear; public CircularQueue() { } /** * 1. 初始化队列 */ public void initQueue() { queueData = new Object[this.MAXLEN]; this.front = 0; this.rear = 0; } /** * 2. 将队列清空 */ public void clearQueue() { while (this.front != this.rear) { this.queueData[this.front] = null; // 队头指针后移 this.front = (this.front + 1) % this.MAXLEN; } // 重置指向 this.front = this.rear = 0; } /** * 3. 判断队列是否为空 * @return true为空,false不为空 */ public boolean queueEmpty() { return this.front == this.rear; } /** * 4. 获取队头元素 * @return 队头元素 */ public T getHead() { return (T) this.queueData[this.front]; } /** * 5. 入队 * @param data 待入队元素 */ public void enQueue(T data) { // 判断是否栈满 (rear+1)%maxlen == front if ((this.rear + 1)%this.MAXLEN == this.front) { throw new OutOfMemoryError(); } this.queueData[this.rear] = data; // rear后移 this.rear = (this.rear + 1) % this.MAXLEN; } /** * 6. 出队 * @return 返回出队元素 */ public T deQueue(){ if (this.queueEmpty()) { return null; } T data = (T) this.queueData[this.front]; // 原队头元素置空 this.queueData[this.front] = null; // 队头指针后移 this.front = (this.front + 1) % this.MAXLEN; // 返回出队元素 return data; } /** * 7. 获取队列实际存储数据个数 * @return */ public int queueLength() { return (this.rear - this.front + this.MAXLEN) % this.MAXLEN; } /** * 8. 遍历 */ public void getAll() { int index = this.front; while (index != this.rear) { System.out.println(this.queueData[index]); // 队头指针后移 index = (index+ 1) % this.MAXLEN; } } }
-
测试
package com.fc.queue; import org.junit.Test; import static org.junit.Assert.*; /** * @ClassName CircularQueueTest * @Description * @Author Fclever * @Date 2021/7/13 15:35 **/ public class CircularQueueTest { @Test public void test() { // 创建循环队列 CircularQueue<String> circularQueue = new CircularQueue<>(); // 初始化 circularQueue.initQueue(); // 入队 circularQueue.enQueue("1"); circularQueue.enQueue("2"); circularQueue.enQueue("3"); circularQueue.enQueue("4"); circularQueue.enQueue("5"); circularQueue.deQueue(); circularQueue.deQueue(); circularQueue.enQueue("88"); System.out.println(circularQueue.queueEmpty()); String head = circularQueue.getHead(); System.out.println(head); System.out.println(circularQueue.queueLength()); circularQueue.getAll(); circularQueue.clearQueue(); } }
-
循环队列比普通的顺序队列在时间性能上有了提高,但是面临着数组溢出问题(因为数组长度是固定的)
4.3.6 链式循环队列
-
队列的链式存储方式,采用单链表,尾进头出,简称为链队列。设置队头指针指向头结点,队尾指针指向终端节点。
-
Java代码
package com.fc.queue; /** * @ClassName ChainQueue 链队列 * @Description * @Author Fclever * @Date 2021/7/13 16:42 **/ public class ChainQueue<T> { // 队头指针 private Node<T> front; // 队尾指针 private Node<T> rear; // 实际元素个数 private int length; public ChainQueue() { } /** * 1. 初始化 */ public void initQueue() { // 队头和队尾指向相同 this.front = this.rear = new Node<>(); this.length = 0; } /** * 2. 清空队列 */ // public void clearQueue() { // Node<T> p = this.front; // Node<T> q = this.front.next; // while (q != null) { // p = q; // q = q.next; // p = null; // } // q = null; // } /** * 3. 判断队列是否为空 * @return 是否为空标志 */ public boolean queueEmpty() { return this.front == this.rear; } /** * 4. 获取队头元素 * @return 队头 */ public Node<T> getHead(){ return this.front.next; } /** * 5. 入队 * @param data */ public void enQueue(T data) { // 构建结点 Node<T> node = new Node<>(data, null); /** * 当队列为空时,front=rear都指向头结点,只需要改变 * rear的next指向即可,此时front也跟着改变 */ // 直接在队尾插入 this.rear.next = node; this.rear = node; this.length++; } /** * 6. 出队 * @return */ public Node<T> deQueue(){ // 队列为空,直接返回 if (this.queueEmpty()){ return null; } // 队头元素 Node<T> node = this.front.next; // 判断队头元素是否是队尾** if (this.rear == node) { // 将队尾指向头结点 this.rear = this.front; } this.front.next = node.next; this.length--; return node; } /** * 7. 获取队列实际长度 * @return */ public int queueLength() { return this.length; } /** * 8. 遍历 */ public void getAll() { if (!this.queueEmpty()) { Node<T> p = this.front.next; while (p != null) { System.out.println(p.data); p = p.next; } } } }
-
测试
package com.fc.queue; import org.junit.Test; import javax.print.DocFlavor; import static org.junit.Assert.*; /** * @ClassName ChainQueueTest * @Description * @Author Fclever * @Date 2021/7/14 10:03 **/ public class ChainQueueTest { @Test public void test() { // 创建 ChainQueue<String> chainQueue = new ChainQueue<>(); // 初始化 chainQueue.initQueue(); // 入队 chainQueue.enQueue("1"); chainQueue.enQueue("2"); chainQueue.enQueue("3"); chainQueue.enQueue("4"); chainQueue.enQueue("5"); // System.out.println(chainQueue.getHead().data); System.out.println(chainQueue.queueLength()); chainQueue.deQueue(); chainQueue.deQueue(); chainQueue.deQueue(); System.out.println(chainQueue.queueEmpty()); chainQueue.getAll(); } }
4.3.7 顺序队列和链队列
- 从时间上来看,基本操作都是O(1),循环队列是实现申请好空间,使用期间不释放;链队列,每次需要时申请和释放结点。
- 从空间上来看,循环队列长度固定,有了存储元素个数和空间浪费的问题;链队列不存在这个问题,虽然需要指针域,会产生一定的空间开销。
- 队列长度固定,使用顺序循环队列;队列长度无法预估,使用链队列。
.enQueue(“5”);
//
System.out.println(chainQueue.getHead().data);
System.out.println(chainQueue.queueLength());
chainQueue.deQueue();
chainQueue.deQueue();
chainQueue.deQueue();
System.out.println(chainQueue.queueEmpty());
chainQueue.getAll();
}
}
###### 4.3.7 顺序队列和链队列
- 从时间上来看,基本操作都是O(1),循环队列是实现申请好空间,使用期间不释放;链队列,每次需要时申请和释放结点。
- 从空间上来看,循环队列长度固定,有了存储元素个数和空间浪费的问题;链队列不存在这个问题,虽然需要指针域,会产生一定的空间开销。
- <font color=red>队列长度固定,使用顺序循环队列;队列长度无法预估,使用链队列。</font>