1. 程式人生 > 實用技巧 >佇列初相識

佇列初相識

什麼是佇列

棧是先進者後出,後進者先出,而佇列是先進者先出,對於在記憶體中如何儲存並沒有要求,如果通過陣列來實現叫做順序佇列,如果通過連結串列實現,則叫做鏈式佇列,可以看出上圖就是順序佇列,就好比地鐵過安檢時,每個人排隊,依次進入檢查

順序佇列

佇列的特點是先進者先出,所以需要兩個指標,一個指標指向第一條資料,另一個指向最後一條資料

因為陣列建立時便已經指定大小,所以順序佇列的儲存空間有限,為了提高空間的利用率,每當有出對操作時,我們可以進行資料搬移

但是如果頻繁的進行資料搬移,會使得效能降低,所以當沒有空閒空間時,我們只需要在入隊時,集中觸發一次資料的搬移操作即可.

public class ArrayQueue<E> {
    // 陣列:items,陣列大小:n
    private E[] items;
    private int n = 0;
    // head表示隊頭下標,tail表示隊尾下標
    private int head = 0;
    private int tail = 0;

    // 申請一個大小為capacity的陣列
    public ArrayQueue(int capacity) {
        items = (E[])new Object[capacity];
        n = capacity;
    }

    // 入隊操作,將item放入隊尾
    public boolean enqueue(E item) {
        // tail == n表示佇列末尾沒有空間了
        if (tail == n) {
            // tail ==n && head==0,表示整個佇列都佔滿了
            if (head == 0) return false;
            // 資料搬移
            for (int i = head; i < tail; ++i) {
                items[i-head] = items[i];
            }
            // 搬移完之後重新更新head和tail
            tail -= head;
            head = 0;
        }

        items[tail] = item;
        ++tail;
        return true;
    }

    // 出隊
    public E dequeue() {
        // 如果head == tail 表示佇列為空
        if (head == tail) return null;
        E ret = items[head];
        ++head;
        return ret;
    }
}

迴圈佇列

上面使用陣列實現了佇列的基本操作,但是總會涉及到搬移資料,這樣入隊操作的效能就會受到影響,那有沒有辦法避免資料搬移資料呢?

從上圖我們可以看出這是一個大小為9,head=1,tail=5的佇列,再有資料入隊時,將會放到下標為5的位置,tail+1,但是當tail=8時,如何處理呢?

從圖中我們可以很明顯的看出當有資料入隊時,資料放到下標為8的位置,而tail則要更新為0,轉化為程式碼時,我們要如何判斷呢?

使用陣列實現佇列時,個人覺得:最關鍵的是,確定隊空和隊滿的判定條件.

在用陣列實現的非迴圈佇列中,隊滿的判斷條件是 tail == n,隊空的判斷條件是 head == tail。那針對迴圈佇列,如何判斷隊空和隊滿呢?

佇列為空的判斷條件仍然是 head == tail。但佇列滿的判斷條件是什麼呢?

在一般情況下,隊滿的判斷條件為 tial + 1 = head

但是有個特殊情況, head = 0, tail = n-1, 如下圖所示

綜上所述:

當head !=0時,
	tail+1=head
當head =0時,
	tail+1=n
因為tail+1的最大值為n
所以
(tail+1)%n=head

程式碼實現如下

public class CircularQueue<E> {
  // 陣列:items,陣列大小:n
  private E[] items;
  private int n = 0;
  // head表示隊頭下標,tail表示隊尾下標
  private int head = 0;
  private int tail = 0;

  // 申請一個大小為capacity的陣列
  public CircularQueue(int capacity) {
    items = (E[])new Object[capacity];
    n = capacity;
  }

  // 入隊
  public boolean enqueue(E item) {
    // 佇列滿了
    if ((tail + 1) % n == head) return false;
    items[tail] = item;
    tail = (tail + 1) % n;
    return true;
  }

  // 出隊
  public E dequeue() {
    // 如果head == tail 表示佇列為空
    if (head == tail) return null;
    E ret = items[head];
    head = (head + 1) % n;
    return ret;
  }
}

連結串列佇列

public class QueueByList<E> {

  // 佇列的隊首和隊尾
  private Node head = null;
  private Node tail = null;

  // 入隊
  public void enqueue(E value) {
    if (tail == null) {
      Node newNode = new Node(value, null);
      head = newNode;
      tail = newNode;
    } else {
      tail.next = new Node(value, null);
      tail = tail.next;
    }
  }

  // 出隊
  public E dequeue() {
    if (head == null) return null;

    E value = (E) head.data;
    head = head.next;
    if (head == null) {
      tail = null;
    }
    return value;
  }

  //結點
  private static class Node<E> {
    private E data;
    private Node next;

    public Node(E data, Node next) {
      this.data = data;
      this.next = next;
    }

    public E getData() {
      return data;
    }
  }
}

阻塞佇列

阻塞佇列是一個支援兩個附加操作的佇列。

  1. 當佇列滿時,佇列會阻塞插入元素的執行緒,直到佇列不滿。
  2. 當佇列為空時,獲取元素的執行緒會等待佇列變為非空

阻塞佇列常用於生產者和消費者的場景,生產者是往佇列裡新增元素的執行緒,消費者是從佇列裡拿元素的執行緒。阻塞佇列就是生產者存放元素的容器,而消費者也只從容器裡拿元素。

JDK7提供了7個阻塞佇列。分別是

  • ArrayBlockingQueue :一個由陣列結構組成的有界阻塞佇列。
  • LinkedBlockingQueue :一個由連結串列結構組成的有界阻塞佇列。
  • PriorityBlockingQueue :一個支援優先順序排序的無界阻塞佇列。
  • DelayQueue:一個使用優先順序佇列實現的無界阻塞佇列。
  • SynchronousQueue:一個不儲存元素的阻塞佇列。
  • LinkedTransferQueue:一個由連結串列結構組成的無界阻塞佇列。
  • LinkedBlockingDeque:一個由連結串列結構組成的雙向阻塞佇列。