当前位置: 首页 > news >正文

数据结构初阶(4)栈

1. 栈

1.1 栈的概念及结构

 栈 :一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。

普通线性表:允许在两头、中间任意一点进行操作(数据管理)

栈:只允许在一个端口进行数据操作。

队列:只允许在两个端口进行数据操作。

线性表:逻辑上数据是依次存储的。

顺序表:数据在物理上也连续依次存储的线性表。

链表:数据在物理上不连续依次存储的线性表。

进行数据插入和删除操作的一端称为 栈顶 另一端称为 栈底 

中的数据元素遵守 后进先出原则(Last In First Out,LIFO)

类比:肉串、弹夹——只能在一端进——后串进去的先被吃、后压入的先射出。

 压栈 :栈的插入操作叫做进栈/压栈/入栈入数据在栈顶

 出栈 :栈的删除操作叫做出栈出数据也在栈顶

1.2 栈的实现

逻辑分析

栈的实现一般可以使用顺序表(数组)或者链表实现。

相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小


栈和队列的数据结构都是数组或链表。

堆的数据结构是二叉树

栈分为数组栈和链式栈,一般都是选择数组栈。

栈的适用场景:所有后进先出的场景

1.括号匹配问题 ——后进来的左括号在s遍历到右括号时优先匹配

2.递归改非递归

因为递归的核心缺陷——要连续创建函数栈帧,而进程里面给栈区分配的空间并不多,所以当函数调用递归的深度太深时,栈空间可能会溢出,所以有些地方需要把递归该为非递归。  

        a. 简单一点的递归改非递归就直接在这个地方就改成循环就可以了

        b. 复杂一点的则需要借助栈来进行辅助

代码实现

(1)Stack.h

栈:只允许在一个端口进行数据操作的线性表。

// 入栈——不像顺序表需要指明在哪push,默认在栈顶push
void StackPush(Stack* ps, STDataType data); 
// 出栈——不像顺序表需要指明在哪pop,默认在栈顶pop
void StackPop(Stack* ps); 

有些书上会命名为Insert、erase,这里的栈的入栈、出栈命名风格跟随C++的STL会更好。

// 下面是定长的静态栈的结构,实际中一般不实用,所以我们主要实现下面的支持动态增长的栈
typedef int STDataType;
#define N 10
typedef struct Stack
{STDataType _a[N];int _top; // 栈顶
}Stack;//支持动态增长的栈
typedef int STDataType;
typedef struct Stack
{STDataType* _a;int _top;          // 栈顶(下标)——记录了有效数据个数int _capacity;     // 容量
}Stack;// 初始化栈
void StackInit(Stack* ps); 
// 入栈
void StackPush(Stack* ps, STDataType data); 
// 出栈
void StackPop(Stack* ps); 
// 获取栈顶元素
STDataType StackTop(Stack* ps); 
// 获取栈中有效元素个数
int StackSize(Stack* ps); 
// 获取栈的元素个数
int STSize(ST* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps); 
// 销毁栈
void StackDestroy(Stack* ps);

这些是栈这个数据结构常见的会实现的接口(不一定都实现这些,也不一定只有这些)。


支持动态增长的栈——动态顺序表

注意:这个数组栈st在栈区,而数组栈保存的数据*a在堆区

(2)Stack.c

两个注意点

  • 初始化的top影响其他的接口函数的书写。
  • 扩容的逻辑。
① 初始化

代码实现。

//初始化
void STInit(ST* ps)
{assert(ps);//可以初始化为空,也可以给一段空间ps->a = NULL;//初始化为0ps->top = 0;ps->capacity = 0;
}

规定

栈顶是指最后一个有效数据的下一个位置的地址——一开始就是a[0]。

(根据《微机原理与接口技术》关于内存区域——栈,的规定)

但是一般来讲

注意

栈(数据结构)并没有规定top指向哪里

但是 初始化函数 决定了top指向哪里

——top初始化为-1,指向最后一个有效数据(栈顶元素),则入/出栈操作的就是a[++top]

——top初始化为0,指向最后一个有效数据的下一位置,则入/出栈操作的就是a[top++]

要是top=0指向栈顶元素——则区分不开没数据和有一个数据。

② 入栈

注意

//在栈顶插入不一定是尾插——数组是尾插,链表就不一定(头插 / 尾插)。
//故栈插入不说尾和头,只是在一端,这一端叫作“栈顶”
//插入:--Push是C++的STL的命名风格,而一般常见的还有insert......

 代码实现。

//插入
void STPush(ST* ps, STDataType x)
{assert(ps);//如果空间满了/一开始没开空间//——>就扩容if (ps->top == ps->capacity)    //当top(下标) == capacity(容量){//如果空间为0就给4个初始空间,如果空间不为0就翻倍空间int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;//即使一开始为空,也可以直接用realloc扩容——当指针为空,realloc的行为相当于malloc//使用临时指针tmp接收STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail");return;//或者直接结束掉程序exit(1);都可以}//成功开辟空间就赋值ps->a = tmp;ps->capacity = newcapacity;}//由于top是0——>指向栈顶元素的下一个,所以直接先插入,再++ps->a[ps->top] = x;//赋值栈顶之上的元素ps->top++;
}

注意——操作符的优先级问题

有些操作符优先级表中会把++(后缀)和->放到同一优先级(1-最高)。

实际上

简单助记法:先取元素,再对元素进行单目运算,再双目运算,再三目运算,最后聚成一个可以带;的完整表达式。

取元素操作符不按“单目/双目”论,ps->x的x本身并不是->的右操作数,而是ps的成员名

约束->的条件是左侧必须是 左值/指针


实际上

根据结合性,表达式应自左向右计算,先计算ps->top,再计算(ps->top)++。

只有 一元/前缀运算符、全部赋值运算符 是从右到左结合;其余均为从左到右。

此外,++必须作用于可修改的左值,产生一个右值,而->不能作用于右值。

语法上天然阻止了ps->(x++)这种解析,因此编译器永远只会把它看成(ps->x)++。

所谓的(++a)++也不行。

所以ps->x++会被编译器解析成(ps->x)++。

③ 出栈

 代码实现。

//弹出
void STPop(ST* ps)
{assert(ps);assert(!STEmpty(ps));    //不为空栈,就弹出ps->top--;               //不用抹除数据
}
④ 获取栈顶元素

 代码实现。

//取
STDataType STTop(ST* ps)
{//报错——>当ps为空(假)assert(ps);//报错——>当STEmpty(ps)为真assert(!STEmpty(ps));return ps->a[ps->top - 1];
}
⑤ 获取有效数据个数

 代码实现。

//获取有效数据个数——只要指针不为空,直接返回top
int STSize(ST* ps)
{assert(ps);return ps->top;	//相当于数组下标的下一个
}
⑥ 检空

 代码实现。

//检空
bool STEmpty(ST* ps)
{assert(ps);//不需要麻烦地使用//if(...)...return true//else(...)...return false//一句话搞定——直接用表达式的结果作为返回值,是更好的return ps->top == 0;
}
⑦ 销毁

 代码实现。

//销毁
void STDestroy(ST* ps)
{assert(ps);free(ps->a);ps->a = NULL;ps->top = ps->capacity = 0;
}

动态栈才需要 销毁函数 ,因为动态栈维护的数据在堆区。

注意区分

数据结构

  • “栈”:特殊线性表
  • “堆”:特殊二叉树

操作系统内存区域

  • “栈”:局部变量、函数指针、……
  • “堆”:动态内存管理、……

它们几乎没有关联,仅仅是两个不同研究领域的同名名词而已。

它们的命名都是依据使用习惯来的。

(3)Test.c

两个注意点

  1. 使用栈之前不要忘了先初始化(不初始化的话里面就会有随机值)。
  2. 使用完栈之后不要忘了销毁,避免内存泄漏。
#include"Stack.h"void test()
{//定义一个栈的数据结构出来ST s;//1.使用栈之前不要忘了先初始化(不初始化的话里面就会有随机值)STInit(&s);//插入数据STPush(&s, 1);STPush(&s, 2);STPush(&s, 3);//获取栈顶元素——3int top = STTop(&s);//打印栈顶元素——3printf("%d ", top);//弹出栈顶元素——剩1、2STPop(&s);//插入数据——1、2、4、5STPush(&s, 4);STPush(&s, 5);while (!STEmpty(&s)){//取栈顶元素——5、4、2、1int top = STTop(&s);//打印栈顶元素——5、4、2、1printf("%d ", top);//弹出栈顶元素——5、4、2、1STPop(&s);}//取完一次——栈就空了——栈的需求就是这样//2.使用完栈之后不要忘了销毁,避免内存泄漏STDestroy(&s);
}int main()
{test();return 0;
}

遍历一遍,栈就空了——会不会不好?

——不会,栈的需求就是这样。

注意

  • 入栈顺序1-2-3-4-5
  • 出栈顺序不一定就是5-4-3-2-1,可能在4-5进栈之前,3就已经出栈了
  • 甚至有可能1-2-3-4-5(入一个,就立刻出一个)
  • 模拟出栈顺序,是常考的选择题

(后进先出是相对于同一时刻栈内的元素而言)

    http://www.dtcms.com/a/319040.html

    相关文章:

  1. Python生成统计学公式
  2. 数据结构:双向链表(Doubly Linked List)
  3. 快速搭建开源网页编辑器(vue+TinyMCE)
  4. 大屏数据展示页面,数据可视化可以用到的框架和插件
  5. 剧本杀小程序系统开发:推动社交娱乐产业创新发展
  6. requests模块
  7. Web3.0引领互联网未来,助力安全防护升级
  8. 基于django的非物质文化遗产可视化网站设计与实现
  9. 重学React(三):状态管理
  10. Java如何快速实现短信登录?
  11. 【指南版】网络与信息安全岗位系列(三):安全运维工程师
  12. 农作物优选,耕耘希望的田野
  13. Vue3获取当前页面相对路径
  14. 008 前端vue
  15. Android-Kotlin基础(Jetpack①-ViewModel)
  16. 【遥感图像入门】近三年遥感图像建筑物细粒度分类技术一览
  17. 前端开发(HTML,CSS,VUE,JS)从入门到精通!第七天(Vue)(二)
  18. Tiger任务管理系统-10
  19. vue打包号的文件如何快速查找文件打包后的位置
  20. 聚水潭API数据接口开发手机端网页查询商品仓位库位库存工具,支持扫描识别,预览图片
  21. Numpy科学计算与数据分析:Numpy线性代数基础与实践
  22. 决策树技术详解:从理论到Python实战
  23. RabbitMQ-日常运维命令
  24. 华为开源CANN,再次释放“昇腾转向”信号
  25. 【数据结构初阶】--排序(五)--计数排序,排序算法复杂度对比和稳定性分析
  26. C语言memmove函数详解:安全高效的内存复制利器
  27. ELK基础环境安装准备
  28. 飞算JavaAI深度解析:Java开发者的智能革命
  29. 构建一个简洁优雅的 PHP 参数验证器 —— php-schema-validator
  30. 大疆前端笔试题目详解