目录
[(二)Queue.h 完整代码](#(二)Queue.h 完整代码)
[六、队列算法题1 ------ 用队列实现栈](#六、队列算法题1 —— 用队列实现栈)
[七、队列算法题2 ------ 用栈实现队列](#七、队列算法题2 —— 用栈实现队列)
[八、队列算法题3 ------ 设计循环队列](#八、队列算法题3 —— 设计循环队列)
一、队列核心概念与结构
(一)基本定义
队列是特殊的线性表 ,仅允许在一端(队尾)执行插入操作(入队),在另一端(队头)执行删除操作(出队),严格遵循先进先出(FIFO,First In First Out) 原则。
(二)核心术语
**1、队头:**进行删除操作的一端,数据从这里取出。
**2、队尾:**进行插入操作的一端,数据从这里存入。
**3、入队:**在队尾插入新元素的操作。
**4、出队:**在队头删除元素的操作。
**5、空队列:**无有效元素的队列
(三)逻辑与物理结构
**1、逻辑结构:**线性结构,数据元素呈线性排列。
**2、物理结构:**取决于底层实现方式,可采用数组或链表存储。
二、队列底层结构选型
(一)非最佳方案
1、使用数组实现
(1)操作特性
① 队尾插入数据:时间复杂度 O (1),直接在数组末尾添加元素。
② 队头删除数据:时间复杂度 O (n),删除头部元素后需整体移动后续数据。
(2)缺陷
存在效率瓶颈,即便反过来,变成队头插入数据、队尾删除数据,队头的时间复杂度也是O(n),元素需要整体后移,队头才可以插入元素。
所以无论头尾如何定义,也至少有一个操作的时间复杂度为 O (n),且无法通过优化规避数据挪移问题。
2、使用单链表实现
(1)操作特性
**1、头部操作(删除 / 插入):**时间复杂度 O (1),直接修改指针指向。
**2、尾部操作(插入 / 删除):**时间复杂度 O (n),需遍历至尾结点。
(2)缺陷
无论谁是单链表两端哪里是队头,处理单链表尾部元素 的时候,都会因为需要遍历得到尾结点,从而达到 O(n) 的时间复杂度。
3、使用双向链表实现
**(1)优势:**头尾操作时间复杂度均为 O (1)。
**(2)缺陷:**每个结点需额外存储前驱指针,空间开销为单链表的 2 倍(32 位系统下,单链表结点占 8 字节,双向链表节点占 16 字节),仅在必要时选用。
(二)最佳实现方案
数组的 O(n) 复杂度是无法优化的,因为这是数组本身的性质决定的;双向链表的空间浪费也是无法优化的,因为这也是双向链表的性质决定的。
那我们能够优化的就只有单链表,只要把尾部指针处理好,就可以实现尾部操作也是O(1)的复杂度,从而得到好的实现。
所以我们的解决方案是,采用 **"单链表 + 头尾指针"**的实现方式,在队列的结构体中定义队头指针与队尾指针,从而平衡时间效率与空间开销。
三、环境搭建与结构体定义
(一)工程文件结构
需创建 3 个文件,分工明确,便于维护:
**1、Queue.h:**结构体定义、函数声明、头文件引入(对外提供接口)。
**2、Queue.c:**所有函数的具体实现(内部逻辑)。
**3、test.c:**测试用例编写,验证功能正确性(调用接口)。
(二)Queue.h 完整代码
cpp
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>
// 1. 定义队列节点结构(单链表节点)
typedef int QDataTpe;
typedef struct QueueNode {
QDataTpe data; // 存储数据
struct QueueNode* next; // 指向下一结点的指针
} QueueNode;
// 2. 定义队列管理结构(维护头尾指针与元素个数)
typedef struct Queue {
QueueNode* phead; // 队头指针(指向第一个元素)
QueueNode* ptail; // 队尾指针(指向最后一个元素)
int size; // 有效元素个数(优化计数效率)
} Queue;
// 3. 函数声明
//1、队列的初始化与销毁
void QueueInit(Queue* pq); // 初始化队列
void QueueDestroy(Queue* pq); // 销毁队列
//2、辅助函数
void Print(Queue* pq); // 打印
bool QueueEmpty(Queue* pq); // 判空
3、队操作
void QueuePush(Queue* pq, QDataTpe x); // 入队(队尾插入)
void QueuePop(Queue* pq); // 出队(队头删除)
QDataTpe QueueFront(Queue* pq); // 取队头数据
QDataTpe QueueBack(Queue* pq); // 取队尾数据
int QueueSize(Queue* pq); // 统计有效元素个数
四、核心函数的实现
**(一)**队列初始化(QueueInit)
cpp
void QueueInit(Queue* pq)
{
assert(pq); // 断言:确保pq不为空指针(避免野指针操作)
// 空队列时,头尾指针均指向NULL,元素个数为0
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
(二)辅助函数
1、打印操作(Print)
cpp
// 打印队列
void Print(Queue* pq)
{
assert(pq);//判空操作
QueueNode* tmp = pq->phead;
printf("队列为:");
while (tmp) {
printf("%d -> ", tmp->data);
tmp = tmp->next;
}
printf("NULL\n");
}
2、判空操作(QueueEmpty)
cpp
// 判空:队头为空则队列空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead == NULL;// 队头为空则队列空
}
(三)队列操作
1、入队操作(QueuePush)
cpp
void QueuePush(Queue* pq, QDataTpe x)
{
assert(pq);
// 创建新结点
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL) {
perror("malloc fail!");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
// 空队列时,新节点既是队头也是队尾
if (pq->phead == NULL) {
pq->phead = pq->ptail = newnode;
} else {
// 非空队列时,直接在队尾插入
pq->ptail->next = newnode; // 原队尾的next指向新结点
pq->ptail = newnode; // 更新队尾指针为新结点
}
pq->size++; // 元素个数递增
}
2、出队操作(QueuePop)
cpp
void QueuePop(Queue* pq)
{
assert(pq && !QueueEmpty(pq)); // 队列非空断言
// 单个结点的特殊处理(避免野指针)
if (pq->phead == pq->ptail) {
//free的是pq->phead还是pq->ptail都是一样的,因为都是那个指针
free(pq->phead);
pq->phead = pq->ptail = NULL;
}else {
// 多个结点时,删除队头并更新指针
QueueNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--; // 元素个数递减
}
**Tip:**单结点的情况不要忘记处理。
3、取队头/队尾元素(QueueFront & QueueBack)
cpp
// 取队头数据(仅读取,不删除)
QDataTpe QueueFront(Queue* pq)
{
assert(pq && !QueueEmpty(pq)); // 队列非空断言
return pq->phead->data; // 直接返回队头节点的数据
}
// 取队尾数据(仅读取,不删除)
QDataTpe QueueBack(Queue* pq)
{
assert(pq && !QueueEmpty(pq)); // 队列非空断言
return pq->ptail->data; // 直接返回队尾节点的数据
}
4、统计个数(QueueSize**)**
cpp
// 统计个数:直接返回size(O(1)效率)
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
(四)队列销毁(QueueDestroy)
cpp
void QueueDestroy(Queue* pq)
{
assert(pq);
QueueNode* pcur = pq->phead;
while (pcur != NULL)
{
QueueNode* next = pcur->next; // 暂存下一结点(避免释放后找不到)
free(pcur); // 释放当前结点
pcur = next; // 移动到下一结点
}
// 重置队列状态(避免野指针)
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
五、测试代码(test.c)
cpp
#include "Queue.h"
void test_queue()
{
Queue q;
// 初始化队列
QueueInit(&q);
printf("=== 初始化后 ===\n");
Print(&q); // 预期:队列为:NULL(空队列)
printf("是否为空:%s\n\n", QueueEmpty(&q) ? "是" : "否");
// 入队操作
QueuePush(&q, 10);
QueuePush(&q, 20);
QueuePush(&q, 30);
printf("=== 入队3个元素后 ===\n");
Print(&q); // 预期:队列为:10 -> 20 -> 30 -> NULL
printf("队头:%d,队尾:%d,元素个数:%d\n\n",
QueueFront(&q), QueueBack(&q), QueueSize(&q)); // 10, 30, 3
// 出队操作
QueuePop(&q);
printf("=== 出队1个元素后 ===\n");
Print(&q); // 预期:队列为:20 -> 30 -> NULL
printf("队头:%d,队尾:%d,元素个数:%d\n\n",
QueueFront(&q), QueueBack(&q), QueueSize(&q)); // 20, 30, 2
// 继续出队至空
QueuePop(&q);
QueuePop(&q);
printf("=== 出队至空后 ===\n");
Print(&q); // 预期:队列为:NULL
printf("是否为空:%s\n", QueueEmpty(&q) ? "是" : "否");
// 销毁队列
QueueDestroy(&q);
}
int main()
{
test_queue();
return 0;
}
六、队列算法题1 ------ 用队列实现栈
(一)题目描述
1、题目简述
使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty),即包括以下函数:
**(1)**void push(int x) 将元素 x 压入栈顶。
**(2)**int pop() 移除并返回栈顶元素。
**(3)**int top() 返回栈顶元素。
**(4)**boolean empty() 如果栈是空的,返回 true ;否则,返回 false 。
2、题目链接:https://leetcode.cn/problems/implement-stack-using-queues/description/
(二)解题思路
1、核心矛盾:队列 FIFO(先进先出) 特性与栈 LIFO(后进先出)特性冲突,需用两个队列(q1和q2)交替存储,模拟栈的 "后入先出"。
2、实现思路
**(1)入栈:**始终向非空队列插入数据(初始时任意选择一个队列)。
(2)出栈: 将非空队列中前 n-1 个元素转移到空队列,剩余最后一个元素即为 "栈顶",弹出该元素。【从而实现后进先出】
**(3)取栈顶:**直接返回非空队列的队尾元素(栈顶对应队列尾)。
**(4)判空:**两个队列均为空时,栈为空。
简单说:往一个队列存数据后,若想像栈一样 "后进先出" 取数据,就把前 n-1 个元素移到另一个队列,原队列剩下的最后一个元素,就是最晚存入的,取出它就实现了栈的效果。
(三)代码实现
部分相关函数此前已完成实现,此处不再过多阐释,可直接调用。做题时,需注意补充这些函数的具体实现部分。
cpp
#include "Queue.h"
// 用两个队列实现栈
typedef struct
{
Queue q1;
Queue q2;
} MyStack;
// 初始化栈
MyStack* myStackCreate()
{
MyStack* st = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&st->q1);
QueueInit(&st->q2);
return st;
}
// 入栈(向非空队列插入)
void myStackPush(MyStack* obj, int x)
{
if (!QueueEmpty(&obj->q1))
{
QueuePush(&obj->q1, x); // q1非空,插入q1
} else {
QueuePush(&obj->q2, x); // q1空,插入q2
}
}
// 出栈(转移前n-1个元素,弹出最后一个)
int myStackPop(MyStack* obj)
{
// 确定空队列(emp)和非空队列(nonEmp)
Queue* emp = &obj->q1;
Queue* nonEmp = &obj->q2;
if (!QueueEmpty(&obj->q1))
{
emp = &obj->q2;
nonEmp = &obj->q1;
}
// 转移非空队列中前size-1个元素到空队列
while (QueueSize(nonEmp) > 1) {
QueuePush(emp, QueueFront(nonEmp)); // 非空队列的队头插入空队列
QueuePop(nonEmp); // 非空队列出队
}
// 弹出非空队列中最后一个元素(栈顶)
int top = QueueFront(nonEmp);
QueuePop(nonEmp);
return top;
}
// 取栈顶(返回非空队列的队尾)
int myStackTop(MyStack* obj)
{
if (!QueueEmpty(&obj->q1))
{
return QueueBack(&obj->q1);
} else {
return QueueBack(&obj->q2);
}
}
// 判空(两个队列均为空)
bool myStackEmpty(MyStack* obj)
{
return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}
// 销毁栈
void myStackFree(MyStack* obj)
{
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
}
七、队列算法题2 ------ 用栈实现队列
(一)题目描述
1、题目简述
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty),即包括以下函数:
**(1)**void push(int x) :将元素 x 推到队列的末尾
**(2)**int pop() :从队列的开头移除并返回元素
**(3)**int peek() :返回队列开头的元素
**(4)**boolean empty(): 如果队列为空,返回 true ;否则,返回 false
2、题目链接:https://leetcode.cn/problems/implement-queue-using-stacks/description/
(二)解题思路
**1、****结构定义:**创建包含两个栈的队列结构(MyQueue),分为入栈(pushST)和出栈(popST)。
**2、入队操作:**直接将数据压入pushST。
**3、出队操作:**若popST非空,直接弹出栈顶;若为空,将pushST中所有数据转移至popST后弹出栈顶。
**4、****取队头元素:**逻辑同出队,仅返回栈顶元素不删除。
**5、****判空操作:**两个栈均为空时队列为空。
**Tip:**与上一道题目的思路就是一致的。
(三)代码实现
cpp
typedef struct {
Stack pushST; // 入队栈
Stack popST; // 出队栈
} MyQueue;
MyQueue* myQueueCreate()
{
MyQueue* pq = (MyQueue*)malloc(sizeof(MyQueue));
STInit(&pq->pushST);
STInit(&pq->popST);
return pq;
}
void myQueuePush(MyQueue* obj, int x)
{
//往pushST中插入数据
StackPush(&obj->pushST, x);
}
//检查popST是否为空
// 1)不为空,取栈顶
// 2)为空,pushST倒入到popST中,再取popST的栈顶
int myQueuePop(MyQueue* obj)
{
if (StackEmpty(&obj->popST))
{
//导数据
while (!StackEmpty(&obj->pushST))
{
StackPush(&obj->popST, StackTop(&obj->pushST));
StackPop(&obj->pushST);
}
}
//popST不为空
int top = StackTop(&obj->popST);
StackPop(&obj->popST);
return top;
}
//返回队头元素,逻辑同出队
int myQueuePeek(MyQueue* obj)
{
if (StackEmpty(&obj->popST))
{
//导数据
while (!StackEmpty(&obj->pushST))
{
StackPush(&obj->popST, StackTop(&obj->pushST));
StackPop(&obj->pushST);
}
}
//popST不为空
return StackTop(&obj->popST);
}
//判断队列是否为空
bool myQueueEmpty(MyQueue* obj) {
return StackEmpty(&obj->pushST) && StackEmpty(&obj->popST);
}
void myQueueFree(MyQueue* obj) {
STDestroy(&obj->pushST);
STDestroy(&obj->popST);
free(obj);
obj = NULL;
}
八、队列算法题3 ------ 设计循环队列
(一)题目描述
1、题目简述
设计你的循环队列实现。
循环队列是一种线性数据结构 ,其操作表现基于 **FIFO(先进先出)**原则并且队尾被连接在队首之后以形成一个循环。它也被称为"环形缓冲器"。
循环队列的一个好处是我们可以利用这个队列之前用过的空间。
在一个普通队列里,一旦一个队列满了,我们就不能插入下一个元素,即使在队列前面仍有空间。但是使用循环队列,我们能使用这些空间去存储新的值。
你的实现应该支持如下操作:
**(1)**MyCircularQueue(k): 构造器,设置队列长度为 k 。
**(2)**Front: 从队首获取元素。如果队列为空,返回 -1 。
**(3)**Rear: 获取队尾元素。如果队列为空,返回 -1 。
**(4)**enQueue(value): 向循环队列插入一个元素。如果成功插入则返回真。
**(5)**deQueue(): 从循环队列中删除一个元素。如果成功删除则返回真。
**(6)**isEmpty(): 检查循环队列是否为空。
**(7)**isFull(): 检查循环队列是否已满。
2、题目链接:https://leetcode.cn/problems/design-circular-queue/description/
(二)循环队列核心概念
1、定义与核心特征
循环队列又称 "环形缓冲器",是队尾与队首逻辑相连的特殊队列,核心特征如下:
**① 操作规则:**队首(front)仅用于删除数据,队尾(rear)仅用于插入数据,遵循 FIFO 原则。
**② 空间限制:**初始化时指定固定容量,使用过程中不可扩容。
**③ 满队约束:**空间占满后需先删除数据,才能插入新数据,插入失败时返回 false。
2、两种实现方式对比
循环队列可通过数组或循环链表实现,二者各有优劣,具体对比如下:
| 对比维度 | 数组实现 | 链表实现 |
| 初始化复杂度 | O (1),单次内存分配完成 | O (n),需创建并链接所有节点 |
| 操作逻辑 | 下标加减 + 取模运算实现循环 | 指针跳转实现节点操作 |
| 空间利用率 | 连续内存,无额外指针开销 | 每个节点含数据域 + 指针域,开销较大 |
| 满队判断方式 | 牺牲一个空间或增加计数器 | 通过指针关系直接判断 |
| 适用场景 | 空间需求固定、追求高效访问的场景 | 空间需求灵活、插入删除频繁的场景 |
|---|
实际开发与算法题中,数组实现因初始化简单、操作高效更常被采用,以下重点围绕数组实现展开。
(三)数组实现核心设计
1、核心问题:空队与满队的区分
数组实现的核心难点的是:空队和满队时均可能出现 front == rear,无法直接区分。解决方案为牺牲一个空间法(不额外增加计数器,更节省内存):
**(1)实际申请空间:**k+1 个(k 为队列容量,预留 1 个空间用于区分状态)。
**(2)空队条件:**front == rear(队头与队尾指向同一位置)。
(3)满队条件:(rear + 1) % (k + 1) == front(队尾的下一个位置为队头,循环意义上)。

2、数据结构定义
需定义结构体存储核心成员,包含 4 个关键部分:
**(1)数组指针:**存储队列数据的连续内存空间。
**(2)队头指针(front):**记录队首元素下标,初始值为 0。
**(3)队尾指针(rear):**记录下一个可插入位置下标,初始值为 0。
**(4)容量(capacity):**队列实际可存储的有效数据个数(即初始化时的 k)。
cpp
typedef struct {
int* arr; // 存储数据的数组
int front; // 队头下标
int rear; // 队尾下标(下一个可插入位置)
int capacity; // 队列容量(有效数据个数)
} MyCircularQueue;
(四)关键操作实现步骤
1、初始化(创建队列)
**(1)申请结构体空间:**为 MyCircularQueue 类型分配内存。
**(2)申请数组空间:**按capacity + 1(即 k+1)个 int 类型空间分配,预留区分空满的空间。
**(3)初始化指针:**front 和 rear 均设为 0,表示初始为空队。
cpp
MyCircularQueue* myCircularQueueCreate(int k)
{
MyCircularQueue* pq = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
pq->arr = (int*)malloc(sizeof(int) * (k + 1)); // 申请k+1个空间
pq->front = 0;
pq->rear = 0;
pq->capacity = k;
return pq;
}
2、状态判断(isEmpty /isFull)
**(1)判空:**直接通过front == rear判断,成立则为空队。
**(2)判满:**通过(rear + 1) % (capacity + 1) == front判断,成立则为满队。
3、插入元素(enQueue)
**功能:**向队尾插入一个元素,成功返回 true,失败返回 false。
**(1)步骤 1:**先通过满队条件判断队列是否已满,满则返回 false。
**(2)步骤 2:**将元素存入 rear 指向的位置。
**(3)步骤 3:**rear 指针后移,通过取模(rear + 1) % (capacity + 1)处理循环越界。
**(4)步骤 4:**返回 true 表示插入成功。
cpp
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value)
{
// 先判断是否满队
if (myCircularQueueIsFull(obj)) {
return false;
}
// 插入数据
obj->arr[obj->rear] = value;
// rear指针后移(循环处理)
obj->rear = (obj->rear + 1) % (obj->capacity + 1);
return true;
}
4、删除元素(deQueue)
**功能:**从队首删除一个元素,成功返回 true,失败返回 false。
**(1)步骤 1:**通过空队条件判断队列是否为空,空则返回 false。
**(2)步骤 2:**front 指针后移,通过取模处理循环越界(无需实际删除数据,指针移动即表示元素失效)。
**(3)步骤 3:**返回 true 表示删除成功。
cpp
bool myCircularQueueDeQueue(MyCircularQueue* obj)
{
// 先判断是否空队
if (myCircularQueueIsEmpty(obj)) {
return false;
}
// front指针后移(循环处理)
obj->front = (obj->front + 1) % (obj->capacity + 1);
return true;
}
5、获取队首 / 队尾元素(Front / Rear)
**(1)获取队首:**空队返回 - 1,否则返回 arr[front]。
**(2)获取队尾:**空队返回 - 1,否则需计算实际队尾位置(rear 指向的是下一个插入位置,实际队尾为 rear 的前一个位置):若 rear == 0,实际队尾为 capacity(循环到数组末尾)。否则,实际队尾为rear - 1。
cpp
// 获取队首元素
int myCircularQueueFront(MyCircularQueue* obj)
{
if (myCircularQueueIsEmpty(obj)) {
return -1;
}
return obj->arr[obj->front];
}
// 获取队尾元素
int myCircularQueueRear(MyCircularQueue* obj) {
if (myCircularQueueIsEmpty(obj)) {
return -1;
}
int prev = obj->rear - 1;
// 处理rear为0的循环情况
// 此时的队尾元素在最后,刚好就是capacity指向的数据
if (obj->rear == 0) {
prev = obj->capacity;
}
return obj->arr[prev];
}
6、销毁队列(Free)
功能:释放动态分配的内存,避免内存泄漏。
**(1)步骤 1:**释放数组指针 arr 指向的空间。
**(2)步骤 2:**释放队列结构体指针 obj 指向的空间。
cpp
void myCircularQueueFree(MyCircularQueue* obj)
{
if (obj->arr != NULL) {
free(obj->arr);
obj->arr = NULL; // 避免野指针
}
free(obj);
obj = NULL;
}
(五)核心注意事项
1、取模运算:所有指针后移操作必须加取模 (capacity + 1),确保循环特性,避免数组越界。
2、空间预留:数组实际申请 k+1 个空间是区分空满的关键,不可省略。
**3、队尾计算:**获取队尾元素时需处理 rear=0 的边界情况,避免出现负下标。
4、内存释放:销毁队列时需先释放数组空间,再释放结构体空间,养成置空指针的习惯。
以上即为 一篇文章掌握"队列" 的全部内容,创作不易,麻烦三连支持一下呗~
