【JavaDS】队列与集合Queue,Deque的理解和使用

在这里插入图片描述博客主页: XIN-XIANG荣
系列专栏:【Java实现数据结构】
一句短话: 难在坚持,贵在坚持,成在坚持!

一. 什么是队列

1. 队列的特点

队列也是一种组织数据的方式 , 队列中的元素具有先进先出的特点 , 是只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表 .

img

队列的几个术语:

  • 进行插入操作的一端称做队尾(tail/rear)。
  • 进行删除操作的一端称做队首或队头(head/front)。
  • 向队列中插入新元素称为进队或入队,新元素进队后就成为新的队尾元素。
  • 从队列中删除元素称为出队或离队,元素出队后,其后继元素就成为队首元素。

2. 队列的模拟实现

队列可以使用链表或者顺序表来实现 , 在Java集合框架中的队列就是用双链表来实现的 , 下文会介绍到双端队列和循环队列 , 在这里采用单链表来实现队列 .

我们这里设置单链表中的两个引用head和tail用来指向头节点和尾节点 , 需要注意的是我们应该让单链表的头做队头 , 单链表的尾做队尾 , 也就是从单链表的尾入队 , 头出队 , 此时入队和出队操作的时间时间复杂度都为O(1) ; 而如果反过来入队使用头插 , 入队的时间复杂度为O(1) , 此时出队要删除单链表最后一个节点 , 需要先找到其前一个节点 , 出队的时间复杂度就为O(N)了;

如果使用双链表来实现栈的话那就简单了 , 由于是是双向的 , 不管哪一端来做队头/队尾 , 时间复杂度都为O(1) ; 会了单链表的 , 双链表的不在话下了 .

img

MyQueue.java

public class MyQueue {
    static class ListNode {
        public int val;
        public ListNode next;

        public ListNode(int val) {
            this.val = val;
        }
    }
    public ListNode head;
    public ListNode tail;
    public int usedSzie;

    public void offer(int val) {
        ListNode node = new ListNode(val);
        if(this.head == null) {
            this.head = node;
            this.tail = node;
        }else {
            this.tail.next = node;
            this.tail = this.tail.next;
        }
        this.usedSzie++;
    }

    public int poll() {
        if(this.head == null) {
            throw new MyEmptyQueueException("当前队列为空");
        }
        int ret = this.head.val;
        this.head = this.head.next;
        //如果只有一个节点,要将tail置空
        if(this.head == null) {
            this.tail = null;
        }
        this.usedSzie--;
        return ret;
    }

    public int peek() {
        if(this.head == null) {
            throw new MyEmptyQueueException("当前队列为空");
        }
        return this.head.val;
    }

    public boolean empty() {
        return this.head == null;
    }

    public int getUsedSzie() {
        return this.usedSzie;
    }
}

MyEmptyQueueException.java

public class MyEmptyQueueException extends RuntimeException{
    public MyEmptyQueueException() {
    }

    public MyEmptyQueueException(String message) {
        super(message);
    }
}

加泛型的写法:

// 单链表实现队列
class Node<V> {
    public V val;
    public Node<V> next;

    public Node(V val) {
        this.val = val;
    }
}
public class MyQueue<V> {
    private Node<V> head;
    private Node<V> tail;
    private int size;

    // 判断队列是否为空
    public boolean isEmpty() {
        return size == 0;
    }
    // 获取队列中元素的个数
    public int size() {
        return size;
    }
    // 元素入队
    public void offer(V val) {
        Node<V> cur = new Node<>(val);
        if (head == null) {
            head = cur;
            tail = cur;
        } else {
            // 尾插入队
            tail.next = cur;
            tail = cur;
        }
        size++;
    }
    // 元素出队
    public V poll() {
        V ret = null;
        if (head != null) {
            ret = head.val;
            head = head.next;
            size--;
        }
        if (head == null) {
            tail = null;
        }
        return ret;
    }
    // 查看队头元素
    public V peek() {
        V ret = null;
        if (head != null) {
            ret = head.val;
        }
        return ret;
    }
}

3. 循环队列

上面已经实现了用链表来实现队列 , 那么用顺序表如何来实现队列呢 ; 我们知道顺序表是具有随机访问的特点的 , 将若干个元素入队后 , 每次出队操作后 , 该元素原来所在的空间就无法再使用了 , 这就使得顺序表得空间利用不充分 ;

而如果采用循环队列就可以解决这个问题 , 如果数组最后一个空间已经有了元素 , 但前面由于出队有了空缺 , 此时再有元素入队就能重新从数组的尾部跳到数组的头部 , 对已经出队的空间进行重新利用 , 这样就避免了空间的浪费 .

img

实现循环队列还需要考虑的一个问题是,如何确定队列是空还是满

这里我们设置队头引用为front,队尾引用为rear,顺序表长度为len

  • 方式1

记录队列元素个数size,当size的值与顺序表的长度相等时,代表队列已满 ; size值为0表示队列为空

  • 方式2

使用一个boolean类型的成员变量flag标记,初始为false,当每次成功入队时将flag设置为true,成功出队时将flag设置为false ; 那么,当front == rear && flag == true表示“在入队操作之后front == rear”, 显然入队造成的front == rear的原因就是队列满了 ; 所以 , 当rear == front && flag == true表示队列已满,同理 , 当rear == front && flag == false表示队列为空

  • 方式3

牺牲一个单位的空间,front 就指向队列的第一个元 , rear 指向队列的最后一个元素的后一个位置 , 在每次入队前判断(rear+1)% len是否与front相等,如果相等表示队列已满,如果rear == front则表示队列为空。

img

  • 数组下标循环的小技巧
  1. 往后走(offset 小于 array.length): index = (index + offset) % array.length

img

这里的意思是 , 长度为9的循环链表中 , 7下标位置 , 偏移4个位置 , 到了2位置

  1. 往前走(offset 小于 array.length): index = (index + array.length - offset) % array.length

img

这里的意思是 , 长度为9的循环链表中 , 2下标位置 , 偏移4个位置 , 到了7位置

4. 双端队列

双端队列是指两端都可以进行进队和出队操作的队列,将队列的两端分别称为前端和后端,两端都可以入队和出队。

双端队列同时遵守了先进先出和后进先出的原则,所以可以说它是一种把队列和栈相结合的一种数据结构 ;

所以双端队列既能够当队列使用,也能当栈使用,Java底层是使用双链表(LinkedList)来实现双端队列(Deque)和队列(Queue)的 ;

限制一端进行出队或入队的双端队列称为受限的双端队列。

img
代码实现:

// 用双链表实现双端队列
public class MyDeque<V> {
    public static class Node<V> {
        public V val;
        public Node<V> last;
        public Node<V> next;

        public Node(V val) {
            this.val = val;
        }
    }

    private Node<V> head;
    private Node<V> tail;
    private int size;

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

    public int size() {
        return size;
    }

    // 从队头入队
    public void pushHead(V val) {
        Node<V> cur = new Node<>(val);
        if (head == null) {
            head = cur;
            tail = cur;
        } else {
            cur.next = head;
            head.last = cur;
            head = cur;
        }
        size++;
    }
    // 从队头出队
    public V pollHead() {
        V ret = null;
        if (head == null) {
            return ret;
        }
        size--;
        ret = head.val;
        if (size() == 1) {
            head = null;
            tail = null;
        } else {
            head = head.next;
            head.last = null;
        }
        return ret;
    }

    public V peekHead() {
        V ret = null;
        if (head != null) {
            ret = head.val;
        }
        return ret;
    }

    // 从队尾入队
    public void pushTail(V val) {
        Node<V> cur = new Node<>(val);
        if (tail == null) {
            head = cur;
            tail = cur;
        } else {
            tail.next = cur;
            cur.last = tail;
            tail = cur;
        }
        size++;
    }
    // 从队尾出队
    public V pollTail() {
        V ret = null;
        if (tail == null) {
            return ret;
        }
        size--;
        ret = tail.val;
        if (size() == 1) {
            head = null;
            tail = null;
        } else {
            tail = tail.last;
            tail.next = null;
        }
        return ret;
    }

    public V peekTail() {
        V ret = null;
        if (tail != null) {
            ret = tail.val;
        }
        return ret;
    }
}

二. 集合-Queue,Deque

1. 结构介绍

在Java中,Queue和Deque是两个接口,底层是通过双链表实现的 , 使用时必须创建LinkedList的对象 ;

这里观察Queue,Deque接口与LinkedList类关系 ;

LinkedList类实现了Queue,Deque接口,Deque接口扩展了Queue接口,三者都扩展或继承了Collection和Iterable接口。

img

2. 方法介绍

Queue接口提供了队列增删查改等基本操作的方法,Deque接口提供了双端队列操作的一系列方法,LinkedList类实现了Queue,Deque接口,因此可以使用Queue引用LinkedList对象当队列使用,Deque引用LinkedList对象当双端队列使用,当然也可以直接使用LinkedList引用LinkedList对象当队列,双端队列,双链表等数据结构使用

由于在实际开发中Deque使用的并不是非常多 , 所以这里只列出Queue接口中常用的方法 :

方法作用
boolean add(E e)队尾入队,入队失败引发异常
boolean offer(E e)队尾入队,入队失败不引发异常(推荐)
E remove()对头出队,并返回队头元素,出队失败引发异常
E poll()对头出队,并返回队头元素,出队失败不引发异常(推荐)
E element()获取队头元素,获取失败引发异常
E peek()获取队头元素,获取失败不引发异常(推荐)
  • add系列方法与offer系列方法的区别

两者都是在队列队头或队尾插入元素,前者(add)插入元素失败会引发异常,后者(offer)插入元素失败不会引发异常,只会以返回false的形式表示插入元素失败,如果是有容量限制的队列,使用offer系列方法更加合适。

  • remove系列方法与poll系列方法的区别

两者都是在队列队头或队尾删除并返回元素,前者(remove)删除元素失败会引发异常,后者(poll)删除元素失败不会引发异常,只会以返回null的形式表示删除元素失败,如果是有容量限制的队列,使用poll系列方法更加合适。

  • get系列方法与peek系列方法的区别

两者都是在队列队头或队尾获取并返回元素,前者(get)获取元素失败会引发异常,后者(peek)获取元素失败不会引发异常,只会以返回null的形式表示获取元素失败,如果是有容量限制的队列,使用peek系列方法更加合适。

代码示例 :

public static void main(String[] args) {
        Queue<Integer> q = new LinkedList<>();
        q.offer(1);
        q.offer(2);
        q.offer(3);
        q.offer(4);
        q.offer(5); // 从队尾入队列
        System.out.println(q.size());
        System.out.println(q.peek()); // 获取队头元素
        q.poll();
        System.out.println(q.poll()); // 从队头出队列,并将删除的元素返回
        if(q.isEmpty()){
            System.out.println("队列空");
        }else{
            System.out.println(q.size());
        }
    }
  • 31
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 18
    评论
### 回答1: Java中的队列queue)和双端队列deque)都是常用的数据结构,用于存储一系列元素并进行操作。它们在不同的场景下有不同的用途。 当我们需要在队列尾部插入元素,并从队列头部取出元素时,使用队列queue)是最好的选择。队列queue)遵循先进先出(FIFO)的原则,保证插入的元素排在最后,而取出元素时从队列头部开始。在该场景下对于Java Queue接口的实现类LinkedList即可满足需求。 而在需要在队列头部和尾部同时插入删除元素时,我们可以选择双端队列deque)。双端队列deque)支持在队列头部和尾部两端插入和删除元素,并且可以使用队列和栈两种方式进行操作。JavaDeque接口即为双端队列的接口,常见实现有ArrayDeque和LinkedList。ArrayDeque由于是基于循环数组的实现,因此相比LinkedList在插入删除操作时更加高效。 因此,根据具体的需求和场景选择合适的队列或双端队列实现即可。如果只需要普通的队列功能,则使用Queue或LinkedList即可;需要更多的操作则可使用Deque或ArrayDeque。 ### 回答2: Java中的队列是一种用于存储元素的数据结构,其中存储的元素可以按照先进先出(FIFO)的顺序进行排列。Queue接口是Java集合框架中的一个接口,用于表示队列,可以实现队列的操作,如添加元素、删除元素、检索元素等。 Queue接口继承了Collection接口,该接口提供了添加元素、删除元素、以及检索队列中的元素的方法。Java中的Queue接口有两个主要的实现类,即LinkedList和PriorityQueue。LinkedList实现了Queue接口,是Java中最常见的队列类,而PriorityQueue实现了Queue和Comparable接口,可以用于创建优先队列。 另一个Java中的队列实现类是Deque(Double Ended Queue)接口。与Queue接口不同的是,Deque接口允许在队列的两端添加或删除元素,因此它支持FIFO和LIFO(后进先出)两种模式。 总之,当我们需要实现一个简单的队列时,可以考虑使用Queue接口,如果需要队列可以在两端添加或删除元素,则可以使用Deque接口。但具体使用哪个接口,还需要根据具体情况具体分析。 ### 回答3: Java队列可以使用QueueDeque两种数据结构来表示,选择使用哪种数据结构主要根据具体的业务需求和设计目的而定。 Queue是一种“先进先出”(FIFO)的队列结构,它表示一组元素的集合,其中新元素被添加到队列的尾部,而元素被提取时从队列的头部开始。Queue接口包括了add,offer,remove,poll,peek等方法。 Deque是一种“双向队列”,它实现了Queue接口并提供了高效的插入和删除操作。Deque允许在队列的两端添加或删除元素,可以被用作栈和队列双重行的结构。Deque接口包括了addFirst,addLast,offerFirst,offerLast,removeFirst,removeLast,pollFirst,pollLast,peekFirst,peekLast等方法。 如果只需要实现简单的先进先出队列的功能,使用Queue就足够了,而如果需要在队列两端进行高效的插入和删除操作,建议使用Deque。 总之,根据具体的业务需求和设计目的来选择使用Queue还是Deque

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

韵秋梧桐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值