數據結構——順序存儲結構(隊列)

說到隊列其實很簡單,第一反應肯定就是排隊買奶茶。排隊的一隊人就可以看成是一個隊列結構,先來的人排在前面先點餐然後拿到自己要的東西就先走了。走了以後,後面的人一個一個的向前移動。後來的人總是排在後面的。(槓精的不要說可能後來的點的餐會先好,然後比先來的先走。我們按常規來好嗎?)

隊列

所以隊列與棧結構是反正來的,隊列說:先到先得啊;而棧說:先來的沒肉次,晚點來。(兩個死對頭打一架,看誰能贏。咳咳,扯遠了,迴歸正題!)那麼順序表中隊列也是拿數組實現的,那麼它也有一個總接口,話不多說,上圖和代碼:

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

好了,終於把循環隊列這個骨頭啃得碎碎的了,困死我了,睡覺了!

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