栈
栈(Stack)是一种操作受限的线性数据结构,它的核心规则是先进后出(FILO, First In Last Out)最先放入栈的元素,最后才能取出;最后放入的元素,最先能取出
比如:叠盘子:新盘子总是叠在最上面(入栈),取盘子也只能从最上面拿(出栈),不能从中间或底部抽,这就是栈的核心逻辑

栈的操作范围被严格限制在栈顶(Stack Top)只有栈顶的元素能被读取或删除,栈底(Stack Bottom)的元素只能等上面所有元素都取出后才能操作



栈可以用很多种方式实现,我们来用较为简单的方法实现,动态顺序表,也就是动态数组
typedef int STDataType;
typedef struct stack
{
STDataType* a;
int top;
int capacity;
}ST;
//首先要创建一个结构体
初始化
void STInit(ST* pst);
//初始化
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
pst->top = pst->capacity = 0;
//top=-1说明这是一个栈顶元素的当前位置
}
pst->a 是栈结构体中用于存储栈元素的动态数组指针(通常定义为 int* a 或其他数据类型指针);
赋值为 NULL 表示:栈初始化时没有分配任何内存空间,此时栈里没有任何元素,避免野指针(指向无效内存的指针)风险
pst->top:表示栈顶指针(核心变量),初始化为 0,这里体现了栈的一种常见设计规则:
当 top=0 时,top 指向栈顶下一个可插入元素的位置(空栈状态下,第一个元素会插入到下标 0 的位置)
销毁
void STDestroy(ST* pst);
//销毁
void STDestroy(ST* pst)
{
assert(pst);
pst->a = NULL;
pst->top = pst->capacity = 0;
}
pst->a 是栈结构体中用于存储栈元素的动态数组指针(通常定义为 int* a 或其他数据类型指针);
赋值为 NULL 表示:栈初始化时没有分配任何内存空间,此时栈里没有任何元素,避免野指针(指向无效内存的指针)风险
pst->top:表示栈顶指针(核心变量),初始化为 0,这里体现了栈的一种常见设计规则:
当 top=0 时,top 指向栈顶下一个可插入元素的位置(空栈状态下,第一个元素会插入到下标 0 的位置)
入栈
void STPush(ST* pst, STDataType x);
//入栈
void STPush(ST* pst, STDataType x)
{
assert(pst);
//扩容
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc");
return;
}
else
{
pst->a = tmp;
pst->capacity = newcapacity;
}
}
pst->a[pst->top] = x;
pst->top++;
}
功能:将元素 x 压入栈顶,栈容量不足时自动动态扩容;
核心逻辑:
先校验栈指针有效性(assert),避免空指针操作;
若栈顶指针 top 等于容量 capacity(栈满),触发扩容:首次扩容为 4 个元素空间,后续扩容为原容量 2 倍;
用 realloc 调整内存(临时指针接收结果,防止扩容失败丢失原内存),扩容失败则打印错误并退出;
扩容完成 / 栈未满时,将元素存入 top 指向的位置,top 后移一位(指向新插入位);
关键设计:2 倍扩容兼顾效率,临时指针接收 realloc 结果避免内存泄漏

出栈
void STPop(ST* pst);
void STPop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
pst->top--;
}
pst->capacity:表示栈的容量(即栈最多能存储的元素个数),初始化为 0,说明栈一开始没有可存储元素的空间;
功能:逻辑上移除栈顶元素(栈遵循 "后进先出",仅移动 top 指针,无需主动清理元素值);
双重校验:
assert(pst):确保传入的栈指针非空,避免空指针解引用崩溃;
assert(pst->top > 0):确保栈内有元素(top>0 表示栈非空),防止空栈执行出栈操作;
核心操作:pst->top-- 让栈顶指针前移一位,原栈顶元素不再被访问,即完成出栈
取出栈顶数据
STDataType STTop(ST* pst);
//取出栈顶的数据
STDataType STTop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
功能:读取并返回栈顶元素的值,仅查询不修改栈的结构(栈顶指针 top 不变);
双重校验:
assert(pst):确保栈指针非空,避免空指针解引用;
assert(pst->top > 0):确保栈内有元素,防止访问空栈的无效内存;
核心逻辑:因栈设计中 top 指向 "栈顶下一个可插入位置",所以栈顶元素的下标是top - 1,直接返回该位置的值即可。
判空
bool STEmpty(ST* pst);
//判空
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
返回值类型是布尔型,这里需要包含一个头文件<stdbool.h>
功能:判断栈是否为空,返回 bool 类型结果(需包含<stdbool.h>头文件);
校验逻辑:assert(pst)确保传入的栈指针非空,避免空指针解引用崩溃;
判空依据:因栈设计中top指向栈顶下一个可插入位置,top=0表示栈内无任何元素,即栈为空
获取数据个数
int STSize(ST* pst);
//获取数据个数
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
功能:查询并返回栈内有效元素的总个数,仅读不修改栈;
校验逻辑:assert(pst)确保传入指针非空,避免程序崩溃;
计数依据:因栈的top指向栈顶下一个可插入位置,top的数值恰好等于栈内已存元素数(如 top=3 表示栈内有 3 个元素)
现在打印输出看一下效果
fun1()
{
ST s;
STInit(&s);
STPush(&s, 1);
STPush(&s, 2);
STPush(&s, 3);
STPush(&s, 4);
while (!STEmpty(&s))
{
printf("%d ", STTop(&s));
STPop(&s);
}
STDestroy(&s);
}
int main()
{
fun1();
return 0;
}

队列
队列是一种线性数据结构,其核心遵循 先进先出(First In First Out,简称 FIFO) 的访问原则
比如:排队买票、食堂打饭、打印机的任务队列,先到达的元素,先被处理;后到达的元素,只能排在队尾等待
注:只能在队列的一端插入元素,在另一端删除元素

typedef int DataType;
typedef struct Linked
{
DataType data;
struct Linked* pnext;
}List;
typedef struct NodeQueue
{
List* phead;
List* ptail;
int size;
}Queue;
这里为什么要定义两个结构体呢?
第一个结构体仅负责存储队列中的单个数据元素,以及通过next指针和其他节点建立链接,是链式队列的最小数据单元,就像排队时的 "每个人",只承载自己的信息,并知道自己后面是谁
第二个结构体不存储具体数据,而是管理整个队列的核心信息,是操作队列的 "总控入口"

初始化
void QueueInit(Queue* ps);
void QueueInit(Queue* ps)
{
assert(ps);
ps->phead = ps->ptail = NULL;
ps->size = 0;
}
销毁
void QueueDestroy(Queue* ps);
void QueueDestroy(Queue* ps)
{
assert(ps);
List* newphead = ps->phead;
while (newphead)
{
List* cur = newphead->pnext;
free(newphead);
newphead = cur;
}
ps->phead = ps->ptail = NULL;
ps->size = 0;
}
这里的销毁其实和单链表的销毁差不多
尾入
void QueuePush(Queue* ps, DataType x);
void QueuePush(Queue* ps, DataType x)
{
assert(ps);
List* space = (List*)malloc(sizeof(List));
if (space == NULL)
{
perror("malloc");
return;
}
else
{
space->pnext = NULL;
space->data = x;
}
if (ps->ptail == NULL)
{
ps->phead = ps->ptail = space;
}
else
{
ps->ptail->pnext = space;
ps->ptail = space;
}
ps->size++;
}
上一章接提到过,单链表的节点不需要扩容,每一个节点都是固定的大小

功能:基于链表实现队列入队,将元素 x 插入队列尾部(队列遵循 "先进先出");
核心逻辑:
校验队列指针有效性,避免空指针操作;
申请新链表节点并初始化(存值 x、后继置空),内存申请失败则报错退出;
空队列:头尾指针均指向新节点;非空队列:尾节点后继指向新节点,尾指针移至新节点;
队列元素个数 size 自增,记录有效元素数;
设计特点:链表实现队列,入队仅操作尾指针,时间复杂度 O (1),效率高
头出
void QueuePop(Queue* ps);
void QueuePop(Queue* ps)
{
assert(ps);
if (ps->phead->pnext == NULL)
{
free(ps->phead);
ps->phead = ps->ptail = NULL;
}
else
{
List* newphead = ps->phead->pnext;
free(ps->phead);
ps->phead = newphead;//ps->phead->pnext错误,这里的phead已经被释放掉了
}
ps->size--;
}

功能:基于链表实现队列出队,移除队列头部元素(队列遵循 "先进先出"),释放节点内存并更新指针 / 元素个数;
核心逻辑:
校验队列指针有效性,避免空指针操作;
分两种场景处理:
队列仅 1 个节点:释放该节点,头尾指针均置空(防止野指针);
队列多节点:先保存原头节点的下一个节点(新头),再释放原头节点,最后更新头指针;
关键注释提示:不能直接用ps->phead->pnext赋值,因原头节点已被free,指针失效;
队列元素个数size自减,同步更新数量;
判断是否为空
bool QueueEmpty(Queue* ps);
bool QueueEmpty(Queue* ps)
{
assert(ps);
return ps->size == 0;
}
取对头和对尾
DataType QueueFront(Queue* ps);
DataType QueueBack(Queue* ps);
DataType QueueFront(Queue* ps)
{
assert(ps);
assert( !QueueEmpty(ps));
return ps->phead->data;
}
DataType QueueBack(Queue* ps)
{
assert(ps);
assert(ps->ptail);
return ps->ptail->data;
}
功能区分:
QueueFront:查询并返回队列头部元素(队列 "先进先出",头部是最先入队的元素);
QueueBack:查询并返回队列尾部元素(尾部是最后入队的元素);
校验逻辑:
两个函数均先校验队列指针ps非空,避免空指针解引用;
QueueFront通过!QueueEmpty(ps)校验队列非空;QueueBack直接校验ps->ptail非空(等价于队列非空),两种写法均为防止访问空队列的无效节点;
核心操作:仅返回对应指针(phead/ptail)指向节点的data值,不修改队列任何结构(指针、size 均不变)
取出队列个数
DataType QueueSize(Queue* ps);
DataType QueueSize(Queue* ps)
{
assert(ps);
return ps->size;
}
看一下最终运行结果
func1()
{
Queue q;
QueueInit(&q);
QueuePush(&q, 4);
QueuePush(&q, 3);
QueuePush(&q, 2);
QueuePush(&q, 1);
while (!QueueEmpty(&q))
{
printf("%d ",QueueFront(&q));
QueuePop(&q);
}
QueueDestroy(&q);
}
int main()
{
func1();
}

