
🏠个人主页:黎雁
🎬作者简介:C/C++/JAVA后端开发学习者
❄️个人专栏:C语言、数据结构(C语言)、EasyX、游戏、规划、程序人生
✨ 从来绝巘须孤往,万里同尘即玉京

文章目录
- 栈与队列之栈入门攻略:从核心概念到数组实现✨
-
- 文章摘要
- 一、知识回顾:线性表的核心本质
- 二、栈的核心概念:什么是"后进先出"?📚
-
- [1. 栈的定义](#1. 栈的定义)
- [2. 栈的关键操作术语](#2. 栈的关键操作术语)
- [3. 栈的实现方式对比:数组 vs 链表](#3. 栈的实现方式对比:数组 vs 链表)
- 三、栈的工程化实现:数组版栈完整代码💻
-
- [1. 头文件定义(stack.h):接口声明](#1. 头文件定义(stack.h):接口声明)
- [2. 源文件实现(stack.c):核心逻辑](#2. 源文件实现(stack.c):核心逻辑)
-
- (1)栈的初始化
- (2)栈的销毁
- (3)入栈操作(压栈)
- (4)出栈操作(弹栈)
- (5)获取栈顶元素
- [(6)获取栈大小 & 判断栈为空](#(6)获取栈大小 & 判断栈为空)
- 四、核心设计思想:低耦合、高内聚🎯
- 五、写在最后
栈与队列之栈入门攻略:从核心概念到数组实现✨
你好!欢迎来到线性表系列的全新篇章------栈与队列篇的第一讲。
在前面的内容中,我们已经吃透了顺序表和链表这两大基础线性表,从底层实现到实战刷题,完成了从理论到实践的跨越。而今天要学习的栈,作为一种特殊的线性表,是数据结构世界里不可或缺的核心角色,它的"后进先出"特性在算法解题、工程开发中都有着广泛应用,比如括号匹配、函数调用栈、表达式求值等场景都离不开它。
准备好了吗?让我们一起解锁栈的核心知识,从概念到实现,彻底掌握这个"个性十足"的线性表!🚀
文章摘要
本文聚焦线性表中的栈结构,系统讲解栈的核心概念、"后进先出"特性及数组/链表两种实现方式的优劣对比,最终选择数组作为最优实现方案。通过完整的工程化代码(头文件+源文件),详细拆解栈的初始化、销毁、入栈、出栈等核心操作,补充top指针两种初始化方式的关键细节,结合软件工程"低耦合、高内聚"思想解析代码设计,夯实栈的底层实现基础。
阅读时长 :约20分钟
阅读建议:
- 基础薄弱者:先吃透栈的核心特性,再对照代码理解实现逻辑
- 工程开发者:重点关注数组扩容、内存释放等细节,借鉴代码封装思想
- 面试备考者:牢记栈的特性、实现方式及典型应用场景
- 查漏补缺者:直接查看代码实现中的关键注释和易错点
一、知识回顾:线性表的核心本质
在学习栈之前,我们先回顾下线性表的核心特征:
- 线性表是数据元素呈线性排列的结构,每个元素有唯一的前驱和后继(除首尾)
- 顺序表:物理存储连续,随机访问快,但插入删除效率低
- 链表:物理存储不连续,插入删除效率高,但随机访问慢
而栈,正是基于线性表实现的受限数据结构------它只允许在一端进行操作,这也让它拥有了独一无二的特性!
二、栈的核心概念:什么是"后进先出"?📚
1. 栈的定义
栈是一种特殊的线性表,只允许在固定的一端 进行插入和删除操作,遵循后进先出(LIFO = Last In First Out) 原则。
举个生活中的例子:
- 往弹夹里装子弹:后装进去的子弹,先被打出来 → 这就是栈的"后进先出"
- 叠盘子:最后叠上去的盘子,最先被拿走 → 完美契合栈的特性
2. 栈的关键操作术语
| 操作名称 | 说明 | 操作位置 |
|---|---|---|
| 压栈/入栈/进栈 | 向栈中添加元素 | 栈顶(唯一操作端) |
| 出栈/弹栈 | 从栈中删除元素 | 栈顶(唯一操作端) |
| 栈顶 | 允许操作的这一端(栈的"顶端") | - |
| 栈底 | 固定不动的另一端(栈的"底部") | - |
3. 栈的实现方式对比:数组 vs 链表
栈有两种主流实现方式,我们来详细对比优劣,选择最优方案:
| 实现方式 | 核心思路 | 优点 | 缺点 |
|---|---|---|---|
| 数组(推荐) | 用数组尾端作为栈顶,入栈=尾插、出栈=尾删 | ① 操作效率O(1) ② 随机访问快 ③ 实现简单 | 空间不足时需要扩容(可通过预分配缓解) |
| 链表 | ① 单链表:头节点作为栈顶(头插头删) ② 双向链表:尾节点作为栈顶 | 无需扩容,按需分配内存 | ① 单链表需额外处理指针 ② 双向链表实现复杂 ③ 缓存命中率低 |
✅ 结论:数组实现栈是最优选择!
- 数组尾插尾删的时间复杂度都是O(1),完全匹配栈的操作需求
- 虽然存在扩容成本,但扩容频率低(通常按2倍扩容),整体效率远高于链表
- 符合《深入理解计算机系统》中"局部性原理",缓存命中率更高
三、栈的工程化实现:数组版栈完整代码💻
我们采用"头文件+源文件"的工程化结构实现栈,保证代码的规范性和可维护性。
1. 头文件定义(stack.h):接口声明
头文件负责定义栈的结构体和函数接口,是栈的"对外接口规范":
c
#pragma once
#include <stdio.h>
#include <stdbool.h>
#include <assert.h>
#include <stdlib.h>
// 定义栈存储的数据类型(方便后续修改)
typedef int STDataType;
// 栈的结构体定义(数组实现)
typedef struct Stack
{
STDataType* a; // 存储数据的数组指针
int top; // 栈顶指针(关键!)
int capacity; // 数组的容量
}ST;
// 栈的核心操作接口
void StackInit(ST* ps); // 初始化栈
void StackDestroy(ST* ps); // 销毁栈(释放内存)
void StackPush(ST* ps, STDataType x); // 入栈(压栈)
void StackPop(ST* ps); // 出栈(弹栈)
STDataType StackTop(ST* ps); // 获取栈顶元素
int StackSize(ST* ps); // 获取栈中元素个数
bool StackEmpty(ST* ps); // 判断栈是否为空
2. 源文件实现(stack.c):核心逻辑
源文件负责实现头文件声明的函数,是栈的"内部实现细节":
(1)栈的初始化
c
#include "stack.h"
void StackInit(ST* ps) // 初始化栈
{
assert(ps); // 确保传入的栈指针不为NULL
// 初始分配4个元素的空间(可根据需求调整)
ps->a = (STDataType*)malloc(sizeof(STDataType)*4);
if (ps->a == NULL) // 内存分配失败处理
{
perror("malloc fail");
exit(1); // 终止程序
}
ps->capacity = 4; // 初始容量为4
ps->top = 0; // 🌟 关键:top=0表示指向栈顶元素的下一个位置
// 补充:若top=-1,表示指向栈顶元素本身(两种方式都可,需统一逻辑)
}
💡 关键细节:top指针的两种初始化方式
| top初始值 | 含义 | 入栈逻辑 | 出栈逻辑 | 栈顶元素获取 |
|---|---|---|---|---|
| 0 | 指向栈顶元素的下一个位置 | ps->a[ps->top] = x; ps->top++ | ps->top-- | ps->a[ps->top-1] |
| -1 | 指向栈顶元素本身 | ps->top++; ps->a[ps->top] = x | ps->top-- | ps->a[ps->top] |
(2)栈的销毁
c
void StackDestroy(ST* ps) // 销毁栈(必须!避免内存泄漏)
{
assert(ps);
free(ps->a); // 释放数组内存
ps->a = NULL; // 置空指针,防止野指针
ps->top = 0; // 重置栈顶
ps->capacity = 0; // 重置容量
}
(3)入栈操作(压栈)
c
void StackPush(ST* ps, STDataType x) // 入栈(尾插)
{
assert(ps);
// 扩容判断:栈满时扩容(2倍扩容)
if (ps->top == ps->capacity)
{
STDataType* tmp = (STDataType*)realloc(ps->a, ps->capacity * 2 * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
exit(1);
}
ps->a = tmp;
ps->capacity *= 2; // 容量翻倍
}
// 入栈核心逻辑
ps->a[ps->top] = x;
ps->top++;
}
(4)出栈操作(弹栈)
c
void StackPop(ST* ps) // 出栈(尾删)
{
assert(ps);
assert(!StackEmpty(ps)); // 栈空时禁止出栈
ps->top--; // 只需移动top指针,无需真正删除数据(覆盖即可)
}
(5)获取栈顶元素
c
STDataType StackTop(ST* ps) // 获取栈顶元素
{
assert(ps);
assert(!StackEmpty(ps)); // 栈空时禁止获取
return ps->a[ps->top - 1]; // 对应top=0的初始化方式
}
(6)获取栈大小 & 判断栈为空
c
int StackSize(ST* ps) // 获取栈中元素个数
{
assert(ps);
return ps->top; // top的值就是元素个数(top=0的情况)
}
bool StackEmpty(ST* ps) // 判断栈是否为空
{
assert(ps);
return ps->top == 0; // top=0 → 空栈
}
四、核心设计思想:低耦合、高内聚🎯
思考:为什么要把StackSize、StackEmpty封装成函数?
可能有同学会问:这两个函数逻辑简单,直接在测试代码里写ps->top == 0不就行了?
这就涉及到软件工程的核心思想------低耦合、高内聚:
- 高内聚:栈的所有操作逻辑都集中在stack.c中,对外只暴露统一接口,便于维护和修改(比如后续把top初始值改成-1,只需修改栈内部代码,无需改测试代码)
- 低耦合:测试代码只需调用接口,无需关心栈的内部实现细节,降低代码之间的依赖
- 鲁棒性 :函数内部有断言检查(如
assert(ps)),能提前发现错误,而直接写表达式无法做到
简单来说:封装是为了让代码更健壮、更易维护!
五、写在最后
恭喜你!在这一篇中,你已经掌握了栈的核心知识:
- 理解了栈"后进先出"的核心特性
- 对比了数组/链表两种实现方式,选择了最优方案
- 完成了栈的工程化代码实现,掌握了top指针的关键细节
- 理解了"低耦合、高内聚"的软件工程思想
栈作为基础数据结构,是后续学习的重要铺垫:
- 算法层面:括号匹配、表达式求值、DFS(深度优先搜索)都离不开栈
- 工程层面:函数调用栈、浏览器前进后退功能都基于栈实现
下一篇,我们将学习栈的"好搭档"------队列,它遵循"先进先出"原则,和栈形成完美互补。敬请期待!😜
点赞+收藏+关注,跟着系列内容一步步吃透数据结构!你的支持是我创作的最大动力~👍