数据结构——栈(C++实现)

news2024/11/28 6:28:37

数据结构——栈

  • 什么是栈
  • 栈的实现
    • 顺序栈的实现
    • 链栈的实现

今天我们来看一个新的数据结构——

什么是栈

栈是一种基础且重要的数据结构,它在计算机科学和编程中扮演着核心角色。栈的名称源于现实生活中的概念,如一叠书或一摞盘子,新添加的物品总是放在顶部,而取出物品时也总是从顶部开始。这种后进先出(Last In, First Out, LIFO)的特性决定了栈的行为。

以下是栈的核心特征和操作:

1. 结构与访问限制:
栈是一个线性数据结构,其中元素按照一定的顺序排列。然而,不同于数组或链表,栈只允许在一端(通常称为栈顶)进行数据的插入(也称为压入,push)和删除(也称为弹出,pop)操作。另一端(栈底)是固定的,不参与数据的直接增删。这意味着栈的元素访问受到严格的限制,用户只能与栈顶元素进行交互。
2. 后进先出(LIFO)原则:
栈遵循后进先出原则。这意味着最近添加到栈中的元素最先被移除。换句话说,最后压入栈的元素是离栈顶最近的,因此在弹出操作时会第一个被访问和移除。相反,最早压入栈的元素(即那些距离栈顶最远的元素)只有在所有后来压入的元素都被弹出后才能被访问。
3. 基本操作:

  • Push(压入): 将一个元素添加到栈顶。
  • Pop(弹出): 移除并返回栈顶元素。如果栈为空,尝试弹出操作通常会导致错误或异常。
  • Peek(查看)/Top: 返回栈顶元素的值,但不改变栈的状态(不移除元素)。
  • IsEmpty(是否为空)/Size(大小): 检查栈是否为空或获取栈中元素的数量。
    4. 应用场景:
    栈因其简单且高效的特性在许多编程任务中得到广泛应用,包括但不限于:
  • 函数调用栈: 在编程语言实现中,每当一个函数被调用时,其局部变量、返回地址等信息会被压入一个系统维护的栈中。函数执行完毕后,通过弹出操作清除这些信息,返回到调用函数的位置继续执行。
  • 表达式求值和符号解析: 在计算逆波兰表示法(RPN)表达式或处理编程语言的括号匹配时,栈用于临时存储操作数和运算符,确保正确的计算顺序。
  • 深度优先搜索(DFS)和回溯算法: 在遍历树形结构或解决涉及多种可能路径的问题时,栈用于存储待访问节点或中间状态,以便回溯到前一个状态。
  • 浏览器历史记录: 用户浏览网页时,后访问的页面压入历史记录栈,前进和后退操作对应于栈的弹出和压入。

总的来说,栈是一种高效、受限的线性数据结构,通过其特有的后进先出性质,为处理需要保持数据顺序、尤其是需要频繁撤销最近操作的场景提供了简洁而强大的工具。

通俗理解,栈的确可以看作是一种操作受限的线性表。线性表是一类数据结构,其中的元素按一定顺序排列,每个元素都有一个唯一的前驱和后继(除了首尾元素外)。栈继承了线性表的这一基本特征,即元素间的线性关系。但是,与常规线性表相比,栈对元素的插入和删除操作施加了严格限制:

操作限制:
常规线性表通常允许在任意位置插入和删除元素,而栈只允许在表的一端(栈顶)进行这两种操作。这意味着你不能随意地在栈的中间或底部插入或删除元素,只能对栈顶进行操作。
行为特点:
由于这种操作限制,栈体现出后进先出(LIFO)的特性。想象一下一个真实的堆栈,比如一叠书或者一叠盘子。当你把新的物品(书或盘子)放在堆栈顶部时,它们就成了最新的“后进”元素。当你需要取走一个物品时,你只能从最上面拿走,所以取出的是最晚放入的那个“先进”元素。这就是所谓的“后进先出”。这种特性使得栈非常适合处理那些需要按照“最后来,最先走”顺序处理数据的场景。
通俗比喻:
可以把栈比作一个只能从上面放东西和取东西的箱子。往箱子里放物品(压入)时,新物品总是在最上面;取出物品(弹出)时,也只能拿走最上面那个。这样,箱子里的物品就像排队一样,后放入的总是在前面,先放入的在后面,想要取走一个物品,必须先把所有后来放入的物品都拿出来。

总结来说,虽然栈具备线性表的基本结构特点,但它通过严格限制操作位置,使其成为一种具有特定行为(后进先出)的特殊线性表。这种操作上的约束赋予了栈独特的应用场景和价值。

栈的实现

我们可以用数组来模拟栈的行为:

template<class T>
class MyStack
{
public:
    MyStack() //无参构造
        :_capacity(10)
        ,_size(0)
    {
        //开辟空间
        _data = new T(_capacity); //开辟这么大的空间
    }
		
    MyStack(const size_t& capacity) //带参构造
        :_capacity(capacity)
        ,_size(0)
    {
        //开辟空间
        _data = new T(_capacity);
    }	


private:
    //动态数组
    T* _data;

    //最大容量
    size_t _capacity;

    //当前数量
    size_t _size;
};

我们这里开了一个动态数组:
在这里插入图片描述
我们一般想象的栈是竖着的,我们可以把这个数组倒一头:
在这里插入图片描述
然后我们可以用一个_size的下标,指向我们0号位置(这里的下标有妙用):

在这里插入图片描述
如果有元素入栈,我们先入栈:
在这里插入图片描述

_size加一:
在这里插入图片描述
模拟这样的行为,我们发现_size总是指向栈顶位置的下一个的位置,但是又因为数组的下标又是从0开始,_size也可以表示栈中有多少个元素。

我们可以用这样的特性,来实现push和top和pop:

    //push
    void push(const T& data)
    {
        assert(_size < _capacity);

        _data[_size++] = data; //在_data[_size]放入之后,_size+1,指向下一个位置
    }

    //pop
    const T& pop()
    {
        assert(_size != 0);

        return _data[--_size]; //因为_size指向栈顶元素的下一个位置,首先先减一取到栈顶
    }

    //top
    const T& top()
    {
        assert(_size != 0);

        return _data[_size - 1];
    }

既然这里的_size是指向栈顶元素的下一个位置,我们也可以让_size指向栈顶元素,这样_size的初始位置就得从-1开始
在这里插入图片描述这样元素入栈,首先要加一(防止下标越界)
在这里插入图片描述然后再放入元素:
在这里插入图片描述

大家可以根据这个写一下这个版本的栈,这里不再赘述。

顺序栈的实现

下面是完整顺序栈的实现:

#pragma once
#include <cassert>
#include <iostream>

template <class T>
class MyStack
{
public:
    // 无参构造函数,使用默认容量创建栈
    MyStack()
        : _capacity(10)
        , _size(0)
    {
        // 开辟空间,创建动态数组,初始容量为 _capacity
        _data = new T[_capacity];
    }

    // 带参构造函数,根据指定容量创建栈
    MyStack(const size_t& capacity)
        : _capacity(capacity)
        , _size(0)
    {
        // 开辟空间,创建动态数组,容量为传入的 _capacity
        _data = new T[_capacity];
    }

    // 压栈操作,将新元素添加到栈顶
    void push(const T& data)
    {
        // 断言检查当前栈是否已满,若已满则抛出断言失败
        assert(_size < _capacity);

        // 将新元素存入动态数组的当前位置,并递增栈大小
        _data[_size++] = data;
    }

    // 出栈操作,删除栈顶元素并返回其值
    const T& pop()
    {
        // 断言检查当前栈是否为空,若为空则抛出断言失败
        assert(_size != 0);

        // 返回栈顶元素的值,并递减栈大小
        return _data[--_size];
    }

    // 查看栈顶元素的值,不改变栈状态
    const T& top()
    {
        // 断言检查当前栈是否为空,若为空则抛出断言失败
        assert(_size != 0);

        // 返回栈顶元素的值(动态数组的最后一个元素)
        return _data[_size - 1];
    }

    // 判断栈是否为空
    bool empty()
    {
        // 返回当前栈大小是否为0,即栈是否为空
        return _size == 0;
    }

    // 返回栈中元素数量
    size_t size()
    {
        // 返回当前栈大小(元素数量)
        return _size;
    }

private:
    // 动态数组,用于存储栈中的元素
    T* _data;

    // 最大容量,即动态数组的容量
    size_t _capacity;

    // 当前数量,即栈中元素的数量
    size_t _size;
};

我们可以测试一下:

#include"my_stack.h"

int main()
{
    MyStack<int> mystack;

    mystack.push(23);
    mystack.push(2);
    mystack.push(20);
    mystack.push(11);
    mystack.push(20);

    std::cout<<"size of mystack: "<<mystack.size()<<std::endl;
   
   
    while(!mystack.empty())
    {
        std::cout<<mystack.pop()<<std::endl;
    }

    if(mystack.empty()==true)
    {
        std::cout<<"stack is empty"<<std::endl;
    }


    return 0;
}


在这里插入图片描述

链栈的实现

上面我们使用的是数组来模拟实现的栈,我们也可以用链表来模拟栈(我这里用带尾指针双向链表来模拟):

我们还是定义一个结点类:
在这里插入图片描述

//结点类
template<class T>
struct Node
{
   Node()
    :_data(T())
    ,_next(nullptr)
    ,_prve(nullptr)
   {

   }

   Node(const T& data)
    :_data(data)
    ,_next(nullptr)
    ,_prve(nullptr)
   {

   }
  //数据域
  T _data;

  //指针域
  Node<T>* _next;
  Node<T>* _prve;
};

这是单链表中的结构:
在这里插入图片描述
我们可以稍微改一下名字:
在这里插入图片描述
在这里我没有用带头结点的双向链表,所以一开始_top和_first会指向nullptr:
在这里插入图片描述
等要插入时,才插入第一个结点:
在这里插入图片描述在这里插入图片描述

void push(const T& data)
{
    // 当栈为空时,直接创建新的节点作为栈的第一个元素和栈顶元素
    if (_first == nullptr)
    {
        _first = new _Node(data);
        _top = _first;
    }
    // 当栈非空时,创建新节点并插入到链表末尾,更新栈顶指针
    else
    {
        _Node* newnode = createNode(data); // 创建新节点
        // 更新链表结构:将新节点的 _prve 指针指向当前栈顶节点
        newnode->_prve = _top;

        // 将当前栈顶节点的 _next 指针指向新节点
        _top->_next = newnode;

        // 更新栈顶指针,使新节点成为新的栈顶元素
        _top = newnode;
    }
    // 增加栈大小
    _size++;
}

下面是完整代码:

#pragma once
#include<iostream>

// 结点类
template<class T>
struct Node
{
   // 默认构造函数,初始化数据和指针为默认值
   Node()
    : _data(T())
    , _next(nullptr)
    , _prve(nullptr)
   {}

   // 带参构造函数,根据给定数据初始化结点
   Node(const T& data)
    : _data(data)
    , _next(nullptr)
    , _prve(nullptr)
   {}

   // 数据域,存储链栈中实际的元素值
   T _data;

   // 指针域,分别指向下一个结点和前一个结点
   Node<T>* _next;
   Node<T>* _prve;
};

// 链栈类
template<class T>
class MyStack 
{
    // 内部类型定义,简化代码中的类型书写
    typedef Node<T> _Node;

public:
    // 构造函数,初始化栈为空
    MyStack()
        : _first(nullptr)
    {
        _top = _first;
    }

    // 创建结点
    _Node* createNode(const T& data)
    {
        _Node* newnode = new _Node(data);

        if (newnode == nullptr)
        {
            exit(EXIT_FAILURE); // 如果内存分配失败,直接终止程序
        }

        return newnode;
    }

    // 压栈操作,将新元素添加到栈顶
    void push(const T& data)
    {
        if (_first == nullptr) // 当栈为空时
        {
            _first = new _Node(data); // 创建新节点作为栈的第一个元素和栈顶元素
            _top = _first;
        }
        else // 当栈非空时
        {
            _Node* newnode = createNode(data); // 创建新节点

            // 更新链表结构:将新节点的 _prve 指针指向当前栈顶节点
            newnode->_prve = _top;
            // 将当前栈顶节点的 _next 指针指向新节点
            _top->_next = newnode;

            // 更新栈顶指针,使新节点成为新的栈顶元素
            _top = newnode;
        }

        // 增加栈大小
        _size++;
    }

    // 出栈操作,删除栈顶元素并返回其值
    T pop()
    {
        T top = _top->_data; // 保存栈顶元素的值

        if (_top == _first) // 当栈只剩一个元素时
        {
            delete _top; // 删除栈顶元素
            _top = nullptr;
            _first = nullptr; // 清空栈
        }
        else // 当栈中有多个元素时
        {
            _Node* prve = _top->_prve; // 获取栈顶元素的前一个结点

            prve->_next = nullptr; // 断开与已删除节点的连接
            delete _top; // 删除栈顶元素

            _top = prve; // 更新栈顶指针
        }

        // 减小栈大小
        --_size;
        return top; // 返回栈顶元素的值
    }

    // 查看栈顶元素的值,不改变栈状态
    const T& top() const
    {
        return _top->_data;
    }

    // 判断栈是否为空
    bool empty() const
    {
        return _size == 0;
    }

    // 返回栈中元素数量
    size_t size() const
    {
        return _size;
    }

private:
    // 第一个结点,用于初始化栈
    _Node* _first = nullptr;

    // 栈顶指针,始终指向栈顶元素
    _Node* _top;

    // 当前元素数量,表示栈大小
    size_t _size = 0;
};

测试一下:

//#include"my_stack_sequence.h"
#include"my_stack_link.h"

int main()
{
    MyStack<int> mystack;

    mystack.push(23);
    mystack.push(2);
    mystack.push(20);
    mystack.push(11);
    mystack.push(20);

    std::cout<<"size of mystack: "<<mystack.size()<<std::endl;
   
    while(!mystack.empty())
    {
        std::cout<<mystack.pop()<<std::endl;
    }
    
    if(mystack.empty()==true)
    {
        std::cout<<"stack is empty"<<std::endl;
    }


    return 0;
}


在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

贝锐蒲公英企业路由器X5 Pro:无需专线和IT人员,分钟级异地组网

尽管我们公司规模较小&#xff0c;只有十几个人&#xff0c;但为了确保项目资料的安全&#xff0c;依旧在公司内部自建了文件存储服务器和办公系统。 但是&#xff0c;随着项目数量的增加&#xff0c;大家出差办公的情况也愈发普遍&#xff0c;如何解决远程访问内部系统成了问…

AIGC专栏10——EasyAnimate 一个新的类SORA文生视频模型 轻松文生视频

AIGC专栏10——EasyAnimate 一个新的类SORA文生视频模型 &#x1f4fa;轻松文生视频 学习前言源码下载地址技术原理储备&#xff08;DIT/Lora/Motion Module&#xff09;什么是Diffusion Transformer (DiT)LoraMotion Module EasyAnimate简介EasyAnimate原理界面展示快速启动云…

《中医临床诊疗术语》数据库

最新版的《中医临床诊疗术语》于2023年3月17日由国家中医药管理局提出的&#xff0c;由国家市场监督管理总局和国家标准化管理委员会共同发布。新版的修订是为落实相关政策文件要求&#xff0c;推进中医医疗服务规范化、标准化管理&#xff0c;提高中医医疗服务标准化水平和管理…

Web前端开发——Ajax,Axios概述及在Vue框架中的使用

前言&#xff1a; 整理下学习笔记&#xff0c;打好基础&#xff0c;daydayup!!! Ajax Ajax是什么&#xff1f; Ajax全称Asynchromous JavaScript And Xml&#xff0c;是异步的JavaScript和Xml。 Ajax的作用&#xff1f; 1&#xff0c;数据交换&#xff1a;通过Ajax可以给服务器…

【Redis 神秘大陆】003 数据类型使用场景

三、Redis 数据类型和使用场景 Hash&#xff1a;对象类型的数据&#xff0c;购物车List&#xff1a;队列/栈Set&#xff1a;String类型的无序集合&#xff0c;intset&#xff0c;抽奖、签到、打卡&#xff0c;商品评价标签Sorted Set&#xff1a;存储有序的元素&#xff0c;zip…

二叉树的先中后序遍历

什么是遍历呢&#xff1f; 遍历:按照某种次序把所有结点都访问一遍 先/中/后序遍历:基于树的递归特性确定的次序规则 二叉树的递归特性: ①要么是个空二叉树 ②要么就是由“根节点左子树右子树”组成的二叉树 先序遍历:根左右&#xff08;NLR) ——先访问根结点&#xff0c;…

抖音小店新店铺起飞式玩法,这几步一定要做好,前期很重要

大家好&#xff0c;我是电商笨笨熊 进入抖音小店后不知道该怎么操作&#xff0c;不清楚如何让新店快速起店&#xff1b; 今天我们就来聊聊新店铺快速起店的几个关键步骤&#xff0c;新手玩家一定要按照流程去做。 第一步&#xff1a;店铺搭建 小店开通之后不要着急选品上架&…

FreeRTOS_day1

1.总结keil5下载代码和编译代码需要注意的事项 下载代码前要对仿真进行设置 勾选后代码会立刻执行 勾选后会导致代码不能执行 写代码的时候要写在对应的begin和end之间&#xff0c;否则会被覆盖 2.总结STM32Cubemx的使用方法和需要注意的事项 ①打开软件&#xff0c;新建工程…

项目7-音乐播放器2(上传音乐+查询音乐+拦截器)

0.加入拦截器 之后就不用对用户是否登录进行判断了 0.1 定义拦截器 0.2 注册拦截器 生效 1.上传音乐的接口设计 请求&#xff1a; { post, /music/upload {singer&#xff0c;MultipartFile file}&#xff0c; } 响应&#xff1a; { "status": 0, "message&…

【Android】Activity task和Instrumentation杂谈

文章目录 activity taskInstrumentation机制参考 Android不仅可以装载众多的系统组件&#xff0c;还可以将它们跨进程组成ActivityTask&#xff0c;这个特性使得每个应用都不是孤立的。 activity task 从数据结构角度看&#xff0c;Task有先后之分&#xff0c;源码实现上采取了…

AI人工智能老师大模型讲师叶梓 OneLLM:开创性的多模态大型语言模型技术

在人工智能领域&#xff0c;多模态大型语言模型&#xff08;MLLM&#xff09;的研究一直是一个热门话题。近期&#xff0c;一种名为OneLLM的创新技术引起了业界的广泛关注。OneLLM通过其独特的统一框架&#xff0c;实现了多种不同模态与自然语言的高效对齐&#xff0c;为多模态…

加拿大银行入局,强化数字货币的量子安全防护

领先的量子网络安全公司EvolutionQ此前宣布与加拿大银行签订合同&#xff0c;为加拿大银行的一个研究项目做出贡献&#xff0c;该项目涉及绿地数字货币的量子安全网络安全技术。这项工作强调了EvolutionQ致力于理解量子计算机对新兴金融技术&#xff08;如数字货币&#xff09;…

社区养老服务系统|基于springboot社区养老服务系统设计与实现(源码+数据库+文档)

社区养老服务系统目录 目录 基于springboot社区养老服务系统设计与实现 一、前言 二、系统功能设计 三、系统实现 1、管理员部分功能 &#xff08;1&#xff09; 用户管理 &#xff08;2&#xff09;服务种类管理 &#xff08;3&#xff09;社区服务管理 &#xff08…

怎样编写测试团队通用的JMeter脚本

1、确定测试目标和场景&#xff1a; 与团队成员共同明确测试的目标&#xff0c;例如性能评估、负载测试、稳定性测试等。 确定要测试的具体业务场景和使用案例&#xff0c;比如用户登录、搜索功能、购物流程等。 2、学习 JMeter 工具和基础知识&#xff1a; 阅读 JMeter 官…

JS -a标签和this在DOM的使用、使用DOM完成点击按钮操作div块和获取div块的CSS样式

a标签的索引问题和this <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthde…

QT 串口助手 学习制作记录

QT 串口助手qt 学习制作记录 参考教程&#xff1a;​​​​​​QT初体验&#xff1a;手把手带你写一个自己的串口助手_qt设计串口助手的流程图-CSDN博客 Qt之串口编程&#xff08;添加QSerialPort模块&#xff09;_如何安装 qt串口模块教程-CSDN博客 串口调试助手&#xff1…

聚道云软件连接器助力医疗器械有限公司打通金蝶云星辰与飞书

摘要 聚道云软件连接器成功将金蝶云星辰与飞书实现无缝对接&#xff0c;为某医疗器械有限公司解决采购订单、付款单同步、审批结果回传、报错推送等难题&#xff0c;实现数字化转型升级。 客户介绍 某医疗器械有限公司是一家集研发、生产、销售为一体的综合性医疗器械企业。…

揭秘AI精准输出:如何构建完美的AIGC提示词?

揭秘AI精准输出&#xff1a;如何构建完美的AIGC提示词&#xff1f;&#x1f916; 文章目录 揭秘AI精准输出&#xff1a;如何构建完美的AIGC提示词&#xff1f;&#x1f916;摘要引言正文&#x1f4d8; 提示词的基本概念1. 什么是提示词&#xff1f;2. 提示词的作用 &#x1f4d…

锁策略^o^

锁策略 一&#xff0c;悲观锁 VS 乐观锁 悲观锁&#xff1a;总是假设最坏的情况&#xff0c;每次去拿数据的时候都认为别人会修改&#xff0c;所以每次在拿数据的时候都会碰上锁&#xff0c;这样别人想拿这个数据就会阻塞&#xff0c;直到它拿到锁。 乐观锁&#xff1a;假设…

如何理解服务器的硬防和软防

关于服务器防御相关的知识很多新手都不是很了解&#xff0c;服务器防御分为服务器硬防和软防。 一、服务器硬防 服务器硬防主要指的是硬件防火墙&#xff0c;能够在硬件设备中嵌入防火墙的防御程序&#xff0c;是一种专门用来保护网络不受未授权访问所设计的设备&#xff0c;硬…