3,常見數據結構-隊列

想了解更多數據結構以及算法題,可以關注微信公衆號“數據結構和算法”,每天一題爲你精彩解答。也可以掃描下面的二維碼關注
在這裏插入圖片描述
基礎知識
隊列是一種特殊的線性表,他的特殊性在於我們只能操作他頭部和尾部的元素,中間的元素我們操作不了,我們只能在他的頭部進行刪除,尾部進行添加。就像大家排隊到銀行取錢一樣,先來的肯定要排到前面,後來的只能排在隊尾,所有元素都要遵守這個操作,沒有VIP會員,所以走後門插隊的現象是不可能存在的,他是一種先進先出的數據結構。我們來看一下隊列的數據結構是什麼樣的
1,一般隊列
在這裏插入圖片描述
他只能從左邊進,右邊出,隊列實現方式一般有兩種,一種是基於數組的,還一種是基於鏈表的,如果基於鏈表的倒還好說,因爲鏈表的長度是隨時都可以變的,這個實現起來比較簡單。如果是基於數組的,就會稍微有點不同,因爲數組的大小在初始化的時候就已經固定了,我們來看一下基於數組的實現,假如我們初始化一個長度是10的隊列
在這裏插入圖片描述
front指向的是隊列的頭,tail指向的是隊列尾的下一個存儲空間,最初始的時候front=0,tail=0,每添加一個元素tail就加1,每移除一個元素front就加1,但是這樣會有一個問題,如果一個元素不停的加入隊列,然後再不停的從隊列中移除,會導致tail和front越來越大,最後會導致隊列無法再加入數據了,但實際上隊列前面全部都是空的,這導致空間的極大浪費。我們自己來寫一個簡單的隊列看一下

 	public class MyQueue<E> {
 	
 	    private final Object[] data;
 	    private final int maxSize;
 	    private int size;
 	    private int front = 0;
 	    private int tail = 0;
 	
 	    public MyQueue(int maxSize) {
	        if (maxSize <= 0) {
	            throw new IllegalArgumentException("隊列容量必須大於0 : " + maxSize);
	        }
	        this.maxSize = maxSize;
	        data = new Object[this.maxSize];
	    }
	
	    public void add(E e) {
	        if (isFull()) {//這地方可以擴容也可以拋異常,爲了方便這裏我們就不在擴容了。
	            throw new IllegalStateException("隊列已經滿了,無法再加入……");
	        }
	        data[tail++] = e;
	        size++;
	    }
	
	    public E remove() {
	        if (isEmpty()) {
	            throw new IllegalStateException("隊列是空的,無法移除……");
	        }
	        E t = (E) data[front];
	        data[front++] = null;
	        size--;
	        return t;
	    }
	
	    //隊列頭和隊列尾指向同一空間的時候,並且沒到隊尾,表示隊列是空的
	    public boolean isEmpty() {
	        return front == tail && !isFull();
	    }
	
	    public boolean isFull() {//最後一個位置是不存儲數據的
	        return tail == maxSize - 1;
    }

	    public int getSize() {
	        return size;
	    }
	}

代碼非常簡單,當然隊列的實現不一定是這一種方式,比如我們可以讓tail指向隊尾的元素,或者以鏈表的形式來實現都是可以的,不同的實現方式會導致上面的方法有所不同。我們來測試一下

 	public static void main(String[] args) {
 	    MyQueue myQueue = new MyQueue(10);
 	    System.out.println("isEmpty()=" + myQueue.isEmpty());
 	    System.out.println("isFull()=" + myQueue.isFull());
 	    System.out.println("getSize()=" + myQueue.getSize());
 	    for (int i = 0; i < 9; i++) {
 	        myQueue.add(i * 100);
 	        myQueue.remove();
 	    }
	    System.out.println("----------------------------");
	    System.out.println("isEmpty()=" + myQueue.isEmpty());
	    System.out.println("isFull()=" + myQueue.isFull());
	    System.out.println("getSize()=" + myQueue.getSize());
	}

看一下打印的結果

    isEmpty()=true
    isFull()=false
    getSize()=0
    ----------------------------
    isEmpty()=false
    isFull()=true
    getSize()=0

我們添加了9次,然後又移除了9次,結果隊列竟然滿了,如果我們再添加一次的話肯定會拋異常,但實際上隊列的size是0,還是空的,也就是說數組的每個位置只能使用一次,這樣就造成了極大的浪費。那麼前面使用過的空間還能不能再次利用了呢,實際上是可以的,我們可以把隊列看成是環形的,當tail到達數組末尾的時候,如果數組的前面有空位子,我們可以讓tail從頭開始,這個時候一個新的隊列就產生了,那就是雙端隊列。

2,雙端隊列
雙端隊列也是有兩個指針,front指向隊首,tail指向隊尾的下一個存儲單元,我們來看一下圖
在這裏插入圖片描述
這樣空間就可以循環利用了,不會造成浪費,我們來看下代碼

  	public class MyQueue<E> {
  	
  	    //存儲的元素
  	    private Object[] data;
  	
  	    //指向隊列頭,這個頭並不是數組的第0個元素,如果這樣
  	    // front就沒有意義了,這個從下面的addFirst(E e)方
  	    // 法也可以看出,如果當front等於0的時候,在添加到
  	    // first,那麼會添加到數組的末尾,並且front也指向
 	    // 數組的末尾
 	    private int front;
 	
 	    //指向隊列尾的下一個空間,可以這樣理解,front指向
 	    // 的是第一個元素,tail指向的是最後一個元素的下一
 	    // 個,指的是空的。
 	    private int tail;
 	
 	
 	    public MyQueue(int numElements) {
 	        data = new Object[numElements];
 	    }
 	
 	    //空間擴容,我們這裏選擇擴大一倍,當然也可以選擇其
 	    // 他值,僅僅當滿的時候才能觸發擴容, 這時候front
 	    // 和tail都會指向同一個元素
 	    private void doubleCapacity() {
 	        int p = front;
 	        int n = data.length;//數組的長度
 	        //關鍵是r不好理解,舉個例子,在數組中,front
 	        // 不一定是之前0位置的,他可以指向其他位置,
 	        // 比如原來空間大小爲16,front爲13,也就是第
 	        // 14個元素(數組是從0開始的),那麼r就是16-13=3,
 	        // 也就是從front往後還有多少元素,待會copy的時候
 	        // 也是先從最後的r個元素開始
 	        int r = n - p;
 	        Object[] a = new Object[n << 1];//擴大一倍
 	        System.arraycopy(data, p, a, 0, r);//先copy後面的r個
 	        System.arraycopy(data, 0, a, r, p);//再copy前面的p個
 	        data = a;
 	        //重新調整front和tail的值
 	        front = 0;
 	        tail = n;
 	    }
 	
 	    public void addFirst(E e) {
 	        //添加到front的前面,所以front-1
 	        front = (front - 1 + data.length) % data.length;
 	        data[front] = e;
 	        if (front == tail)//判斷是否滿了
 	            doubleCapacity();
 	    }
 	
 	    public void addLast(E e) {
 	        //添加到最後一個,這個方法和addFirst有很明顯的不同,
 	        // addFirst是添加的時候就要計算front的位置,而addLast
 	        // 方法是存值之後在計算tail的,/因爲tail位置是沒有
 	        // 存值的,他表示的末端元素的下一個,是空,所以存值之後
 	        //要計算tail的值
 	        data[tail] = e;
 	        tail = (tail + 1) % data.length;
 	        if (tail == front)//判斷是否滿
 	            doubleCapacity();
 	    }
 	
 	    public E removeFirst() {//刪除第一個
 	        if (isEmpty())
 	            throw new IllegalStateException("隊列是空的,無法移除……");
 	        E result = (E) data[front];
 	        data[front] = null;
 	        // 刪除第一個,這裏的所有第一我們都認爲是front所指的,
 	        // 不是數組的0位置,然後在計算front的值
 	        front = (front + 1) % data.length;
 	        return result;
 	    }
 	
 	    public E removeLast() {//刪除最後一個
            if (isEmpty())
 	            throw new IllegalStateException("隊列是空的,無法移除……");
	        tail = (tail - 1 + data.length) % data.length;
 	        E result = (E) data[tail];
 	        data[tail] = null;
 	        return result;
 	    }
 	
 	    public E peekFirst() {
 	        if (isEmpty())
 	            throw new IllegalStateException("隊列是空的,無法獲取……");
 	        return (E) data[front];
 	    }
 	
 	    public E peekLast() {
 	        if (isEmpty())
 	            throw new IllegalStateException("隊列是空的,無法獲取……");
 	        return (E) data[(tail - 1 + data.length) % data.length];
 	    }
 
 	    public int size() {//元素的size
 	        return (tail - front + data.length) % data.length;
 	    }
	
	    //是否爲空,在上面添加元素的時候也可能front==tail,當添加
	    // 元素之後front==tail的時候就認爲是滿了,然後擴容,重新
	    // 調整front和tail的值,所以擴容之後front就不可能等於tail。
	    //如果沒有觸發上面添加元素的時候front等於tail我們就認爲他是空的。
	    public boolean isEmpty() {
	        return front == tail;
	    }
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章