# 数据结构 ☞ 双端队列

本节介绍队列的一种变型⎯⎯双端队列（Double-ended queue），简称为Deque。顾名思义， 也就是前端与后端都支持插入和删除操作的队列。

-[双端队列的ADT](#双端队列的ADT) -[双端队列的接口](#双端队列的接口) -[基于双向链表实现的双端队列](#基于双向链表实现的双端队列) -[Java实现双端队列](#Java实现双端队列)

## 双端队列的ADT

相比于栈和队列，双端队列的抽象数据类型要复杂很多，其中基本的方法如下：

![deque01](https://735524062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MRbj3eYUlD_MfAN3jI9%2Fuploads%2Fgit-blob-bbaefd8aa3797617874949ad62bedc8b476ed7b4%2Fdeque01.png?alt=media)

此外，还可能支持以下方法：

![deque02](https://735524062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MRbj3eYUlD_MfAN3jI9%2Fuploads%2Fgit-blob-73c42f82a3e13965c45d567d2df7137ab1365fca%2Fdeque02.png?alt=media)

给出了从一个空双端队列开始，在依次执行一系列操作的过程中，队列中内容的相应变化。

![deque03](https://735524062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MRbj3eYUlD_MfAN3jI9%2Fuploads%2Fgit-blob-586a039911b263909fa7ad076ff271a407afad7f%2Fdeque03.png?alt=media)

## 双端队列的接口

双端队列接口如代码所示

```java
public interface Deque<E> {
    /**
     * 返回队列中元素数目
     * @return
     */
    int size();

    /**
     * 判断队列是否为空
     * @return
     */
    boolean isEmpty();

    /**
     * 取首元素（但不删除）
     * @return
     * @throws ExceptionQueueEmpty
     */
    E first() throws ExceptionQueueEmpty;

    /**
     * 取末元素（但不删除）
     * @return
     * @throws ExceptionQueueEmpty
     */
    E last() throws ExceptionQueueEmpty;

    /**
     * 将新元素作为首元素插入
     * @param obj
     */
    void addFirst(E obj);

    /**
     * 将新元素作为末元素插入
     * @param obj
     */
    void aadLast(E obj);

    /**
     * 删除首元素
     * @return
     * @throws ExceptionQueueEmpty
     */
    E removeFirst() throws ExceptionQueueEmpty;

    /**
     * 删除末元素
     * @return
     * @throws ExceptionQueueEmpty
     */
    E removeLast() throws ExceptionQueueEmpty;

    /**
     * 遍历
     */
    void iterator();
}
```

在前面讲过我们需要花费O(n)的时间才能从单链表中删除末节点。因此，若依然采 用单链表来实现双端队列，将不可能同时支持对首、末节点的快速删除。为此，我们需要对单链表 结构进行必要的扩充，以保证无论是哪一端的删除操作，都能够在O(1)的时间内完成。本节将要介 绍的双向链表就能够满足这一要求。顾名思义，这类链表中的每一节点不仅配有next引用，同时还 有一个prev引用，指向其直接前驱节点（没有前驱时为null）。代码 给出了双向链表节点类型的 具体实现：

```java
public class Node<E> implements Position<E> {
    //数据对象
    private E element;
    //指向前驱节点
    private Node<E> prev;
    //指向后继节点
    private Node<E> next;

    public Node() {
        this(null,null,null);
    }

    /**
     *  注意三个参数的次序：数据对象、前驱节点、后继节点
     * @param e 数据对象
     * @param p 前驱节点
     * @param n 后继节点
     */
    public Node(E e, Node<E> p, Node<E> n)
    {
        element = e;
        prev = p;
        next = n;
    }


    @Override
    public E getElem() {
        return element;
    }

    /**
     * 将给定的元素放入该位置，并返回原来的元素
     * @param e 插入元素
     * @return
     */
    @Override
    public E setElem(E e) {
        E oldElem = element;
        element = e;
        return oldElem;
    }

    /**
     * 找到后继位置
     * @return
     */
    public Node<E> getNext()
    { 
        return next; 
    }

    /**
     * 找到前驱位置
     * @return
     */
    public Node<E> getPrev()
    { 
        return prev; 
    }

    /**
     * 修改后继位置
     * @param newNext
     */
    public void setNext(Node<E> newNext)
    { 
        next = newNext; 
    }

    /**
     * 修改前驱位置
     * @param newPrev
     */
    public void setPrev(Node<E> newPrev)
    { 
        prev = newPrev; 
    }
}
```

## 基于双向链表实现的双端队列

![deque04](https://735524062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MRbj3eYUlD_MfAN3jI9%2Fuploads%2Fgit-blob-75820202406018fbcd2e0abc039bf8d6d5006aa8%2Fdeque04.png?alt=media)

在基于Node类实现双向链表的时候，为了使编程更加简洁，通常我们都要在最前端和最后 端各设置一个哑元节点（Dummy node）。这两个节点分别称作头节点（Header node）和尾节点 （Trailer node）㈠，起哨兵（Sentinel）的作用。也就是说，它们并不存储任何实质的数据对象，头 （尾）节点的next（prev）引用指向首（末）节点，而prev（next）引用为空。如此构成的双向链表 结构，如上图所示。

* 首、末节点的插入 对双端队列的插入操作，与单链表类似。图 给出了在链表前端插入节点的过程。 ![deque05](https://735524062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MRbj3eYUlD_MfAN3jI9%2Fuploads%2Fgit-blob-3b263523f5aeddc25dca14912bfb4e7b0e06cdeb%2Fdeque05.png?alt=media)
* 首、末节点的删除 更重要的是，我们可以在O(1)时间内删除双向链表任何一端的节点。如图 所示的，就是 删除末节点的过程。首节点的删除过程是对称的，请读者自行给出。 ![deque06](https://735524062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MRbj3eYUlD_MfAN3jI9%2Fuploads%2Fgit-blob-98917f5a9b612a1490833ce6250364e2f15a7b22%2Fdeque06.png?alt=media) 需要特别指出的是，与单链表不同，这里能够在常数时间内删除末节点。之所以能够这样，是 因为可以直接通过末节点的prev 引用找到它的直接前驱（新的末节点），而无需遍历整个链表。
* 一般节点的插入与删除 ![deque07](https://735524062-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-MRbj3eYUlD_MfAN3jI9%2Fuploads%2Fgit-blob-3cb750da3cdceb06609dae1eb130c0f1f8d90299%2Fdeque07.png?alt=media)

一般地，若要在相邻的一对节点p 和q 之间插入新元素e，我们只需生成一个新节点t，将e 存放至其中，然后将t 的prev 和next 引用分别指向p 和q，然后让p（q）的next（prev）引用指向t。

反过来。若要删除节点t，我们可以找到其直接前驱节点p 和直接后继节点q，然后将p（q） 的next（prev）引用指向q（p）。对于节点t，我们无需做任何处理⎯⎯既然它不再被引用，就将被 内存回收器回收。请读者自行画出具体过程。

需要特别指出的是，p 和q 都有可能是哨兵节点。请读者自行验证：即使p 或（和）q 是头（尾） 节点，上述操作过程依然正确。

## Java实现双端队列

```java
public class LinkedDeque<E> implements Deque<E>{

    //指向头节点（哨兵）
    protected Node<E> header;
    //指向尾节点（哨兵）
    protected Node<E> trailer;
    //队列中元素的数目
    protected int size;

    public LinkedDeque() {
        header = new Node<E>();
        trailer = new Node<E>();
        header.setNext(trailer);
        trailer.setPrev(header);
        size = 0;
    }

    @Override
    public int size() {
        return size;
    }

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

    @Override
    public E first() throws ExceptionQueueEmpty {
        if (isEmpty()){
            throw new ExceptionQueueEmpty("意外：双端队列为空");
        }
        return header.getNext().getElem();
    }

    @Override
    public E last() throws ExceptionQueueEmpty {
        if (isEmpty()){
            throw new ExceptionQueueEmpty("意外：双端队列为空");
        }
        return trailer.getPrev().getElem();
    }

    @Override
    public void addFirst(E obj) {
        Node<E> second = header.getNext();
        Node<E> first = new Node<>(obj, header, second);
        second.setPrev(first);
        header.setNext(first);
        size++;
    }

    @Override
    public void aadLast(E obj) {
        Node<E> second = trailer.getPrev();
        Node<E> first = new Node<E>(obj, second, trailer);
        second.setNext(first);
        trailer.setPrev(first);
        size++;
    }

    @Override
    public E removeFirst() throws ExceptionQueueEmpty {
        if (isEmpty()) {
            throw new ExceptionQueueEmpty("意外：双端队列为空");
        }
        Node<E> first = header.getNext();
        Node<E> second = first.getNext();
        E obj = first.getElem();
        header.setNext(second);
        second.setPrev(header);
        size--;
        return obj;
    }

    @Override
    public E removeLast() throws ExceptionQueueEmpty {
        if (isEmpty()) {
            throw new ExceptionQueueEmpty("意外：双端队列为空");
        }
        Node<E> first = trailer.getPrev();
        Node<E> second = first.getPrev();
        E obj = first.getElem();
        trailer.setPrev(second);
        second.setNext(trailer);
        size--;
        return obj;
    }

    @Override
    public void iterator() {
        Node<E> p = header.getNext();
        while (p != trailer) {
            System.out.print(p.getElem()+" ");
            p = p.getNext();
        }
        System.out.println();
    }
}
```
