队列
队列(queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。
同样,作为一种特殊的线性表,队列也有顺序存储结构和链式存储结构两种类型。
队列顺序存储的不足:
假设一个队列有n个元素,则顺序存储的队列需要建立一个大于n的数组,并把队列所有元素存储在数组的前n个单元,数组下标为0的一端即是队头。入队则是在队尾添加一个元素,不需要移动任何元素,时间复杂度为O(1);出队则是在队头下标为0的位置,也就意味着队列中所有元素都得向前移动,时间复杂度为O(n)。
.
但是为什么出队列时需要移动其他元素呢?如果不移动,出队列的效率会大大增加。为了避免当只有一个元素时,队头和队尾重合变得麻烦,引入两个指针front和rear。front指针指向队头元素,rear指针指向队尾元素的下一个位置,这样当front等于rear时此队列不是还剩一个元素而是空队列.
以初始长度为5的数组为例,入队a1、a2、a3、a4,如上图左一所示。出队a1和a2,front指针移动到下标为2的位置,rear指针不变,再入队a5时,rear指针移动到数组之外产生数组越界。而此时下标为0和1的位置是空闲的,我们把这种现象称为假溢出。
解决假溢出的方法就是使用循环队列。
顺序循环队列
头尾相接的顺序存储结构称为顺序循环队列
继续上例,插入a5时,rear指针指向0位置。就会避免 数组越界和假溢出问题。接着入队a6和a7,此时rear指针和front指针指向同一个位置2,那么如何判断队列为空呢? 之前是front=rear时队列为空,现在队列为满队列时front=rear也成立。
- 方法1:设置一个变量flag,当front==rear且flag=0时为空队列,front==rear且flag=1时为满队列
- 方法2:当队列空时,就是front=rear,当队列满时,我们修改条件,保留一个元素空间。如下图所示
对于第二种方法,由于rear可能比front大,也可能他比front小,所以队列满的条件为(rear+1)%QueueSize==front。而当rear>front时,队列长度为rear-front;当rear<front时,队列长度为QueueSize-front + 0+rear。所以综合看,通用的计算队列长度的公式为(rear-front+QueueSize)%QueueSize
类结构图
顺序循环队列封装类
package com.company.datastructure;
public class MyCircleQueue<E> {
private final int MAX_SIZE = 10; //最大长度为10(为了演示方便)
private Object[] elements; //定义一个数组用来存储元素
private int front; //头指针
private int rear; //尾指针
public MyCircleQueue(){
elements = new Object[MAX_SIZE];
front = 0;
rear = 0;
}
/**
* 入队列
* @param element 元素值
*/
public void EnQueue(E element){
if((rear+1)%MAX_SIZE==front){ //判断队列是否已满条件
throw new IndexOutOfBoundsException("队列已满");
}else{
elements[rear] = element;
rear = (rear+1)%MAX_SIZE; //队列满的话移到数组开始位置
}
}
/**
* 出队列
* @return 出队列元素值
*/
public E DeQueue(){
if(rear==front){
throw new NullPointerException("队列为空");
}else{
E OldElement = (E)elements[front];
front = (front+1)%MAX_SIZE;
return OldElement;
}
}
/**
* 获得队列长度
* @return 队列长度
*/
public int getSize(){
return (rear-front+MAX_SIZE)%MAX_SIZE; //队列长度通用公式
}
/**
* 获得队列头部元素
* @return
*/
public E get(){
return (E)elements[front];
}
/**
* 队列是否为空
* @return 是返回true,否则返回false
*/
public boolean isEmpty(){
return rear==front;
}
}
测试类
package com.company.datastructure;
public class TestMyCircleQueue {
public static void main(String[] args) {
MyCircleQueue<String> mcq = new MyCircleQueue<>();
for (int i = 0; i < 9; i++) {
mcq.EnQueue("chen"+i);
}
System.out.println(mcq.getSize());
System.out.println(mcq.get());
String str = mcq.DeQueue();
System.out.println(str);
mcq.DeQueue();
mcq.DeQueue();
System.out.println(mcq.get());
System.out.println(mcq.getSize());
mcq.EnQueue("chen"+10);
int size = mcq.getSize();
for (int i = 0; i < size; i++) {
System.out.println(mcq.DeQueue());
}
}
}
链队列
从上面的顺序循环队列可以看到,可能存在数组溢出的情况,或者是考虑数组溢出时进行数组扩容,但效率会适当降低,所以我们需要不需要考虑长度的链队列。
队列的链式存储,其实就是线性表的单链表,只不过是需要尾进头出而已,我们把它称为链队列。
队列的入队,其实就是在单链表的末尾插入一个结点。
队列的出队,就是头结点的后继结点出队,将头节点的后继改为它后面的结点,若链表除头结点外只剩一个结点,需要将rear指针指向头结点。
类结构图
链队列封装类
package com.company.datastructure;
public class MyLinkQueue<E> {
private class Node{
private E element;
private Node next;
public Node(){
}
public Node(E element){
this.element = element;
}
}
private Node front;
private Node rear;
private int size;
public MyLinkQueue(){
front = new Node();
rear = front;
}
/**
* 入队
* @param element 元素值
*/
public void EnQueue(E element) {
Node newNode = new Node(element);
rear.next = newNode;
rear = newNode;
size++;
}
/**
* 出队
* @return 出队元素值
*/
public E DeQueue(){
if(front==rear){
throw new NullPointerException("队列为空");
}else{
Node OldNode = front.next;
front.next = OldNode.next;
if(rear==OldNode){
rear = front;
}
size--;
return OldNode.element;
}
}
/**
* 获得链队列长度
* @return 长度
*/
public int getSize(){
return size;
}
/**
* 返回队列头部元素
* @return
*/
public E get(){
return front.next.element;
}
}
测试类
package com.company.datastructure;
public class TestMyLinkQueue {
public static void main(String[] args) {
MyLinkQueue<String> mlq = new MyLinkQueue<>();
mlq.EnQueue("chen0");
mlq.EnQueue("chen1");
mlq.EnQueue("chen2");
System.out.println(mlq.getSize());
int size = mlq.getSize();
for (int i = 0; i < size; i++) {
System.out.println(mlq.get());
mlq.DeQueue();
}
}
}