队列
队列,进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表。
队列特点:先进先出
队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明
队列分为:
①、单向队列(Queue):只能在一端插入数据,另一端删除数据。
②、双向队列(Deque):每一端都可以进行插入数据和删除数据操作。
③、优先级队列(Priority):优先级队列是比栈和队列更专用的数据结构,在优先级队列中,数据项按照关键字进行排序,关键字最小(或者最大)的数据项往往在队列的最前面,而数据项在插入的时候都会插入到合适的位置以确保队列的有序。
在实现之前,我们先看下面几个问题:
①、与栈不同的是,队列中的数据不总是从数组的0下标开始的,移除一些队头front的数据后,队
头指针会指向一个较高的下标位置,如下图:
②、我们再设计时,队列中新增一个数据时,队尾的指针rear 会向上移动,也就是向下标大的方
向。移除数据项时,队头指针 front 向上移动。
总结:因为队列的是输出、输入是分别从前后端来处理,因此需要两个变量front(队头指针)、rear(队尾指针)分别记录队列前后端的下标,front会随着数据输出而改变,而rear则是随着数据输入而改变。maxSize是队列的最大容量。
1.数组实现简单的单向队列(非循环):
当我们将数据存入队列时称为addQueue,addQueue的处理需要有两个步骤。
1)将尾指针往后移:rear+1(因为是顺序存储结构,地址是连续的,所以这两个指针可以直接用int型,所以直接+1),当front==rear表示空队列。
2)若尾指针rear小于队列的最大下标maxSize-1,则将数据存入rear所指的数组元素中,否则无法存入数据。rear == maxSize-1表示队满。
注意,rear是指向队列最后元素(包含),而front指向队列最前元素(不包含)
public class Queue { // 用数组模拟一个单向肺循环队列 private int maxSize; // 数组最大容量 private int front; // 队头指针 private int rear; // 队尾 private int[] queue; // 模拟队列 // 创建队列的构造器 public Queue(int maxSize) { this.maxSize = maxSize; queue = new int[maxSize]; // 初始化,不new无法初始化 front = rear = -1; // 对空,指向队列头部,而不是队列第一个元素 } // 判断队列是否满 public boolean isFull(){ return rear == maxSize - 1; } // 判断队列是否空 public boolean isEmpty(){ return front == rear; // 不止为-1时,在中间取完了也有可能 } // 添加数据到队列 public void addQueue(int n){ // 首先判断队列是否满 if(isFull()){ System.out.println("队列已满。不能再添加元素!"); }else{ rear++; // 尾指针后移动 queue[rear] = n; // 元素保存进去 } } // 从队列删除数据,并获取 public int deleteQueue(){ // 只能从表头删 int result; if(isEmpty()){ // 抛出异常 throw new RuntimeException("队列为空,不能取数据!"); // 立马终止,所以不用再写return } result = queue[front+1]; front++; return result; } // 显示队列所有数据 public void show(){ // 遍历 if(isEmpty()) System.out.println("队列为空"); for (int i = front + 1; i <= rear; i++) { System.out.printf("queue[%d]=%d\n",i,queue[i]); // 格式化写法 } } // 显示队列的头数据,注意不是取出数据 public int headQueue(){ if(isEmpty()) throw new NullPointerException("队列为空,不存在头数据"); return queue[front + 1]; } }
public static void main(String[] args) { Queue queue = new Queue(15); // 构造方法创建最大容量队列 // 判断是否为空 System.out.println(queue.isEmpty()); // 判断是否满 System.out.println(queue.isFull()); // 添加元素 queue.addQueue(3); queue.addQueue(3); queue.addQueue(5); queue.addQueue(4); // 显示队列 queue.show(); System.out.println("---------"); // 从队头移除两个数据 queue.deleteQueue(); queue.deleteQueue(); // 再次显示队列 queue.show(); // 显示队头数据 System.out.println(queue.headQueue()); }
运行截图:
循环队列
③、如果向第②步这样移动指针,相信队尾指针很快就移动到数据的最末端了,这时候可能移除过
数据,那么队头会有空着的位置,然后新来了一个数据项,由于队尾不能再向上移动了,那该怎么办
呢?如下图:
为了避免队列不满却不能插入新的数据,我们可以让队尾指针绕回到数组开始的位置,这也称为“循环队列”。
核心使用取模%。
思路分析:
1.front指针含义做一个调整:front指向队列的第一个元素(而不是前一个)
2.rear指针含义做一个调整:rear指向队列最后一个元素的后一个位置。
调整原因:空出一个空间(最后一个元素和第一个元素之间空一个位置)作为约定。
循环队列为什么要空一个位置呢?
这个是根据需要来用的
循环队列中,由于入队时尾指针向前追赶头指针;z出队时头指针向前追赶尾指针,造成队空和队满时头尾指针均相等。因此,无法通过条件front==rear来判别队列是"空"还是"满"。
解决这个问题的方法至少有三种:
①另设一布尔变量以区别队列的空和满;
②少用一个元素的空间。约定入队前,测试尾指针在循环意义下加1后是否等于头指针,若相等则认为队满(注意:rear所指的单元始终为空);
③使用一个计数器记录队列中元素的总数(即队列长度)。
当队列满时:(rear + 1) % maxSize == front
当队列空时:rear == front
初始值:front = rear = 0
此时队列中有效的数据个数为:(rear + maxSize - front)% maxSize
添加队尾元素的时候:rear后移,为保证不越界,rear = (rear + 1) % maxSize
删除队头元素的时候:front后移,为保证不越界,front = (front + 1) % maxSize
遍历有效数据的时候:从front开始,因为有效数据个数为(rear + maxSize - front)% maxSize,所以到front + ((rear + maxSize - front)% maxSize)为止。
代码:
package 数据结构; public class CircularQueue { // 用数组模拟一个循环能循环队列 private int maxSize; // 数组最大容量 private int front; // 队头指针,指向第一个元素 private int rear; // 队尾,指向队尾元素的下一个位置 // 默认maxSize留空一位 private int[] queue; // 模拟队列 // 创建队列的构造器 public CircularQueue(int maxSize) { this.maxSize = maxSize; queue = new int[maxSize]; // 初始化,不new无法初始化 front = rear = 0; // 队空 } // 判断队列是否满 public boolean isFull(){ return (rear + 1) % maxSize == front; } // 判断队列是否空 public boolean isEmpty(){ return front == rear; // 不止为0时,在中间取完了也有可能 } // 添加数据到队列 public void addQueue(int n){ // 首先判断队列是否满 if(isFull()){ System.out.println("队列已满。不能再添加元素!"); }else{ queue[rear] = n; // 元素保存进去 // 尾指针向后移动,因为循环,这里必须考虑取模,否则可能越界 rear = (rear + 1) % maxSize; } } // 从队列删除数据,并获取 public int deleteQueue(){ // 只能从表头删 int result; if(isEmpty()){ // 抛出异常 throw new RuntimeException("队列为空,不能取数据!"); // 立马终止,所以不用再写return } result = queue[front]; // 也要考虑循环因素 front = (front + 1) % maxSize; return result; } // 求出当前有效数据个数 public int size(){ return (rear + maxSize - front) % maxSize; } // 显示队列所有数据 public void show(){ // 遍历 if(isEmpty()) System.out.println("队列为空"); // 思路:从front遍历 // 由于有效个数为(front+maxSize-rear)%maxSize for (int i = front; i <= front + size(); i++) { System.out.printf("queue[%d]=%d\n",i % maxSize,queue[i % maxSize]); // 格式化写法 } } // 显示队列的头数据,注意不是取出数据 public int headQueue(){ if(isEmpty()) throw new NullPointerException("队列为空,不存在头数据"); return queue[front]; } }
双端队列
双端队列就是一个两端都是结尾或者开头的队列, 队列的每一端都可以进行插入数据项和移除数据项,这些方法可以叫做:insertRight()、insertLeft()、removeLeft()、removeRight()
1)如果严格禁止调用insertLeft()和removeLeft()(或禁用右端操作),那么双端队列的功能就和前面讲的栈功能一样。
2)如果严格禁止调用insertLeft()和removeRight(或相反的另一对方法),那么双端队列的功能就和单向队列一样了。
参考博客:
https://blog.csdn.net/qq_39513430/article/details/104949994?utm_medium=distribute.pc_relevant.none-task-blog-baidujs_baidulandingword-0&spm=1001.2101.3001.4242
https://blog.csdn.net/yrwan95/article/details/82464118?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.baidujs&dist_request_id=&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.baidujs
https://blog.csdn.net/weixin_41845059/article/details/107222179
https://blog.csdn.net/changshuchao/article/details/87861542