数据结构——顺序存储结构(队列)

说到队列其实很简单,第一反应肯定就是排队买奶茶。排队的一队人就可以看成是一个队列结构,先来的人排在前面先点餐然后拿到自己要的东西就先走了。走了以后,后面的人一个一个的向前移动。后来的人总是排在后面的。(杠精的不要说可能后来的点的餐会先好,然后比先来的先走。我们按常规来好吗?)

队列

所以队列与栈结构是反正来的,队列说:先到先得啊;而栈说:先来的没肉次,晚点来。(两个死对头打一架,看谁能赢。咳咳,扯远了,回归正题!)那么顺序表中队列也是拿数组实现的,那么它也有一个总接口,话不多说,上图和代码:

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();
	}
}

好了,终于把循环队列这个骨头啃得碎碎的了,困死我了,睡觉了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章