【码道初阶】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 的数组长度是实现循环队列最优雅的方式之一,它避免了引入复杂的标志位,使得代码逻辑更加清晰简洁。


相关推荐
Angelina_Jolie12 分钟前
一文搞懂 SCI、SSCI、CSSCI、C 刊、核心期刊:定义、作用、层级对比及投稿选择
考研·职场和发展·创业创新
m0_7487080513 分钟前
C++中的观察者模式实战
开发语言·c++·算法
然哥依旧13 分钟前
【轴承故障诊断】基于融合鱼鹰和柯西变异的麻雀优化算法OCSSA-VMD-CNN-BILSTM轴承诊断研究【西储大学数据】(Matlab代码实现)
算法·支持向量机·matlab·cnn
电商API_1800790524722 分钟前
第三方淘宝商品详情 API 全维度调用指南:从技术对接到生产落地
java·大数据·前端·数据库·人工智能·网络爬虫
qq_5375626725 分钟前
跨语言调用C++接口
开发语言·c++·算法
一点程序36 分钟前
基于SpringBoot的选课调查系统
java·spring boot·后端·选课调查系统
Tingjct37 分钟前
【初阶数据结构-二叉树】
c语言·开发语言·数据结构·算法
C雨后彩虹38 分钟前
计算疫情扩散时间
java·数据结构·算法·华为·面试
2601_9498095942 分钟前
flutter_for_openharmony家庭相册app实战+我的Tab实现
java·javascript·flutter
vx_BS813301 小时前
【直接可用源码免费送】计算机毕业设计精选项目03574基于Python的网上商城管理系统设计与实现:Java/PHP/Python/C#小程序、单片机、成品+文档源码支持定制
java·python·课程设计