持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第6天,点击查看活动详情
队列(Queue)是什么
- 队列是一种有序列表,它可以用数组和链表所实现
- 队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。允许插入的一端成为队尾,允许删除的一端成为队头
- 队列遵循着先进先出的原则,即:先存入队列的数据,要先取出。而后入队列的数据,要后取出
- 就跟我们排队一样,排在第一个的要优先出队,后来的当然排在队伍的最后
队列的常用操作
InitQueue(Queue):初始化操作,建立一个空队列ClearQueue(Queue):将队列清空QueueEmpty(Queue):判断队列是否为空GetHead(Queue):若队列存在且非空,返回队列的头部元素EnQueue(Queue,e):若队列存在,将新元素e插入到队列的队尾DeQueue():删除队列中的头部元素,并返回QueueLength(Queue):返回队列中的元素个数
队列的顺序存储结构(数组模拟队列)
思路分析
-
队列本身是有序列表,若使用数组的结构来存储队列的数据,则队列数组的声明如下图, 其中 maxSize 是该队列的最大容量。
-
因为队列的输出、输入是分别从前后端来处理,因此需要两个变量 front及 rear分别记录队列前后端的下标,front 会随着数据输出而改变,而 rear则是随着数据输入而改变,如图所示:
-
为了避免当只有一个元素时,队头和队尾重合使处理变得麻烦,所以我们将
front指向队头元素的前一个位置,rear指向队尾元素,这样当front == rear时,此队列不是还有一个元素,而是空队列 -
若
rear小于队列的长度maxszie-1,元素可以放进队列里。否则无法存入数据,即队列已满的判断条件为rear = maxsize - 1;
具体操作代码
顺序队列的结构
class ArrayQueue {
private int maxsize;//队列的长度
private int[] queue;//数组模拟队列,数据就放在数组里
private int front = -1;//指向队列的队头前一个位置
private int rear = -1;//指向队列的队尾
/**
* 构造器
* @param maxsize
*/
public ArrayQueue(int maxsize) {
this.maxsize = maxsize;
queue = new int[this.maxsize];
}
}
清空队列
public void Clear() {
front = -1;
rear = -1;
}
判断队列满
public boolean isFull() {
return rear == maxsize-1;
}
判断队列空
public boolean isKong() {
return front == rear;
}
入队操作
public void add(int value) {
if (isFull()) {
System.out.println("队列已满,无法往里添加元素");
return;
}
rear++;
queue[rear] = value;
}
出队操作
public int get() {
if (isKong()) {
throw new RuntimeException("队列是空的");
}
front++;
return queue[front];
}
若队列存在且非空,返回队列头部元素
public int GetHead() {
if (isKong()) {
throw new RuntimeException("队列是空的,无法获取元素");
}
return queue[front + 1];
}
遍历队列元素
public void show() {
if (isKong()) {
throw new RuntimeException("队列是空的,无法获取元素");
}
for (int i = front+1; i <= rear;i++) {
System.out.println("queue["+i+"] = "+ queue[i]);
}
}
获取队列元素个数
public int Length() {
if (isKong()) {
throw new RuntimeException("队列是空的,无元素");
}
int count = 0;
while (front < rear) {
count++;
front++;
}
return count;
}
优化
我们在使用时,会发现顺序结构的队列会存在一种问题,即数组只能用一次,达不到复用的效果。 举例来说:
- 假设有一个长度为
3的数组,初始时,front指针和rear指针分别指向数组下标为-1的位置 - 入队
a1,a2,a3,front指向下标为-1的位置,rear指向下标为1的位置 - 出队
a1元素,则front指向下标为0的位置,rear不变 - 再添加元素
a3到队列中,因为此时数组末尾元素已满,如果再添加元素就会导致数组越界的错误,可实际上,我们的队列在下标0处还是空闲的,这种现象就叫做假溢出 - 再比如说,你上了公交车,发现后排没有空座了,但前排还有两个空座,正常来说,你肯定不会直接下车等下一个后排有空座的车,而是会坐到前排有空座的位置上
- 所以我们如果需要解决这种假溢出的问题,就需要使用一种方法,即当后面满了,我们直接从头开始,也就是头尾相接的循环。我们就将这种头尾相接的顺序存储结构称为循环队列
头尾相接的顺序存储结构 (环形队列)
思路分析
- 尾部索引的下一个为头索引时表示队列已满,即将队列空出一个作为约定,也就是说,当数组中还有一个空闲单元时,我们就认为此队列已经满了。即
(rear + 1) % maxsize == front; - 队列为空:
rear == front3.front变量含义做一个调整:front指向队列的第一个元素,front初始值为0 rear变量含义也做一个调整:rear指向队列的队尾的后一个元素,因为要空出一个空间单元,rear初始值也为0- 队列中有效的元素个数:
- 当
rear > front时,此时队列元素个数为rear - front; - 当
front > rear时,此时队列分成两段,一段是maxsize - front,一段是0 + rear;即rear - front + maxsize - 因此通用的计算队列长度的公式为:
(rear + front + maxsize) % maxsize;
有了这些思路,具体实现代码就不难了
具体操作代码
环形队列的结构和初始化
class ArrayQueue {
private int maxsize;//队列的长度
private int[] queue;//数组模拟队列,数据就放在数组里
private int front = 0;//队列队头的前一个位置
private int rear = 0;//队列的队尾
/**
* 构造器
* @param maxsize
*/
public ArrayQueue(int maxsize) {
this.maxsize = maxsize;
queue = new int[this.maxsize];
}
}
入队列
public void push(int value) {
if (isFull()) {
System.out.println("队列已满,无法往里添加元素");
return;
}
queue[rear] = value;
rear = (rear + 1) % maxsize;
}
出队列
public int pop() {
if (isKong()) {
throw new RuntimeException("队列是空的");
}
int val = queue[front];
front = (front + 1) % maxsize;
return val;
}
单是顺序结构,如果不是循环队列,时间复杂度还不是很高,但顺序队列又有可能发生数组溢出的情况,所以我们还是要说一下不需要担心队列长度的链式存储结构
队列的链式顺序结构(链表模拟队列)
思路分析
- 队列的链式存储结构,就是线性表的单链表,只不过它只能尾进头出而已,我们将它简称为链队列
- 为了操作上的方便,我们将队头指针指向链队列的头节点,而队尾指针指向链队列的终端结点
- 为了更方便理解链表,我们又引入一个虚拟头结点指针
具体操作代码
链队列的结构
public static class LinkListQueue {
private int data;
private LinkListQueue next;
public int getData() {
return data;
}
public void setData(int data) {
this.data = data;
}
public LinkListQueue getNext() {
return next;
}
public void setNext(LinkListQueue next) {
this.next = next;
}
}
public LinkListQueue front;//队列队头
public LinkListQueue rear;//队列队尾
public LinkListQueue header = null;
/**
* 构造器
*/
public LinkListQueueDemo() {
header = new LinkListQueue();
front = new LinkListQueue();
rear = new LinkListQueue();
front = header;
rear = header;
}
判断队列是否为空
public boolean isKong () {
return rear == header;
}
入队操作
public void push (int val) {
LinkListQueue s = new LinkListQueue();
s.setData(val);
s.setNext(null);
if (isKong()) {
rear = s;
header.next = s;
front.next = s;
}else {
//把拥有元素val的新结点s赋值到原链表尾结点的后继
rear.next = s;
//把当前的s设置为队尾结点,rear指向s
rear = s;
}
}
出队操作
public int pop () {
int o = 0;
if (isKong()) {
throw new RuntimeException("队列是空的,里面没有元素呢~");
}else {
o = front.next.getData();
//队列只有一个元素
if (front.next == rear) {
front.next = header;
rear = header;
}else {//队中大于一个元素
front.next = front.next.next;
}
}
return o;
}
获取队列元素个数
public int getQueueSize () {
LinkListQueue temp = header;
if (isKong()) {
throw new RuntimeException("队列是空的,里面没有元素呢~");
}
int count = 0;
while (temp.next != null) {
count++;
temp = temp.next;
}
return count;
}
总结
- 从时间上来说,循环队列是事先申请好空间,使用期间不释放,而对于链队列来说,每次申请和释放结点也会存在一些时间开销
- 从空间上来说,循环队列必须有一个固定的长度,所以就会存在存储元素个数和空间浪费的问题,而链队列就不存在这个问题,所以从空间上来说,链队列更加灵活
- 总的来说,在确定了队列长度最大值的情况下,建议使用循环队列,如果你无法估计队列的长度,则用链队列