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

数据结构之《栈》

在之前我们已经学习了数据结构中线性表里面的顺序表与链表,了解了如何实现顺序表与链表增、删、查、该等功能。其实在线性表中除了顺序表和链表还有其他的类别,在本篇中我们就将学习另外一种线性表——栈,在通过本篇的学习后,你将会对栈的结构有充足的了解,在了解完结构后我们还将进行栈的实现。一起加油吧!!!


1.栈的概念与结构

栈:一种特殊的线性表其只允许在固定的一端进行插入删除元素操作。进行数据插入和删除操作的⼀端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出(先进后出)LIFO(Last In First Out的原则。

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。

那么栈的底层结构是基于什么的呢?其实基于数组还是基于单链表或者是双链表都是可以的,那么哪一种是最优解呢?接下来我们就来分析基于不同底层结构的利与弊

若底层结构基于数组,则可以让数组的低地址处表示栈底,数组的高地址处表示栈顶,使用一个size变量来表示数组有效数据个数,这样就可以要入栈时直接在size位置插入数据,之后size加一;出栈时就可以直接让size减一,这样就可以让数组的有效个数减一。以上使用数组来作为栈的底层结构时间复杂度为O(1)

在数组当中虽然每次在空间不足时都会以之前二倍的方式申请新空间,这时可能会存在内存的浪费,当时由于数组在内存中是连续存放的,这就使得在入栈和出栈不需要再每次都申请空间,而且数组在取数据的时候可能缓存中就有不一定要在主存中取。

若栈的底层结构为单链表,如果是在单链表尾部表示栈顶,头部表示栈底这样在无论是在入栈还是在出栈时因为都需要通过遍历来找到单链表的尾节点,所以时间复杂度都为O(N)。所以这时就要改变栈顶为单链表的头部,栈底为单链表的尾部,这时在入栈和出栈时就不需要遍历单链表,时间复杂度就都为O(1)

但是单链表每次在入栈和出栈时都要申请节点和销毁节点,并且由于单链表每个节点空间在内存当中不一定是连续的,所以每次访问数据都需要区主存取。

而在双链表中无论是让头部作为栈顶,尾部作为栈底还是让尾部作为栈顶,头部作为栈底,由于双链表每个节点内部都有指向前一个节点和下一个节点的指针,所以在入栈和出栈时都不需要遍历链表,所以入栈和出栈时的时间复杂度都为O(1)

但是双链表在每个节点内部相比单链表都多一个指针变量,在32位环境下每个节点大小就会多4个字节;在64位环境下每个节点大小就会多8字节。因此使用双链表就会造成更多的内存损耗。

通过以上的分析可以发现无论栈的底层结构是基于数组还是单链表还是双链表在入栈和出栈时的时间复杂度都为O(1),但是综合其他因素使用数组是最优解 


 

2.栈的实现

在实现栈的代码内在Stack.h头文件内定义栈的结构以及对各种功能函数进行声明,在Stack.c文件内实现各个函数,在test.c文件内对实现的各函数进行测试


2.1栈结构的定义

在Stack.h中创建一个结构体来定义栈的结构,在该结构体中的成员变量和顺序表中基本是相同的,只不过将顺序表中表示有效元素个数的size改名位top,让top来表示栈顶

//定义栈的结构
typedef int STDataType;
typedef struct stack
{STDataType* a;int capacity;//栈空间大小int top;//栈顶
}stack;

2.2栈的初始化

要完成栈的初始化函数首先要在Stack.h完成初始化函数的声明

//初始化栈
void stackInit(stack* ps);

将该函数命名为stackInit,函数的参数就为指向结构体的指针

接下来就是在stack.c内完成初始化函数的实现
由于ps指针是指向结构体的指针,所以该指针不能为空,所以要对ps进行assert断言

//初始化栈
void stackInit(stack* ps)
{assert(ps);ps->a = NULL;ps->top = ps->capacity = 0;
}

2.3检测栈是否为空

要完成检测栈是否为空函数首先要在Stack.h完成该函数函数的声明

//检测栈是否为空
bool stackEmpty(stack* ps);

将该函数命名为stackEmpty,函数的参数就为指向结构体的指针,函数的返回类型为布尔类型

接下来就是在stack.c内完成检测栈是否为空函数的实现
由于ps指针是指向结构体的指针,所以该指针不能为空,所以要对ps进行assert断言
在该函数中若数组内的有效个数top为0函数就会返回true,不为0就返回false

//检测栈是否为空
bool stackEmpty(stack* ps)
{assert(ps);return ps->top==0;
}

2.4入栈

要完成入栈函数首先要在Stack.h完成初始化函数的声明

void stackPush(stack* ps,STDataType x);

将该函数命名为stackPush,函数的参数有两个,第一个为指向结构体的指针,第二个为要进入栈的数据

接下来就是在stack.c内完成入栈函数的实现
由于在入栈时数组的空间可以已经满了,因此在进行插入数据之前要判断数组的有效个数是否与数组的空间大小相同,如果相同就需要进行增容。增容的代码就和之前的顺序表检测数组空间大小函数一样。
在调整完空间大小之后的入栈就和顺序表中得尾插一样,ps->a[ps->top++] = x一句代码就可以实现

//入栈
void stackPush(stack* ps,STDataType x)
{assert(ps);if (ps->top == ps->capacity){int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail!");exit(1);}ps->a=tmp;ps->capacity = newcapacity;}ps->a[ps->top++] = x;
}

2.5出栈 

要完成出栈函数首先要在Stack.h完成出栈函数的声明

//出栈
void stackPop(stack* ps);

将该函数命名为stackPop,函数的参数为指向结构体的指针

接下来就是在stack.c内完成出栈函数的实现
由于ps指针是指向结构体的指针,所以该指针不能为空,所以要对ps进行assert断言
同时在出栈时栈不能为空,所以要对satckEmpt(ps)进行assert断言

在出栈就和顺序表中得尾删一样将top减一就可

//出栈
void stackPop(stack* ps)
{assert(ps);assert(!stackEmpty(ps));--ps->top;
}

2.6取栈顶元素

要完成取栈顶元素函数首先要在Stack.h完成取栈顶元素函数的声明

//获取栈顶元素
STDataType stackTop(stack* ps);

将该函数命名为stackTop,函数的参数为指向结构体的指针,函数的返回值就为栈顶的元素

接下来就是在stack.c内完成取栈顶原始函数的实现
由于ps指针是指向结构体的指针,所以该指针不能为空,所以要对ps进行assert断言
同时在出栈时栈不能为空,所以要对satckEmpt(ps)进行assert断言
由于top指向数组的尾元素的后一位,所以只要将size-1就可以得到栈顶元素

//获取栈顶元素
STDataType stackTop(stack* ps)
{assert(ps);assert(!stackEmpty(ps));return ps->a[ps->top - 1];
}

2.7获取栈中有效元素个数

要完成获取栈中有效元素个数函数首先要在Stack.h完成获取栈中有效元素个数函数的声明

//获取栈中有效元素个数
int stackSize(stack* ps);

将该函数命名为stackSize,函数的参数为指向结构体的指针,函数的返回值就为栈的元素个数

接下来就是在stack.c内完成获取栈中有效元素个数函数的实现
由于ps指针是指向结构体的指针,所以该指针不能为空,所以要对ps进行assert断言
因为在结构体中的top就为数组的有效元素个数,所以在该函数中返回ps->top即可

//获取栈中有效元素个数
int stackSize(stack* ps)
{assert(ps);return ps->top;
}

2.8销毁栈

要完成销毁栈函数首先要在Stack.h完成销毁栈函数的声明

//销毁栈
void stackDestory(stack* ps);

将该函数命名为stackDestory,函数的参数为指向结构体的指针

接下来就是在stack.c内完成销毁栈函数的实现
由于ps指针是指向结构体的指针,所以该指针不能为空,所以要对ps进行assert断言
在销毁时,由于栈的空间是由realloc申请来的,所以在使用完后要用free来释放内存空间,并且在释放后将指针ps->a置为NULL,且将top和capacity都赋值为0

//销毁栈
void stackDestory(stack* ps)
{assert(ps);if (ps->a){free(ps->a);}ps->a = NULL;ps->top = ps->capacity = 0;
}

2.9栈的打印

因为在栈和顺序表以及链表不同,由于栈只能在一端进和出所以栈是不能遍历,所以我们不能通过创建一个函数来遍历栈

所以要打印栈就只能在test.c内调用相关函数来实现
例如以下栈先依次入栈1,2,3,4,之后要打印就通过以下循环来实现

#include"stack.h"void test()
{stack ps;stackInit(&ps);stackPush(&ps, 1);stackPush(&ps, 2);stackPush(&ps, 3);stackPush(&ps, 4);while (!stackEmpty(&ps)){STDataType p = stackTop(&ps);printf("%d ", p);stackPop(&ps);}stackDestory(&ps);
}int main()
{test();return 0;
}

3.栈的实现完整代码 

stack.h

#define  _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdbool.h>
#include<assert.h>
#include<stdlib.h>
//定义栈的结构
typedef int STDataType;
typedef struct stack
{STDataType* a;int capacity;//栈空间大小int top;//栈顶
}stack;//初始化栈
void stackInit(stack* ps);
//销毁栈
void stackDestory(stack* ps);
//检测栈是否为空
bool stackEmpty(stack* ps);
//入栈
void stackPush(stack* ps,STDataType x);
//出栈
void stackPop(stack* ps);
//获取栈顶元素
STDataType stackTop(stack* ps);
//获取栈中有效元素个数
int stackSize(stack* ps);

stack.c

#include"stack.h"//初始化栈
void stackInit(stack* ps)
{assert(ps);ps->a = NULL;ps->top = ps->capacity = 0;
}//销毁栈
void stackDestory(stack* ps)
{assert(ps);if (ps->a){free(ps->a);}ps->a = NULL;ps->top = ps->capacity = 0;
}//检测栈是否为空
bool stackEmpty(stack* ps)
{assert(ps);return ps->top==0;
}//入栈
void stackPush(stack* ps,STDataType x)
{assert(ps);if (ps->top == ps->capacity){int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));if (tmp == NULL){perror("realloc fail!");exit(1);}ps->a=tmp;ps->capacity = newcapacity;}ps->a[ps->top++] = x;
}//出栈
void stackPop(stack* ps)
{assert(ps);assert(!stackEmpty(ps));--ps->top;
}//获取栈顶元素
STDataType stackTop(stack* ps)
{assert(ps);assert(!stackEmpty(ps));return ps->a[ps->top - 1];
}//获取栈中有效元素个数
int stackSize(stack* ps)
{assert(ps);return ps->top;
}

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Redis:十大数据类型
  • react antd upload custom request处理多个文件上传
  • letcode - string
  • springboot rbac Security
  • 向量数据库(二):Qdrant
  • 2024网络安全学习路线 非常详细 推荐学习
  • Kafka面试三道题
  • 感知融合算法学习1
  • 讲一下我对C语言指针入门过程
  • Web3 开发教程
  • IP 泄露: 原因与避免方法
  • 27-《木芙蓉》
  • docker环境安装kafka/Flink/clickhouse镜像
  • spring 不同service事务如何传递
  • Vue3自研开源Tree组件:人性化的拖拽API设计
  • Android Studio:GIT提交项目到远程仓库
  • android百种动画侧滑库、步骤视图、TextView效果、社交、搜房、K线图等源码
  • CentOS 7 修改主机名
  • Java|序列化异常StreamCorruptedException的解决方法
  • Laravel Telescope:优雅的应用调试工具
  • Lucene解析 - 基本概念
  • maya建模与骨骼动画快速实现人工鱼
  • python3 使用 asyncio 代替线程
  • vue2.0项目引入element-ui
  • vue-cli3搭建项目
  • Vue源码解析(二)Vue的双向绑定讲解及实现
  • 初识MongoDB分片
  • 翻译--Thinking in React
  • 开年巨制!千人千面回放技术让你“看到”Flutter用户侧问题
  • 强力优化Rancher k8s中国区的使用体验
  • 如何进阶一名有竞争力的程序员?
  • 掌握面试——弹出框的实现(一道题中包含布局/js设计模式)
  • ​LeetCode解法汇总2808. 使循环数组所有元素相等的最少秒数
  • ​直流电和交流电有什么区别为什么这个时候又要变成直流电呢?交流转换到直流(整流器)直流变交流(逆变器)​
  • ![CDATA[ ]] 是什么东东
  • #define与typedef区别
  • #java学习笔记(面向对象)----(未完结)
  • #宝哥教你#查看jquery绑定的事件函数
  • #前后端分离# 头条发布系统
  • #知识分享#笔记#学习方法
  • (14)学习笔记:动手深度学习(Pytorch神经网络基础)
  • (C++)栈的链式存储结构(出栈、入栈、判空、遍历、销毁)(数据结构与算法)
  • (C语言)二分查找 超详细
  • (done) ROC曲线 和 AUC值 分别是什么?
  • (echarts)echarts使用时重新加载数据之前的数据存留在图上的问题
  • (JS基础)String 类型
  • (多级缓存)多级缓存
  • (附程序)AD采集中的10种经典软件滤波程序优缺点分析
  • (附源码)ssm基于jsp的在线点餐系统 毕业设计 111016
  • (亲测有效)解决windows11无法使用1500000波特率的问题
  • (五)c52学习之旅-静态数码管
  • (转)为C# Windows服务添加安装程序
  • .net core MVC 通过 Filters 过滤器拦截请求及响应内容
  • .NET Core6.0 MVC+layui+SqlSugar 简单增删改查
  • .net 程序 换成 java,NET程序员如何转行为J2EE之java基础上(9)