mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4mobile wallpaper 5mobile wallpaper 6
1999 字
5 分钟
数据结构:队列、栈和循环队列
2026-05-12

题目概览#

范围#

来源内容
class013QueueStackAndCircularQueue

题型归纳#

题型: 基础数据结构实现

适用条件: 需要自己实现队列、栈、循环队列,或者题目要求在固定数组空间内完成入队、出队、取队头、取队尾等操作。

解法: 队列用数组配合左右边界 lr,栈用数组配合 size,循环队列额外维护当前元素个数 size 和容量 limit

易错点: 队列的有效范围是 [l, r),栈顶位置是 size - 1,循环队列的 r 指向下一个可写位置,取尾元素时要处理 r == 0 的回绕。

看到”固定数组空间内实现队列或栈”就是这类题的信号——数组队列用 [l, r) 半开区间管理有效范围,数组栈用 size 同时表示元素个数和下一个写入位置。循环队列的核心是让 r 到达末尾后回绕到 0,并用独立的 size 区分空和满;最容易出错的是 Rear() 直接返回 queue[r]——r 是下一个可写位置,真正的队尾在 r 的前一个位置,且要处理 r == 0 时回绕到 limit - 1 的情况。

基础结构#

队列#

队列是先进先出结构,常见操作如下:

操作含义
offer从尾部加入元素
poll从头部弹出元素
peek / head查看头部元素但不弹出
tail查看尾部元素
size返回当前元素数量

#

栈是后进先出结构,常见操作如下:

操作含义
push压入栈顶
pop弹出栈顶
peek查看栈顶但不弹出
size返回当前元素数量

队列实现#

Java 内部实现#

如果直接使用 Java 内置结构,可以用 LinkedList 当队列。

public static class Queue1 {
public Queue<Integer> queue = new LinkedList<>();
public boolean isEmpty() {
return queue.isEmpty();
}
public void offer(int num) {
queue.offer(num);
}
public int poll() {
return queue.poll();
}
public int peek() {
return queue.peek();
}
public int size() {
return queue.size();
}
}

这种写法好处是简单,坏处是 LinkedList 内部是双向链表,常数时间不如数组实现。

数组队列#

刷题时更常见的写法是提前准备一个数组,用 lr 表示队列范围:

[l, r)

也就是:

  • l 指向当前队头。
  • r 指向下一个可写入的位置。
  • 队列为空时,l == r
  • 当前元素个数是 r - l

核心模板如下:

public static class Queue2 {
public int[] queue;
public int l;
public int r;
public Queue2(int n) {
queue = new int[n];
l = 0;
r = 0;
}
public boolean isEmpty() {
return l == r;
}
public void offer(int num) {
queue[r++] = num;
}
public int poll() {
return queue[l++];
}
public int head() {
return queue[l];
}
public int tail() {
return queue[r - 1];
}
public int size() {
return r - l;
}
}

指针角色#

变量作用
queue保存队列元素的数组
l队头位置,弹出时向右移动
r下一个可写位置,加入时向右移动

例如依次加入 3, 5, 7 后:

queue = [3, 5, 7, ...]
l = 0
r = 3
有效范围 = [0, 3)

弹出一次后:

l = 1
r = 3
有效范围 = [1, 3)
队头 = queue[1]
队尾 = queue[2]

这种普通数组队列适合能确定加入操作总次数不超过 n 的场景。即使中间有弹出,l 之前的空间也不会复用。

栈实现#

Java 内部实现#

可以直接使用 Java 的 Stack

public static class Stack1 {
public Stack<Integer> stack = new Stack<>();
public boolean isEmpty() {
return stack.isEmpty();
}
public void push(int num) {
stack.push(num);
}
public int pop() {
return stack.pop();
}
public int peek() {
return stack.peek();
}
public int size() {
return stack.size();
}
}

这种写法也很方便,但刷题时如果数据范围明确,数组栈通常更直接。

数组栈#

数组实现栈只需要一个 size

public static class Stack2 {
public int[] stack;
public int size;
public Stack2(int n) {
stack = new int[n];
size = 0;
}
public boolean isEmpty() {
return size == 0;
}
public void push(int num) {
stack[size++] = num;
}
public int pop() {
return stack[--size];
}
public int peek() {
return stack[size - 1];
}
public int size() {
return size;
}
}

size 有两层含义:

  • 当前栈里有几个元素。
  • 下一个新元素应该写到哪个位置。

所以入栈是 stack[size++] = num,出栈是 return stack[--size]——栈顶元素永远在 size - 1 位置,弹出前要先让 size 回到栈顶下标。

例题:622. 设计循环队列#

题目链接

题意#

设计一个循环队列实现。循环队列是一种线性数据结构,其操作表现基于 FIFO(先进先出)原则,并且队尾被连接在队首之后形成一个循环。它也被称为”环形缓冲器”。

循环队列的好处是可以利用队列之前用过的空间。普通队列一旦队尾走到数组末尾,就不能继续插入下一个元素,即使前面因为出队操作空出了位置。循环队列则可以让队尾回到数组开头,继续使用这些空位。

需要实现 MyCircularQueue 类:

  • MyCircularQueue(k) 构造器,设置队列长度为 k
  • Front() 从队首获取元素,如果队列为空,返回 -1
  • Rear() 获取队尾元素,如果队列为空,返回 -1
  • enQueue(value) 向循环队列插入一个元素,如果成功插入则返回 true
  • deQueue() 从循环队列中删除一个元素,如果成功删除则返回 true
  • isEmpty() 检查循环队列是否为空。
  • isFull() 检查循环队列是否已满。

示例#

创建一个容量为 3 的循环队列:

MyCircularQueue circularQueue = new MyCircularQueue(3);
步骤操作返回值队列内容
1enQueue(1)true[1]
2enQueue(2)true[1, 2]
3enQueue(3)true[1, 2, 3]
4enQueue(4)false[1, 2, 3],队列已满
5Rear()3队尾是 3
6isFull()true当前容量已满
7deQueue()true[2, 3]
8enQueue(4)true[2, 3, 4]
9Rear()4队尾是 4
LeetCode 调用格式

平台上的原始输入会写成方法名数组和参数数组,输出则是每一步调用的返回值:

输入:
["MyCircularQueue", "enQueue", "enQueue", "enQueue", "enQueue", "Rear", "isFull", "deQueue", "enQueue", "Rear"]
[[3], [1], [2], [3], [4], [], [], [], [4], []]
输出:
[null, true, true, true, false, 3, true, true, true, 4]

提示#

  • 所有的值都在 01000 的范围内
  • 操作数将在 11000 的范围内
  • 请不要使用内置的队列库

解法#

核心思路#

普通数组队列的问题是:弹出后,前面的空间不会再使用。循环队列要解决的就是这个问题:当 r 走到数组最后一个位置后,如果前面有空位,就让 r 回到 0 继续写。

关键在于:循环数组里 l == r 既可能表示空,也可能表示满。额外维护 size 后,size == 0 表示空,size == limit 表示满,判断就变得非常清楚。

普通队列的 l 之前是”死空间”,循环队列让 r 回绕到数组开头,把死空间变成可复用空间——代价是需要一个独立的 size 来区分空和满。

核心模板#

class MyCircularQueue {
public int[] queue;
public int l, r, size, limit;
public MyCircularQueue(int k) {
queue = new int[k];
l = r = size = 0;
limit = k;
}
public boolean enQueue(int value) {
if (isFull()) {
return false;
} else {
queue[r] = value;
r = r == limit - 1 ? 0 : (r + 1);
size++;
return true;
}
}
public boolean deQueue() {
if (isEmpty()) {
return false;
} else {
l = l == limit - 1 ? 0 : (l + 1);
size--;
return true;
}
}
public int Front() {
if (isEmpty()) {
return -1;
} else {
return queue[l];
}
}
public int Rear() {
if (isEmpty()) {
return -1;
} else {
int last = r == 0 ? (limit - 1) : (r - 1);
return queue[last];
}
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == limit;
}
}

变量角色#

queue 是固定长度的底层数组;l 指向当前队头位置;r 指向下一个可写位置,入队后向右移动并在末尾回绕到 0size 记录当前队列里的元素个数,用来区分空和满;limit 是队列容量,即数组长度。

过程拆解#

  1. 入队:先判满,没满则把值写到 r 位置,移动 r(到末尾回绕),size++
  2. 出队:先判空,不空则移动 l(到末尾回绕),size--。不需要清空旧值,边界变量决定有效数据。
  3. 取队头:直接返回 queue[l]
  4. 取队尾r 是下一个可写位置,真正的队尾在 r 的前一个位置。r == 0 时回绕到 limit - 1,否则就是 r - 1
  5. 判空判满size == 0 为空,size == limit 为满。

易错点#

  • 入队成功后移动 r 并且 size++,出队成功后移动 l 并且 size--,两步缺一不可。
  • 数组里的旧值不用清理,边界变量才决定当前有效数据。
enQueue() 只写 queue[r++]

错误写法:

public boolean enQueue(int value) {
if (isFull()) return false;
queue[r++] = value; // 错!r 可能走到 limit,下一次访问就越界
size++;
return true;
}

为什么错:循环队列的 r 到达数组最后一个位置后,不能继续 ++limit,而是要回绕到 0

正确写法:

public boolean enQueue(int value) {
if (isFull()) return false;
queue[r] = value;
r = r == limit - 1 ? 0 : r + 1;
size++;
return true;
}
deQueue() 只改 size,不移动 l

错误写法:

public boolean deQueue() {
if (isEmpty()) return false;
size--; // 错!队头 l 没动,Front() 还会读到旧队头
return true;
}

为什么错:出队删除的是当前队头,成功出队后必须让 l 移到下一个位置。数组里的旧值不用清理,但队头边界必须更新。

正确写法:

public boolean deQueue() {
if (isEmpty()) return false;
l = l == limit - 1 ? 0 : l + 1;
size--;
return true;
}
Rear() 直接返回 queue[r]

错误写法:

public int Rear() {
if (isEmpty()) return -1;
return queue[r]; // 错!r 是下一个可写位置,不是队尾
}

为什么错:r 指向的是下一个将要写入的位置,不是最后一个已写入的位置。如果 r == 0,实际队尾在 queue[limit - 1];否则队尾在 queue[r - 1]

正确写法:

public int Rear() {
if (isEmpty()) return -1;
int last = r == 0 ? (limit - 1) : (r - 1);
return queue[last];
}
l == r 判断空或满

错误写法:

public boolean isEmpty() {
return l == r; // 循环队列中 l == r 可能是空,也可能是满!
}

为什么错:在循环数组中,队列从空开始入队直到满,lr 会再次相遇。仅靠 l == r 无法区分这两种状态。

正确写法:用独立的 size 变量区分:

public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == limit;
}
口诀

l 指队头,r 指下一个可写位置,size 决定空和满,到末尾就回绕。

复杂度#

  • 队列、栈、循环队列的每个操作时间复杂度都是 O(1)
  • 数组队列和数组栈的空间复杂度是 O(n)
  • 循环队列的空间复杂度是 O(k)k 是初始化时给定的容量。
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

数据结构:队列、栈和循环队列
https://github.com/algorithmzuo/algorithm-journey/tree/main/src/class013
作者
黎明
发布于
2026-05-12
许可协议
MIT

部分信息可能已经过时

目录