栈与队列之栈入门攻略:从核心概念到数组实现

🏠个人主页:黎雁

🎬作者简介:C/C++/JAVA后端开发学习者

❄️个人专栏:C语言数据结构(C语言)EasyX游戏规划程序人生

✨ 从来绝巘须孤往,万里同尘即玉京

文章目录

栈与队列之栈入门攻略:从核心概念到数组实现✨

你好!欢迎来到线性表系列的全新篇章------栈与队列篇的第一讲。

在前面的内容中,我们已经吃透了顺序表和链表这两大基础线性表,从底层实现到实战刷题,完成了从理论到实践的跨越。而今天要学习的,作为一种特殊的线性表,是数据结构世界里不可或缺的核心角色,它的"后进先出"特性在算法解题、工程开发中都有着广泛应用,比如括号匹配、函数调用栈、表达式求值等场景都离不开它。

准备好了吗?让我们一起解锁栈的核心知识,从概念到实现,彻底掌握这个"个性十足"的线性表!🚀


文章摘要

本文聚焦线性表中的栈结构,系统讲解栈的核心概念、"后进先出"特性及数组/链表两种实现方式的优劣对比,最终选择数组作为最优实现方案。通过完整的工程化代码(头文件+源文件),详细拆解栈的初始化、销毁、入栈、出栈等核心操作,补充top指针两种初始化方式的关键细节,结合软件工程"低耦合、高内聚"思想解析代码设计,夯实栈的底层实现基础。

阅读时长 :约20分钟
阅读建议

  1. 基础薄弱者:先吃透栈的核心特性,再对照代码理解实现逻辑
  2. 工程开发者:重点关注数组扩容、内存释放等细节,借鉴代码封装思想
  3. 面试备考者:牢记栈的特性、实现方式及典型应用场景
  4. 查漏补缺者:直接查看代码实现中的关键注释和易错点

一、知识回顾:线性表的核心本质

在学习栈之前,我们先回顾下线性表的核心特征:

  • 线性表是数据元素呈线性排列的结构,每个元素有唯一的前驱和后继(除首尾)
  • 顺序表:物理存储连续,随机访问快,但插入删除效率低
  • 链表:物理存储不连续,插入删除效率高,但随机访问慢

而栈,正是基于线性表实现的受限数据结构------它只允许在一端进行操作,这也让它拥有了独一无二的特性!


二、栈的核心概念:什么是"后进先出"?📚

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不就行了?

这就涉及到软件工程的核心思想------低耦合、高内聚

  1. 高内聚:栈的所有操作逻辑都集中在stack.c中,对外只暴露统一接口,便于维护和修改(比如后续把top初始值改成-1,只需修改栈内部代码,无需改测试代码)
  2. 低耦合:测试代码只需调用接口,无需关心栈的内部实现细节,降低代码之间的依赖
  3. 鲁棒性 :函数内部有断言检查(如assert(ps)),能提前发现错误,而直接写表达式无法做到

简单来说:封装是为了让代码更健壮、更易维护


五、写在最后

恭喜你!在这一篇中,你已经掌握了栈的核心知识:

  • 理解了栈"后进先出"的核心特性
  • 对比了数组/链表两种实现方式,选择了最优方案
  • 完成了栈的工程化代码实现,掌握了top指针的关键细节
  • 理解了"低耦合、高内聚"的软件工程思想

栈作为基础数据结构,是后续学习的重要铺垫:

  • 算法层面:括号匹配、表达式求值、DFS(深度优先搜索)都离不开栈
  • 工程层面:函数调用栈、浏览器前进后退功能都基于栈实现

下一篇,我们将学习栈的"好搭档"------队列,它遵循"先进先出"原则,和栈形成完美互补。敬请期待!😜


点赞+收藏+关注,跟着系列内容一步步吃透数据结构!你的支持是我创作的最大动力~👍

相关推荐
郝学胜-神的一滴1 天前
Linux线程使用注意事项:骈文技术指南
linux·服务器·开发语言·数据结构·c++·程序人生
星火开发设计1 天前
折半插入排序原理与C++实现详解
java·数据结构·c++·学习·算法·排序算法·知识
福楠1 天前
模拟实现list容器
c语言·开发语言·数据结构·c++·list
海天一色y1 天前
python--数据结构--链表
数据结构·链表
三川6981 天前
数据结构设计高频题目
数据结构·哈希算法·散列表
yangpipi-1 天前
《C++并发编程实战》第6章 设计基于锁的并发数据结构
开发语言·数据结构·c++
wen__xvn1 天前
代码随想录算法训练营DAY7第三章 哈希表part02
数据结构·算法·散列表
越努力^越幸运1 天前
C中部分的字符函数
c语言
C雨后彩虹1 天前
亲子游戏问题
java·数据结构·算法·华为·面试