C语言栈和队列的实现

栈(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();
}
相关推荐
源代码•宸2 小时前
Golang语法进阶(定时器)
开发语言·经验分享·后端·算法·golang·timer·ticker
期待のcode2 小时前
TransactionManager
java·开发语言·spring boot
郝学胜-神的一滴2 小时前
Linux系统编程:深入理解读写锁的原理与应用
linux·服务器·开发语言·c++·程序人生
Larry_Yanan2 小时前
Qt多进程(十一)Linux下socket通信
linux·开发语言·c++·qt
蓝桉~MLGT2 小时前
中级软考(软件工程师)第三章知识点——数据结构与数据运算
数据结构
代码游侠2 小时前
学习笔记——ESP8266 WiFi模块
服务器·c语言·开发语言·数据结构·算法
0和1的舞者2 小时前
Python 中四种核心数据结构的用途和嵌套逻辑
数据结构·python·学习·知识
行者962 小时前
Flutter跨平台开发适配OpenHarmony:进度条组件的深度实践
开发语言·前端·flutter·harmonyos·鸿蒙
DYS_房东的猫2 小时前
《 C++ 零基础入门教程》第3章:结构体与类 —— 用面向对象组织代码
开发语言·c++