队列(queue
)是一种线性数据结构,就像我们排队一样,队列中的元素有着“先进先出”的特点,队列的出口端叫作队头,队列的入口端叫作队尾,队列只允许在队头进行出队操作(删除),队列只允许在队尾进行入队操作(添加)。
队列按照实现机制的不同分为:单队列和循环队列。
队列的实现方式:
数组实现的队列叫作顺序队列 ,用链表实现的队列叫作链式队列
一、单队列的常见操作
1、入队
入队(enqueue
)就是把新元素放入队列中,只允许在队尾的位置放入元素,新元素的下一个位置将会成为新的队尾。注:队尾总是指向空闲的位置
2、出队
出队操作(dequeue
)就是把元素移出队列,只允许在队头一侧移出元素,出队元素的后一个元素将会成为新的队头。
3、“假溢出”
顺序队列存在“假溢出”的问题,也就是明明有位置却不能添加。
下图是一个顺序队列,我们将前两个元素 1
,2
出队,并入队两个元素 7
,8
。当进行入队、出队操作的时候,front
和 rear
都会持续往后移动,当 rear
移动到最后的时候,我们无法再往队列中添加数据,即使数组中还有空余空间,这种现象就是 “假溢出” 。除了假溢出问题之外,如下图所示,当添加元素 8
的时候,rear
指针移动到数组之外(越界)。
为了解决“假溢出”带来的问题,我们引进了循环队列。
二、 循环队列
用数组实现的队列可以采用循环队列的方式来维持队列容量的恒定。
例如:
步骤1 : 一个队列经过反复的入队和出队操作,还剩下2个元素,在“物理”上分布于数组的末尾位置。这时又有一个新元素将要入队。
步骤2 : 在数组不做扩容的前提下,我们可以利用已出队元素留下的空间,让队尾指针重新指回数组的首位。
步骤3 : 队尾指针指向数组首位后,整个队列的元素就“循环”起来了。在物理存储上,队尾的位置也可以在队头之前。当再有元素入队时,将其放入数组的首位, 队尾指针继续后移即可。
步骤4 : 直到(队尾下标+1)%数组长度 = 队头下标,代表此队列真的已经满了。需要注意的是,队尾指针指向的位置永远空出1
位,所以队列最大容量比数组长度小1
基于数组实现循环队列代码如下:
package com.hpc.demo01;
import java.util.Arrays;
public class Demo03 {
public static void main(String[] args) {
CircularQueue queue=new CircularQueue(6);
try {
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
queue.enqueue(4);
queue.enqueue(5);
queue.enqueue(6);
System.out.println(queue);
} catch (Exception e) {
e.printStackTrace();
}
try {
queue.dequeue();
queue.enqueue(6);
System.out.println(queue);
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* 循环队列
* @author 我
*
*/
class CircularQueue{
//基于数组实现循环队列
int[] array;
int front; //队头
int rear; //队尾
//有参构造方法:传入数组的容量
public CircularQueue(int capecity) {
array=new int[capecity];
}
/**
* 入队操作
* @param n
* @throws Exception
*/
public void enqueue(int n) throws Exception {
if((rear+1)%array.length==front) {
dequeue(); //出队
}
//队尾入队
array[rear]=n;
rear=(rear+1)%array.length; //移动队尾下标
}
/**
* 出队操作
* @return
* @throws Exception
*/
public int dequeue() throws Exception {
if(rear==front) {
throw new Exception("出队异常!");
}
//获取对头元素
int n=array[front];
front=(front+1)%array.length; //移动队头下标
return n;
}
//打印输出队列中的所有元素
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
for(int i=front;i!=rear;i=(i+1)%array.length) {
sb.append(array[i]);
}
return sb.toString();
}
}
上述代码的输出结果如下:
23456
34566
这样,就实现了循环队列,从而避免了“假溢出”。
三、队列的应用场景
当我们需要按照一定顺序来处理数据的时候可以考虑使用队列这个数据结构。
1、KTV点歌列表
使用队列保存已点歌曲列表,每次点歌时,将歌曲放入队尾。播放歌曲时,从队头取出。符合FIFO
存取特点。
2、阻塞队列
阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。
3、线程池的任务队列
线程池中没有空闲线程时,新的线程任务请求线程资源时,线程池会将这些线程任务放在工作队列中,当有空闲线程的时候,会循环反复地从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如 :FixedThreadPool
使用无界队列 LinkedBlockingQueue
。但是有界队列就不一样了,当队列已满的话,后面再有线程任务就会判断是否超出最大线程数,如果超出则执行拒绝策略。
四、队列常见手撕代码
1、使用栈模拟队列
队列的特点是“先进先出”,而栈的特点是“后进先出”,要使用栈来模拟队列,我们需要使用两个栈,一个入队栈,一个出队栈。当我们需要执行入队操作时,我们会把元素放入入队栈中,但是在放之前,我们需要判断出队栈是否为空,如果不为空,则需要先将出队栈的元素先出栈并放入入队栈,然后再放入入队元素;出队操作也与入队操作类似,在出队之前,我们也需要先判断入队栈是否为空,如果不为空,则需先将入队栈的所有元素出栈并放入出队栈,然后执行出队操作。
这时,我们需要设计一个类Queue
,使用两个栈来模拟队列的实现,对外提供三个方法:入队列,出队列,判断是否为空 。具体代码实现如下:
package com.hpc.demo01;
import java.util.Stack;
/**
* 栈模拟队列
* @author 我
*
*/
public class Queue {
//入队栈
private Stack<Integer> inStack=new Stack<Integer>();
//出队栈
private Stack<Integer> outStack=new Stack<Integer>();
/**
* 入队
* @param n
*/
public void offer(int n) {
while(!outStack.isEmpty()) {
inStack.push(outStack.pop());
}
inStack.push(n);
}
/**
* 出队
* @param n
*/
public int poll() {
while(!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
return outStack.pop();
}
/**
* 判断队列是否为空
* @return
*/
public boolean isEmpty() {
return inStack.size()==0&&outStack.size()==0;
}
}
测试类如下:
package com.hpc.demo01;
public class Test01 {
public static void main(String[] args) {
Queue queue=new Queue();
queue.offer(1);
queue.offer(2);
queue.offer(4);
queue.offer(3);
queue.offer(5);
while(!queue.isEmpty()) {
System.out.print(queue.poll());
}
}
}
测试结果如下:
12435
这样,我们就使用栈模拟出了一个队列。