数据结构梳理(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也提供了现成的实现类,不过我就不过多说了,这块内容还是比较大的,需要慢慢消化。

按照计划,下一篇也是一个大块内容,----树!

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