说到队列,我们先来回顾一下之前所讲的栈,栈是一种先进后出的线性表,而队列与栈是相反的一种线性结构,对!没错,队列也是一种线性表(顺序表),所以队列的顺序存储结构可能在学过栈之后就显得很简单了,相同的思路,但是要注意的是队列是先进先出(也就是早期的鸟儿有虫吃,早来的鸟儿早虫)
队列
先来看一看队列的概念:队列是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。SO?刚刚我没有说错吧,队列也是一种线性表。
-
队列的接口Queue:
public interface Queue <E>{
public int getSize();
public boolean isEmpty();
public void clear();
/**
* 入队一个新元素e
* @param e
*/
public void enqueue(E e) ;
/**
* 出队一个元素e
* @return
*/
public E dequeue();
/**
* 获取队头元素
* @return
*/
public E getFront();
/**
* 获取队尾元素
* @return
*/
public E getRear();
}
关于队列的实现,同样是实现队列的接口Queue,并且实现其接口里面的方法
public class ArrayQueue<E> implements Queue<E> {
}
接下来我们来看一看入队和出队的具体操作,因为队列是一种线性表,所以它所要实现的方法可以完全调用线性表的方法入
- 入队:入队元素的时候我们的rear指针要往后移动
- 出队:出队的时候我们的rear指针往前移,和入队相反
private ArrayList<E> list; //实现线性顺序存储结构ArrayList
public ArrayQueue() { //无参的构造函数,创建一个默认大小的线性表
list = new ArrayList<E>();
}
public ArrayQueue(int capacity) { //有参的构造函数,创建一个指定容量的队列(线性表)
list = new ArrayList<E>(capacity);
}
@Override
public int getSize() { //获得有效长度
// TODO Auto-generated method stub
return list.getSize();
}
@Override
public boolean isEmpty() { //判断队列是否为空
// TODO Auto-generated method stub
return list.isEmpty();
}
@Override
public void clear() { //清空对列
// TODO Auto-generated method stub
list.clear();
}
@Override
public void enqueue(E e) { //入队列一个新元素,相当于线性表在末尾插入一个新的元素
// TODO Auto-generated method stub
list.addLast(e);
}
@Override
public E dequeue() { //出队列,队列先入先出,所以相当于线性表中删除第一个元素
// TODO Auto-generated method stub
return list.removeFirst();
}
@Override
public E getFront() { //获取队头
// TODO Auto-generated method stub
return list.getFirst();
}
@Override
public E getRear() { //获取队尾
// TODO Auto-generated method stub
return list.getLast();
}
@Override
public String toString() { //toString()方法的重写
StringBuilder sb = new StringBuilder();
sb.append("ArrayQueue: size=" + getSize() + ",capacity=" + list.getCapacity() + "\n");
if (isEmpty()) {
sb.append("[]");
} else {
sb.append('[');
for (int i = 0; i < getSize(); i++) {
sb.append(list.get(i));
if (i == getSize() - 1) {
sb.append(']');
} else {
sb.append(',');
}
}
}
return sb.toString();
}
@Override
public boolean equals(Object obj) { //equals()方法比较的是两个队列的内容
if (obj == null) {
return false;
}
if (obj == this) {
return true;
}
if (obj instanceof ArrayQueue) {
ArrayQueue queue = (ArrayQueue) obj;
return list.equals(queue.list); //因为实现的是线性表,所以直接调用list方法
}
return false;
}
我们的队列存储结构本身是由ArrayList实现的,入队相当于线性表表尾添加元素,出队相当于线性表表头删除元素,所以入队的时间复杂度是O(1),而出队则是O(n),那么能否进行优化,使得出队和入队的时间都是O(1)呢?好,那么接下来就引入了循环队列的概念。
循环队列
今天的重点来了,请睁大你的双眼,动起你的小脑瓜,认真学习,因为循环队列很重要!很重要!很重要!(重要的事情说三遍)
接着我们上面的这个问题,怎样进行优化呢?我们分为三步:
第一步:因为之前的队列我们只有一个rear指针,那么我们能否引进两个指针,使得一个指向尾一个指向头,所以我们就有了front指针指向队列的头结点,rear指针指向队列的尾结点,从而在移动元素的时候(入队元素和出队)头指针和尾指针也随之移动。
这样的话我们是解决了出队和入队的时间复杂度都是O(1)的问题,但是当rear指针移动到队列尾端的时候现在是不能插入元素了,rear指针也不能再移动了,同时也浪费了很多空间,于是乎我们又有了新的解决方案
第二步:当我们遇到第一步所遇到的问题时,不管是头指针还是尾指针当他们达到队列最后端的时候不能再移动的时候,我们将他们重新移动到对头。
也就相当于头和尾连接起来了,相当于一个环形
那么在这种情况下我们如何判断这个队列什么时候已经满了,什么时候队列为空呢?
这样设计的我们完美的解决了上一步所说的空间浪费的问题,但是判空和判满的条件都是 (rear+1)%n==front???纳尼??????这要怎么办,别着急,这不?优化第三步
第三步:预留出来一个空空间,不存放任何元素,尾指针rear始终指向这个空空间,当插入元素的时候,rear指针始终往后移,指向一个null
那么现在你肯定想要问,怎么判断队列为空?又怎样判断队列满了呢?别着急,接着往下看
完美!!!!这样就解决了第二步中的判断满和判断空条件一致的问题,这个时候你肯定要说那这样尾指针一直指向一个空空间,不就造成了空间浪费吗?那么我就想反问你一句咯,这一个空间和第一步那么多空间相比,是不是小到可以忽略不记,简直就是小巫见大巫,不值得一提
那么接下来就来看看代码部分的实现吧!
public class ArrayQueueLoop<E> implements Queue<E>{
}
- 成员变量:我们需要有的成员变量有指向对头的头指针,指向队尾的尾指针,另外还需要创建一个新的数组,以及一个能够表示有效长度的变量,同时还需要一个静态常量用来表示默认队列大小
private E[] data;
private int front;
private int rear;
private int size;
private static int DEFAULT_SIZE=10;
- 构造函数(有参无参):分为为指定大小的队列和默认大小的队列
public ArrayQueueLoop() {
this(DEFAULT_SIZE);
}
public ArrayQueueLoop (int capacity) {
data=(E[]) new Object[capacity+1];
front=0;
rear=0;
size=0;
}
- getSize():获取有效长度,只要返回size即可
@Override
public int getSize() {
// TODO Auto-generated method stub
return size;
}
- isEmpty():判断队列是否为空,只要满足头指针等于尾指针以及有效长度为零即可
@Override
public boolean isEmpty() {
// TODO Auto-generated method stub
return front==rear&&size==0;
}
- clear() :清空队列也就是让头指针和尾指针均指向null,有效长度size等于0
@Override
public void clear() {
// TODO Auto-generated method stub
front=0;
rear=0;
size=0;
}
- enqueue(E e):入队元素,入队一个元素,尾指针后移就可以了,但是要注意的是扩容的问题,当队列已满的时候,我们需要扩容的操作
@Override
public void enqueue(E e) {
// TODO Auto-generated method stub
if((rear+1)%data.length==front) {
//扩容
resize(data.length*2-1);
}
data[rear]=e;
rear=(rear+1)%data.length;
size++;
}
- dequeue():出队操作是出队元素的时候头指针后移即可,但是出队同样要考虑容量的问题,必要的时候需要缩容,缩容的条件是当有效长度小于队列长度的四分之一的时候,同时还有满足此时队列的长度要大于默认的队列长度,才可以进行缩容的操作
@Override
public E dequeue() {
// TODO Auto-generated method stub
if(isEmpty()) {
throw new NullPointerException("队列为空");
}
E e=data[front];
front=(front+1)%data.length;
size--;
if(size<=data.length/4&&data.length>DEFAULT_SIZE) {
//缩容
resize(data.length/2+1);
}
return e;
}
- resize():改变容量大小的一个方法,缩容和扩容都需要调用这个方法
第一种扩容的情况就是建立一个新的队列,长度是原来的二倍,直接把原来的队列放到新的队列中,第二种则是需要从头开始遍历直至尾指针所在的位置,然后将遍历结果放到新数组中,头指针和尾指针做出相应的更新。
缩容和扩容的情况一致,这里就不做过多的介绍了,如果看到文章的你有问题的话,可以评论区call我
private void resize(int newLen) {
// TODO Auto-generated method stub
E[] newData=(E[]) new Object[newLen];
int index=0;//表示新数组的角标
for(int i=front;i!=rear;i=(i+1)%data.length) {
newData[index++]=data[i];
}
front=0;
rear=index;
data=newData;
}
- getFront():获得对头元素
@Override
public E getFront() {
// TODO Auto-generated method stub
return data[front];
}
- getRear():获取队尾元素(要注意是循环队列)
@Override
public E getRear() {
// TODO Auto-generated method stub
return data[(data.length+rear-1)%data.length];
}
- toString():toString()方法的重写,拼接字符串
@Override
public String toString() {
// TODO Auto-generated method stub
StringBuilder sb=new StringBuilder();
sb.append("ArrayQueueLoop:size="+getSize()+",capacity="+(data.length-1)+"\n");
if(isEmpty()) {
sb.append("[]");
}else {
sb.append('[');
for(int i=front;i!=rear;i=(i+1)%data.length) {
sb.append(data[i]);
if((i+1)%data.length==rear) {
sb.append(']');
}else {
sb.append(',');
}
}
}
return sb.toString();
}
- 测试类:
public static void main(String[] args) {
// TODO Auto-generated method stub
ArrayQueueLoop<Integer> queue=new ArrayQueueLoop<Integer> ();
/* for(int i=1;i<=10;i++) {
queue.enqueue(i);
}
System.out.println(queue);*/
for(int i=1;i<=15;i++) {
queue.enqueue(i);
}
System.out.println(queue);
for(int i=1;i<=10;i++) {
queue.dequeue();
}
System.out.println(queue);
}
运行结果:
好啦,队列的顺序结够总结完啦,不知不觉都这么晚了。。。。。