队列知识点最详细整理+总结(基于《王道数据结构考研复习指导》)
一、队列基础概念
队列是一种 先进先出(FIFO) 的线性结构,操作受限,仅允许在队尾插入(入队),队头删除(出队)。
二、顺序队列
1. 结构定义
- 存储方式:基于数组,
front
和rear
分别指向队头和队尾。 - 代码实现:
#define MAX_SIZE 100 typedef struct { int data[MAX_SIZE]; int front; // 队头指针 int rear; // 队尾指针 } SeqQueue;
2. 优缺点
- 优点:实现简单,访问速度快(数组随机访问)。
- 缺点:固定大小导致空间浪费或溢出;出队需移动元素,时间复杂度高。
3. 核心操作代码
-
入队(时间复杂度O(1)):
int EnQueue(SeqQueue *q, int x) { if (q->rear == MAX_SIZE) { printf("队列已满"); return 0; } q->data[q->rear++] = x; return 1; }
-
出队(时间复杂度O(n),需移动元素):
int DeQueue(SeqQueue *q, int *x) { if (q->front == q->rear) { printf("队列为空"); return 0; } *x = q->data[0]; for (int i = 0; i < q->rear - 1; i++) q->data[i] = q->data[i + 1]; // 元素前移 q->rear--; return 1; }
三、循环队列
1. 结构定义
- 核心思想:数组首尾相连,通过取模运算实现循环,解决“假溢出”。
- 判空/满条件:
- 队空:
front == rear
- 队满:
(rear + 1) % MAX_SIZE == front
(牺牲一个存储单元)。
- 队空:
2. 优缺点
- 优点:空间利用率高,入队/出队O(1)。
- 缺点:容量固定,实现复杂(需处理模运算)。
3. 核心操作代码
typedef struct {
int data[MAX_SIZE];
int front, rear;
} CircularQueue;
// 入队
int EnQueue(CircularQueue *q, int x) {
if ((q->rear + 1) % MAX_SIZE == q->front) {
printf("队列已满");
return 0;
}
q->data[q->rear] = x;
q->rear = (q->rear + 1) % MAX_SIZE;
return 1;
}
// 出队
int DeQueue(CircularQueue *q, int *x) {
if (q->front == q->rear) {
printf("队列为空");
return 0;
}
*x = q->data[q->front];
q->front = (q->front + 1) % MAX_SIZE;
return 1;
}
四、双向队列(Deque)
1. 结构定义
- 特点:允许两端插入和删除,结合队列和栈的特性。
- 实现方式:数组或链表(常用循环数组)。
2. 优缺点
- 优点:操作灵活,支持随机访问(数组实现)。
- 缺点:数组实现需处理扩容,链表实现需额外指针开销。
3. 核心操作代码(数组实现)
#define DEQUE_SIZE 100
typedef struct {
int data[DEQUE_SIZE];
int front, rear;
} Deque;
// 队头插入
int PushFront(Deque *dq, int x) {
if ((dq->front - 1 + DEQUE_SIZE) % DEQUE_SIZE == dq->rear) {
printf("队列已满");
return 0;
}
dq->front = (dq->front - 1 + DEQUE_SIZE) % DEQUE_SIZE;
dq->data[dq->front] = x;
return 1;
}
// 队尾删除
int PopRear(Deque *dq, int *x) {
if (dq->front == dq->rear) {
printf("队列为空");
return 0;
}
*x = dq->data[(dq->rear - 1 + DEQUE_SIZE) % DEQUE_SIZE];
dq->rear = (dq->rear - 1 + DEQUE_SIZE) % DEQUE_SIZE;
return 1;
}
五、时间复杂度分析
操作 | 顺序队列 | 循环队列 | 双向队列(数组) |
---|---|---|---|
入队(尾部) | O(1) | O(1) | O(1) |
出队(头部) | O(n) | O(1) | O(1) |
随机访问 | O(1) | O(1) | O(1) |
解释:顺序队列出队需移动元素,故为O(n);循环队列通过模运算避免移动,所有操作O(1)。
六、考研高频考点
- 循环队列的判空/满条件(如:
(rear + 1) % size == front
)。 - 双端队列的应用:滑动窗口、页面置换算法。
- 队列与层次遍历(BFS) :求二叉树层数、最短路径。
七、经典算法题解析(补充至3题)
题目1:用队列实现栈(LeetCode 225)
问题描述:
使用两个队列实现一个后进先出(LIFO)的栈,并支持普通栈的全部操作(push
, pop
, top
, empty
)。
解法思路:
- 核心矛盾:队列是FIFO,而栈是LIFO。
- 关键操作:每次
push
新元素时,先将元素加入辅助队列,再将主队列中的元素依次移入辅助队列,最后交换两个队列的角色。 - 时间复杂度:
push
为O(n),其他操作为O(1)。
C语言代码(基于循环队列实现):
#include <stdlib.h>
#define MAX_SIZE 100
typedef struct {
CircularQueue q1; // 主队列
CircularQueue q2; // 辅助队列
} MyStack;
MyStack* myStackCreate() {
MyStack *stack = (MyStack*)malloc(sizeof(MyStack));
stack->q1.front = stack->q1.rear = 0;
stack->q2.front = stack->q2.rear = 0;
return stack;
}
void myStackPush(MyStack* obj, int x) {
// 先将元素加入辅助队列q2
EnQueue(&obj->q2, x);
// 将q1中的元素全部转移到q2
while (!QueueEmpty(&obj->q1)) {
int val;
DeQueue(&obj->q1, &val);
EnQueue(&obj->q2, val);
}
// 交换q1和q2的角色
CircularQueue temp = obj->q1;
obj->q1 = obj->q2;
obj->q2 = temp;
}
int myStackPop(MyStack* obj) {
int val;
DeQueue(&obj->q1, &val);
return val;
}
int myStackTop(MyStack* obj) {
return obj->q1.data[obj->q1.front];
}
int myStackEmpty(MyStack* obj) {
return QueueEmpty(&obj->q1);
}
易错点:
- 队列判空条件错误:未正确实现
QueueEmpty
函数,导致逻辑混乱。 - 未及时交换队列:在
push
操作后必须交换队列,否则后续操作会出错。
题目2:二叉树的层序遍历(LeetCode 102)
问题描述:
给定一个二叉树,返回其按层序遍历得到的节点值(即逐层从左到右访问所有节点)。
解法思路:
- BFS框架:用队列存储每层的节点,每次处理一层节点时,记录当前队列长度(即该层节点数),依次出队并将子节点入队。
- 时间复杂度:O(n),每个节点进出队列一次。
C语言代码(需结合队列结构):
typedef struct TreeNode {
int val;
struct TreeNode *left, *right;
} TreeNode;
int** levelOrder(TreeNode* root, int* returnSize, int** returnColumnSizes) {
if (!root) return NULL;
// 初始化队列
CircularQueue q;
q.front = q.rear = 0;
EnQueue(&q, root);
int**res = (int**)malloc(sizeof(int*) * MAX_SIZE);
*returnColumnSizes = (int*)malloc(sizeof(int) * MAX_SIZE);
*returnSize = 0;
while (!QueueEmpty(&q)) {
int level_size = (q.rear - q.front + MAX_SIZE) % MAX_SIZE;
res[*returnSize] = (int*)malloc(sizeof(int) * level_size);
(*returnColumnSizes)[*returnSize] = level_size;
for (int i = 0; i < level_size; i++) {
TreeNode* node;
DeQueue(&q, &node); // 出队当前层节点
res[*returnSize][i] = node->val;
if (node->left) EnQueue(&q, node->left); // 左子节点入队
if (node->right) EnQueue(&q, node->right); // 右子节点入队
}
(*returnSize)++;
}
return res;
}
关键点解析:
- 记录每层大小:
level_size
用于确定当前层的节点数量,确保结果数组正确分配内存。 - 子节点入队条件:需判断子节点非空再入队,避免队列中混入空指针。
题目3:设计循环双端队列(LeetCode 641)
问题描述:
设计实现一个循环双端队列,支持以下操作:
insertFront()
:在队头添加元素。insertLast()
:在队尾添加元素。deleteFront()
:删除队头元素。deleteLast()
:删除队尾元素。getFront()
:获取队头元素。getRear()
:获取队尾元素。isEmpty()
:检查队列是否为空。isFull()
:检查队列是否已满。
解法思路:
- 循环数组实现:利用取模运算处理索引循环,牺牲一个存储单元区分队空和队满。
- 边界处理:插入和删除时需分别处理
front
和rear
的移动方向。
C语言代码:
typedef struct {
int *data;
int front, rear;
int capacity;
} MyCircularDeque;
MyCircularDeque* myCircularDequeCreate(int k) {
MyCircularDeque* dq = (MyCircularDeque*)malloc(sizeof(MyCircularDeque));
dq->data = (int*)malloc(sizeof(int) * (k + 1)); // 多分配一个空间用于判满
dq->front = dq->rear = 0;
dq->capacity = k + 1;
return dq;
}
int myCircularDequeInsertFront(MyCircularDeque* obj, int value) {
if (myCircularDequeIsFull(obj)) return 0;
obj->front = (obj->front - 1 + obj->capacity) % obj->capacity; // 队头前移
obj->data[obj->front] = value;
return 1;
}
int myCircularDequeInsertLast(MyCircularDeque* obj, int value) {
if (myCircularDequeIsFull(obj)) return 0;
obj->data[obj->rear] = value;
obj->rear = (obj->rear + 1) % obj->capacity; // 队尾后移
return 1;
}
int myCircularDequeDeleteFront(MyCircularDeque* obj) {
if (myCircularDequeIsEmpty(obj)) return 0;
obj->front = (obj->front + 1) % obj->capacity;
return 1;
}
int myCircularDequeDeleteLast(MyCircularDeque* obj) {
if (myCircularDequeIsEmpty(obj)) return 0;
obj->rear = (obj->rear - 1 + obj->capacity) % obj->capacity;
return 1;
}
int myCircularDequeGetFront(MyCircularDeque* obj) {
return myCircularDequeIsEmpty(obj) ? -1 : obj->data[obj->front];
}
int myCircularDequeGetRear(MyCircularDeque* obj) {
return myCircularDequeIsEmpty(obj) ? -1 : obj->data[(obj->rear - 1 + obj->capacity) % obj->capacity];
}
int myCircularDequeIsEmpty(MyCircularDeque* obj) {
return obj->front == obj->rear;
}
int myCircularDequeIsFull(MyCircularDeque* obj) {
return (obj->rear + 1) % obj->capacity == obj->front;
}
易错点:
- 索引计算错误:在队头插入时,
front
应前移(front - 1
),而非后移。 - 判满条件错误:必须牺牲一个存储单元,否则队空和队满条件会冲突。
八、易错点总结
- 循环队列的判满条件:必须牺牲一个存储单元,否则与判空条件冲突。
- 指针初始值:
front
和rear
通常初始化为0,但需根据实现方式调整。 - 边界检查:操作前必须检查队列是否为空/满,避免溢出或空指针。
九、解题方法论
- 选择合适结构:空间固定用循环队列,动态扩展用链队列。
- BFS模板:队列维护当前层节点,逐层扩展。
- 滑动窗口:双端队列维护极值,减少重复计算。
附:考研复习建议
- 重点掌握:循环队列的判空判满、双端队列的应用。
- 典型习题:设计循环队列(LeetCode 622)、二叉树的右视图(BFS应用)。
- 易错题:循环队列长度计算
(rear - front + size) % size
。
通过系统梳理和代码实践,可深入理解队列的核心思想与应用场景。