零基础入门C语言之C语言实现数据结构之栈

在阅读本篇文章之前,建议读者优先阅读本专栏内前面的文章。

目录

前言

一、栈的相关概念

二、栈的实现

三、栈的应用

总结


前言

本文主要介绍与C语言实现的栈相关的知识


一、栈的相关概念

栈是一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO的原则。 压栈是指栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。 出栈是指栈的删除操作叫做出栈。出数据也在栈顶。

栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。

二、栈的实现

我们首先需要定义一个栈,对于栈来说,既然它是一种特殊的线性表,那么我们自然而然就有顺序栈和链栈这两种栈,其中顺序栈又分为静态栈和动态栈,他们的区别就在于长度是否固定不变。一般来说我们用的都是动态栈,但是我们还是把静态栈的定义列出来给读者看看:

cpp 复制代码
typedef int STDataType;
#define N 10
typedef struct Stack
{
 STDataType _a[N];
 int _top; // 栈顶
}Stack;

然后是动态栈的定义:

cpp 复制代码
typedef int STDatatype;

typedef struct Stack {
	STDatatype* data;
	int top;
	int capacity;
}ST;

可以看到其实栈是静态还是动态其实就取决于其中的数组是静态开辟还是动态开辟。但是你要是静态栈的话,就不再需要额外开辟一个用作存储栈容量的变量。然后我们针对动态栈来继续之后的方法的讲解。首先是栈的初始化,其实它的本质和我们之前讲的线性表的初始化是一样的,请读者思考代码该如何实现,我这里给出示例代码:

cpp 复制代码
void STInit(ST* pst)
{
	assert(pst);
	pst->a = NULL;
	pst->top = 0;
	pst->capacity = 0;
}

接下来是对于创建的栈的销毁,代码的核心设计思路是首先通过assert(pst)对传入的栈结构体指针做合法性校验,防止空指针解引用导致的程序崩溃,这是保障后续操作有效执行的基础;接着调用free(pst->a)释放栈底层动态开辟的数组内存,避免因动态内存未手动回收造成的内存泄漏问题,且此处仅释放栈内部管理的资源,而非pst本身,因为pst的内存分配方式由调用者决定,函数不负责其销毁;随后将pst->a置为NULL,解决free后指针仍指向已回收内存的野指针问题,避免后续误解引用该指针引发的非法内存访问;最后把栈顶标记top和容量标识capacity重置为0,让栈恢复到初始空栈状态。请读者思考代码实现,我这里给出示例代码:

cpp 复制代码
void STDestroy(ST* pst)
{
	assert(pst);
	free(pst->a);
	pst->a = NULL;
	pst->top = pst->capacity = 0;
}

然后是入栈的代码,我们首先通过assert(pst)校验传入的栈指针有效性,避免空指针解引用;接着判断栈顶指针top是否等于容量capacity,若相等说明栈已满,需进行动态扩容,扩容规则为初始容量4、后续翻倍,通过realloc重新分配内存,同时处理扩容失败的情况(打印错误并返回),扩容成功后更新栈的底层数组指针和容量;最后将待入栈数据存入栈顶位置,并将栈顶指针自增。请读者思考代码如何实现,我这里给出示例代码:

cpp 复制代码
void STPush(ST* pst, STDatatype data)
{
	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 failed");
			return;
		}
		pst->a = tmp;
		pst->capacity = newcapacity;
	}
	pst->a[pst->top++] = data;
}

然后我们再来尝试出栈的函数,首先用assert(pst)检查栈指针是否有效,再通过assert(pst->top > 0)确保栈非空,避免空栈出栈导致栈顶指针为负的非法操作;由于该栈基于动态数组实现,出栈无需手动释放内存,仅需将栈顶指针top减1,使原栈顶元素脱离栈的逻辑管理范围(后续入栈会直接覆盖该位置),以此高效完成出栈操作。请读者思考如何实现,我这里给出示例代码:

cpp 复制代码
void STPop(ST* pst)
{
	assert(pst);
	assert(pst->top > 0);
	pst->top--;
}

接下来我们来实现获取栈顶元素的函数,首先通过assert(pst)校验栈指针的有效性,再用assert(pst->top > 0)确保栈内有元素可获取,防止空栈时访问无效内存;结合栈的实现逻辑,top标记的是栈顶的下一个位置,因此通过pst->a[pst->top - 1]就能精准定位到栈顶元素的内存地址,并返回该位置的数值,实现栈顶元素的查询,且不会改变栈的结构和状态。请读者思考如何实现,我这里给出示例代码:

cpp 复制代码
STDatatype STTop(ST* pst)
{
	assert(pst);
	assert(pst->top > 0);
	return pst->a[pst->top - 1];
}

然后我们来思考一下如何判断栈中是否为空,首先用assert(pst)检查栈指针是否有效,避免对空指针进行成员访问;基于该栈的实现语义,栈顶指针top的数值代表栈中元素的个数(初始化时top为 0,入栈则自增,出栈则自减),因此直接通过判断pst->top == 0即可得出栈是否为空的结论,那么请读者思考如何实现,我给出示例代码:

cpp 复制代码
bool STEmpty(ST* pst)
{
	assert(pst);
	return pst->top == 0;
}

最后我们来实现一下获取栈内元素个数的函数,首先通过assert(pst)校验栈指针的有效性,防止空指针解引用;在该栈的实现中,栈顶指针top的取值与栈内元素个数完全一致(每入栈一个元素top自增1,每出栈一个元素top自减1,初始为0),因此无需额外计算或遍历,直接返回pst->top的值即可得到栈的元素个数,请读者思考如何实现,我这里给出示例代码:

cpp 复制代码
int STSize(ST* pst) 
{
	assert(pst);
	return pst->top;
}

然后在这里,我们介绍一下访问栈内全部元素的代码,在上述函数的基础下,在测试源文件中输入如下代码:

cpp 复制代码
#include "stack.h"
int main() {
	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);
	return 0;
}

三、栈的应用

我这里先贴出测试的链接:

20. 有效的括号 - 力扣(LeetCode)

题目详细叙述如下:

该题就可以通过栈的后进先出特性实现括号有效性校验,核心思路是左括号入栈、右括号匹配出栈,首先进行预处理优化,通过strlen获取字符串长度,若长度为奇数,直接返回false(因为有效括号必须成对出现,奇数长度不可能完全匹配);接着定义一个数组模拟栈结构,stk存储左括号,top作为栈顶指针(初始为0)。遍历字符串时,先通过pairs函数判断当前字符是否为右括号:若是右括号,会返回对应的左括号;此时检查栈是否为空(无匹配的左括号)或栈顶元素不是对应的左括号,若满足任一条件则返回false,否则栈顶指针减一(完成匹配出栈);若当前字符是左括号,pairs函数返回0,直接将左括号压入栈中,栈顶指针自增。遍历结束后,若栈顶指针top为0,说明所有左括号都找到了对应的右括号,返回true,否则存在未匹配的左括号,返回false。整个过程时间复杂度为O(n)(仅遍历一次字符串),空间复杂度为O(n)(最坏情况下栈存储所有左括号)。请读者思考如何实现,我这里给出我的代码:

cpp 复制代码
char pairs(char a) {
    if (a == '}') return '{';
    if (a == ']') return '[';
    if (a == ')') return '(';
    return 0;
}

bool isValid(char* s) {
    int n = strlen(s);
    if (n % 2 == 1) {
        return false;
    }
    int stk[n + 1], top = 0;
    for (int i = 0; i < n; i++) {
        char ch = pairs(s[i]);
        if (ch) {
            if (top == 0 || stk[top - 1] != ch) {
                return false;
            }
            top--;
        } else {
            stk[top++] = s[i];
        }
    }
    return top == 0;
}

总结

本文详细介绍了C语言实现栈的相关知识,包括栈的基本概念(后进先出原则)以及两种实现方式(静态栈和动态栈)。重点讲解了动态栈的实现方法,涵盖初始化、销毁、入栈、出栈、获取栈顶元素、判空和获取元素个数等核心操作。文章还通过有效的括号匹配问题展示了栈的实际应用,给出基于栈的解决方案和代码实现。所有操作都通过assert进行指针有效性检查,确保程序健壮性。

相关推荐
后端小张1 小时前
【JAVA 进阶】SpringBoot 事务深度解析:从理论到实践的完整指南
java·开发语言·spring boot·后端·spring·spring cloud·事务
y***54881 小时前
C++在游戏引擎中的开发
开发语言·c++·游戏引擎
郝学胜-神的一滴2 小时前
Python高级编程技术深度解析与实战指南
开发语言·python·程序人生·个人开发
charlie1145141912 小时前
使用 Poetry + VS Code 创建你的第一个 Flask 工程
开发语言·笔记·后端·python·学习·flask·教程
Codeking__2 小时前
查缺补漏c语言——c标准字符串函数
c语言·开发语言
铅笔小新z2 小时前
【C++】从理论到实践:类和对象完全指南(中)
开发语言·c++
徐子童2 小时前
数据结构----排序算法
java·数据结构·算法·排序算法·面试题
千疑千寻~2 小时前
【C++】std::move与std::forward函数的区别
开发语言·c++
Murphy_lx2 小时前
C++ 条件变量
linux·开发语言·c++