数据结构篇(五):线性表——栈

前言

前面几篇讲了顺序表、单链表和双链表,它们都是线性表 的基础实现方式。而栈和队列,本质上是对线性表的操作方式做了限制之后得到的两种特殊线性结构。本文来讲讲栈------一种"后进先出"的数据结构,理解它是后续学习递归、DFS、表达式求值、括号匹配等内容的基础。


一、什么是栈

栈(Stack)是一种只允许在一端 进行插入和删除操作的线性表。允许操作的一端叫栈顶(top) ,另一端叫栈底(bottom)

栈的核心特性是后进先出(LIFO,Last In First Out):数据压入栈的顺序和弹出栈的顺序正好相反,最后进栈的元素最先出来。

生活中的例子:

1.一摞盘子,只能从最上面拿,也只能从最上面放;

2.浏览器的"后退"功能;

3.函数调用时的调用栈(每次函数调用都会压栈,返回时弹栈)。


二、栈的实现方式

栈可以用数组实现,也可以用链表实现。

  • 数组实现:从数组尾部进行插入删除,不需要挪动数据,效率是O(1),且实现简单,是实际中最常用的方式(类似于顺序表的尾插尾删);
  • 链表实现:如果用链表实现,一般选择在链表头部进行插入删除,这样效率也是O(1),但相对数组实现要多一层指针操作,实际使用较少。

本文以数组实现为主进行讲解。


三、栈的结构定义

栈本质上就是一个支持动态扩容的顺序表,只是把操作限制在了一端(栈顶)。

cpp 复制代码
typedef int STDataType;

typedef struct Stack {
    STDataType* array;  // 存储数据的数组
    int top;             // 栈顶下标(也可以用size表示元素个数)
    int capacity;         // 容量
} Stack;

四、栈的基本操作

4.1 初始化

cpp 复制代码
void StackInit(Stack* ps) {
    ps->array = NULL;
    ps->top = 0;       // top指向栈顶元素的下一个位置,初始为0表示空栈
    ps->capacity = 0;
}

4.2 检查容量(扩容)

cpp 复制代码
​void StackCheckCapacity(Stack* ps) {
    if (ps->top == ps->capacity) {
        int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
        STDataType* tmp = (STDataType*)realloc(ps->array, newCapacity * sizeof(STDataType));
        if (tmp == NULL) {
            perror("realloc fail");
            exit(-1);
        }
        ps->array = tmp;
        ps->capacity = newCapacity;
    }
}

4.3 入栈(Push)

cpp 复制代码
void StackPush(Stack* ps, STDataType x) {
    assert(ps != NULL);
    StackCheckCapacity(ps);

    ps->array[ps->top] = x;
    ps->top++;
}

4.4 出栈(Pop)

cpp 复制代码
void StackPop(Stack* ps) {
    assert(ps != NULL);
    assert(ps->top > 0); // 栈不能为空

    ps->top--;
}

4.5 取栈顶元素(Top)

cpp 复制代码
STDataType StackTop(Stack* ps) {
    assert(ps != NULL);
    assert(ps->top > 0);

    return ps->array[ps->top - 1];
}

4.6 判空

cpp 复制代码
bool StackEmpty(Stack* ps) {
    assert(ps != NULL);
    return ps->top == 0;
}

4.7 获取有效元素个数

cpp 复制代码
int StackSize(Stack* ps) {
    assert(ps != NULL);
    return ps->top;
}

4.8 销毁

cpp 复制代码
void StackDestroy(Stack* ps) {
    assert(ps != NULL);

    free(ps->array);
    ps->array = NULL;
    ps->top = ps->capacity = 0;
}

五、完整测试代码

cpp 复制代码
int main() {
    Stack st;
    StackInit(&st);

    StackPush(&st, 1);
    StackPush(&st, 2);
    StackPush(&st, 3);
    StackPush(&st, 4);

    printf("栈顶元素: %d\n", StackTop(&st)); // 4

    while (!StackEmpty(&st)) {
        printf("%d ", StackTop(&st));
        StackPop(&st);
    }
    printf("\n"); // 4 3 2 1,正好和入栈顺序相反

    StackDestroy(&st);
    return 0;
}

六、时间复杂度分析

操作 时间复杂度 说明
入栈 push O(1) 均摊,不涉及数据搬移
出栈 pop O(1) 只需移动top
取栈顶 top O(1) 直接访问下标
判空 empty O(1) 判断top是否为0

栈的所有标准操作都是O(1),这也是为什么用数组实现栈是最优选择------完全不需要像顺序表那样考虑头部/中间插入删除,因为栈根本不允许这样操作。


七、栈的经典应用场景

栈在实际开发和算法题中出镜率非常高,常见应用包括:

  • 括号匹配 :判断字符串中的()[]{}是否匹配,利用栈"后进先出"的特性,左括号入栈,遇到右括号就弹栈比对;
  • 逆波兰表达式(后缀表达式)求值:操作数入栈,遇到运算符就弹出栈顶两个元素运算,结果再入栈;
  • 函数调用栈:程序运行时,每次函数调用都会在栈上开辟一块栈帧,保存局部变量、返回地址等信息,函数返回时栈帧弹出,这也是递归可能导致栈溢出(Stack Overflow)的根本原因;
  • 浏览器前进后退、编辑器撤销重做(Undo/Redo):本质上都是用栈记录历史状态;
  • 深度优先遍历(DFS):可以用递归实现,也可以用显式栈模拟递归过程,避免递归层数过深导致栈溢出。

八、数组实现 vs 链表实现栈

特性 数组实现 链表实现(头部操作)
push/pop效率 O(1)(均摊) O(1)
空间利用率 可能有扩容冗余 每个节点有指针开销,但按需申请
实现复杂度 简单 相对复杂,需要动态申请/释放节点
缓存利用率 高(连续内存) 低(不连续)
实际使用 更常用 较少,一般只在教学中讲解

综合来看,实际开发和面试中,栈基本都用数组来实现,C++ STL中的stack默认底层容器也是deque(本质上也是连续存储结构的组合)。


九、总结

栈是一种操作受限的线性表,核心特性只有一句话:后进先出 。它的实现并不复杂,本质上就是"只能在一端操作的顺序表",所有标准操作的时间复杂度都是O(1)。真正的难点和考察重点在于灵活运用------括号匹配、表达式求值、单调栈、DFS模拟等,都是建立在栈这个基础结构之上的经典问题,建议在掌握实现之后,多刷一些栈相关的算法题巩固理解。

如果这篇文章对你有帮助,欢迎点赞收藏,下一篇预告:队列的实现与应用,敬请期待!

相关推荐
.道阻且长.8 小时前
2.LeetCode算法习题讲解--双指针--复写零
算法·leetcode·职场和发展
·薯条大王8 小时前
经济实惠玩云服务器|一台云服务器多人共用,子账号配置教程
java·linux·运维·服务器·汇编·c++·python
To_OC10 小时前
LC 438 找到所有字母异位词:暴力超时后,我靠滑动窗口一招搞定
javascript·算法·leetcode
白狐_79810 小时前
408数据结构第5章:二叉树遍历序列题——技巧、判断与真题型总结
数据结构
Forever Nore12 小时前
学完C语言力扣第一题做不来正常吗
数据结构·算法
hansang_IR13 小时前
【题解】LC:倍增 / 区间并查集(Range Parallel Unionfind)
c++·算法·并查集
东东最爱敲键盘13 小时前
day7.c++继承
c++
沫璃染墨14 小时前
《从零入门Linux系统篇(十五):系统工具篇·六——GDB调试器详解:从程序执行控制到高级调试技巧》
linux·运维·服务器·c语言·c++
吃着火锅x唱着歌14 小时前
Effective C++ 学习笔记 条款40 明智而审慎地使用多重继承
c++·笔记·学习