【初阶数据结构与算法】线性表之栈的定义与实现(含源码和有效的括号练习)

在这里插入图片描述

文章目录

  • 一、栈的概念与结构
    • 1.栈的概念与操作
    • 2.栈的底层结构选型
  • 二、栈的实现
    • 1.栈结构的定义
    • 2. 栈的初始化和销毁
      • 栈的初始化
      • 栈的销毁
    • 3.栈的扩容与入栈
      • 栈的扩容
      • 入栈
    • 4.判断栈是否为空和出栈
      • 判断栈是否为空
      • 出栈
    • 5.取栈顶元素和获取栈中有效元素个数
      • 取栈顶元素
      • 获取栈中有效元素个数
  • 三、栈实现的源码
  • 四、栈的简单应用之有效的括号
    • 思路1
    • 思路2

一、栈的概念与结构

1.栈的概念与操作

   栈是⼀种特殊的线性表,其只允许在固定的⼀端进⾏插⼊和删除元素操作。进⾏数据插⼊和删除操作的⼀端称为栈顶,另⼀端称为栈底。栈中的数据元素遵守后进先出(先进后出)的原则
   其中栈的插入操作就叫压栈/进栈/入栈,插入操作只能对栈顶进行,而删除栈顶元素的操作就叫出栈,出栈也只在栈顶操作,我们画个图理解一下:
在这里插入图片描述
在这里插入图片描述

2.栈的底层结构选型

   栈其实是通过我们之前写过的两个数据结构——数组(顺序表)/链表实现的,它的底层既可以是数组也可以是链表,我们一般情况下就选择数组来实现栈,因为栈只在数据结构的尾部进行操作,数组在尾上插⼊数据的代价⽐较小
   删除数据也比较简单,而链表除非使用双链表,否则使用单链表操作尾部数据,必须遍历整个链表,付出的代价更大,所以我们一般选择使用数组做我们栈的底层
   为什么不使用双链表呢?我们之前也讲过单链表和双链表的区别,单链表结构更简单,适合做其它数据结构的底层,双链表结构复杂,更适合直接存放数据,所以我们在考虑数据结构的底层时,一般考虑的是单链表还有数组,在实现栈的时候我们就使用数组作为底层的结构

二、栈的实现

   在将栈的实现之前,强烈建议先去学习顺序表,栈和顺序表很相似,如果顺序表能够学懂,栈就是小菜一碟,推荐文章:【初阶数据与算法】线性表之顺序表的定义与实现
   栈和顺序表最大的区别是,栈中有效元素个数叫top,栈顶元素就是栈中最后一个数据,也就是top-1位置上的数据,而顺序表中有效元素个数叫size,这是最容易搞混的

1.栈结构的定义

   栈结构的定义与顺序表类似,还是需要三个成员,一个需要动态开辟空间的数组,记录数组有效元素个数和总容量的整型
   但是我们要注意,在栈中,有效元素个数-1就是我们的栈顶元素的位置,所以在栈中有效元素个数换了个名字,不再叫size而叫top了,接下来我们来看看它的结构:

typedef int STDataType;

typedef struct Stack
{
	STDataType* arr;//需要动态开辟的数组
	int top;//有效元素个数
	int capacity;//数组总容量
}ST;

   这里我们提一下,如果顺序表学懂了那么栈就会很简单,等我们实现完了栈就会发现,栈其实是特殊形式的顺序表,我们拭目以待吧!

2. 栈的初始化和销毁

栈的初始化

   栈的初始化和顺序表类似,就是将栈中的数组置为空,将数组总容量和有效数据个数置为0,由于我们需要修改栈,所以我们要传地址,如下:

//初始化栈
void STInit(ST* ps)
{
	assert(ps);
	ps->arr = NULL;
	ps->capacity = ps->top = 0;
}

栈的销毁

   栈的销毁就是手动释放我们申请的资源,就是我们动态开辟的空间,如果不释放掉就会造成内存泄漏,释放完后将数组置空,将数组总容量和有效数据个数置为0,如下:

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

3.栈的扩容与入栈

栈的扩容

   由于栈的底层还是数组,所以我们在进行入栈,也就是插入操作的时候,避免不了要对栈的容量进行判断,如果容量不够要进行扩容,也就是对数组进行扩容,扩容方法也跟顺序表差不多,如下:

//检查栈是否满了,满了就增容
void STCheckCapacity(ST* ps)
{
	assert(ps);
	if (ps->top == ps->capacity)
	{
		ps->capacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		STDataType* tmp = (STDataType*)realloc(ps->arr, sizeof(STDataType) * ps->capacity);
		if (tmp == NULL)
		{
			perror("realloc");
			return;
		}
		ps->arr = tmp;
	}
}

入栈

   我们在上面讲到过,对栈数据的操作都是在栈顶进行操作,那么我们就要界定一下在数组中数组的头作为栈顶好还是尾作为栈顶好,很明显数组的尾作为栈顶更好
   因为对数组的尾进行插入删除操作相当于顺序表的尾插和尾删,时间复杂度为O(1),比较简单,而对数组的头进行插入删除操作相当于顺序表的头插和头删,时间复杂度为O(N),更加复杂
   那么有了这个认知我们实现入栈操作就很简单了,就相当于对顺序表进行尾插,就是对栈中top位置进行插入操作,因为栈顶元素就是top-1,top是有效元素个数,它指向的位置就刚好是我们要插入的位置,具体实现如下:

//入栈操作
void STPush(ST* ps, STDataType x)
{
	STCheckCapacity(ps);
	ps->arr[ps->top++] = x;
}

4.判断栈是否为空和出栈

判断栈是否为空

   判断栈是否为空只需要判断一下有效元素个数top是否为0,这里我们可以巧妙地直接用一条语句写出来,如下:

//判断栈是否为空
bool STEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}

出栈

   出栈其实就是删除栈顶元素,也就是删除数组的最后一个元素,对应顺序表中的尾删,顺序表中就是让size直接- -,在栈中就是让top直接- -,我们在顺序表中解释了多次原因,这里就不再多说,代码如下:

//出栈
STDataType STPop(ST* ps)
{
	assert(!STEmpty(ps));
	ps->top--;
}

5.取栈顶元素和获取栈中有效元素个数

取栈顶元素

   取栈顶元素就是直接返回数组中最后一个有效的数据,注意不是直接返回top位置的数据,top是有效元素个数,top-1才是数组中最后一个有效的数据的下标,知道了这一点我们就可以直接写代码了,如下:

//取栈顶元素
int STTop(ST* ps)
{
	assert(ps);
	return ps->arr[ps->top - 1];
}

获取栈中有效元素个数

   在栈中,top就是有效元素个数,直接返回top即可,如下:

//获取栈中有效元素个数
void STSize(ST* ps)
{
	assert(ps);
	return ps->top;
}

三、栈实现的源码

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

typedef char STDataType;

typedef struct Stack
{
	STDataType* arr;//需要动态开辟的数组
	int top;//有效元素个数
	int capacity;//数组总容量
}ST;

//初始化栈
void STInit(ST* ps)
{
	assert(ps);
	ps->arr = NULL;
	ps->capacity = ps->top = 0;
}

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

//检查栈是否满了,满了就增容
void STCheckCapacity(ST* ps)
{
	assert(ps);
	if (ps->top == ps->capacity)
	{
		ps->capacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		STDataType* tmp = (STDataType*)realloc(ps->arr, sizeof(STDataType) * ps->capacity);
		if (tmp == NULL)
		{
			perror("realloc");
			return;
		}
		ps->arr = tmp;
	}
}

//入栈操作
void STPush(ST* ps, STDataType x)
{
	STCheckCapacity(ps);
	ps->arr[ps->top++] = x;
}

//判断栈是否为空
bool STEmpty(ST* ps)
{
	assert(ps);
	return ps->top == 0;
}

//出栈
void STPop(ST* ps)
{
	assert(!STEmpty(ps));
	ps->top--;
}

//取栈顶元素
STDataType STTop(ST* ps)
{
	assert(ps);
	return ps->arr[ps->top - 1];
}

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

四、栈的简单应用之有效的括号

题目链接:https://leetcode.cn/problems/valid-parentheses/description/

   我们先来看看题目描述与示例:
在这里插入图片描述
在这里插入图片描述
   这道题目要求我们根据给出的字符串来判断它是否是有效的括号,这个字符串中只包含括号,如果括号可以一一匹配那么就说明它是有效的括号,这道题就可以用上我们刚刚写过的栈,我们一起来看看思路

思路1

   我们要利用栈,就要清楚栈的特点先进后出(后进先出),它跟我们的括号的匹配类似,前面的括号和后面的括号匹配,类似于先进后出,如图:
在这里插入图片描述

   在上图演示的思路中,我们可以看出来括号的匹配和栈先进后出(后进先出)的特点相似,所以我们就可以想办法利用栈来做这个题,具体思路如下:

  1. 我们先来看括号匹配的情况:首先创建一个栈,如果碰到左括号就入栈,碰到右括号就检查栈顶元素是否和对应的右括号匹配,如果匹配就出栈,如果最后遍历了整个字符串后,发现栈为空,那么这个字符串就是一个括号匹配字符串,如图:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  2. 接着我们来看括号匹配失败的情况:
    (1)首先就是如果我们遍历到了右括号,但是栈是空的,说明前面没有对应的左括号了,那么字符串肯定就不满足括号匹配的条件,直接返回false
    (2)然后就是,我们遍历到了右括号,但是栈顶元素和这个右括号不匹配,也能说明出现了不匹配的括号,不满足条件,直接返回false
    (3)最后就是,我们遍历完了整个字符串,出了循环结果发现栈里面还不为空,这也说明出现了不匹配的括号,也要返回false
    (4)**注意:**题目没有提供栈,我们需要复制我们写的栈过去,以后学了C++就可以直接使用栈了,不用自己写,现在我们暂时先使用我们写的栈,还有就是我们在返回结果前,要把栈销毁掉,以免造成内存泄漏

   那么括号匹配成功的情况和失败的情况我们都分析完了,条理清晰,接着我们就可以照着思路直接写代码了,题解如下(自行将栈的实现拷贝到题目,这里为了简洁就直接只给出了函数的实现):

bool isValid(char* s) 
{
    ST st;
    STInit(&st);
    
    while(*s != '\0')
    {
        if(*s == '(' || *s == '[' || *s == '{')
        {
            //碰到右括号就入栈
            STPush(&st, *s);
        }
        else
        {
            //走到这里说明碰到了右括号
            //如果栈为空,说明这个右括号没有左括号匹配
            //直接返回假
            if(STEmpty(&st))
            {
                STDestroy(&st);
                return false;
            }
            char top = STTop(&st);
            //取到栈顶元素后查看是否匹配
            //不匹配直接返回假
            if((top == '(' && *s != ')')
             || (top == '[' && *s != ']')
             || (top == '{' && *s != '}'))
             {
                STDestroy(&st);
                return false;
             }
             //走到这里说明栈顶元素和右括号匹配
             //将栈顶元素出栈
             STPop(&st);
        }
        s++;
    }
    //查看栈是否为空
    //为空就是有效的括号,否则就不是
    bool ret = STEmpty(&st);
    STDestroy(&st);
    return ret;
}

思路2

   刚刚我们的思路1是查看取出的栈顶元素和右括号是否匹配,因此我们需要判断条件,但是那个判断条件很复杂,有没有办法简化一下呢?我们想到在Ascll码表中,数字和字母挨得很近,左右括号应该也不会很远
   所以我们思路2就是利用字符底层存储的是它的Ascll码值来解决,我们先来看看Ascll码表中左右括号相隔的距离:
在这里插入图片描述
   我已经在图中标注了几个括号的位置,我们不用去记每个括号的Ascll码值,只需要记得它们之间的相对位置,小括号之间是紧紧挨着的,中大括号中间隔了一个元素,所以我们的条件可以用或连接起来
   我们再大致总结一下思路,首先判断是否遍历到了左括号,遍历到了就直接入栈,如果碰到右括号首先判断栈是否为空,为空直接返回假
   如果不为空取出栈顶的括号,查看当前左括号+1或者+2后是否是遍历到的右括号,如果是的话说明括号匹配成功了,直接让栈顶的括号出栈,否则就没有匹配上,然后直接返回假,最后出循环后再判断一下栈是否为空就大功告成了,题解如下:

bool isValid(char* s) 
{
    ST st;
    STInit(&st);
    while(*s != '\0')
    {   
       if(*s == '(' || *s == '[' || *s == '{')
       {
            STPush(&st, *s);
       }
       else
       {
           if(STEmpty(&st))
           {
              STDestroy(&st);
              return false;
           }
           char top = STTop(&st);
           if(top + 1 == *s || top + 2 == *s)
           {
                STPop(&st);
           }
           else
           {
              STDestroy(&st);
              return false;
           } 
       }
       s++;
    }
    bool ret = STEmpty(&st);
    STDestroy(&st);
    return ret;
}

   最后我们做一个提示,这道题说了只会输入括号,所以我们才能这样做,如果题目存在其它符号就不一定能够使用这个方法,谨慎使用哦~

   那么今天栈的定义与实现就分享到这里啦,希望大家有所收获,如果有什么问题欢迎私信我,文章有什么不足也欢迎纠错
   bye~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/921184.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于Spring Boot+Unipp的博物馆预约小程序(协同过滤算法、二维码识别)【原创】

&#x1f388;系统亮点&#xff1a;协同过滤算法、二维码识别&#xff1b; 一.系统开发工具与环境搭建 1.系统设计开发工具 后端使用Java编程语言的Spring boot框架 项目架构&#xff1a;B/S架构 运行环境&#xff1a;win10/win11、jdk17 前端&#xff1a; 技术&#xff1a;框…

Python 快速入门(上篇)❖ Python基础知识

Python 基础知识 Python安装**运行第一个程序:基本数据类型算术运算符变量赋值操作符转义符获取用户输入综合案例:简单计算器实现Python安装** Linux安装: yum install python36 -y或者编译安装指定版本:https://www.python.org/downloads/source/ wget https://www.pyt…

【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段

文章目录 一、MyBatis-Plus简介二、快速入门1、环境准备2、将mybatis项目改造成mybatis-plus项目&#xff08;1&#xff09;引入MybatisPlus依赖&#xff0c;代替MyBatis依赖&#xff08;2&#xff09;配置Mapper包扫描路径&#xff08;3&#xff09;定义Mapper接口并继承BaseM…

【人工智能】PyTorch、TensorFlow 和 Keras 全面解析与对比:深度学习框架的终极指南

文章目录 PyTorch 全面解析2.1 PyTorch 的发展历程2.2 PyTorch 的核心特点2.3 PyTorch 的应用场景 TensorFlow 全面解析3.1 TensorFlow 的发展历程3.2 TensorFlow 的核心特点3.3 TensorFlow 的应用场景 Keras 全面解析4.1 Keras 的发展历程4.2 Keras 的核心特点4.3 Keras 的应用…

Chrome 浏览器 131 版本新特性

Chrome 浏览器 131 版本新特性 一、Chrome 浏览器 131 版本更新 1. 在 iOS 上使用 Google Lens 搜索 自 Chrome 126 版本以来&#xff0c;用户可以通过 Google Lens 搜索屏幕上看到的任何图片或文字。 要使用此功能&#xff0c;请访问网站&#xff0c;并点击聚焦时出现在地…

2.不同语音ai任务dataset类写法

主流语音任务 语音数据读取基本原则 直接保存语音会将该对象保存在内存中&#xff08;Dataset类调用__getitem__方法&#xff09; 所以一般保存这些数据的存储路径文档&#xff08;表单&#xff09;而不是数据的直接copy&#xff08;不然占用内存太大了&#xff09; 通常用nump…

K8S + Jenkins 做CICD

前言 这里会做整体CICD的思路和流程的介绍&#xff0c;会给出核心的Jenkins pipeline脚本&#xff0c;最后会演示一下 实验/实操 结果 由于整体内容较多&#xff0c;所以不打算在这里做每一步的详细演示 - 本文仅作自己的实操记录和日后回顾用 要看保姆式教学的可以划走了&…

力扣 LeetCode 701. 二叉搜索树中的插入操作(Day10:二叉树)

解题思路&#xff1a; 全部插入到叶子节点即可 class Solution {public TreeNode insertIntoBST(TreeNode root, int val) {if (root null) {TreeNode node new TreeNode(val);return node;}if (root.val < val) {root.right insertIntoBST(root.right, val);}if (root…

2024年11月22日Github流行趋势

项目名称&#xff1a;twenty 项目维护者&#xff1a;charlesBochet, lucasbordeau, Weiko, FelixMalfait, bosiraphael 项目介绍&#xff1a;正在构建一个由社区驱动的现代Salesforce替代方案。 项目star数&#xff1a;22,938 项目fork数&#xff1a;2,413 项目名称&#xff1…

Qt之QMainWidget相关

QMainWindow 继承于QWidget的子类 自带一个菜单栏,一个工具栏,可以设置状态栏与铆钉部件 菜单栏:QMenuBar 注意:一个窗口最多一个菜单栏 API: 创建 QMenuBar(parent) 获取QMainWindow自带的菜单栏 QMenuBar* menuBar() 添加菜单:QMenu addMenu(QMenu *menu); 菜单添加活动:QAct…

【深度学习之一】2024最新pytorch+cuda+cudnn下载安装搭建开发环境

兵马未动&#xff0c;粮草先行。作为深度学习的初学者&#xff0c;快速搭建一个属于自己的开发环境就是头等大事&#xff0c;可以让我们节省许多的时间。这一期我们主要讲一讲2024年最新pytorchcudacudnn下载安装搭建开发环境&#xff0c;以及安装过程中可能遇到的一些问题以及…

SQL 复杂查询

目录 复杂查询 一、目的和要求 二、实验内容 &#xff08;1&#xff09;查询出所有水果产品的类别及详情。 查询出编号为“00000001”的消费者用户的姓名及其所下订单。&#xff08;分别采用子查询和连接方式实现&#xff09; 查询出每个订单的消费者姓名及联系方式。 在…

如何在 UniApp 中实现 iOS 版本更新检测

随着移动应用的不断发展&#xff0c;保持应用程序的更新是必不可少的&#xff0c;这样用户才能获得更好的体验。本文将帮助你在 UniApp 中实现 iOS 版的版本更新检测和提示&#xff0c;适合刚入行的小白。我们将分步骤进行说明&#xff0c;每一步所需的代码及其解释都会一一列出…

ssm面向品牌会员的在线商城小程序

摘要 随着Internet的发展&#xff0c;人们的日常生活已经离不开网络。未来人们的生活与工作将变得越来越数字化&#xff0c;网络化和电子化。它将是直接管理面向品牌会员的在线商城小程序的最新形式。本小程序是以面向品牌会员的在线商城管理为目标&#xff0c;使用 java技术制…

《OpenCV 图像缩放、翻转与变换全攻略:从基础操作到高级应用实战》

简介&#xff1a;本文详细阐述了 OpenCV 在图像操作中的关键技术&#xff0c;包括缩放&#xff08;确定尺寸缩放与按比例缩放&#xff09;、翻转&#xff08;沿不同轴的翻转方式&#xff09;以及变换&#xff08;平移、旋转、三点确定变换和四点确定变换即透视变换&#xff09;…

sql注入报错分享(mssql+mysql)

mysql mysql的报错内容比较多 网上也有比较多的 这里重复的就不多介绍了。一笔带过 溢出类 bigint 当超过mysql的整形的时候&#xff0c;就会导致溢出&#xff0c;mysql可能会将错误信息带出。这里user()是字母默认为0 取反以后1可能就会导致异常。 报错特征 BIGINT UNSIG…

FastAPI重载不生效?解决PyCharm中Uvicorn无法重载/重载缓慢的终极方法!

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 重载缓慢 📒📝 问题概述🚨 相关原因📝 解决方案一📝 解决方案二📝 解决方案三📝 解决方案四⚓️ 相关链接 ⚓️📖 介绍 📖 在使用FastAPI开发时,reload=True 本应让你在修改代码后自动重启服务,提升开发效率…

AI智能稿件排版系统订单管理系统

在现代制造业和服务行业中&#xff0c;高效的生产流程和精确的订单管理是企业保持竞争优势的核心要素。AI智能稿件排版系统和订单管理系统作为一体化解决方案&#xff0c;以其强大的自动化能力和智能化技术&#xff0c;帮助企业实现排版效率提升、数据格式兼容性增强和生产流程…

jetson orin系列开发版安装cuda的gpu版本的opencv

opencv安装包下载地址&#xff1a; https://github.com/opencv/opencv/扩展库下载地址&#xff1a; https://github.com/opencv/opencv_contrib1. 删除jetpack包中的opencv版本 原先的opencv库安装在目录/usr/lib/aarch64-linux-gnu/下&#xff08;一般其他的第三方库也都安…

24小时自动监控,自动录制直播蓝光视频!支持抖音等热门直播软件

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 工具特点📒📝 使用🎈 获取方式 🎈⚓️ 相关链接 ⚓️📖 介绍 📖 对于许多直播爱好者和内容创作者而言,错过心爱的直播或难以搜集视频素材始终是一个难题。今天,给大家分享的这款工具可以轻松解决这个问题,它拥有…