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

队列(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.

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

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

相关推荐
Oneforlove_twoforjob12 分钟前
【Java基础面试题033】Java泛型的作用是什么?
java·开发语言
向宇it29 分钟前
【从零开始入门unity游戏开发之——C#篇24】C#面向对象继承——万物之父(object)、装箱和拆箱、sealed 密封类
java·开发语言·unity·c#·游戏引擎
小蜗牛慢慢爬行31 分钟前
Hibernate、JPA、Spring DATA JPA、Hibernate 代理和架构
java·架构·hibernate
清梦20201 小时前
经典问题---跳跃游戏II(贪心算法)
算法·游戏·贪心算法
Dream_Snowar1 小时前
速通Python 第四节——函数
开发语言·python·算法
星河梦瑾1 小时前
SpringBoot相关漏洞学习资料
java·经验分享·spring boot·安全
黄名富1 小时前
Redis 附加功能(二)— 自动过期、流水线与事务及Lua脚本
java·数据库·redis·lua
love静思冥想1 小时前
JMeter 使用详解
java·jmeter
言、雲2 小时前
从tryLock()源码来出发,解析Redisson的重试机制和看门狗机制
java·开发语言·数据库
1nullptr2 小时前
三次翻转实现数组元素的旋转
数据结构