目录
[一、📦 栈:后进先出的"子弹夹"](#一、📦 栈:后进先出的“子弹夹”)
[底层实现:数组 vs 链表](#底层实现:数组 vs 链表)
[二、🚇 队列:先进先出的"排队系统"](#二、🚇 队列:先进先出的“排队系统”)
[三、⚔️ 实战:经典算法题](#三、⚔️ 实战:经典算法题)
[第一题 有效的括号](#第一题 有效的括号)
[💡 核心思路:利用栈的"匹配消除"特性](#💡 核心思路:利用栈的“匹配消除”特性)
[🛠️ 代码演进:从"繁琐"到"优雅"](#🛠️ 代码演进:从“繁琐”到“优雅”)
[🚀 最终专业实现](#🚀 最终专业实现)
[第二题 用队列实现栈](#第二题 用队列实现栈)
[💡 核心思路:利用双队列的"搬运"特性](#💡 核心思路:利用双队列的“搬运”特性)
[🛠️ 代码演进:从"暴力"到"巧妙"](#🛠️ 代码演进:从“暴力”到“巧妙”)
[1. 初学者的直觉(暴力法)](#1. 初学者的直觉(暴力法))
[2. 优化策略:化繁为简(只动一次)](#2. 优化策略:化繁为简(只动一次))
[🚀 最终专业实现](#🚀 最终专业实现)
[📝 复杂度分析](#📝 复杂度分析)
[💡 为什么选这种写法?](#💡 为什么选这种写法?)
[第三題 用栈实现队列](#第三題 用栈实现队列)
[232. 用栈实现队列 - 力扣(LeetCode)](#232. 用栈实现队列 - 力扣(LeetCode))
[🛠️ 代码演进:从"每次必搬"到"按需搬运"](#🛠️ 代码演进:从“每次必搬”到“按需搬运”)
[1. 初学者的直觉(笨办法)](#1. 初学者的直觉(笨办法))
[2. 优化策略:懒惰搬运(推荐)](#2. 优化策略:懒惰搬运(推荐))
[🚀 最终专业实现](#🚀 最终专业实现)
[📝 总结与复杂度分析](#📝 总结与复杂度分析)
[第四题 进阶:循环队列](#第四题 进阶:循环队列)
[💡 1. 为什么要搞个"圈"?(解决假溢出)](#💡 1. 为什么要搞个“圈”?(解决假溢出))
[🧠 2. 核心难点:如何优雅地"转圈"?](#🧠 2. 核心难点:如何优雅地“转圈”?)
[⚔️ 3. 最大的坑:怎么区分"空"和"满"?](#⚔️ 3. 最大的坑:怎么区分“空”和“满”?)
[💻 4. 代码实现(C语言 - 计数器法)](#💻 4. 代码实现(C语言 - 计数器法))
[📝 重点总结](#📝 重点总结)
在数据结构的世界里,栈 和队列是两种最基础但也最重要的线性表。它们不仅在日常编程中无处不在,更是操作系统、编译器乃至高并发系统的基石。
今天我们就结合经典课件与实战经验,彻底搞懂这两个"性格迥异"的数据结构。
一、📦 栈:后进先出的"子弹夹"
什么是栈?
栈是一种特殊的线性表,它有着严格的纪律:只允许在固定的一端进行插入和删除操作。
- 栈顶:进行数据插入和删除的一端。
- 栈底:固定不动的一端。
- 核心原则 :后进先出。
想象一下手枪的弹夹,最后压入的子弹会最先被打出去。
底层实现:数组 vs 链表
虽然栈可以用数组或链表实现,但数组通常是更优的选择。
- 数组实现:在数组尾部进行插入和删除操作的时间复杂度为 O(1),且内存连续,缓存命中率高。
- 链表实现:虽然也能实现,但需要额外的指针空间,且频繁的内存分配效率略低。
代码实现(基于数组)
我们定义一个动态增长的栈结构:
cpp
typedef int STDataType;
typedef struct Stack
{
STDataType* a; // 指向动态开辟的数组
int top; // 栈顶指针(通常指向栈顶元素的位置,或下一个可插入位置)
int capacity; // 容量
} ST;
// 核心接口声明
void STInit(ST* ps); // 初始化
void STDestroy(ST* ps); // 销毁
void STPush(ST* ps, STDataType x); // 入栈
void STPop(ST* ps); // 出栈
STDataType STTop(ST* ps); // 获取栈顶元素
int STSize(ST* ps); // 获取有效元素个数
bool STEmpty(ST* ps); // 判空
💡 补充思考 :在实现
top变量时,有两种常见流派。一种是top指向当前栈顶元素(初始化-1),另一种是top指向下一个可插入的位置(初始化0)。后者在计算元素个数时直接返回top即可,逻辑上往往更简洁。
cpp
void STInit(ST* ps)// 初始化
{
assert(ps);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
void STDestroy(ST* ps) // 销毁
{
assert(ps);
if (ps->a)
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x) // 入栈
{
assert(ps);
if (ps->top == ps->capacity)
{
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail:\n");
exit(1);
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top++] = x;
}
bool STEmpty(ST* ps) // 判空
{
assert(ps);
return ps->top == 0;
}
void STPop(ST* ps) // 出栈
{
assert(!STEmpty(ps));
ps->top--;
}
STDataType STTop(ST* ps) // 获取栈顶元素
{
assert(ps);
return ps->a[ps->top-1];
}
int STSize(ST* ps) // 获取有效元素个数
{
assert(ps);
return ps->top;
}
二、🚇 队列:先进先出的"排队系统"
什么是队列?
队列也是一种特殊的线性表,但它允许在一端插入 ,在另一端删除。
- 队尾:进行插入操作。
- 队头:进行删除操作。
- 核心原则 :先进先出。
就像我们在超市结账排队,先来的人先结账离开。
底层实现:链表胜出
与栈不同,队列的实现通常推荐使用链表。
- 数组的劣势:如果使用普通数组,出队列需要删除头部元素,这会导致后续所有元素向前移动,时间复杂度高达 O(N)。
- 链表的优势:维护一个头指针和一个尾指针,入队在尾部插,出队在头部删,时间复杂度均可为 O(1)。
代码实现(基于链表)
为了高效操作,我们需要维护头尾两个指针:
cpp
typedef int QDataType;
// 队列结点结构
typedef struct QueueNode {
QDataType val;
struct QueueNode* next;
} QNode;
// 队列结构(包含头尾指针和大小)
typedef struct Queue {
QNode* phead; // 队头
QNode* ptail; // 队尾
int size; // 元素个数
} Queue;
// 核心接口声明
void QueueInit(Queue* pq);
void QueueDestroy(Queue* pq);
void QueuePush(Queue* pq, QDataType x); // 入队(尾)
void QueuePop(Queue* pq); // 出队(头)
QDataType QueueFront(Queue* pq); // 取队头数据
QDataType QueueBack(Queue* pq); // 取队尾数据
bool QueueEmpty(Queue* pq);
int QueueSize(Queue* pq);
cpp
#include"queue.h"
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* pcur = pq->phead;
while (pcur)
{
QNode* next = pcur->next;
free(pcur);
pcur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
void QueuePush(Queue* pq, QDataType x) // 入队(尾)
{
assert(pq);
QNode* newnode = (QNode*)mealloc(sizeof(QNode));
if (newnode == NULL)
{
perror("QNode fail:");
exit(1);
}
newnode->val = x;
newnode->next = NULL;
if (pq->ptail == NULL)
{
pq->phead = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->phead == NULL;
}
void QueuePop(Queue* pq) // 出队(头)
{
assert(pq);
assert(!QueueEmpty(pq));
if (pq->phead == pq->ptail)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
QDataType QueueFront(Queue* pq) // 取队头数据
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->phead->val;
}
QDataType QueueBack(Queue* pq) // 取队尾数据
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->ptail->val;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
三、⚔️ 实战:经典算法题
掌握了基础结构后,我们来看看 LeetCode 上的经典面试题,这些题目考察的是对数据结构的灵活运用。
第一题 有效的括号

💡 核心思路:利用栈的"匹配消除"特性
解决括号匹配问题,最直觉的方法就是利用栈(Stack)的"后进先出"特性。我们可以把整个过程想象成一种"消除游戏":
- 遇到左括号 :说明需要等待一个右括号来闭合,将其入栈。
- 遇到右括号 :说明需要寻找最近的左括号进行匹配,将栈顶元素出栈比对。
- 最终判定:如果遍历完字符串后,栈为空(所有括号都完美消除),则字符串有效;否则无效。
🛠️ 代码演进:从"繁琐"到"优雅"
在写具体逻辑时,初学者很容易写出下面这种"繁琐"的代码: 遇到右括号时,先判断栈是否为空,再取栈顶元素,然后用一大串 if-else 去判断左右括号是否对应。如果不匹配,还要记得销毁栈再返回 false。
痛点分析:
- 匹配逻辑嵌套太深,阅读体验差。
- 在循环中多次调用
STDestroy(&st); return false;,一旦后续增加逻辑,极易遗漏清理步骤,导致内存泄漏。
优化策略 : 我们不妨换个思路------取反面 。不要一直去判断"什么时候该继续",而是直接找出"什么时候该直接判定失败"。只要遇到以下三种情况,直接 break 跳出循环:
- 栈为空,但遇到了右括号。
- 栈顶的左括号与当前右括号不匹配。
- 字符串遍历结束,但栈里还有未消除的左括号。
🚀 最终专业实现
结合上述思路,我们可以将代码重构得更加清晰、健壮:
cpp
/**
* @brief 判断给定的括号字符串是否有效
* @param s 待检测的括号字符串
* @return true 如果括号有效,否则返回 false
*/
bool isValid(const char* s)
{
// 1. 防御性编程:检查空指针
if (s == NULL) return false;
ST st;
STInit(&st);
bool valid = true; // 默认假设有效,遇到异常再置为 false
// 2. 遍历字符串进行匹配消除
while (*s != '\0')
{
// 遇到左括号,直接入栈等待匹配
if (*s == '(' || *s == '{' || *s == '[')
{
STPush(&st, *s);
}
// 遇到右括号,进行匹配验证
else
{
// 异常1:栈为空,说明没有左括号可以匹配
if (STEmpty(&st))
{
valid = false;
break;
}
// 异常2:栈顶左括号与当前右括号不匹配
char topChar = STTop(&st);
bool isMatch = (topChar == '(' && *s == ')') ||
(topChar == '{' && *s == '}') ||
(topChar == '[' && *s == ']');
if (!isMatch)
{
valid = false;
break;
}
// 匹配成功,弹出栈顶左括号(完成一次消除)
STPop(&st);
}
s++;
}
// 异常3:遍历结束,但栈内还有残留的左括号
if (valid && !STEmpty(&st))
{
valid = false;
}
// 3. 统一释放栈资源,防止内存泄漏
STDestroy(&st);
return valid;
}
第二题 用队列实现栈

💡 核心思路:利用双队列的"搬运"特性
首先我们要明确两个数据结构的本质区别:
- 队列 (Queue):先进先出(FIFO),像排队买票,先来的先走。
- 栈 (Stack):后进先出(LIFO),像叠盘子,最后放上去的最先拿走。
矛盾点 在于:队列只能从队头取数据,而栈要求取"最新"的数据。 解决策略:既然队列取不到最新的,我们就把旧的都移走,让"最新"的那个变成队头。我们需要两个队列:
- 主队列 (
q1):用来存储最终排好序的数据,保证队头永远是栈顶元素。 - 辅助队列 (
q2):作为"中转站",在插入新元素时暂存旧数据。
🛠️ 代码演进:从"暴力"到"巧妙"
1. 初学者的直觉(暴力法)
很多人第一反应是:每次 push 进 q1,然后执行 pop 操作时,把 q1 的前 N-1 个元素全部倒腾到 q2,取出最后一个,再把 q2 倒回 q1。
缺点 :太麻烦了!每次
pop都要把数据搬来搬去两次,效率极低。
2. 优化策略:化繁为简(只动一次)
我们可以换个角度:不要等到 pop 时才整理数据,而是在 push 时就把它排好序。
具体步骤:
- 新元素
x进来时,先放入空的辅助队列q2。 - 将主队列
q1中的所有旧元素依次出队,并进入q2。- 此时,
q2的顺序变成了:[新元素 x, 旧元素1, 旧元素2...]
- 此时,
- 交换
q1和q2的角色(或者直接赋值)。- 现在
q1的队头就是刚才进来的x,完美符合栈的特性!
- 现在
这样,pop、top 和 empty 操作就变得极其简单,直接对 q1 操作即可。
🚀 最终专业实现
这种写法将复杂逻辑集中在 push 中,保持了其他操作的高效(O(1) 时间复杂度)。
cpp
//上面是 Queue.h 和 Queue.c
(包含 QueueInit, QueuePush, QueuePop, QueueFront, QueueEmpty, QueueDestroy)
typedef struct {
Queue q1; // 主队列:始终保持栈的顺序(队头即栈顶)
Queue q2; // 辅助队列:用于 push 时的临时中转
} MyStack;
/**
* 创建并初始化栈
*/
MyStack* myStackCreate() {
MyStack* obj = (MyStack*)malloc(sizeof(MyStack));
QueueInit(&obj->q1);
QueueInit(&obj->q2);
return obj;
}
/**
* 入栈操作
* 核心逻辑:先把新元素放进空队列,再把旧元素全搬过来
*/
void myStackPush(MyStack* obj, int x) {
// 1. 新元素先入辅助队列 q2
QueuePush(&obj->q2, x);
// 2. 将主队列 q1 的所有旧元素搬运到 q2
// 这样 q2 的顺序就变成了:[新元素, 旧元素1, 旧元素2 ...]
while (!QueueEmpty(&obj->q1)) {
int front = QueueFront(&obj->q1);
QueuePop(&obj->q1);
QueuePush(&obj->q2, front);
}
// 3. 交换 q1 和 q2
// 现在 q1 又变成了主队列,且队头是最新的元素
Queue tmp = obj->q1;
obj->q1 = obj->q2;
obj->q2 = tmp;
}
/**
* @brief 出栈操作
* 因为 push 时已经排好序,直接取队头即可
*/
int myStackPop(MyStack* obj) {
int val = QueueFront(&obj->q1);
QueuePop(&obj->q1);
return val;
}
/**
* @brief 获取栈顶元素
*/
int myStackTop(MyStack* obj) {
return QueueFront(&obj->q1);
}
/**
* @brief 判断栈是否为空
*/
bool myStackEmpty(MyStack* obj) {
return QueueEmpty(&obj->q1);
}
/**
* @brief 销毁栈,释放内存
*/
void myStackFree(MyStack* obj) {
QueueDestroy(&obj->q1);
QueueDestroy(&obj->q2);
free(obj);
}
📝 复杂度分析
- 时间复杂度 :
push: O(N)O(N) 。因为每次入栈都需要把旧元素搬运一次。pop,top,empty: O(1)O(1) 。直接操作队头,非常快。
- 空间复杂度: O(N)O(N) 。需要两个队列来存储 NN 个元素。
💡 为什么选这种写法?
虽然 push 慢了一点,但在实际业务中,我们往往希望读取数据(pop/top)越快越好。这种"写入时整理"的策略,保证了后续操作的丝滑流畅,非常适合读多写少的场景。
第三題 用栈实现队列
232. 用栈实现队列 - 力扣(LeetCode)

核心思路:双栈的"负负得正"效应
首先我们要明确两个数据结构的本质区别:
- 栈 (Stack) :后进先出 (LIFO)。像叠盘子,最后放上去的在最上面。
- 队列 (Queue) :先进先出 (FIFO)。像排队,最先来的最先走。
矛盾点 :栈只能从顶部取数据(最新的),而队列要求从底部取数据(最老的)。
解决策略 :利用数学上的"负负得正"原理。如果我们把数据从一个栈倒入另一个栈,数据的顺序就会反转一次。
我们需要两个栈:
- 输入栈 (
pushST):专门负责接收新进来的数据。 - 输出栈 (
popST) :专门负责提供队头数据。当需要pop或peek时,如果输出栈为空,就将输入栈的数据全部倒过来。
🛠️ 代码演进:从"每次必搬"到"按需搬运"
1. 初学者的直觉(笨办法)
很多人第一反应是:为了保证顺序,每次 push 一个新元素,我都把旧元素倒腾一遍,让新元素沉底;或者每次 pop 完,为了维持原状,又把数据倒回去。
痛点分析:这样做的复杂度太高了!每次操作都要移动所有元素,时间复杂度是 O(N),效率极低。
2. 优化策略:懒惰搬运(推荐)
其实我们不需要时刻保持数据是有序的。只有当我们需要取数据(pop 或 peek),且输出栈为空时,才进行搬运。
具体逻辑:
- 出队/查看 (
pop/peek) :- 检查
popST是否有数据? - 如果有:直接弹出/查看栈顶(这就是最早进入的那个元素)。
- 如果没有 :将
pushST里的所有数据依次弹出,压入popST。此时popST的栈顶就是我们要找的"队头"。
- 检查
为什么这样是对的?
假设依次入栈 1, 2, 3。
pushST从底到顶是 1, 2, 3。倒入
popST后,popST从底到顶变成 3, 2, 1。此时
popST的栈顶是 1,正是最先入队的元素!完美符合队列特性。
🚀 最终专业实现
结合你之前提供的 Stack 接口,我们可以写出非常优雅的代码。
cpp
typedef struct {
Stack pushST; // 输入栈:只管进
Stack popST; // 输出栈:只管出
} MyQueue;
/** 初始化队列 */
MyQueue* myQueueCreate() {
MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
StackInit(&obj->pushST);
StackInit(&obj->popST);
return obj;
}
/** 入队操作:直接压入输入栈 */
void myQueuePush(MyQueue* obj, int x) {
StackPush(&obj->pushST, x);
}
/**
* 辅助函数:确保输出栈有数据
* 只有当输出栈为空时,才把输入栈的数据导过来
*/
void moveDataIfEmpty(MyQueue* obj) {
if (StackEmpty(&obj->popST)) {
while (!StackEmpty(&obj->pushST)) {
// 将输入栈顶元素弹出,压入输出栈
StackPush(&obj->popST, StackTop(&obj->pushST));
StackPop(&obj->pushST);
}
}
}
/** 出队操作 */
int myQueuePop(MyQueue* obj) {
// 1. 先保证输出栈里有数据
moveDataIfEmpty(obj);
// 2. 弹出输出栈顶(即队头)
int front = StackTop(&obj->popST);
StackPop(&obj->popST);
return front;
}
/** 查看队头元素 */
int myQueuePeek(MyQueue* obj) {
// 1. 先保证输出栈里有数据
moveDataIfEmpty(obj);
// 2. 返回输出栈顶,但不弹出
return StackTop(&obj->popST);
}
/** 判断队列是否为空 */
bool myQueueEmpty(MyQueue* obj) {
// 只有两个栈都为空,队列才为空
return StackEmpty(&obj->pushST) && StackEmpty(&obj->popST);
}
/** 释放内存 */
void myQueueFree(MyQueue* obj) {
StackDestroy(&obj->pushST);
StackDestroy(&obj->popST);
free(obj);
}
📝 总结与复杂度分析
这种"双栈法"是面试中的标准答案。
- 时间复杂度 :
push是 O(1)。
- 空间复杂度:O(N),需要两个栈来存储数据。
第四题 进阶:循环队列

💡 1. 为什么要搞个"圈"?(解决假溢出)
想象你在排队买奶茶:
- 普通队列(数组版) :队伍一直往后排。前面的人买完走了,前面的位置就空了。但是后面不断有人来,队伍越来越长,最后超出了店门(数组越界)。哪怕前面空了一万个位置,你也得说"满了",这就是假溢出。
- 循环队列 :把队伍首尾相连。当最后一个人买完走了,新来的人可以回到最前面的空位继续排队。这就叫循环利用空间。
🧠 2. 核心难点:如何优雅地"转圈"?
在代码里,我们不能真的把数组连成环,我们是通过**取模运算(%)**来模拟"回头"的动作。
假设数组长度为 k,当前下标是 i:
- 普通前进 :
i + 1 - 循环前进 :
(i + 1) % k
原理解析 :
如果
k=5,当前下标是 4(最后一个位置)。
(4 + 1) % 5=5 % 5= 0 。看!下标瞬间回到了起点。这就是"循环"的魔法。
⚔️ 3. 最大的坑:怎么区分"空"和"满"?
这是这道题唯一的考点。
当 front(头指针)和 rear(尾指针)重合时,既可能是空 (没人),也可能是满(挤爆了)。
有三种解法。
方法一:少用一个空间(最常用,推荐)
- 规定 :数组虽然开了
k个大小,但我们只存k-1个数据。永远留一个空位作为"警戒线"。 - 判空 :
front == rear - 判满 :
(rear + 1) % k == front(意思是:尾指针再走一步就要追上头指针了,这时候就算满了)。
方法二:多开一个空间(代码最简)
- 做法 :题目要求容量为
k,我们实际分配k + 1的空间。 - 好处 :逻辑和方法一完全一样,但对外接口来说,你就是存了
k个数,不需要在计算时减来减去,非常不容易出错。
方法三:使用计数器 (size)
- 做法 :额外定义一个变量
size记录当前元素个数。 - 判空 :
size == 0 - 判满 :
size == capacity - 评价:这种方法最简单粗暴,完全避开了指针重合的问题,也是工程上常用的做法。
💻 4. 代码实现(C语言 - 计数器法)
这里使用**方法三(计数器法)**来实现,因为它的逻辑最清晰,最不容易在下标计算上晕头转向。
cpp
typedef struct {
int* a; // 动态数组
int front; // 队头下标
int rear; // 队尾下标(指向下一个插入位置)
int k; // 容量限制
int size; // 当前元素个数(关键变量)
} MyCircularQueue;
// 1. 创建
MyCircularQueue* myCircularQueueCreate(int k) {
MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
// 注意:这里分配 k 个空间就够了
obj->a = (int*)malloc(sizeof(int) * k);
obj->k = k;
obj->front = 0;
obj->rear = 0;
obj->size = 0;
return obj;
}
// 2. 入队
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) {
// 先检查满没满
if (obj->size == obj->k) {
return false;
}
// 放入数据
obj->a[obj->rear] = value;
// 移动尾指针(利用 % 实现循环)
obj->rear = (obj->rear + 1) % obj->k;
// 计数加 1
obj->size++;
return true;
}
// 3. 出队
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
// 先检查空没空
if (obj->size == 0) {
return false;
}
// 移动头指针(利用 % 实现循环)
obj->front = (obj->front + 1) % obj->k;
// 计数减 1
obj->size--;
return true;
}
// 4. 获取队头
int myCircularQueueFront(MyCircularQueue* obj) {
if (obj->size == 0) {
return -1;
}
return obj->a[obj->front];
}
// 5. 获取队尾
int myCircularQueueRear(MyCircularQueue* obj) {
if (obj->size == 0) {
return -1;
}
// 难点:rear 指向的是"下一个空位",所以真正的队尾是 rear 的前一个
// 为了防止 rear 为 0 时减 1 变成负数,我们需要加上 k 再取模
int index = (obj->rear - 1 + obj->k) % obj->k;
return obj->a[index];
}
// 6. 判空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
return obj->size == 0;
}
// 7. 判满
bool myCircularQueueIsFull(MyCircularQueue* obj) {
return obj->size == obj->k;
}
// 8. 销毁
void myCircularQueueFree(MyCircularQueue* obj) {
free(obj->a);
free(obj);
}
📝 重点总结
-
取模运算
%是灵魂 :在循环队列中,任何指针的移动都不能直接
+1,必须是(index + 1) % capacity。这保证了指针永远在0到capacity-1之间跳动。 -
Rear函数的陷阱 :大多数时候
rear指向的是"下一个要插入的位置"(空位)。所以获取最后一个元素时,需要找rear - 1。特别注意 :当
rear在0号位置时,rear - 1会变成-1,导致数组越界。万能公式 :
(rear - 1 + k) % k。这个公式能完美处理rear为 0 的情况(例如(0 - 1 + 5) % 5 = 4,正确回到了数组末尾)。
📌 总结
| 特性 | 栈 | 队列 |
|---|---|---|
| 操作原则 | 后进先出 | 先进先出 |
| 操作端 | 仅栈顶 | 队头出,队尾入 |
| 推荐底层 | 数组(尾插尾删效率高) | 链表(头删尾插效率高) |
| 典型应用 | 函数调用栈、括号匹配、浏览器后退 | 消息队列、广度优先搜索、打印机任务 |
希望这篇博客能帮你理清栈和队列的脉络!如果有代码实现上的疑问,欢迎在评论区留言。
🌟 写在最后
代码写完了,但我想对正在阅读这篇博客的你说几句心里话。
最近身边有很多同学在迷茫、害怕,大体原因是只能靠自己一个人在这个世界打拼,无依无靠;而世界又在不断变化进步,不知道自己的出路在哪里。
我把送给他们的话,也送给正在屏幕前努力的你:
保持乐观,悲观者正确,乐观者前行。
相信时间的力量,就不必问何时才有回报。
拥有坚持的毅力,就不必在意暂时的苟且。
保持每天的进步,就不必对未来感到迷茫。
愿你在代码的世界里披荆斩棘,也在人生的道路上步履不停。