【码道初阶】LeetCode 622:设计循环队列:警惕 Rear() 方法中的“幽灵数据”陷阱

【LeetCode 622】设计循环队列:警惕 Rear() 方法中的"幽灵数据"陷阱

在数据结构的实现中,循环队列(Circular Queue) 是一个非常经典的题目。它通过复用数组空间,避免了普通队列在元素出队后空间浪费的问题。

然而,在使用数组模拟循环队列时,有一个非常容易被忽视的细节:当队列"逻辑上"为空时,数组中可能残留着"物理上"存在的旧数据。 如果处理不当,这些旧数据就会像幽灵一样被错误的读取,导致判定失败。

本文将以 LeetCode 622 题为例,分析一种常见的实现漏洞。

1. 核心设计策略:多开辟一个空间

在设计循环队列时,最关键的问题是如何区分 "队列空""队列满"

如果不采取特殊策略,当 front == rear 时,既可能表示空,也可能表示满。

一种通用的、无须维护额外 size 变量的解决方案是:数组长度设为 k + 1

  • 队列容量k
  • 数组实际长度k + 1
  • 判空条件front == rear
  • 判满条件(rear + 1) % length == front

在这种设计下,rear 指针总是指向下一个存放数据的位置,始终保留一个空位不存数据,用来作为满队列的标志位。

2. 案发现场:测试用例为何失败?

在实现 Rear()(获取队尾元素)方法时,初学者容易写出如下代码:

java 复制代码
// ❌ 存在隐患的实现
public int Rear() {
    // 计算 rear 的前一个位置(因为 rear 指向的是下一个空位)
    int index = (rear == 0) ? queue.length - 1 : rear - 1;
    return queue[index];
}

乍一看逻辑是正确的:因为 rear 指向的是新元素将要插入的位置,所以当前的队尾元素自然在 rear - 1 的位置(考虑数组越界需取模处理)。

然而,在以下测试场景中,该代码会报错:

  1. 入队enQueue(7)。此时数组中 queue[0] = 7rear 移动到 1。
  2. 出队deQueue()。此时 front 移动到 1。
    • 关键点 :此时 front == rear,队列逻辑上已经为空
    • 但在物理内存中queue[0] 的位置依然存储着 7,并没有被清零。
  3. 获取队尾 :调用 Rear()
    • 上述代码计算 index 为 0。
    • 程序直接返回了 queue[0],也就是 7
  4. 预期结果 :队列为空,根据题目要求,应该返回 -1

这就是**"逻辑删除"**带来的副作用。deQueue 只是移动了指针,数据依然残留在数组里。如果不先检查队列是否为空,直接通过坐标回溯去取值,就会读到本该"不存在"的脏数据。

3. 修复方案与正确代码

问题的根源在于:在读取数据前,缺少了对队列状态的校验。

Front() 方法通常会检查 isEmpty() 一样,Rear() 方法也必须严格执行判空逻辑。

修正后的逻辑:

java 复制代码
public int Rear() {
    // ✅ 必须先检查队列是否为空
    if (isEmpty()) {
        return -1;
    }
    // 确认非空后,再安全地回溯位置
    int index = (rear == 0) ? queue.length - 1 : rear - 1;
    return queue[index];
}

完整且健壮的代码实现:

java 复制代码
class MyCircularQueue {
    public int front = 0;
    public int rear = 0;
    public int[] queue;
    
    public MyCircularQueue(int k) {
        // 策略:多开一个空间,用于利用 (rear+1)%len == front 来判满
        queue = new int[k + 1];
    }
    
    public boolean enQueue(int value) {
        if (isFull()) return false;
        queue[rear] = value;
        rear = (rear + 1) % queue.length;
        return true;
    }
    
    public boolean deQueue() {
        if (isEmpty()) return false;
        // 逻辑删除:只移动指针,不需要物理擦除数据
        front = (front + 1) % queue.length;
        return true;
    }
    
    public int Front() {
        if (isEmpty()) return -1;
        return queue[front];
    }
    
    public int Rear() {
        // 【关键】防止读取到已出队的残留数据
        if (isEmpty()) return -1;
        
        int index = (rear == 0) ? queue.length - 1 : rear - 1;
        return queue[index];
    }
    
    public boolean isEmpty() {
        return front == rear;
    }
    
    public boolean isFull() {
        return (rear + 1) % queue.length == front;
    }
}

4. 技术总结

  1. 逻辑删除 vs 物理删除

    在基于数组的缓冲区设计中,删除操作通常是 O(1) 的指针移动。这意味着内存中的旧数据会一直存在,直到被新数据覆盖。因此,所有读取操作(Get/Peek)必须建立在"数据有效性"的校验之上

  2. 方法的一致性

    在编写 Front()Rear() 这种成对的方法时,逻辑结构应当保持对称。如果 Front() 进行了判空处理,Rear() 也绝不能省略,否则极易产生边缘 Case 的 Bug。

  3. 循环队列的判满策略

    使用 k+1 的数组长度是实现循环队列最优雅的方式之一,它避免了引入复杂的标志位,使得代码逻辑更加清晰简洁。


相关推荐
程序员根根3 小时前
SpringBoot Web 入门核心知识点(快速开发案例 + 分层解耦实战)
java·spring boot
Dylan的码园3 小时前
链表与LinkedList
java·数据结构·链表
【非典型Coder】3 小时前
JVM 垃圾收集器中的记忆集与读写屏障
java·开发语言·jvm
feathered-feathered3 小时前
Redis【事务】(面试相关)与MySQL相比较,重点在Redis事务
android·java·redis·后端·mysql·中间件·面试
大大大大物~3 小时前
JVM 之 内存溢出实战【OOM? SOF? 哪些区域会溢出?堆、虚拟机栈、元空间、直接内存溢出时各自的特点?以及什么情况会导致他们溢出?并模拟溢出】
java·jvm·oom·sof
mit6.8243 小时前
博弈-翻转|hash<string>|smid
算法
仪***沿3 小时前
探索三相、五相电机的容错控制奥秘
java
代码游侠3 小时前
复习——Linux 系统编程
linux·运维·c语言·学习·算法
码界奇点3 小时前
基于Spring MVC与JdbcTemplate的图书管理系统设计与实现
java·spring·车载系统·毕业设计·mvc·源代码管理