一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第 25 天,点击查看活动详情
日积月累,水滴石穿 😄
前言
上一篇讲到了数据结构中的栈,说到栈是一种受限线性表,其实,本文要讲的队列也是一种受限线性表。
队列的定义
队列是一种受限线性表,它只允许在表的前端(front)进行删除操作,进行删除操作的端称为队头。在表的后端(rear)进行插入操作,进行插入操作的端称为队尾。队列中没有元素时,称为空队列。
队列的数据元素称为队列元素,在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。 因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出(FIFO—first in first out)线性表,LIFO。
比如:可以把队列想成排队买票,先来的先排队,后来的人只能站末尾,不允许插队。先来者先买票,这就是典型的“队列”。
队列的分类
(1)顺序(单向)队列:(Queue) 只能在一端插入数据,另一端删除数据。
(2)循环(双向)队列(Deque):每一端都可以进行插入数据和删除数据操作
自定义队列
数组方式实现
package queue;
/**
* 队列:先进先出
* 使用数组实现队列
*/
public class MyArrayQueue {
/**
* 数据
*/
String[] data;
/**
* 头部元素下标
*/
int head;
/**
* 尾部元素下标
*/
int tail;
/**
* 元素个数
*/
int size;
/**
* 初始化数组容量
*/
public MyArrayQueue(int cap) {
data = new String[cap];
}
}
准备四个属性,分别是 data、head、tail、size。解释一下 head、tail的含义,我们已经知道,队列的存储结构是对一头进行删除,对一尾进行添加操作。所以用 head、tail 分别代表队头元素、队尾元素的下标。
以及一个有参构造方法用来初始化队列的长度。
offer
/**
* 入队 如果队列已满,返回 false
*/
public boolean offer(String e){
//判断队列是否已满
if(size == data.length){
return false;
}
data[tail] = e;
tail++;
size++;
return true;
}
poll
/**
* 出队,当队列为空时,返回 null
*/
public String poll(){
//判断队列是否已空
if(isEmpty()){
return null;
}
String value = data[head];
data[head] = null;
head++;
size--;
return value;
}
peek
/**
* 获得队列头部元素,如果队列为空,则返回null
*/
public String peek(){
if(isEmpty()){
return null;
}
return data[head];
}
isEmpty
/**
* 队列是否已空
*/
public boolean isEmpty(){
//也可以使用 head == tail
return size == 0;
}
/**
* 打印数据
*/
public void print() {
if (isEmpty()) {
System.out.println("队列为空");
return;
}
System.out.print("队列元素为:");
for (int i = 0; i < data.length; i++) {
System.out.print(data[i] +" ");
}
System.out.println();
}
测试
public static void main(String[] args) {
MyArrayQueue myQueue = new MyArrayQueue(4);
System.out.println("获得头元素:"+myQueue.peek());
System.out.println("插入元素:" +myQueue.offer("a"));
System.out.println("插入元素:" +myQueue.offer("b"));
System.out.println("插入元素:" +myQueue.offer("c"));
System.out.println("插入元素:" +myQueue.offer("d"));
System.out.println("插入元素:" +myQueue.offer("e"));
System.out.println("获得头元素:"+myQueue.peek());
myQueue.print();
System.out.println("移除元素:" + myQueue.poll());
System.out.println("移除元素:" + myQueue.poll());
System.out.println("移除元素:" + myQueue.poll());
System.out.println("移除元素:" + myQueue.poll());
System.out.println("移除元素:" + myQueue.poll());
System.out.println("获得头元素:"+myQueue.peek());
System.out.println("插入元素:" +myQueue.offer("a"));
}
结果:
获得头元素:null
插入元素:true
插入元素:true
插入元素:true
插入元素:true
插入元素:false
获得头元素:a
队列元素为:a b c d
移除元素:a
移除元素:b
移除元素:c
移除元素:d
移除元素:null
获得头元素:null
插入元素:false
最开始创建了一个长度为 4 的队列,队列中的属性关系如图所示:
然后执行offer(a)方法,向队列中插入元素,结构如图:
可以看到,插入一个元素后 size、tail 两个属性的值发生了变化。tail 向后移动了一个位置指向第二个元素位置。然后我们再将队列添满,结构如下:
tail指向一个不存在的位置,这时候,我们再执行offer("e"),满足队列是否已满的判断,返回 false。
接下来再看看下出队情况,执行 myQueue.poll()
出队的时候,size 的数值减一,并将 head 位置的值赋值为 null,然后将 head 向后移动了一个位置指向第二个元素位置。
执行第四个 myQueue.poll(),结构如下:
队列中的 size = 0,或者 tail = head,代表队列中已经没有元素了。
执行最后一个 myQueue.poll(),这时满足 size = 0的判断,返回 null。
所有元素移除之后,这时再进行插入操作,会发现元素插入不了,返回了 false。按道理是要插入成功的,但是由于 tail == data.length满足了队列已满的判断,这个问题就是我们常说的“假溢出”。
那这个“假溢出”问题怎么解决呢?
- 解决方式1:当有元素出队时,出队元素后面的所有元素都向前移动,保证下次出队的元素处在队列头部,也就是下标为0的位置,但是这种方式会大大增加时间复杂度。
- 解决方式2:循环队列,头尾相接的循环结构。其实就是修改 tail、head 的值!那什么时候进行修改呢?当 tail、head等于队列长度就重置为 0 。
解决假溢出
修改 offer、poll方法,内容如下:
offer
/**
* 入队 如果队列已满,返回 false
*/
public boolean offer(String e){
//判断队列是否已满 这是使用 size 判断
if(size == data.length){
return false;
}
data[tail] = e;
tail++;
//如果 tail 的值 等于 data.length
//说明 tail 需要被重置
if(tail == data.length){
tail = 0;
}
size++;
return true;
}
poll
/**
* 出队,当队列为空时,返回 null
*/
public String poll(){
//判断队列是否已空
if(isEmpty()){
return null;
}
String value = data[head];
data[head] = null;
head++;
//如果 head 的值 等于 data.length
//说明 head 需要被重置
if(head == data.length){
head = 0;
}
size--;
return value;
}
测试
public static void main(String[] args) {
MyArrayQueue myQueue = new MyArrayQueue(4);
System.out.println("获得头元素:"+myQueue.peek());
System.out.println("插入元素:" +myQueue.offer("a"));
System.out.println("插入元素:" +myQueue.offer("b"));
System.out.println("插入元素:" +myQueue.offer("c"));
System.out.println("插入元素:" +myQueue.offer("d"));
System.out.println("插入元素:" +myQueue.offer("e"));
System.out.println("获得头元素:"+myQueue.peek());
myQueue.print();
System.out.println("移除元素:" + myQueue.poll());
System.out.println("插入元素:" +myQueue.offer("a2"));
System.out.println("移除元素:" + myQueue.poll());
System.out.println("插入元素:" +myQueue.offer("c2"));
System.out.println("移除元素:" + myQueue.poll());
System.out.println("插入元素:" +myQueue.offer("d2"));
System.out.println("移除元素:" + myQueue.poll() + " 第一次插入的 a、b、c、d 移除完毕");
System.out.println("移除元素:" + myQueue.poll());
System.out.println("获得头元素:"+myQueue.peek());
System.out.println("插入元素:" +myQueue.offer("a"));
System.out.println("移除元素:" + myQueue.poll());
System.out.println("移除元素:" + myQueue.poll());
System.out.println("移除元素:" + myQueue.poll());
System.out.println("移除元素:" + myQueue.poll());
}
对于循环队列小杰还是画图来表示它们的过程吧!
- 初始化
- 执行完第三次
offer,tail = 3。 - 执行完第四次
offer,满足tail == data.length条件,将tail置为 0 。
- 执行完第五次
offer,满足size == data.length条件,拒绝添加元素。 - 执行完第一次
poll,head = 1。
- 再执行
offer,各位应该还记得tail已经置为 0 了。 - 间隔执行两次
poll、offer - 执行完第四次
poll,满足head == data.length条件,将head置为 0 。
- 执行第五次
poll,这时候的 head 为 0,所以移除第一个元素。执行完后,head = 1。
后面就是周而复始了,tail、head 等于了数组的长度就进行重置。
链表实现
链表实现队列很简单,就是单链表的添加和删除操作。
public class MyLinkedListQueue {
/**
* 头节点,存储整个链表的元素,可以从头节点遍历整个链表
*/
private MyNode head;
/**
* 尾节点,存储单独的尾节点
*/
private MyNode tail;
/**
* 队列元素个数
*/
private int size;
private static class MyNode{
String item;
MyNode next;
public MyNode(String item){
this.item = item;
}
}
}
offer
/**
* 入队:添加尾节点
*/
public boolean offer(String e){
MyNode newNode = new MyNode(e);
//如果队列为空
if(isEmpty()){
//头节点、尾节点 指向同一个节点
head = tail = newNode;
}else{
//将新节点作为尾节点的下个节点
//由于这尾节点与头结点的内存地址是一致的
//尾节点,则也是修改头节点
tail.next = newNode;
//新节点作为尾节点
tail = newNode;
}
size++;
return true;
}
poll
/**
* 移除头部节点
*/
public String poll(){
//如果队列为空
if(isEmpty()){
return null;
}
MyNode oldHead = head;
//获得头节点的值
String item = oldHead.item;
//获得头节点的下个节点,并将下个节点赋值给头节点
head = oldHead.next;
//把旧的表头的下一个节点指向设置为null,让gc回收
oldHead.next = null;
//队列为空,尾节点也需要置为 null
if(head == null){
tail = null;
}
size--;
return item;
}
peek
/**
* 获得队列头部元素,如果队列为空,则返回null
*/
public Object peek(){
return isEmpty() ? null : head.item;
}
isEmpty
/**
* 队列是否已空
*/
public boolean isEmpty(){
//也可以使用 head == tail
return size == 0;
}
public void print(){
System.out.print("链表元素:");
MyNode cur = head;
while (cur != null){
System.out.println(cur.item + " ");
cur = cur.next;
}
}
测试
public static void main(String[] args) {
MyLinkedListQueue queue = new MyLinkedListQueue();
System.out.println("获得头元素:" + queue.peek());
System.out.println("插入元素:" + queue.offer("a"));
System.out.println("插入元素:" + queue.offer("b"));
System.out.println("插入元素:" + queue.offer("c"));
System.out.println("插入元素:" + queue.offer("d"));
queue.print();
System.out.println("移除元素:" + queue.poll());
System.out.println("移除元素:" + queue.poll());
System.out.println("移除元素:" + queue.poll());
System.out.println("移除元素:" + queue.poll());
System.out.println("移除元素:" + queue.poll());
System.out.println("获得头元素:" + queue.peek());
}
两者区别
- 使用数组是有边界的队列,而单链表是无界的。
- 初始化时,数组需要分配空间,而链表则不需要。
- 单链表存储数据同时还需要额外存储下一个节点的地址,空间开销比较大。
队列的其他应用
- 优先队列
- 阻塞队列
- 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞和关注。