初阶数据结构(6)(队列的概念、常用的队列方法、队列模拟实现【用双向链表实现、用数组实现】、双端队列 (Deque)、OJ练习【用队列实现栈、用栈实现队列】)

接上次博客:初阶数据结构(5)(栈的概念、栈的模拟实现、栈的应用及练习【改变元素的序列 、 将递归转化为循环、括号匹配、逆波兰表达式求值、出栈入栈次序匹配、最小栈】、链栈和顺序栈栈、虚拟机栈、栈帧的区别)_di-Dora的博客-CSDN博客

目录

队列(Queue)的概念

 常用的队列方法

队列模拟实现

我们先用双向链表来实现一个吧:

现在考虑用数组实现:​

双端队列 (Deque)

OJ练习:

1、用队列实现栈:

2、用栈实现队列:


队列(Queue)的概念

队列是一种特殊的线性表,它只允许在一端进行插入数据操作,而在另一端进行删除数据操作。这个特性使得队列的数据按照先进先出(FIFO)的顺序进行处理。这意味着最先插入的元素将最先被删除,而最后插入的元素将最后被删除。

在队列中,进行插入操作的一端称为队尾(Tail/Rear),新元素将被添加到队尾;而进行删除操作的一端称为队头(Head/Front),最早插入的元素将从队头被删除。

这个过程就好像人们排队等候服务,先来的人先被服务,后来的人排在队尾等待。

举个例子来说明队列的概念。假设有一个队列,初始时队列为空。现在按照顺序依次执行以下操作:

  • 将元素A插入队尾。
  • 将元素B插入队尾。
  • 将元素C插入队尾。
  • 删除队头元素。
  • 将元素D插入队尾。
  • 删除队头元素。

在这个过程中,元素A首先被插入队尾,然后是元素B,再然后是元素C。当执行删除操作时,队头的元素A被删除。接着,元素D被插入队尾,然后队头的元素B被删除。

根据队列的特性,删除操作总是在队头进行,而插入操作总是在队尾进行,确保了元素的顺序是先进先出的。

队列在实际应用中具有广泛的应用,例如任务调度、消息传递、广度优先搜索等场景都可以使用队列来实现。

 

 在Java中,Queue是个接口,底层是通过链表实现的:

注意:Queue是个接口,在实例化时必须实例化LinkedList的对象,因为LinkedList实现了Queue接口。 

Deque<Integer> queue2 = new LinkedList<>();

Queue<Integer> queue1 = new LinkedList<>();

 常用的队列方法

  • boolean add(E element): 将元素添加到队列的尾部,如果队列已满则抛出异常。
  • boolean offer(E element): 将元素添加到队列的尾部,如果队列已满则返回false。
  • E remove(): 删除并返回队列头部的元素,如果队列为空则抛出异常。
  • E poll(): 删除并返回队列头部的元素,如果队列为空则返回null。
  • E element(): 返回队列头部的元素,如果队列为空则抛出异常。
  • E peek(): 返回队列头部的元素,如果队列为空则返回null。
  • int size(): 返回队列中的元素个数。
  • boolean isEmpty(): 判断队列是否为空。
  • boolean contains(Object element): 判断队列是否包含指定的元素。
  • void clear(): 清空队列中的所有元素。

当然,除了上述方法,Queue接口还继承了Collection接口中的一些方法,如iterator()、addAll(Collection<? extends E> c)等。 

我们可以试验一下这些方法:

import java.util.Queue;
import java.util.LinkedList;

public class QueueExample {
    public static void main(String[] args) {
        // 创建一个队列
        Queue<String> queue = new LinkedList<>();

        // 添加元素到队列
        queue.add("Apple");
        queue.offer("Banana");
        queue.offer("Cherry");

        // 删除并返回队列头部的元素
        String removedElement = queue.remove();
        System.out.println("删除的元素: " + removedElement);

        // 返回队列头部的元素
        String head = queue.element();
        System.out.println("队列头部元素: " + head);

        // 判断队列是否为空
        boolean isEmpty = queue.isEmpty();
        System.out.println("队列是否为空: " + isEmpty);

        // 获取队列的大小
        int size = queue.size();
        System.out.println("队列大小: " + size);

        // 遍历队列并打印元素
        System.out.println("队列元素: ");
        for (String fruit : queue) {
            System.out.println(fruit);
        }

        // 判断队列是否包含指定的元素
        boolean containsElement = queue.contains("Banana");
        System.out.println("队列是否包含Banana: " + containsElement);

        // 清空队列
        queue.clear();
        System.out.println("清空后的队列大小: " + queue.size());
    }
}

 你有没有发现,最上面的几种方法好像有些功能上是重复的:

你可以跳转到源代码去看一看英文的注解:

所以我们可以把它们分为两组,这两组的差别如下:

add(E element)和offer(E element):

add(E element): 将元素添加到队列的尾部。如果队列已满,抛出一个IllegalStateException异常。
offer(E element): 将元素添加到队列的尾部。如果队列已满,则返回false

区别:add()方法在无法添加元素时会抛出异常,而offer()方法在无法添加元素时返回false。

remove()和poll():

remove(): 删除并返回队列头部的元素。如果队列为空,抛出一个NoSuchElementException异常。
poll(): 删除并返回队列头部的元素。如果队列为空,则返回null。
 

区别:remove()方法在队列为空时会抛出异常,而poll()方法在队列为空时返回null。

element()和peek():

element(): 返回队列头部的元素,但不会删除它。如果队列为空,则抛出一个NoSuchElementException异常。

peek(): 返回队列头部的元素,但不会删除它。如果队列为空,则返回null。

区别:element()方法在队列为空时会抛出异常,而peek()方法在队列为空时返回null。

总体上,这三组方法的功能相同,不同之处在于在特定情况下的异常处理方式。add()、remove()和element()方法在无法执行操作时会抛出异常,而offer()、poll()和peek()方法则会返回特定值来指示操作的成功与否。

队列模拟实现

队列中既然可以存储元素,那底层肯定要有能够保存元素的空间,我们通过前面线性表的学习了解到常见的空间类型有两种:顺序结构 链式结构。队列的实现使用顺序结构还是链式结构好?

都可以,随意选。

我们先用双向链表来实现一个吧:

import java.util.List;

/**
 * @Author 12629
 * @Description:
 */
public class MyQueue {
    static class ListNode {
        private int val;
        private ListNode prev;
        private ListNode next;

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

    private ListNode front;//队头
    private ListNode rear;//队尾

    private int usedSize;
 


    //我们和源代码保持一致,用头插法
    public void offer(int x) {
        ListNode node = new ListNode(x);
        if(front == null) {
            front = rear = node;
        }else {
            node.next = front;
            front.prev = node;
            front = node;
        }
        usedSize++;
    }

    //出队列 相当于 删除尾节点
    //先进先出
    public int poll() {
        if(front == null) {
            return -1; //抛异常也可以
        }
        int ret = rear.val;
        if(front == rear) {
            front = null;
            rear = null;
            usedSize--;
            return ret;
        }
        rear = rear.prev;
        rear.next = null;
        usedSize--;
        return ret;
    }

    public int peek() {
        if(front == null) {
            return -1;
        }
        return front.val;
    }

    public int getUsedSize() {
        return usedSize;
    }

    public boolean isEmpty() {
        return usedSize == 0;
    }
}
    public static void main(String[] args) {
    MyQueue myQueue = new MyQueue();
    myQueue.offer(1);
    myQueue.offer(2);
    myQueue.offer(3);
    myQueue.offer(4);
    // 4   3   2   1

    System.out.println(myQueue.poll());
    System.out.println(myQueue.poll());
    System.out.println(myQueue.poll());
    System.out.println(myQueue.poll());
    System.out.println(myQueue.poll());
    System.out.println(myQueue.poll());
    }

来来来,做个选择题,看看你是不是理解了:

下列关于队列的叙述错误的是( )

A.队列可以使用链表实现

B.队列是一种"先入先出"的数据结构

C.数据出队列时一定只影响队尾引用

D.数据入队列时一定从尾部插入

 答案: C.数据出队列时一定只影响队尾引用

C的说法是错误的。数据出队列时会同时影响队头和队尾引用。

在队列中,数据的插入(入队列)是在队尾进行的,而数据的删除(出队列)是在队头进行的。当数据出队列时,队头引用会更新为下一个元素,同时队尾引用也可能需要进行更新,特别是当队列中只有一个元素时,出队列后队尾引用会变为null或者空值。

现在考虑用数组实现:

 就像是一个“开心大转盘”,或者一个飞镖靶子(订奖品的那种):

 数组下标循环的小技巧 :

1. 下标最后再往后(offset 小于 array.length): index = (index + offset) % array.length。

 2. 下标最前再往前(offset 小于 array.length): index = (index + array.length - offset) % array.length。

看看掌握程度:

现有一循环队列,其队头为front,队尾为rear,循环队列长度为N,最多存储N-1个数据。其队内有效长度为( )

A.(rear - front + N) % N + 1
B.(rear - front + N) % N
C.(rear - front) % (N + 1)
D.(rear - front + N) % (N - 1) 

答案:A

这样就已经排除两个了, 我们再来了解一下 A 的原理:

 A. (rear - front + N) % N + 1:

这是计算循环队列队内有效长度的公式。解释如下:

(rear - front + N) % N:首先计算rear和front之间的差值,即队尾和队头的相对位置。由于循环队列的特性,rear可能小于front(表示循环回到队列的开头),因此需要加上N来确保差值为正数。然后再进行取模运算,将差值限定在0到N-1之间。

1:由于队内有效长度是指队列中实际存储的元素个数,需要将前面计算得到的差值加1,即为队内有效长度。

因此,选项A使用了合适的公式来计算循环队列的队内有效长度。

力扣:622. 设计循环队列 - 力扣(Leetcode) 

我们看一下这个代码的具体要求:

class MyCircularQueue {

    public MyCircularQueue(int k) {

    }
    
    public boolean enQueue(int value) {

    }
    
    public boolean deQueue() {

    }
    
    public int Front() {

    }
    
    public int Rear() {

    }
    
    public boolean isEmpty() {

    }
    
    public boolean isFull() {

    }
}
class MyCircularQueue {

    private int[] elem;
    private int front;//队头下标
    private int rear;//队尾下标

    public MyCircularQueue(int k) {
        this.elem = new int[k+1]; //因为会浪费空间,所以为了确保数组不会越界,多给一个空间
    }

    public boolean enQueue(int value) {
        if(isFull()) {
            return false;
        }
        elem[rear] = value;
        rear = (rear+1)%elem.length; //可不能 rear++,会越界!
        return true;
    }

    public boolean deQueue() {
        //1、空的 不能出
        if(isEmpty()) {
            return false;
        }
        //2、不空 则 保存队头元素 然后front往后走
        front = (front+1)%elem.length;
        return true;
    }

    //得到队头元素
    public int Front() {
        if(isEmpty()) {
            return -1;
        }
        return elem[front];
    }

    public int Rear() {
        if(isEmpty()) {
            return -1;
        }
        int index = (rear == 0) ? elem.length-1 : rear-1;
        return elem[index];
        //return elem[(rear - 1 + elem.length) % elem.length];
    }

    public boolean isEmpty() {
        return front == rear;
    }

    public boolean isFull() {
        //rear的下一个是front
        if((rear+1)%elem.length == front) {
            return true;
        }
        return false;
    }
}

return elem[(rear - 1 + elem.length) % elem.length]:由于rear指向队尾的下一个位置,所以要获取队尾元素的索引,需要将rear减1。然后加上elem.length,是为了处理rear-1为负数的情况,确保索引为正数。最后再进行取模运算,将索引限制在0到capacity-1之间,实现循环。
elem[...]:通过计算得到的索引,访问elements数组中对应位置的元素,即为队尾元素的值。

如下是使用链表的方法,是力扣的官方题解:

class MyCircularQueue {
    private ListNode head;
    private ListNode tail;
    private int capacity;
    private int size;

    public MyCircularQueue(int k) {
        capacity = k;
        size = 0;
    }

    public boolean enQueue(int value) {
        if (isFull()) {
            return false;
        }
        ListNode node = new ListNode(value);
        if (head == null) {
            head = tail = node;
        } else {
            tail.next = node;
            tail = node;
        }
        size++;
        return true;
    }

    public boolean deQueue() {
        if (isEmpty()) {
            return false;
        }
        ListNode node = head;
        head = head.next;  
        size--;
        return true;
    }

    public int Front() {
        if (isEmpty()) {
            return -1;
        }
        return head.val;
    }

    public int Rear() {
        if (isEmpty()) {
            return -1;
        }
        return tail.val;
    }

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

    public boolean isFull() {
        return size == capacity;
    }
}

来做个小练习: 

对于循环队列,下列叙述中正确的是 ()

A.队头是固定不变的
B.队头一定大于队尾
C.队头一定小于队尾
D.队头可以大于队尾,也可以小于队尾 

答案:D、队头可以大于队尾,也可以小于队尾

解析:对于循环队列,队头和队尾的相对位置是可以变化的。循环队列通过使用取模运算来实现循环的效果,当队尾达到数组的末尾时,下一个元素将回到数组的开头。这样可以利用数组的空间进行循环利用。

在循环队列中,队头可以在队尾之后,也可以在队尾之前,这取决于队列中元素的个数和队列的操作历史。例如,当队列为空时,队头和队尾指向同一个位置,队列中只有一个元素;当队列已满时,队头可能在队尾之后。

因此,选项D中的说法是正确的,循环队列中队头可以大于队尾,也可以小于队尾。

双端队列 (Deque)

双端队列(Deque)是一种数据结构,它允许在队列的两端进行插入和删除操作。其名称"Deque"是"double ended queue"的缩写。双端队列可以被视为同时具有队列和栈的性质,因为它允许在队列的两端进行元素的添加和移除。

双端队列与普通队列的主要区别在于,普通队列只允许在队尾进行入队操作,并且只能从队头进行出队操作。而双端队列允许在队头和队尾同时进行入队和出队操作。

双端队列的特性使得它在许多场景下非常有用。下面是一些双端队列的常见应用场景:

  • 队列和栈的结合:双端队列可以用作队列或栈的替代品。你可以选择从队头或队尾插入和删除元素,从而灵活地应对不同的需求。
  • 路径搜索算法:在某些路径搜索算法(如广度优先搜索)中,需要在搜索过程中同时从前面和后面进行扩展。双端队列提供了高效的操作,使得这样的算法更容易实现。
  • 滑动窗口问题:滑动窗口问题是一类常见的算法问题,通常涉及到在一个固定大小的窗口内进行计算。双端队列可以用来维护窗口内的元素,并且能够在常数时间内进行插入和删除操作。

双端队列的实现方式有多种,可以使用数组、链表或双向链表来实现。具体选择哪种实现方式取决于应用场景和性能要求。无论使用何种实现方式,双端队列的操作复杂度应该尽量为O(1)。

下面是双端队列的一些常见操作:

  • 入队操作(从队头或队尾插入元素):可以通过 insertFront() 和 insertLast() 等方法来实现。
  • 出队操作(从队头或队尾删除元素):可以通过 deleteFront() 和 deleteLast() 等方法来实现。
  • 获取队头和队尾元素:可以通过 getFront() 和 getLast() 等方法来获取队头和队尾元素的值,而不进行删除操作。
  • 判断双端队列是否为空:可以通过 isEmpty() 方法来判断双端队列是否为空。

总而言之,双端队列是一种非常有用的数据结构,它允许在队列的两端进行插入和删除操作。它可以灵活地应对不同的应用场景,并且能够以常数时间复杂度进行插入和删除操作。

Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
Deque<Integer> queue = new LinkedList<>();//双端队列的链式实现

 ArrayDeque<>() 是 Deque 的数组实现:跳转过去看看:

 ArrayDeque的介绍:

ArrayDeque是Java中的一个双端队列(deque)实现类。

它实现了Deque接口,允许在队列两端进行快速插入和删除操作。

双端队列是一种具有队列和栈特性的数据结构,可以在队列的两端进行元素的插入和删除。ArrayDeque使用动态数组作为其内部实现,它可以根据需要自动调整大小。

与LinkedList相比,ArrayDeque提供了更高效的随机访问和更快的插入/删除操作。然而,ArrayDeque在插入和删除操作中的开销较高,需要移动其他元素来保持队列的连续性。

以下是ArrayDeque常用的一些方法:

  • addFirst(element):将元素添加到队列的开头。
  • addLast(element):将元素添加到队列的末尾。
  • removeFirst():移除并返回队列的第一个元素。
  • removeLast():移除并返回队列的最后一个元素。
  • peekFirst():返回队列的第一个元素,但不移除它。
  • peekLast():返回队列的最后一个元素,但不移除它。
  • size():返回队列中元素的数量。

ArrayDeque可以用于实现双向队列、栈、循环队列等数据结构,并且它是线程不安全的,不支持多线程并发操作。如果需要在多线程环境下使用队列,可以考虑使用ConcurrentLinkedDeque等线程安全的实现类。

        Deque<Integer> stack = new ArrayDeque<>();//双端队列的线性实现
        
        stack.push(1);  //双端队列实现的栈

OJ练习:

1、用队列实现栈:

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。
实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。

力扣:225. 用队列实现栈 - 力扣(Leetcode) 

我想先问你一个问题:我们最普通的队列可以实现一个栈吗?

答案是否定的。

 

 好了,现在来实现一下:

import java.util.LinkedList;
import java.util.Queue;

public class MyStack {

    private Queue<Integer> qu1;
    private Queue<Integer> qu2;

    public MyStack() {
        qu1 = new LinkedList<>();
        qu2 = new LinkedList<>();
    }

    /*
     * 入栈操作:
     * 入到不为空的队列当中,如果都为空 入到qu1
     */
    public void push(int x) {
        if(!qu1.isEmpty()) {
            qu1.offer(x);
        }else if(!qu2.isEmpty()) {
            qu2.offer(x);
        }else {
            qu1.offer(x);
        }
    }

    public int pop() {
        if(empty()) {
            return -1;//栈为空的
        }

        if(!qu1.isEmpty()) {
            int size = qu1.size();
            for (int i = 0; i < size-1; i++) {
                int tmp = qu1.poll();
                qu2.offer(tmp);
            }
            return qu1.poll();
        }else {
            int size = qu2.size();
            for (int i = 0; i < size-1; i++) {
                int tmp = qu2.poll();
                qu1.offer(tmp);
            }
            return qu2.poll();
        }
    }

    //peek
    public int top() {
        if(empty()) {
            return -1;//栈为空的
        }
        int tmp = -1;
        if(!qu1.isEmpty()) {
            int size = qu1.size();
            for (int i = 0; i < size; i++) {
                tmp = qu1.poll();
                qu2.offer(tmp);
            }
            return tmp; //最后一次覆盖掉 tmp 的值就是我们栈顶的元素
        }else {
            int size = qu2.size();
            for (int i = 0; i < size; i++) {
                tmp = qu2.poll();
                qu1.offer(tmp);
            }
            return tmp;
        }
    }

    /*
     * 2个队列都为空 表示 栈为空 !!
     *
     */
    public boolean empty() {
        //2个对列 都为空的时候
        return qu1.isEmpty() && qu2.isEmpty();
    }
}

注意!!! pop() 我这样写可不可以?

if(!qu1.isEmpty()) {
    for (int i = 0; i < qu1.size()-1; i++) {
         int tmp = qu1.poll();
         qu2.offer(tmp);
    }
    return qu1.poll();
 }else {
    for (int i = 0; i < qu2.size()-1; i++) {
                int tmp = qu2.poll();
                qu1.offer(tmp);
    }
    return qu2.poll();
}

当然不可以!!!

因为我们每次 poll ,qu1 和 qu2 的 size 都会改变,是一个变量。

2、用栈实现队列:

力扣:232. 用栈实现队列 - 力扣(Leetcode)

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):
实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

我们的思路如下:用输出栈和输入栈进行模拟,入队的时候把所有元素全部放到输入栈当中,出队的时候,把输入栈当中的所有元素全部倒回输出栈中,输出输出栈栈顶的元素:

import java.util.Stack;

public class Myqu{
    private Stack<Integer> stack1; // 输入栈
    private Stack<Integer> stack2; // 输出栈

    public Myqu() {
        stack1 = new Stack<>();
        stack2 = new Stack<>();
    }

    public void push(int x) {
        // 将stack2中的元素全部转移到stack1
        while (!stack2.isEmpty()) {
            stack1.push(stack2.pop());
        }
        stack1.push(x);
    }

    private void shiftStacks() {
        if (stack2.isEmpty()) {
            // 当输出栈为空时,将输入栈中的元素转移到输出栈
            while (!stack1.isEmpty()) {
                stack2.push(stack1.pop());
            }
        }
    }

    public int pop() {
        shiftStacks(); // 确保输出栈中有元素
        return stack2.pop(); // 从输出栈弹出元素
    }

    public int peek() {
        shiftStacks(); // 确保输出栈中有元素
        return stack2.peek(); // 返回输出栈顶元素
    }

    public boolean empty() {
        return stack1.isEmpty() && stack2.isEmpty(); // 队列为空的条件是输入栈和输出栈都为空
    }

    public static void main(String[] args) {
        Myqu obj = new Myqu();
        obj.push(1);
        obj.push(2);
        int param_2 = obj.pop();
        int param_3 = obj.peek();
        boolean param_4 = obj.empty();
        System.out.println(param_2);  // 输出:1
        System.out.println(param_3);  // 输出:2
        System.out.println(param_4);  // 输出:false
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值