【数据结构与算法】之队列详解

队列(Queue)是一种重要的线性数据结构,遵循先进先出、后进后出的原则。本文将更详细地介绍队列的概念、特点、Java 实现以及应用场景。

模运算小复习:

a % b 的值总是小于b

5 % 4 = 1 5 % 2 = 1

1 % 5 = 1 4 % 5 = 4

1. 队列概念概述

想象一下排队买票,先排队的人总是先买到票。队列就像这样,元素从一端进入,称为队尾(Rear)或尾指针(tail) ,从另一端取出,称为队首(Front)或头指针(head) 。元素的添加操作称为入队(Enqueue)或加入队列 ,删除操作称为出队(Dequeue)或移出队列

队列是一种抽象数据类型 (ADT),这意味着我们只关心它的操作和特性,而不关心具体的实现方式。队列的关键操作包括:

  • enqueue(item): 将元素 item 添加到队尾。

  • dequeue(): 移除并返回队首元素。

  • peek(): 返回队首元素,但不移除它。

  • isEmpty(): 检查队列是否为空。

  • size(): 返回队列中元素的数量 (部分实现可能不包含此方法,需要额外维护一个计数变量)。

2. 队列的特点

  • 先进先出 (FIFO): 这是队列最核心的特点,最先入队的元素总是最先出队。

  • 操作受限: 只能在队尾入队,在队首出队。这限制了访问元素的灵活性,但也保证了操作的高效性 (时间复杂度通常为 O(1))。

3. 队列的 Java 实现

Java 中可以使用链表或环形数组实现队列。

3.1 基于链表的实现

java 复制代码
public class LinkedListQueue<T> {
    private Node<T> head; // 指向队首节点的指针
    private Node<T> tail; // 指向队尾节点的指针
    private int size; // 记录队列大小

    private static class Node<T> { // 链表节点的内部类
        T data;
        Node<T> next;

        Node(T data) {
            this.data = data;
        }
    }

    public LinkedListQueue() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }

    public boolean isEmpty() {
        return size == 0; // 或 head == null
    }

    public int size() {
        return size;
    }


    //入队
    public void enqueue(T item) {
        Node<T> newNode = new Node<>(item);
        if (isEmpty()) {
            head = newNode; // 如果队列为空,新节点既是队首也是队尾
        } else {
            tail.next = newNode; // 否则,将新节点添加到队尾
        }
        tail = newNode; // 更新队尾指针
        size++;
    }


    //出队
    public T dequeue() {
        if (isEmpty()) {
            throw new NoSuchElementException("Queue is empty"); // 使用更具体的异常类型
        }
        T data = head.data;
        head = head.next; // 更新队首指针
        if (head == null) { // 如果队列只有一个元素,出队后队列变空,tail 也需要置空
            tail = null;
        }
        size--;
        return data;
    }


    //返回队首元素
    public T peek() {
        if (isEmpty()) {
            throw new NoSuchElementException("Queue is empty");
        }
        return head.data;
    }
}

3.2 基于环形数组的实现

好处

  • 对比普通数组,起点和终点更为自由,不用考虑数据移动

  • "环"意味着不会存在【越界】问题

  • 数组性能更佳

  • 环形数组比较适合实现有界队列、RingBuffer 等

java 复制代码
public class ArrayQueue<T> {
    private T[] data;
    private int head; // 队首指针
    private int tail; // 队尾指针
    private int capacity; // 数组容量
    private int size; // 队列大小

    public ArrayQueue(int capacity) {
        this.capacity = capacity;
        this.data = (T[]) new Object[capacity];
        this.head = 0;
        this.tail = 0;
        this.size = 0;
    }

    public boolean isEmpty() {
        return size == 0; // 或 head == tail
    }

    public boolean isFull() {
        return size == capacity; // 使用 size 判断是否满
    }

    public int size() { return size; }



    //入队
    public void enqueue(T item) {
        if (isFull()) {
            //throw new RuntimeException("Queue is full");
            resizeArray(); //  扩容操作
        }
        data[tail] = item;
        tail = (tail + 1) % capacity; // 环形数组的关键:使用模运算
        size++;
    }


    //出队
    public T dequeue() {
        if (isEmpty()) {
            throw new NoSuchElementException("Queue is empty");
        }
        T item = data[head];
        data[head] = null; //  避免对象游离
        head = (head + 1) % capacity; // 环形数组的关键:使用模运算
        size--;
        return item;
    }


    //返回队首元素
    public T peek() {
        if (isEmpty()) {
            throw new NoSuchElementException("Queue is empty");
        }
        return data[head];
    }


    private void resizeArray() { // 扩容方法示例
        int newCapacity = capacity * 2;
        T[] newData = (T[]) new Object[newCapacity];
        for (int i = 0; i < size; i++) {
            newData[i] = data[(head + i) % capacity];
        }
        data = newData;
        head = 0;
        tail = size;
        capacity = newCapacity;
    }
}

4. 队列的基本操作图解

4.1 下标计算

例如,数组长度是 5,当前位置是 3 ,向前走 2 步,此时下标为 (3 + 2) % 5 = 0

  • cur 当前指针位置

  • step 前进步数

  • length 数组长度

4.2 判空

引入size属性后有两种方式:

java 复制代码
return tail == size; //队列大小

return size == 0;

4.3 判满

引入size属性后有两种方式:

java 复制代码
return size = capacity; //数组容量

return (tail + 1) % length == head;

4.4 入队

假设入队前队空:此时head == tail

入队后:head 不变,tail + 1,代码中这样书写 :tail = (tail + 1) % length,保证了不会越界的情况,不能设置为 tail ++ 。此时a即是队头也是队尾。

4.5 出队

这里要牢记队列的特点:先进先出、后进后出

假设此时a、b已入队,现在a要出队,出队前:a是队头,b是队尾

a出队后:此时b成为新队头

4.6 返回队头元素

略,具体实现:首先确保不是空队列,然后返回 data[head]

5. 各个操作的时间复杂度

  • 入队 (enqueue): 数组实现: 均摊 O(1) (因为需要考虑扩容的情况,但大多数情况下是 O(1)), 链表实现: O(1)

  • 出队 (dequeue): O(1)

  • 查看队首元素 (peek): O(1)

6. 队列的局限性

  • 数组实现的假溢出: 使用环形数组实现时,虽然解决了普通数组的"一次性"问题,但仍然存在容量限制。需要仔细处理扩容操作。

  • 固定大小:在某些实现中,队列的大小是固定的,这意味着一旦队列满了,就不能再添加新的元素,除非移除一些元素。这可能导致数据丢失或需要额外的逻辑来处理溢出。

  • 性能问题:在基于数组的队列实现中,如果队列经常达到其最大容量,那么在队列的末尾添加元素可能需要数组的复制,这会带来额外的时间成本。虽然这个操作是偶尔发生的,但在高负载情况下可能会影响性能。

  • 不适合随机访问:队列不支持随机访问,这意味着你不能直接访问队列中间的元素。如果你需要随机访问,可能需要使用其他数据结构,如数组或链表。

  • 不适合实时系统:在实时系统中,队列可能不是最佳选择,因为队列的操作(入队和出队)可能需要等待,特别是在有大量并发操作时。

  • 空间效率:在基于数组的实现中,即使队列中没有很多元素,数组也可能被预分配了较大的空间,这可能导致空间的浪费。

  • 操作的局限性:队列只允许在队尾添加元素,在队头移除元素。如果需要在队列中间进行操作,队列可能不是最合适的选择。

  • 并发问题:在多线程环境中,队列的操作需要同步,以避免竞态条件和数据不一致的问题。这可能需要额外的锁机制,从而影响性能。

  • 不适合处理大量数据:如果需要处理大量数据,队列可能不是最佳选择,因为队列的操作可能会因为数据量大而变得缓慢。

  • 不适合需要频繁插入和删除的场景:如果应用场景中需要频繁地在队列的中间进行插入和删除操作,队列可能不是最佳选择,因为这些操作在队列中是不允许的。

  • 不适合需要多种访问模式的场景:如果应用需要多种不同的数据访问模式,如堆栈的后进先出(LIFO)特性,队列可能不足以满足需求。

7. 总结和应用场景

队列是一种简单但强大的数据结构,其 FIFO 特性使其在许多场景下都非常有用,例如:

  • 任务调度: 操作系统中的任务调度通常使用队列来管理待执行的任务,保证先提交的任务先执行。例如打印队列,按照先来先服务的顺序打印文档。

  • 宽度优先搜索 (BFS): 图算法中常用的宽度优先搜索算法使用队列来存储待访问的节点, ensuring that nodes closer to the starting node are visited first.

  • 缓冲区: 在生产者-消费者模型中,队列可以作为缓冲区,平衡生产者和消费者的速度差异。生产者将数据放入队列,消费者从队列中取出数据。队列可以缓解生产和消费速度不匹配的问题,避免数据丢失或程序阻塞. 例如,网络请求中的缓冲区。

  • 消息队列: 在分布式系统中,消息队列用于异步通信。发送方将消息放入队列,接收方从队列中取出消息。消息队列可以解耦发送方和接收方,提高系统的可靠性和可扩展性。 例如,Kafka, RabbitMQ.

理解队列的概念和实现对于程序员来说至关重要,它能帮助我们更好地设计和优化程序。

希望本文能帮助各位看官更好地理解队列这种重要的数据结构!下期见,谢谢~

相关推荐
京东云开发者几秒前
Java的SPI机制详解
java
超级小忍23 分钟前
服务端向客户端主动推送数据的几种方法(Spring Boot 环境)
java·spring boot·后端
程序无bug27 分钟前
Spring IoC注解式开发无敌详细(细节丰富)
java·后端
小莫分享29 分钟前
Java Lombok 入门
java
程序无bug29 分钟前
Spring 对于事务上的应用的详细说明
java·后端
食亨技术团队30 分钟前
被忽略的 SAAS 生命线:操作日志有多重要
java·后端
苦学编程的谢1 小时前
Maven
java·maven·intellij-idea
考虑考虑1 小时前
Maven 依赖范围(Scope)
java·后端·maven
chao_7891 小时前
回溯题解——子集【LeetCode】二进制枚举法
开发语言·数据结构·python·算法·leetcode
张小洛1 小时前
Spring AOP 设计解密:代理对象生成、拦截器链调度与注解适配全流程源码解析
java·后端·spring·spring aop·aop