7、双端队列

一、什么是双端队列

   双端队列(Double-ended Queue,简称deque)是一种线性数据结构,它允许在两端进行插入和删除操作。这意味着与普通队列(FIFO,先进先出)或栈(LIFO,后进先出)不同,双端队列的使用者不仅可以从队列的一端(通常称为“尾”端)添加元素(enqueue),也可以从这一端删除元素(dequeue)。同时,双端队列还支持在另一端(通常称为“头”端)执行相同的操作,即在队列头部添加(enqueue at head)和删除(dequeue at head)元素。

因此,双端队列的特点包括:

  1. 可以在队列头部进行入队和出队操作。
  2. 可以在队列尾部进行入队和出队操作。
  3. 具有动态调整大小的能力(取决于具体实现)。
  4. 既可以作为栈使用(在一端进行插入和删除),又可以作为普通队列使用,还可以用于其他更复杂的数据处理场景,如回文检测、滑动窗口算法等。

二、解决了什么问题

  1. 普通队列(FIFO)只允许在一端(尾部)插入元素,在另一端(头部)删除元素。而在某些情况下,我们可能需要在两端都能进行插入和删除操作。例如,在处理具有“历史”和“未来”概念的场景时,可能既要关注最新的数据也要能随时访问或移除最早的数据。
  2. 栈(LIFO)只能在一端进行插入和删除操作,而双端队列则可以同时作为栈来使用,但更为灵活,因为它可以在两端都执行入栈和出栈操作。
  3. 在算法实现中,双端队列常用于解决一些复杂问题,如滑动窗口最大值、回文串检测、动态规划中的边界更新等,这些场景下往往要求既能向后添加元素,也能向前移除元素。
  4. 双端队列还支持高效地在任意位置插入和删除元素(时间复杂度为O(1)),这使得它非常适合于需要频繁修改序列中间部分的操作。

综上所述,双端队列提供了一种更灵活、功能更强大的线性数据结构,能够适应更多样化的数据处理需求。

三、实现的方式

  双端队列的实现方式主要有两种:数组存储和链表存储

1、数组存储

基本思想:

  • 使用一个动态大小的数组来存储元素,通过维护两个指针(头指针和尾指针)分别记录队列头部和尾部的位置。
  • 当需要在头部插入或删除元素时,只需更新头指针;当在尾部插入或删除元素时,更新尾指针,并可能涉及到数组元素的移动以保持队列连续。

优点:

  • 插入和删除尾部元素的时间复杂度为O(1)。
  • 在某些实现中,在头部插入或删除元素的平均时间复杂度也为O(1)(通过预分配多余空间并适时调整容量实现)。

缺点:

  • 如果频繁在头部插入或删除元素且未预先分配足够的空间,可能导致大量元素移动,此时时间复杂度会退化到O(n)。

2、链表存储

基本思想:

  • 使用双向链表作为底层数据结构,每个节点包含元素以及指向前后节点的指针。
  • 头指针指向队列的第一个元素,尾指针指向队列的最后一个元素。
  • 在头部或尾部插入、删除元素时,只需改变相应指针的指向即可。

优点:

  • 不论是在头部还是尾部进行插入或删除操作,时间复杂度均为O(1)。

缺点:

  • 相对于数组实现,链式存储通常占用更多的内存空间(额外的指针开销)。
  • 随机访问某个元素的效率较低(时间复杂度为O(n))。

在实际应用中,选择何种实现方式取决于具体需求,包括对内存使用、插入/删除效率、随机访问性能等因素的权衡。

四、代码案例

创建一个双端队列公共接口

package deque;

/**
 * 双端队列
 */
public interface Deque<E> {


    /**
     * 在队列开头添加元素
     *
     * @param e
     * @return
     */
    boolean offerFirst(E e);

    /**
     * 在队列结尾添加元素
     *
     * @param e
     * @return
     */
    boolean offerLast(E e);

    /**
     * 在队列开头读取元素,并删除该元素
     *
     * @return
     */
    E pollFirst();

    /**
     * 在队列尾部读取元素,并删除该元素
     *
     * @return
     */
    E pollLast();

    /**
     * 在队列开头查询元素,但不删除
     *
     * @return
     */
    E peekFirst();

    /**
     * 在队列尾部查询元素,但不删除
     *
     * @return
     */
    E peekLast();

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

    /**
     * 检查队列是否已满
     *
     * @return
     */
    boolean isFull();

}

1、链表案例,基于双向循环链表实现

package deque;

/**
 * 使用双向循环链表实现双端队列
 *
 * @param <E>
 */
public class DequeLinkedList<E> implements Deque<E> {

    /**
     * 哨兵
     */
    private Node<E> sentinel = new Node<>(null, null, null);

    /**
     * 队列长度
     */
    private int dequeLength;

    /**
     * 队列值的个数
     */
    private int size;

    /**
     * @param dequeLength 设置队列长度
     */
    public DequeLinkedList(int dequeLength) {
        this.sentinel.prev = sentinel;
        this.sentinel.next = sentinel;
        this.dequeLength = dequeLength;
        this.size = 0;
    }

    /**
     * 节点类
     *
     * @param <E>
     */
    static class Node<E> {

        Node<E> prev;//前节点

        E value;//节点值

        Node<E> next;//后节点

        public Node(Node<E> prev, E value, Node<E> next) {
            this.prev = prev;
            this.value = value;
            this.next = next;
        }
    }

    /**
     * 在队列开头添加元素
     *
     * @param e
     * @return
     */
    @Override
    public boolean offerFirst(E e) {
        //判断队列是否已满
        if (isFull()) {
            return false;
        }
        Node<E> a = sentinel;
        Node<E> b = sentinel.next;
        Node<E> offered = new Node<>(a, e, b);
        a.next = offered;
        b.prev = offered;
        size++;
        return true;
    }

    /**
     * 在队列结尾添加元素
     *
     * @param e
     * @return
     */
    @Override
    public boolean offerLast(E e) {
        //判断队列是否已满
        if (isFull()) {
            return false;
        }
        Node<E> a = sentinel.prev;
        Node<E> b = sentinel;
        Node<E> offered = new Node<>(a, e, b);
        a.next = offered;
        b.prev = offered;
        size++;
        return true;
    }

    /**
     * 在队列开头读取元素,并删除该元素
     *
     * @return
     */
    @Override
    public E pollFirst() {
        //判断队列是否为空
        if (isEmpty()) {
            return null;
        }
        Node<E> data = sentinel.next;
        Node<E> next = data.next;
        sentinel.next = next;
        next.prev = sentinel;
        size--;
        return data.value;
    }

    /**
     * 在队列尾部读取元素,并删除该元素
     *
     * @return
     */
    @Override
    public E pollLast() {
        //判断队列是否为空
        if (isEmpty()) {
            return null;
        }
        Node<E> data = sentinel.prev;
        Node<E> prev = data.prev;
        prev.next = sentinel;
        sentinel.prev = prev;
        size--;
        return data.value;
    }

    /**
     * 在队列开头查询元素,但不删除
     *
     * @return
     */
    @Override
    public E peekFirst() {
        //判断队列是否为空
        if (isEmpty()) {
            return null;
        }
        return sentinel.next.value;
    }

    /**
     * 在队列尾部查询元素,但不删除
     *
     * @return
     */
    @Override
    public E peekLast() {
        //判断队列是否为空
        if (isEmpty()) {
            return null;
        }
        return sentinel.prev.value;
    }

    /**
     * 判断队列是否为空
     *
     * @return
     */
    @Override
    public boolean isEmpty() {
        //队列值个数等于=0则队列为空!
        return size == 0;
    }

    /**
     * 检查队列是否已满
     *
     * @return
     */
    @Override
    public boolean isFull() {
        //队列值个数等于队列长度则表示队列已满。
        return size == dequeLength;
    }

}

2、数组案例,基于双向循环链表实现

思路:

1、数组初始化:
  • 初始化一个空数组,容量可以预设一个初始值或动态调整。
  • 通常设置两个索引变量:head(指向队列头部)和tail(指向队列尾部)。当队列为空时,它们都指向数组的第一个位置。
2、插入操作:
  • 在头部插入元素:
    判断队列是否已满。没有则,将head索引后退一步(-1)。让后在该索引位置插入值。

  • 在尾部插入元素:
    判断队列是否已满。没有则,直接在tail索引位置后添加新元素,并更新tail索引前进一步(+1)。

3、删除操作:
  • 从头部获取元素:
    先获取head索引位置的元素,再将head索引位置的元素清空(垃圾回收),最后将head索引前移一位(+1)。

  • 从尾部删除元素:
    先将tail索引位置后移一位(-1);再获取 tail索引位置的元素,最后将tail索引位置的元素清空(垃圾回收);

代码:

package deque;

/**
 * 使用循环数组实现双端队列
 *
 * @param <E>
 */
public class DequeArray<E> implements Deque<E> {

    /**
     * 队列数组
     */
    private E[] dequeArray;

    /**
     * 队列长度
     */
    private int dequeLength;

    /**
     * 头指针
     */
    private int head;

    /**
     * 尾指针
     */
    private int tail;

    /**
     * 队列值个数
     */
    private int size;

    /**
     * @param dequeLength 设置队列长度
     */
    public DequeArray(int dequeLength) {
        this.dequeArray = (E[]) new Object[dequeLength];
        this.dequeLength = dequeLength;
        this.head = 0;
        this.tail = 0;
        this.size = 0;
    }

    /**
     * 获取增长索引位置
     *
     * @return
     */
    private int increase(int index) {
        //如果索引位置加1大于数组的下表,那么返回索引0;
        if (index + 1 > dequeLength - 1) {
            return 0;
        }
        return index + 1;
    }

    /**
     * 获取下降索引位置
     *
     * @return
     */
    private int decline(int index) {
        if (index - 1 < 0) {
            return dequeLength - 1;
        }
        return index - 1;
    }

    /**
     * 在队列开头添加元素
     *
     * @param e
     * @return
     */
    @Override
    public boolean offerFirst(E e) {
        //检查队列是否已满
        if (isFull()) {
            return false;
        }
        head = decline(head);//头部添加队列要先获取索引值。(索引位置先-1)
        dequeArray[head] = e; //赋值
        size++;
        return true;
    }

    /**
     * 在队列结尾添加元素
     *
     * @param e
     * @return
     */
    @Override
    public boolean offerLast(E e) {
        //检查队列是否已满
        if (isFull()) {
            return false;
        }
        dequeArray[tail] = e; //先赋值
        tail = increase(tail);//再更新索引
        size++;
        return true;
    }

    /**
     * 在队列开头读取元素,并删除该元素
     *
     * @return
     */
    @Override
    public E pollFirst() {
        //判断队列是否为空
        if (isEmpty()) {
            return null;
        }
        E e = dequeArray[head];
        dequeArray[head] = null;//将取出的值清空。回收垃圾;
        head = increase(head);
        size--;
        return e;
    }

    /**
     * 在队列尾部读取元素,并删除该元素
     *
     * @return
     */
    @Override
    public E pollLast() {
        //判断队列是否为空
        if (isEmpty()) {
            return null;
        }
        int tail = decline(this.tail);
        E e = dequeArray[tail];
        dequeArray[tail] = null;//将取出的值清空。回收垃圾;
        size--;
        return e;
    }

    /**
     * 在队列开头查询元素,但不删除
     *
     * @return
     */
    @Override
    public E peekFirst() {
        //判断队列是否为空
        if (isEmpty()) {
            return null;
        }
        return dequeArray[head];
    }

    /**
     * 在队列尾部查询元素,但不删除
     *
     * @return
     */
    @Override
    public E peekLast() {
        //判断队列是否为空
        if (isEmpty()) {
            return null;
        }
        return dequeArray[decline(tail)];
    }

    /**
     * 判断队列是否为空
     *
     * @return
     */
    @Override
    public boolean isEmpty() {
        //队列值个数等于0
        return size == 0;
    }

    /**
     * 检查队列是否已满
     *
     * @return
     */
    @Override
    public boolean isFull() {
        //队列值个数等于队列长度;
        return size == dequeLength;
    }
}

测试结果:

在这里插入图片描述

  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值