一文吃透栈结构:从基本操作到算法应用,零基础也能轻松掌握_栈结构基本操作
引言
栈(Stack)是计算机科学中一种重要的数据结构,遵循\"后进先出(LIFO, Last In First Out)\"的原则。它在程序设计中有着广泛的应用,如函数调用、表达式求值、括号匹配等。本文将从栈的基本概念出发,详细解析栈的实现代码,补充关键知识点与易错点,并通过经典案例展示栈的实际应用。
一、栈的基本概念
1.1 定义
栈是一种操作受限的线性表,仅允许在表的一端(称为栈顶)进行插入和删除操作。
- 入栈(Push):在栈顶插入元素
- 出栈(Pop):从栈顶删除元素
- 栈顶(Top):允许操作的一端
- 栈底(Bottom):固定的一端,不允许直接操作
1.2 图示
栈顶 ↓┌───┐│ 4 │├───┤│ 3 │├───┤│ 2 │├───┤│ 1 │└───┘ ↑ 栈底
1.3 基本操作
- 初始化(STInit)
- 销毁(STDestroy)
- 入栈(STPush)
- 出栈(STPop)
- 取栈顶元素(STTop)
- 判空(STEmpty)
- 获取元素个数(STSize)
二、栈的实现(动态数组版)
2.1 目录结构
stack_demo/├── Stack.h // 栈的声明├── Stack.c // 栈的实现└── main.c // 测试代码
2.2 头文件(Stack.h)
#include #include #include #include #pragma once// 栈中元素的数据类型typedef int STDataType;// 栈的结构体定义typedef struct Stack{ STDataType* arr; // 动态数组存储元素 int top; // 栈顶指针(指向栈顶元素的下一个位置) int capacity; // 栈的容量}ST;// 栈初始化和销毁void STInit(ST* ps);void STDestroy(ST* ps); // 注意:原代码此处函数名拼写错误(STDestory),已修正// 入栈和出栈void STPush(ST* ps, STDataType num);void STPop(ST* ps);// 取栈顶数据STDataType STTop(ST* ps);// 判空bool STEmpty(ST* ps);// 获取数据个数int STSize(ST* ps);
代码解析:
- 使用
typedef
定义STDataType
,方便后续修改栈存储的数据类型 - 栈结构体包含三个成员:动态数组
arr
(存储元素)、top
(栈顶指针)、capacity
(容量) - 函数声明清晰列出了栈的所有基本操作
- 注意:原代码中
STDestroy
误写为STDestory
,实际使用时需修正,否则会出现链接错误
2.3 实现文件(Stack.c)
#include \"Stack.h\"// 栈初始化void STInit(ST* ps){ assert(ps); // 确保传入的指针非空 ps->arr = NULL; ps->capacity = 0; ps->top = 0; // top=0表示栈顶指针指向栈顶元素的下一个位置}// 栈销毁void STDestroy(ST* ps){ assert(ps); free(ps->arr); // 释放动态数组内存 ps->arr = NULL; // 避免野指针 ps->top = ps->capacity = 0; // 重置状态}// 入栈操作void STPush(ST* ps, STDataType num){ assert(ps); // 检查容量,若满则扩容 if (ps->top == ps->capacity) { // 初始容量为4,后续扩容为当前的2倍 int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity; STDataType* tmp = (STDataType*)realloc(ps->arr, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror(\"realloc fail !\"); // 打印错误信息 return; } ps->arr = tmp; ps->capacity = newcapacity; } // 存入元素,栈顶指针上移 ps->arr[ps->top] = num; ps->top++;}// 出栈操作void STPop(ST* ps){ // 确保栈非空才能出栈 assert(ps && ps->top > 0); ps->top--; // 仅移动栈顶指针即可(逻辑删除)}// 取栈顶数据STDataType STTop(ST* ps){ // 确保栈非空才能取栈顶 assert(ps && ps->top > 0); return ps->arr[ps->top - 1]; // top指向栈顶元素的下一个位置,所以-1}// 判空操作bool STEmpty(ST* ps){ assert(ps); return ps->top == 0; // top为0表示栈空}// 获取数据个数int STSize(ST* ps){ assert(ps); return ps->top; // top的值就是元素个数}
代码解析:
-
初始化(STInit):
- 将动态数组置空,容量和栈顶指针初始化为0
- 使用
assert
确保传入的指针有效(非空)
-
销毁(STDestroy):
- 释放动态数组内存,避免内存泄漏
- 将指针置空,防止野指针问题
- 重置
top
和capacity
为0
-
入栈(STPush):
- 核心逻辑:先检查容量,不足则扩容,再存入元素
- 扩容策略:初始容量4,后续每次翻倍(时间复杂度摊还为O(1))
- 使用
realloc
进行内存分配,需判断分配是否成功
-
出栈(STPop):
- 仅通过移动栈顶指针实现(
top--
),无需真正删除元素 - 必须确保栈非空才能出栈(
assert(ps->top > 0)
)
- 仅通过移动栈顶指针实现(
-
取栈顶(STTop):
- 由于
top
指向栈顶元素的下一个位置,所以实际栈顶元素是arr[top-1]
- 由于
2.4 测试代码(main.c)
#include \"Stack.h\"int main(){ // 测试流程:入栈1→入栈2→出栈2→入栈3→入栈4→出栈所有元素 ST s; STInit(&s); STPush(&s, 1); // 栈:[1] STPush(&s, 2); // 栈:[1,2] printf(\"%d \", STTop(&s)); // 打印2 STPop(&s); // 栈:[1] STPush(&s, 3); // 栈:[1,3] STPush(&s, 4); // 栈:[1,3,4] // 出栈所有元素并打印 while (!STEmpty(&s)) { printf(\"%d \", STTop(&s)); // 依次打印4、3、1 STPop(&s); } // 最终输出:2 4 3 1 STDestroy(&s); return 0;}
执行过程解析:
三、栈的经典应用
3.1 括号匹配问题
问题描述:判断一个字符串中的括号是否匹配,包括()
、[]
、{}
三种类型。
匹配规则:
- 左括号必须用相同类型的右括号闭合
- 左括号必须以正确的顺序闭合
- 空字符串视为匹配
实现思路:
- 遍历字符串中的每个字符
- 遇到左括号则入栈
- 遇到右括号则判断栈顶是否为对应的左括号:
- 是则出栈
- 否则匹配失败
- 遍历结束后,栈必须为空才表示完全匹配
代码实现:
#include \"Stack.h\"#include // 判断左右括号是否匹配bool isMatch(char left, char right){ return (left == \'(\' && right == \')\') || (left == \'[\' && right == \']\') || (left == \'{\' && right == \'}\');}// 括号匹配函数bool isValid(char* s){ ST st; STInit(&st); int len = strlen(s); for (int i = 0; i < len; i++) { // 左括号入栈 if (s[i] == \'(\' || s[i] == \'[\' || s[i] == \'{\') { STPush(&st, s[i]); } // 右括号处理 else { // 右括号多了 if (STEmpty(&st)) { STDestroy(&st); return false; } char top = STTop(&st); STPop(&st); // 类型不匹配 if (!isMatch(top, s[i])) { STDestroy(&st); return false; } } } // 左括号多了 bool ret = STEmpty(&st); STDestroy(&st); return ret;}// 测试括号匹配void testBracketMatch(){ char* s1 = \"()\"; char* s2 = \"()[]{}\"; char* s3 = \"(]\"; char* s4 = \"([)]\"; char* s5 = \"{[]}\"; printf(\"测试括号匹配:\\n\"); printf(\"%s: %s\\n\", s1, isValid(s1) ? \"匹配\" : \"不匹配\"); // 匹配 printf(\"%s: %s\\n\", s2, isValid(s2) ? \"匹配\" : \"不匹配\"); // 匹配 printf(\"%s: %s\\n\", s3, isValid(s3) ? \"匹配\" : \"不匹配\"); // 不匹配 printf(\"%s: %s\\n\", s4, isValid(s4) ? \"匹配\" : \"不匹配\"); // 不匹配 printf(\"%s: %s\\n\", s5, isValid(s5) ? \"匹配\" : \"不匹配\"); // 匹配}// 在main函数中调用testBracketMatch()即可测试
3.2 逆波兰表达式求值
逆波兰表达式(后缀表达式)是一种没有括号的表达式,运算符放在两个操作数后面,例如:
- 中缀表达式:
(1+2)*(3+4)
- 逆波兰表达式:
1 2 + 3 4 + *
求值思路:
- 遍历表达式的每个元素
- 遇到数字则入栈
- 遇到运算符则弹出栈顶两个元素,计算结果后将结果入栈
- 遍历结束后,栈中仅剩的元素就是表达式的结果
代码示例:
// 简化版逆波兰表达式求值(仅支持+、-、*、/)int evalRPN(char** tokens, int tokensSize){ ST st; STInit(&st); for (int i = 0; i < tokensSize; i++) { char* token = tokens[i]; // 如果是运算符 if (token[0] == \'+\' || token[0] == \'-\' || token[0] == \'*\' || token[0] == \'/\') { int b = STTop(&st); STPop(&st); int a = STTop(&st); STPop(&st); int res = 0; switch (token[0]) { case \'+\': res = a + b; break; case \'-\': res = a - b; break; case \'*\': res = a * b; break; case \'/\': res = a / b; break; } STPush(&st, res); } // 如果是数字 else { STPush(&st, atoi(token)); } } int result = STTop(&st); STDestroy(&st); return result;}
四、知识点补充
4.1 数组栈 vs 链表栈
总结:数组栈实现简单且访问高效,适合大多数场景;链表栈适合内存受限或元素数量波动大的场景。
4.2 栈的时间复杂度
栈的所有基本操作(入栈、出栈、取栈顶、判空、获取大小)的时间复杂度均为O(1),这使得栈成为效率极高的数据结构。
- 入栈:虽然可能涉及扩容(O(n)),但采用翻倍扩容策略,摊还复杂度为O(1)
- 出栈:仅移动指针,O(1)
- 取栈顶:直接访问数组元素,O(1)
4.3 栈的应用场景
- 函数调用:操作系统用栈保存函数调用上下文(返回地址、局部变量等)
- 表达式求值:如逆波兰表达式求值
- 括号匹配:编译器语法分析
- 浏览器历史记录:前进/后退功能
- 撤销操作:文本编辑器的撤销功能
- 深度优先搜索(DFS):递归本质上是栈的应用
五、易错点总结
-
top指针的初始值:
- 本文实现中
top=0
(指向栈顶元素的下一个位置) - 另一种常见实现是
top=-1
(指向栈顶元素) - 两种方式均可,但需保持一致,否则会导致逻辑错误
- 本文实现中
-
函数名拼写错误:
- 原代码中
STDestroy
误写为STDestory
(少了一个r
) - 这类错误编译器不会报错,但链接时会提示\"未定义的引用\",需特别注意
- 原代码中
-
内存管理:
realloc
可能失败(返回NULL),需判断并处理- 销毁栈后必须将指针置空,避免野指针
- 扩容时原内存可能被释放,需用临时指针接收
realloc
返回值
-
边界检查:
- 出栈和取栈顶操作前必须检查栈是否为空
- 使用
assert
可以帮助快速定位错误,但发布版本中可禁用
-
扩容策略:
- 固定大小扩容(如每次+4)会导致频繁扩容,时间复杂度高
- 翻倍扩容策略更优,摊还时间复杂度为O(1)
六、总结
栈作为一种简单而强大的数据结构,其\"后进先出\"的特性使其在众多场景中发挥重要作用。本文从栈的基本概念出发,详细解析了动态数组实现栈的代码,补充了关键知识点与易错点,并通过括号匹配、逆波兰表达式求值等经典案例展示了栈的实际应用。
掌握栈的实现与应用,不仅能帮助我们解决特定问题,更能培养我们的抽象思维和数据结构设计能力。在实际开发中,应根据具体需求选择合适的栈实现方式,并注意内存管理和边界条件处理,写出健壮高效的代码。
希望本文能帮助你深入理解栈的原理与应用!如果有任何疑问或建议,欢迎在评论区留言讨论。