说到队列其实很简单,第一反应肯定就是排队买奶茶。排队的一队人就可以看成是一个队列结构,先来的人排在前面先点餐然后拿到自己要的东西就先走了。走了以后,后面的人一个一个的向前移动。后来的人总是排在后面的。(杠精的不要说可能后来的点的餐会先好,然后比先来的先走。我们按常规来好吗?)
队列
所以队列与栈结构是反正来的,队列说:先到先得啊;而栈说:先来的没肉次,晚点来。(两个死对头打一架,看谁能赢。咳咳,扯远了,回归正题!)那么顺序表中队列也是拿数组实现的,那么它也有一个总接口,话不多说,上图和代码:
package 队列;
public interface Queue<E> {
/**
* 获取队列的有效长度
* */
public int getSize();
/**
* 判断当前队列是否为空
* */
public boolean isEmpty();
/**
* 清空队列
* */
public void clear();
/**
* 入队一个新元素e
* */
public void enqueue(E e);
/**
* 出队一个元素e
* */
public E dequeue();
/**
* 获取队首元素(不删除)
* */
public E getFront();
/**
* 获取队尾元素(不删除)
* */
public E getRear();
}
在JavaAPI中,队列Queue是在util包下的接口,他有一个实现子类,是用数组实现的叫ArrayDeque,我们在这里实现这个子类,就叫ArrayQueue吧,其实也就是那ArrayList实现的。下面是它的类图和代码实现:
package 队列;
import 线性表.ArrayList;
public class ArrayQueue<E> implements Queue<E> {
private ArrayList<E> list; //该队列拿线性表实现
public ArrayQueue() { //创建一个默认的容量大小的队列
list=new ArrayList<E>();
}
public ArrayQueue(int capacity){ //创建一个指定容量大小的队列
list=new ArrayList<E>(capacity);
}
@Override
public int getSize() { //获取有效元素的个数
return list.getSize(); //调用list中的方法
}
@Override
public boolean isEmpty() { //判空
return list.isEmpty();
}
@Override
public void clear() { //清空队列
list.clear();
}
@Override
public void enqueue(E e) { //入队一个元素
list.addLast(e); //入队在队尾插入元素
}
@Override
public E dequeue() { //出队一个元素
return list.removeFirst(); //出队删除的是头元素,list内部的removeFirst方法,将队头删除之后,再把后面的元素都往前都移动了一个元素的单位
}
@Override
public E getFront() { //获取队头元素
return list.getFirst(); //指的是数组的头元素
}
@Override
public E getRear() { //获取队尾元素
return list.getLast(); //指的是有效元素的最后一个元素
}
@Override
public String 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) {
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;
}
}
说到栈和队列两个是死对头的话,那么栈都有双端栈了,队列怎么能认输呢!因此就出来了一个循环队列,它比双端栈更麻烦,要论输赢的话,循环队列比双端栈更胜一筹喔。现在我们来重点说说循环队列的结构。
循环队列
当时用普通的队列的时候我们发现,入队的时候只在队尾进行插入元素进行了,它的时间复杂度为O(1),是比较方便的;但是在出队的时候就没那么简单方便了,队首出去一个元素,后面的元素相对的就要往前移动。那么算下来时间复杂度就是O(n)。为了优化这一步,我们采取双指针法,将一个指针设置成头指针,表示队头的位置;另一个代表队尾,表示尾部的位置。那么每入队一个元素,尾指针向后移动一位;每出队一个元素,头指针也向后移动一位,如下图:
这样入队和出队的时间复杂度都为O(1)了,但是当达到一个特殊位置的时候,如下图:
当达到队尾了,Rear指针就不能继续向后移动了,如果此时选择扩容的话,前面会有一部分空间浪费。How to do?——循环了解一下。如何循环?当移到末尾的时候再将尾指针移回来,相当于一个循环的队列:
这样空间就不会浪费了,那么如何判满呢?我们发现当Rear指针移动到Front指针位置时,队列就满了,也是(Rear+1)%data.length==Front时,循环队列满了;那么何时为空?也就是Front移动到Rear位置时就空了(如下图所示),那也是(Rear+1)%data.length==Front,
我去,判满判空条件一样?How to do?真是一波未平,一波又起啊!为了解决这样一个问题,我们毅然决定,浪费一个空间,将Rear指针指向元素即将进栈的位置,预留一个位置,这样当尾指针要移动到下一个位置等于头指针此时的位置时,也就是(Rear+1)%data.length==Front时,元素不能再进了,表示满。当出队Front移动到尾指针的位置,Rear==Front时,表示栈空。如下图所示:
好了好了,该说的终于说完了,现在我们开始写代码:
package 队列;
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; //有效元素个数为零
}
@Override
public int getSize() { //获取有效元素的个数
return size;
}
@Override
public boolean isEmpty() { //判空指的是头尾指针在一起且有效元素个数为1
return front==rear&&size==0;
}
@Override
public void clear() { //清空
size=0; //有效元素个数为零
front=0; //头尾指针都在数组头
rear=0;
}
@Override
public void enqueue(E e) { //入队
if((rear+1)%data.length==front){ //入队判满
//【扩容】
resize(data.length*2-1); //满了扩容
}
data[rear]=e; //将元素入队
rear=(rear+1)%data.length; //尾指针位置
size++;
}
private void resize(int newLen) { //扩/缩容
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; //别忘了替换旧数组喔
}
@Override
public E dequeue() { //出队
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; //最后将小崽子显示出去
}
@Override
public E getFront() { //获取头元素
return data[front];
}
@Override
public E getRear() { //获取尾元素
return data[(data.length+rear-1)%data.length];
}
@Override
public String toString() {
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();
}
}
好了,终于把循环队列这个骨头啃得碎碎的了,困死我了,睡觉了!