數據結構梳理(4) -隊列

前言

上次說完了棧,今天我們再來看看它的好兄弟----隊列,大致的梳理內容和棧差不多,不過在實際應用當中,隊列相比棧來說,有很多的變種,而且它們使用都非常的廣泛,我們除了要會最基本的隊列的實現,還要擴展下知識廣度,知道隊列的一系列變種以及使用等。

目錄

一、隊列的特性及種類
二、基於數組實現隊列
三、基於鏈表實現隊列
四、jdk源碼中的Queue實現
五、優先級隊列
六、阻塞隊列
七、雙端隊列

正文

一、隊列的特性及變種

和棧一樣,隊列的特性也可以用四個字來概括,那就是“先進先出",和棧不同,棧是隻能操作一端,都是棧頂操作,而隊列是在隊尾插入元素,在隊頭刪除元素,非常類似排隊的效果,如果喜歡弄混,可以想一想自己平時排隊,是不是都是在隊尾去開始排,等排到隊頭了,也就離開隊列了,這個是隊列的基本特性。

隊列因其先進先出的性質,應用比較廣泛的就是消息隊列,例如在各種異步處理中,當然我們可能發現在平常的開發過程中,聽到的隊列,都是諸如“阻塞隊列”,“優先級隊列”等名詞,真正單純的隊列使用比較少,其實這些都是基於隊列的變種,都是在隊列上“添枝加葉”,然後爲了方便的實現業務場景的需求,才衍生出來的,具體各種不同的類型也有很多,我們沒有必要去全部掌握它們,只要挑幾個比較典型的掌握之後,然後遇到問題時,也可以自己訂製一個這樣特別的隊列來滿足需求,後面會挑幾個典型的特殊隊列來一起學習。

然後,再放張圖加深一下對隊列的映像
在這裏插入圖片描述

二、基於數組實現隊列

我們按照同樣的套路,首先是手動實現一遍隊列這個數據結構,老規矩,先基於數組實現,然後是基於鏈表,好了,開始吧!

有了之前實現棧的經驗,我們同樣的,首先確定基本的成員變量及其初始化工作,因爲是基於數組,所以我們聲明一個數組成員變量來存放數據,然後爲了實現入隊出隊這兩個操作,我們需要分別聲明兩個指針,然後再加上一個隊列當前數據數量的size變量。ok,成員變量基本就是這麼多,然後就是初始化的問題,初始化主要是使用默認大小初始化數組,然後隊頭指針和隊尾指針,一般根據代碼風格,指向0或者-1,都可以,最終代碼如下

private static final int length_default=10;//隊列默認的大小
private int[] arr;
private int head;//隊頭
private int tail;//隊尾
private int size;//隊列元素個數

public ArrayQueue() {
	this(length_default);
}

public ArrayQueue(int length) {
	arr=new int[length];
	head=0;
	tail=-1;
}

接下來就是核心操作入隊和出隊的操作,剛纔說了,我們實現這兩個操作,最核心的點就是藉助兩個隊頭隊尾的指針,我們先來思考入隊操作,當一個元素要插入,我們的原則是在隊尾插入,所以應該是移動隊尾指針,隊尾指針默認是指向-1的,所以我們將隊尾指針++,然後在隊尾指針處填入插入的數據,這樣就實現了隊尾的數據插入,然後同樣的,我們再思考出隊,出隊是在隊頭進行的操作,所以相應的也是移動隊頭這個指針,當我們要出隊的時候,只需將隊頭指針++即可,這樣就實現了出隊操作。

但是爲了提高數組的控件利用率,當我們的數組中存放的數據元素小於數組長度,但是隊頭指針和隊尾指針又指向了數組最後一個元素,那麼可使用取餘操作,來將指針指向0,實現循環效果,這樣只有當數組中的元素數量達到隊列容量時,也就是存滿了,纔會提示隊列已滿。

最終入隊和出隊的代碼如下

//在隊尾插入數據
public void enQueue(int data){
	if(isFull()){
		throw new RuntimeException("隊列滿啦");
	}	
	tail=(tail+1)%arr.length;
	arr[tail]=data;
	size++;
}

//在隊頭刪除數據
public void deQueue(){
	if(isEmpty()){
		throw new NullPointerException("隊列爲空");
	}
	head=(head+1)%arr.length;
	size--;
}

同樣的,爲了使用的方便,我們最好添加一些輔助方法,例如判空,判滿,獲取隊頭和隊尾的元素等,我最後一共添加了如下輔助方法

public int getHead(){
	if(isEmpty()){
		throw new RuntimeException("隊列爲空");
	}
	return arr[head];
}

public int getTail(){
	if(isEmpty()){
		throw new RuntimeException("隊列爲空");
	}
	return arr[tail];
}

public int getSize(){
	return size;
}

public boolean isEmpty(){
	return size==0;
}

public boolean isFull(){
	return size==arr.length;
}

public void clear(){
	head=0;
	tail=-1;
	size=0;
}

@Override
public String toString() {
	if(isEmpty()){
		return "null";
	}
	String str = "[ ";  
	int j=head;
       for (int i = 0; i < size; i++) {  
           str += arr[j%arr.length] + ", ";  
           j++;
       }  
       str = str.substring(0, str.length()-2) + " ]";  
       return str;
}

最後,不要忘了寫測試用例測試它哦!

三、基於鏈表實現隊列

上面實現完了基於數組版本的,接下來實現基於鏈表的,由於和上面差不多,我就不贅述了,首先是節點和成員變量的定義,然後就是初始化工作,代碼如下

public class Node{//節點內部類
	private int data;
	private Node next;
	
	public Node() {
		
	}
	
	public Node(int data,Node next){
		this.data=data;
		this.next=next;
	}
	
	public int getData(){
		return data;
	}
}

private Node head;//隊頭
private Node tail;//隊尾
private int size;

public LinkedQueue() {//構造函數
	head=null;
	tail=null;
	size=0;
}

然後是出隊和入隊的操作,思想和上面的類似

public void enQueue(int data){
	Node node=new Node(data, null);
	if(head==null){
		head=node;
	}else{
		tail.next=node;
	}
	tail=node;
	size++;
}

public void deQueue(){
	if(head==null){
		throw new NullPointerException("隊列爲空啦");
	}
	if(head.next == null){
		tail = null;
	}
	head=head.next;
	size--;
}

然後是一些輔助方法,和上面數組實現的一樣,如下

public int getHead(){
	return head.data;
}

public int getTail(){
	return tail.data;
}

public int getSize(){
	return size;
}

public boolean isEmpty(){
	return size==0;
}

public void clear(){
	head=null;
	tail=null;
	size=0;
}

@Override
public String toString() {
	if (isEmpty()) {
		return "null";
	} else {
		StringBuilder sb = new StringBuilder("");
		for (Node current = head; current != null; current = current.next)// 從head開始遍歷
		{
			sb.append(current.data + "-");
		}
		int len = sb.length();
		return sb.delete(len - 1, len).append("").toString();// 刪除最後一個 -
	}
}

同樣的,不要忘了測試。

四、jdk源碼中的Queue實現

老規矩,在我們自己實現了隊列之後,我們再來看看優秀的jdk設計者是怎麼實現隊列的,隊列在jdk中對應的類是Queue,我們點開它的源碼,去掉註釋之後如下

public interface Queue<E> extends Collection<E> {

    boolean add(E e);
    
    boolean offer(E e);

    E remove();

    E poll();


    E element();

    E peek();
}

我們發現它只是一個接口,提供了一些最基礎的隊列操作,所以我們要看源碼,得看它的實現類,它的實現類也不只一個,最常用的就是LinkList,但是我們點開LinkList的源碼,如下

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{

發現它並不是直接實現的Queue這個接口,而是實現的Deque這個接口,我們再點開Deque這個接口,如下

public interface Deque<E> extends Queue<E> {

原來DequeQueue的一個子接口,其實這個接口就是擴展了一些雙端隊列的操作,雙端隊列待會再詳細介紹,然後LinkList實現了這個接口,所以其實就是一個雙端隊列,我們來看看LinkList中對應的入隊出隊方法,先看入隊方法,如下

public boolean offer(E e) {
    return add(e);
}

調用了add方法,繼續追蹤,如下

public boolean add(E e) {
    linkLast(e);
    return true;
}

繼續追蹤linkLast方法,如下

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

很明顯,我們可以看到這是一個在鏈表尾端插入元素的代碼,即隊尾入隊,這個和我們的相比,沒有什麼太大的區別。

接下來我們看看出隊方法,如下

public E poll() {
    final Node<E> f = first;
    return (f == null) ? null : unlinkFirst(f);
}

繼續追蹤unlinkFirst方法

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC
    first = next;
    if (next == null)
        last = null;
    else
        next.prev = null;
    size--;
    modCount++;
    return element;
}

我們發現就是一個刪除鏈表第一個節點的方法,也就是對應隊頭出隊的效果,和我們上面的實現也差不多,不過我們可以看到中間有一個置空操作,後面還貼心的加了註釋,是help GC,也就是將刪除的元素置空幫助垃圾回收,這個是我們可以借鑑的。

當然源碼的實現,除了上面說的,還有源碼中的隊列實現也是泛型實現的容器,這個在上一篇實現棧的時候已經提到過了,泛型實現的可以兼容所有類型的元素,這裏是我們應該優化的,我就不贅述了。

五、優先級隊列

接下來我們再來學習下實際開發中經常碰到的幾種隊列,首先就拿優先級隊列來說吧。

5.1 優先級隊列的特點及使用

優先級隊列,顧名思義,就是在隊列的概念上,加了個優先級的東東,具體是什麼特點呢,就是在出隊的時候,並不是按照入隊的順序來出隊,而是按照優先級來出隊,誰的優先級高,誰就先出隊,具體的設置每個元素優先級的方式可藉助比較器,也就是Comparator這個類。

爲了方便理解,我們來看一個例子,jdk中PriorityQueue類就是優先級隊列的實現,下面是一個基本用法,主要加深對優先級隊列的理解

//優先級隊列的使用,JDK實現是使用的  小根堆實現的
//自己手動實現的話,和普通隊列不同的也就一個方法,就是插入方法,需要根據優先級插入到合適的位置。
public class TestPriorityQueue {

	public static void main(String args[]) {
		Comparator<People> comparator = new Comparator<People>() {
			public int compare(People o1, People o2) {
				// TODO Auto-generated method stub
				int numbera = o1.getPopulation();
				int numberb = o2.getPopulation();
				if (numberb > numbera) {
					return 1;
				} else if (numberb < numbera) {
					return -1;
				} else {
					return 0;
				}

			}
		};

		Queue<People> priorityQueue = new PriorityQueue<People>(15, comparator);

		People t1 = new People("p1", 1);
		People t3 = new People("p3", 3);
		People t2 = new People("p2", 2);
		People t4 = new People("p4", 0);
		priorityQueue.add(t1);
		priorityQueue.add(t3);
		priorityQueue.add(t2);
		priorityQueue.add(t4);
		System.out.println(priorityQueue.poll().toString());
		System.out.println(priorityQueue.poll().toString());
		System.out.println(priorityQueue.poll().toString());
		System.out.println(priorityQueue.poll().toString());
	}
}

class People {
	private String name;
	private int population;//名聲

	public People(String name, int population) {
		this.name = name;
		this.population = population;
	}

	public String getName() {
		return this.name;
	}

	public int getPopulation() {
		return this.population;
	}

	public String toString() {
		return getName() + " - " + getPopulation();
	}
}

運行這段程序,得到以下結果

p3 - 3
p2 - 2
p1 - 1
p4 - 0

可以看到,我們聲明瞭一個People類,通過Comparator比較器實現了People類的大小比較,也就是優先級比較,具體是比較的People類的population名聲這個字段。然後在PriorityQueue的構造方法中傳入比較器,即可。

最終我們看到輸出結果如預想的,是按照優先級輸出的,也就是按照每個People對象的population字段的值大小來輸出。

5.2 優先級隊列的實現原理

上面我們瞭解了優先級隊列及其使用,現在我們來思考下它是怎麼實現的,要達到按照優先級出隊的效果,我們無非更改兩個核心操作,一個是入隊,一個是出隊,所以是兩個方案。

我們先嚐試更改出隊操作,來實現效果,怎麼實現呢?因爲入隊之後,整個隊列中的元素優先級是混亂的,我並不知道這個隊列中哪個優先級最高,所以我必須每次出隊都遍歷一遍整個隊列,找出優先級最高的來出隊,可想而知,這種方法效率太低,每次出隊,都要遍歷整個隊列,所以我們pass掉。

那麼我們再來嘗試更改入隊操作,我們要保證每次出隊的元素是優先級最高的,那麼也就是隊頭的元素優先級要始終保持爲最高的,所以在入隊的時候,要保證隊列中元素的優先級是一個從隊頭到隊尾優先級遞減的效果,然後出隊操作就自然而然是按照優先級出隊,那麼優先級遞減的效果怎麼保證呢,其實就是一個類似排序的問題,因爲當前序列是有序的,所以可採用二分的思想,先找到元素應該插入的位置,然後再將元素插入入隊即可。

好了,上面的這是我們自己的一些原理實現,按照這個思路去寫一個優先級隊列,是完全ok的,現在我們抱着驗證的心態去看一看jdk中PriorityQueue這個類是不是和我們的思路是一樣的。

點開PriorityQueue的源碼,,由於很多,所以我們看其關鍵代碼,入隊和出隊代碼,入隊代碼如下

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);//擴容
    size = i + 1;
    if (i == 0)//如果是第一個元素
        queue[0] = e;
    else
        siftUp(i, e);//調整,保持堆的性質
    return true;
}

可以看到清晰的思路,首先是空異常處理, 然後是擴容(因爲是基於數組實現),再就是核心方法siftUp()用於調整隊列中的數據,我們再來看這個方法做了什麼

private void siftUp(int k, E x) {
    if (comparator != null)//如果比較器爲空
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}
//下面這兩個方法是用來調整隊列中的數據,使其維持小頂堆的性質
@SuppressWarnings("unchecked")
private void siftUpComparable(int k, E x) {
    Comparable<? super E> key = (Comparable<? super E>) x;
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (key.compareTo((E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = key;
}

@SuppressWarnings("unchecked")
private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

可以發現它將隊列中的數據維護成了一個堆,下標爲0的元素就是堆頂,到這裏我們可以猜想當出隊的時候,我們直接讓堆頂元素出隊即可,抱着這個心態,來看出隊的代碼

@SuppressWarnings("unchecked")
public E poll() {
    if (size == 0)
        return null;
    int s = --size;
    modCount++;
    E result = (E) queue[0];//獲取0號元素,也就是堆頂元素
    E x = (E) queue[s];//記錄最後一個下標處的值
    queue[s] = null;//釋放最後一個元素的空間
    if (s != 0)
        siftDown(0, x);//調整數據繼續保持堆的性質,同時會將記錄的最後一個元素值x插入到前i-1個序列中
    return result;//返回堆頂的元素
}

果然,如預料的一樣,直接返回的堆頂元素。

ok,我們現在再來整理下jdk實現優先級隊列的思路,首先在入隊的時候,進行一些異常和擴容的判斷,然後就是調整隊列中元素的位置,保證新元素插入後,仍然是小根堆的性質,這樣在出隊的時候,就方便多了,直接返回堆頂元素,然後再記錄下最後一個元素的值,及時釋放最後一個元素的空間,再調整除堆頂外的剩下元素的值,使其繼續維持小頂堆的性質。

這時候,我們發現jdk的實現方式和我們的最大區別就是,我們相當於只是一個有序的序列,但是jdk是構建的一個堆,那麼有什麼區別呢,其實這兩種實現方式,在數據量小的時候,確實是沒太大區別,但是一但數據量很大,在入隊的時候,按照我們的思路,在二分找到位置之後,接着插入就會產生大量的數據元素移動,導致效率降低,但是堆結構不一樣,隨着數據量的增加,其維護堆結構的複雜度是遠低於我們之前想的這種方式的,所以這一點是我們應該學習的地方。

5.3 優先級隊列的適用場景

因爲優先級隊列這個特殊的性質,在實際開發中,可能遇到的比較少,更多的使用場景是和業務邏輯掛鉤,所以適用場景就是符合優先級隊列這個性質的業務邏輯場景。

六、阻塞隊列

6.1 阻塞隊列的特點及使用

阻塞隊列,也是顧名思義,就是在出隊和入隊的時候可能會阻塞,入隊的時候, 如果隊列滿了,那麼就阻塞,出隊的時候,如果隊列爲空,那麼就阻塞,ok,這裏的阻塞具體是什麼意思呢,其實就是執行的線程掛起,等待相應條件滿足的時候,就喚醒,繼續執行相關操作。

瞭解了基礎的概念之後,我們再來學習阻塞隊列的使用,阻塞隊列在jdk中對應的類是BlockQueue,但是它只是個接口,真正的實現類是ArrayBlockingQueue好了,下面是一個簡單的使用例子,模擬的生產者消費者模型

final int TOP = 5;
BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
new Thread(new Runnable() {
	int num = 1;

	@Override
	public void run() {
		while (num <= TOP) {
			try {
				queue.put("" + num);
				System.out.println("入隊:" + num);

			} catch (InterruptedException e1) {
				e1.printStackTrace();
			}

			num++;

			try {
				Thread.sleep(1000);
				System.out.println("=============");
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}).start();
new Thread(new Runnable() {
	@Override
	public void run() {
		while (true) {
			try {
				System.out.println("出隊:" + queue.take());
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}).start();

最終運行結果如下

入隊:1
出隊:1
=============
入隊:2
出隊:2
=============
入隊:3
出隊:3
=============
入隊:4
出隊:4
=============
出隊:5
入隊:5
=============

我們簡單看下這段代碼,首先是第一個線程,負責往隊列中每隔一秒添加元素,然後第二個線程不斷的從隊列中取出元素,且必須要隊列中有元素存在,纔可以取到元素,所以每次在出隊之後,都必需要等待入隊纔可以出隊,最終形成一個類似生成者消費者的效果,當然這段代碼只是驗證了take()方法會阻塞,然後我們嘗試註釋掉第二個線程的所有代碼,再看下執行結果,如下

入隊:1
=============
入隊:2
=============
入隊:3
=============

我們發現在入隊三個元素之後,就沒有再繼續打印了,因爲阻塞隊列在聲明的時候,構造方法傳入的3,代表隊列大小,此時隊列已經滿了,所以無法再繼續執行入隊操作,所以此刻線程阻塞了,只有直到隊列中的元素數量小於3,纔可以接着入隊。

6.2 阻塞隊列的實現原理

上面我們瞭解了阻塞隊列的特點和基本的使用,現在我們同樣的,再來思考它的實現原理。

怎麼實現在入隊的時候,如果隊列滿了就掛起,然後一旦隊列不是滿狀態之後,就自動喚醒,執行入隊操作,這個其實就涉及到了線程的相關操作,主要是Condition類,最重要的兩個方法就是await()方法和signal()方法,由於不是本篇的重點, 所以不贅述了,await()方法就是用來掛起線程的,signal()方法就是用來喚醒線程的。

有了Condition這個類之後,我們再來實現阻塞隊列就非常的方便了,我們來看看ArrayBlockingQueue這個類的源碼,首先看put入隊方法的源碼,如下

public void put(E e) throws InterruptedException {
    checkNotNull(e);//判空處理
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();//上鎖
    try {
        while (count == items.length)//如果隊列滿了,那麼就調用await掛起線程
            notFull.await();
        enqueue(e);//一但隊列不滿,上面的循環就跳出,開始進行入隊操作
    } finally {
        lock.unlock();//解鎖
    }
}

追蹤enqueue方法的代碼如下

private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    notEmpty.signal();//喚醒等待執行出隊操作的線程
}

上面的入隊操作很好理解,最重要的就是最下面signal這個操作,因爲當前是入隊操作,會讓隊列中存在元素,也就是不爲空,所以需要喚醒正在阻塞中的執行出隊操作的線程。

接下來,我們其實沒必要再看出隊操作的源碼了,很簡單,只需要把喚醒和等待的線程更換一下即可,在出隊的時候,如果隊列爲空,則死循環阻塞執行出隊的線程,等循環跳出後,執行出隊操作,出隊操作執行完之後,同樣的,執行signal操作來喚醒所有阻塞中的執行入隊操作的線程。

6.3 阻塞隊列的適用場景

關於適用場景這裏,其實最最典型的一個場景就是生產者和消費者模型,或者再抽象一點,就是一個同步問題,在實際開發中,一部分的同步問題就是類似生成者和消費者的業務模型,這個時候你只需要掏出BlockQueue即可解決大部分問題,如果業務場景稍微複雜點的話,既然我們掌握了它的原理,那麼完全可以在它的基礎上再進行相應的擴展和訂製,來滿足複雜的需求。

七、雙端隊列

7.1 雙端隊列的特點及使用

這個也非常的形象,因爲隊列的要求是,必須在隊尾插入元素,在隊頭刪除元素,那麼雙端隊列就是在兩端都可以進行插入和刪除的操作,這就是雙端隊列的特點。

可能有人會奇怪,這個特點有啥用嗎?感覺這個特點沒什麼niao用,是的,我之前也有這個感覺,但是你要知道各種奇怪的場景你都可能遇到,舉個栗子,現在有一倆電車,對電車的一節車廂來說,可以在車廂頭部上車,也可在車廂尾部上車,那麼在經過一次車站之後,列出人數的變化不就正是符合雙端隊列的這個特點嗎,雙端隊列正好用來解決這個問題。

好了,現在對雙端隊列的概念有了瞭解之後,現在再來看看怎麼使用,雙端隊列在jdk源碼對應的也是一個接口,在上面介紹隊列的時候說到了,就是Deque這個接口,Deque這個接口是繼承於Queue這個接口的,由於Deque的接口比較多,所以這裏放一下它的接口列表圖
在這裏插入圖片描述
可以看到和Queue的接口相比,其最核心的擴展方法就是offerFirstofferLastpollFirstpollLats,這四個方法。相信我們即便不看方法的實現,都能猜到這四個方法的具體含義,分別對應隊頭插入,隊尾插入,隊頭刪除,隊尾刪除這四種操作。

然後我們再來看看它的實現類,它有很多實現類,比較常用的就是ArrayDequeLinkedList,它們的區別就是ArrayDeque是不允許元素爲null的,但是LinkedList是允許元素爲null的。這裏我就以ArrayDeque爲例子來學習如何使用雙端隊列。

Deque<String> subway = new ArrayDeque<String>();

System.out.println("第一站上下車情況");
for (int i = 1; i <= 5; i++) {
	subway.offerFirst("車頭" + i);
	subway.offerLast("車尾" + i);
}
System.out.println(subway);

System.out.println("第二站上下車情況");
for (int i = 1; i <= 3; i++) {
	subway.pollFirst();
	subway.pollLast();
}
System.out.println(subway);

運行結果如下

第一站上下車情況
[車頭5, 車頭4, 車頭3, 車頭2, 車頭1, 車尾1, 車尾2, 車尾3, 車尾4, 車尾5]
第二站上下車情況
[車頭2, 車頭1, 車尾1, 車尾2]

總的來說,使用起來還是很方便的,我就不贅述了.

7.2 雙端隊列的實現原理

在瞭解了上面其它隊列之後,相信對於這個雙端隊列的原理,應該還是相對簡單的,只要我們在隊列的相關方法中,增加隊頭插入、隊尾刪除這兩個額外方法即可,而相應的在隊頭和隊尾,我們都是有指針指向的,所以具體的代碼就比較簡單啦,不過我這裏還是去源碼看一下ArrayDeque的核心方法實現,源碼中相關代碼如下

public void addFirst(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[head = (head - 1) & (elements.length - 1)] = e;
    if (head == tail)
        doubleCapacity();
}

public void addLast(E e) {
    if (e == null)
        throw new NullPointerException();
    elements[tail] = e;
    if ( (tail = (tail + 1) & (elements.length - 1)) == head)
        doubleCapacity();
}
public E pollFirst() {
    int h = head;
    @SuppressWarnings("unchecked")
    E result = (E) elements[h];
    // Element is null if deque empty
    if (result == null)
        return null;
    elements[h] = null;     // Must null out slot
    head = (h + 1) & (elements.length - 1);
    return result;
}

public E pollLast() {
    int t = (tail - 1) & (elements.length - 1);
    @SuppressWarnings("unchecked")
    E result = (E) elements[t];
    if (result == null)
        return null;
    elements[t] = null;
    tail = t;
    return result;
}

因爲是基於數組實現的,所以在實現的時候,擴容這個問題要注意一下,其它的沒啥了。

7.3 雙端隊列的適用場景

在應用方面,雙端隊列因其獨特的性質,主要用於一些對稱的場景,例如迴文字符串檢查等,剩下的就是一些需要結合業務邏輯場景來看了。

在雙端隊列的應用中,還有一個非常典型的模式,是非常適合使用雙端隊列來實現的,那就是“工作密取”的模式,在工作密取模式中,每個消費者有其單獨的工作隊列,如果它完成了自己雙端隊列中的全部工作,那麼它就可以從其他消費者的雙端隊列末尾祕密地獲取工作。工作密取模式對比傳統的生產者-消費者模式,更爲靈活,因爲多個線程不會因爲在同一個工作隊列中搶佔內容發生競爭。在大多數時候,它們只是訪問自己的雙端隊列。即使需要訪問另一個隊列時,也是從 隊列的尾部獲取工作,降低了隊列上的競爭程度。

結語

好了,本節到這裏,也差不多介紹完了,從最基礎的隊列實現直到學習隊列的各種變種類型,也介紹了很多,當然說到的這些都是相對基礎的用法和原理,還沒有涉及到併發相關的,當然Java也提供了現成的實現類,不過我就不過多說了,這塊內容還是比較大的,需要慢慢消化。

按照計劃,下一篇也是一個大塊內容,----樹!

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