数据结构——30行代码实现栈和模拟递归

C++栈(stack)详解 C++栈详解 阅读详情

本文始发于个人公众号:TechFlow,原创不易,求个关注


栈的定义


原本今天想给大家讲讲快速选择算法的,但是发现一连写了好几篇排序相关了,所以临时改了题目,今天聊点数据结构,来看看经典并且简单的数据结构——栈。

栈这个结构我想大家应该都耳熟能详,尤其是在很多地方将和堆并列在一起,称作“堆栈”就更广为人知了。但其实堆和栈本质上是两种不同的数据结构,我们不能简单地混为一谈。让我们先从比较简单的栈开始。

栈和队列的本质其实都是数组(严格地说是线性表)。只不过我们在数组上增加了一些限制,使得它满足一定的条件而已,所以很多对数据结构畏首畏尾的同学可以放宽心,栈没什么特别的花样,就是一种特殊的数组。

和其他广义上的线性表数据结构比起来,栈的特殊性只有两条,一条是先进后出,另一条是只能从数组的一侧读写。但本质上来说这两条是一样的,由于我们只能从一侧读写元素,所以进的越早出的越晚,当然是先进后出。从下面这张图应该很容易能看明白。

栈规定了我们只能从一侧进行读写,常规上我们将能够读写的一侧称作是栈顶。不能读写的另一侧称为是栈底。从上面的图可以看到,只有栈顶的元素出栈了之后,才能访问到栈底的元素。

我们用Python的数组来实现栈这个数据结构,去掉注释真的只有30行不到,可以说是非常简单,我们先来看代码。

class Stack(object):

    def __init__(self, size_limit=None):
        self._size_limit = size_limit
        self.elements = []
        self._size = 0

    # 进栈,判断是否越界
    def push(self, value):
        if self._size_limit is not None and len(self.elements) > self._size_limit:
            raise IndexError("Stack is full")
        else:
            self.elements.append(value)
            self._size += 1

    # 判断栈是否为空
    def is_empty(self):
        return self._size == 0

    # 栈清空
    def clear(self):
        self.elements = []
        self._size = 0

    # 访问元素数量
    def size(self):
        return self._size

    # 查询栈顶元素
    def top(self):
        return self.elements[-1]
    
    # 弹出栈顶元素
    def pop(self):
        val = self.elements.pop()
        self._size -= 1
        return val

本质上来说,一般的栈实现只有以上这么几个方法,可能会更少。因为有些语言当中的栈,top和弹出是合并的。意味着访问必须要弹出,不支持非弹出访问。所以栈的实现逻辑是非常简单的,甚至可以说是毫无技术含量,非常适合入门数据结构。

当然,从另一个方面也可以说栈的实现原理并不太重要,相比之下更重要的是栈一般会用在什么地方。


栈的应用


栈最广泛的应用就是在操作系统当中,比如在程序执行调用方法的时候,在编译器内部,其实是记录了一个当前调用的方法栈。举个例子,比如当前调用到的方法是A,如果在A方法中又去调用了方法B,那么计算机就会在系统方法栈当中存储一个指向B方法的指针,如果B方法又调用到了C方法,那么又会新增一个C的指针。当C方法执行结束,那么C就会弹出,计算机会将C的结果带入B,继续执行之前的B,以此类推,直到栈空为止。

那么,问题来了,如果一个方法A自己调用自己会怎么样?

答案是计算机会创建一个新的A的指针填入栈中,如果A继续递归,那么系统再创建一个新的指针入栈……

从上面这个过程,我们可以确定两个事情。第一,我们写程序时候的递归,在编译器内部其实是以栈的形式执行的。第二,如果我们用一个死循环去不停地递归,由于栈存在大小限制,所以当栈的深度超过限制的时候,就会出现SystemStackExceed的错误。也就是说递归并不是无限的,因为除了操作系统对于运行内存的限制之外,编译器还会有最大递归深度的限制,防止递归中死循环导致系统崩溃。虽然各个语言实现机制不完全一样,但是有一点是肯定的,递归深度是有限的,我们不能无限制递归。

那问题来了,如果我们系统就是会存在大规模的递归怎么办?难道还要手动给机器加内存吗?

这是ACM玩家在赛场上经常遇到的问题之一,有经验的选手在第一天的热身赛时一定会做的事情除了配置vim或者其他IDE之外,就是会测试一下电脑的最大递归深度。在C++当中,是支持通过汇编语言强行打开递归深度限制的,但是即使如此也是有限的,并且据我所知只有C++可以这么干,对于其他语言,以及开大了递归深度还是不够用的情况,就只有一种办法,就是手动建栈模拟递归。


手动递归


许多同学可能觉得递归痛苦,但是如果他们试着手动建栈来模拟递归的话,会发现要更加痛苦。不仅要额外增加变量存储中间状态,并且对于编程也是一个巨大的挑战。

我们来看一个例子:

class Node:
    def __init__(self, val):
        self.val = val
        # 左孩子
        self.lchild = None
        # 右孩子
        self.rchild = None


if __name__ == "__main__":
    # 建树
    root = Node(0)
    node1 = Node(1)
    root.lchild = node1
    node2 = Node(2)
    root.rchild = node2
    node3 = Node(3)
    node1.lchild = node3
    node4 = Node(4)
    node1.rchild = node4
    node5 = Node(5)
    node2.rchild = node5

这是一棵简单的二叉树,画出来是这个样子:

          0
         / \
        1   2
       / \   \
      3   4   5

下面我们要通过栈在不使用递归的情况下来中序遍历它,中序遍历我们都知道,就是先遍历左子树,然后输出当前节点,再遍历右子树。写成递归非常方便,只有几行:

def dfs(node):
    if node is None:
        return
    dfs(node.lchild)
    print(node.val)
    dfs(node.rchild)

大家想想,如果不使用递归应该怎么办?如果你真的试着去写,就会发现看起来很简单的问题好像变得非常复杂。我们很容易可以想到,我们把节点存储在栈当中,但是存储数据只是表象。本质问题是当我们从栈当中拿到了一个节点之后,我们怎么判断它究竟应该做什么?应该遍历左节点吗,应该输出吗,还是应该遍历右节点?

对这些问题仔细分析和思考,我们可以发现它们都和递归的回溯有关。

在递归当中,当我们遍历完了当前节点的某棵子树之后,随着栈的弹出,还会回到这个节点。比如上面这棵树当中,在递归过程当中,我们会两次碰到1这个节点。第一次时它不会输出1,而是先去遍历了它的左子树,也就是3,之后再次回到1,由于它的左子树已经遍历过,所以会输出1。这个离开又回来的过程称为回溯。如果你把树结构想象成瀑布的话,这个过程有点像是顺流而下,又逆流而上,翻译成回溯还是蛮合理的。

我们回到之前的问题,所有的搞不清楚的本质都来源于我们无法判断当前遇到的节点究竟是初次见面,还是回溯之后的久别重逢。而这关系到我们要对它做什么。原本在递归当中,由于程序会记录递归时的状态和代码运行的位置,递归回溯之后会回到上次调用的位置,所以我们可以忽略这个问题。而现在我们由于不再使用递归,所以需要我们自己来判断节点的状态。

想通了其实很简单,我们只需要在节点当中加一个状态的字段,表示这个节点是否会发生回溯。显然在一开始的时候,所有的节点状态都是True。

class Node:
    def __init__(self, val):
        self.val = val
        self.lchild = None
        self.rchild = None
        self.flag = True

我们在Node类中加一个flag作为记录,初始化时我们默认它为True。接着就很简单了,我们就按照左中右的顺序遍历节点,只要左子树存在就往左边遍历,在一路往左的过程中遇到的这些节点的flag全部置为False,因为它们的回溯已经开始,以后不会再发生回溯了。由于往右遍历不会存在回溯的问题,所以可以忽略,想明白了,代码也就顺理成章。

# 使用我们自己刚刚创建的数据结构
stack = Stack()
# 插入根节点
stack.push(root)

while not stack.is_empty():
    # 获取栈顶元素,也就是当前遍历的节点
    tmp = stack.top()
    # 如果不曾回溯过,并且左子树存在
    while tmp.flag and tmp.lchild is not None:
        # 回溯标记置为False
        tmp.flag = False
        # 栈顶push左孩子
        stack.push(tmp.lchild)
        # 往左遍历
        tmp = tmp.lchild
    # 弹出栈顶
    tmp = stack.pop()
    # 此时说明左节点已经遍历完了,输出
    print(tmp.val)
    # 往右遍历
    if tmp.rchild is not None:
        stack.push(tmp.rchild)

这段代码虽然短,但其实不简单,想要完全看懂需要对递归和循环有深入的理解。属于典型的看着简单实际不容易的题,我个人比较喜欢这类问题,除了锻炼思维之外也很适合用来面试,候选人的思维能力、代码驾驭能力基本上都一清二楚了。没有看懂的同学也不用担心,因为在实际场景当中并不会遇到这样的场景,以后还会推出其他关于递归和搜索算法的文章,只要你坚持阅读,我相信一定会看懂的。

今天的文章就是这些,如果觉得有所收获,请顺手扫码点个关注吧,你们的举手之劳对我来说很重要。

模拟栈实现手写递归 刚刚计算机组成原理讲到汇编语言的递归,很受启发,所以分享一个用模拟栈实现递归的通用方法。 在模拟递归过程中之前,肯定先得把递归过程弄明白 比如对于下面的递归(伪代码): void dfs(int a0,int a1,...) { { 代码块0; } dfs(a00,a11,...); { 代码块1; } } 它的执行过程如下: ... 阅读详情

相关推荐

c++栈详解

在算法的优化中,有一种经常使用的技巧就是单调性(递增或递减)的运用。比如要保证栈的元素是单调递增的,当要进入的元素比栈顶元素小时,就要弹出栈顶元素或者丢掉这个元素不进入栈。从第1天开始用直线连接后面的某些天的股价,希望连线是递增的,并且所有股价不会超过这些直线。l push操作:向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;l pop操作:从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。例如有任务:增长折线。

浪子回头 3847

C++数据结构之栈

栈的应用场景非常广泛,包括代码中的解析、数据结构中的树和图的遍历以及计算机系统中的内存分配等等。本文会介绍栈。

现役 OIer。 1万+

CH0302 非递归实现组合型枚举 模拟

题目链接 https://www.acwing.com/problem/content/95/ 分析 递归实现组合型枚举是非常简单的,而此处的非递归版其实是手动模拟栈。 所以先来了解下“堆栈结构”实现函数调用的基本过程: 先把函数所需的第 kkk 个,第 k−1k - 1k−1 个,…,第 111 个参数依次入栈,然后将函数调用的下一条语句的地址入栈,再跳转到调用函数处继续执行指令。 AC代码 #...

Kevin的博客 595

【C++】栈~~(很详细哦)

【C++】栈~~(很详细哦)

weixin_71138261的博客 7974

YOLOv8环境搭建+训练自己的数据详细项目

直接按照路径会有问题,找到自己 requirements.txt 文件路径,我这里是pip install -r D:\ultralytics-main\ultralytics-main\requirements.txt。首先去自己的anaconda的安装的envs(虚拟环境),在导航栏输入cmd,进入命令窗口。到官方网站下载yolo模型 ,下载好后解压,里面有个文件requirements.txt。然后安装ultralytics ,这是必须的。

m0_63172128的博客 2万+

从机械到数字:EC11旋转编码器与ESP32的PWM控制艺术

本文详细解析了EC11旋转编码器与ESP32的PWM控制技术,探讨了如何将机械旋转动作转换为精确的数字信号控制。通过硬件连接、信号处理和软件优化等实践技巧,开发者可以构建高效的人机交互系统,适用于智能照明、电机控制等多种应用场景。

android的专栏 782

js 数组 实现 完全树_数据结构——30行代码实现栈和模拟递归

点击上方蓝字,和我一起学技术。原本今天想给大家讲讲快速选择算法的,但是发现一连写了好几篇排序相关了,所以临时改了题目,今天聊点数据结构,来看看经典并且简单的数据结构——栈。栈这个结构我想大家应该都耳熟能详,尤其是在很多地方将和堆并列在一起,称作“堆栈”就更广为人知了。但其实堆和栈本质上是两种不同的数据结构,我们不能简单地混为一谈。让我们先从比较简单的栈开始。栈和队列的本质其实都是数组(严...

weixin_39862985的博客 172

若依RuoYi文件上传漏洞实战:如何安全配置/profile/upload路径(附避坑指南)

本文深入解析若依RuoYi框架中/profile/upload路径的文件上传漏洞,提供详细的安全配置方案和实战指南。通过权限修正、输入验证强化和存储隔离等多层防御策略,有效防范未授权访问和恶意文件上传风险,保障系统安全。

pink的博客 777

递归循环一个无限极数组_数据结构——30行代码实现栈和模拟递归

点击上方蓝字,和我一起学技术。原本今天想给大家讲讲快速选择算法的,但是发现一连写了好几篇排序相关了,所以临时改了题目,今天聊点数据结构,来看看经典并且简单的数据结构——栈。栈这个结构我想大家应该都耳熟能详,尤其是在很多地方将和堆并列在一起,称作“堆栈”就更广为人知了。但其实堆和栈本质上是两种不同的数据结构,我们不能简单地混为一谈。让我们先从比较简单的栈开始。栈和队列的本质其实都是数组(严...

weixin_39740419的博客 107

专为AI打造的浏览器:内存占用仅为Chrome的1/9、比Chrome快11倍(Docker部署教程,支持飞牛nas等服务器部署)

🚀 专为AI打造的无头浏览器来了! 用Zig从零构建,内存仅Chrome的1/9,速度快11倍,原生CDP协议支持Docker部署,适合爬虫、自动化测试和AI代理应用。✨

XiaoqiangClub的博客 730

【WRF安装】WRF编译错误:problems building executables look for error in the build log

WRF编译错误:problems building executables look for error in the build log

WW、forever的博客 751

java树形结构递归实现_数据结构——30行代码实现栈和模拟递归

原本今天想给大家讲讲快速选择算法的,但是发现一连写了好几篇排序相关了,所以临时改了题目,今天聊点数据结构,来看看经典并且简单的数据结构——栈。栈这个结构我想大家应该都耳熟能详,尤其是在很多地方将和堆并列在一起,称作“堆栈”就更广为人知了。但其实堆和栈本质上是两种不同的数据结构,我们不能简单地混为一谈。让我们先从比较简单的栈开始。栈和队列的本质其实都是数组(严格地说是线性表)。只不过我们在数组上增加...

weixin_39816024的博客 136

模型预测控制 (MPC, Model Predictive Control) 算法详解及案例分析

模型预测控制是一种基于模型的控制策略,通过在线求解一个有限时域的优化问题,预测系统的未来行为,并根据预测结果调整控制输入,以实现系统的稳定性和性能优化。

闲人编程的博客 2934

c++调用栈库函数_数据结构——30行代码实现栈和模拟递归

原本今天想给大家讲讲快速选择算法的,但是发现一连写了好几篇排序相关了,所以临时改了题目,今天聊点数据结构,来看看经典并且简单的数据结构——栈。栈这个结构我想大家应该都耳熟能详,尤其是在很多地方将和堆并列在一起,称作“堆栈”就更广为人知了。但其实堆和栈本质上是两种不同的数据结构,我们不能简单地混为一谈。让我们先从比较简单的栈开始。栈和队列的本质其实都是数组(严格地说是线性表)。只不过我们在数组上增加...

weixin_39566387的博客 283

SpringCloud Alibaba - Seata 实现高可用和异地容灾

这里不仅仅要实现高可用,还需要实现异地容灾. 异地容灾就是说,如果你整了好几个节点部署起来,全部放到一个机房里面,那么如果这个机房出了故障,就意味着整个集群就完了,所以对于一个大型的集群来讲,都会把这些服务异地部署.问题2:如果我们要实现异地容灾,也就说某一个机房挂了,需要快速的切换到另一个可用的机房,但是这个配置在配置文件中写死了,如果要改配置文件,还需要重启服务器,这是我们不希望看到的,怎么办?进入 seata/bin 目录,运行 seata-server.bat 文件,默认端口为 8091。

CYK_byte的博客 975

【WRF安装】WRF环境配置移植详细说明

【WRF安装】WRF环境配置移植详细说明

WW、forever的博客 819

如何用递归处理一个数组中的数据成为一个树结构_数据结构——30行代码实现栈和模拟递归...

点击上方蓝字,和我一起学技术。原本今天想给大家讲讲快速选择算法的,但是发现一连写了好几篇排序相关了,所以临时改了题目,今天聊点数据结构,来看看经典并且简单的数据结构——栈。栈这个结构我想大家应该都耳熟能详,尤其是在很多地方将和堆并列在一起,称作“堆栈”就更广为人知了。但其实堆和栈本质上是两种不同的数据结构,我们不能简单地混为一谈。让我们先从比较简单的栈开始。栈和队列的本质其实都是数组(严...

weixin_39583623的博客 97

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

数据结构和算法学习笔记二_栈_递归(Java)

数据结构和算法学习笔记二_栈_递归 (学自尚硅谷数据结构与算法30-49节(java版)) 一、栈(stack) 1.栈简介 先入后出,FILO 插入和删除都是在一端操作,这一端就栈顶,相对的一端叫栈底 2、栈的应用场景 子程序的调用:在调用:在跳往子程序前,会先将下一个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。 处理递归调用:和子程序的调用类似,只是除了存储下一个指令的地址外,也将参数、区域变量、区域变量等数据存入堆栈中。 表达式的转换【中缀表达式转后缀表达式

Inmaturity_7的博客 456

STM32F407中文手册

STM32f407中文手册 本参考手册面向应用开发人员,提供有关使用 STM32F405xx/07xx、STM32F415xx/17xx、 STM32F42xxx 和 STM32F43xxx 微控制器存储器与外设的完整信息。STM32F405xx/07xx、STM32F415xx/17xx、STM32F42xxx 和 STM32F43xxx 构成一个微 控制器系列,各产品具有不同的存储器大小、封装和外设。有关订购信息以及器件的机械与电气特性,请参见数据手册。有关 ARM Cortex™-M4F 内核的信息,请参见《Cortex™-M4F 技术参考手册》。

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

【数据结构】栈和队列

今天我们要学的是栈和队列。栈和队列是两种重要的线性结构,让我们一起进入数据结构的世界去认识栈和队列,感受栈和队列的魅力。以上就是本次关于栈和队列的内容及用法,在学习这部分知识一定要多画图去方便我们理解,自己动手去编写代码,才能更加熟悉与掌握,小欣下次将与大家带来关于树和二叉树的知识,我们下次再见。

m0_74336101的博客 2047

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

数据结构 栈

摘要 栈是一种后进先出(LIFO)的线性数据结构,仅允许在栈顶进行插入(push)和删除(pop)操作。其核心特性包括:仅操作栈顶、不支持随机访问、默认基于deque实现。栈的提出源于1950年代鲍尔团队对逆波兰表达式计算和函数调用机制的研究,现广泛应用于程序调用栈、表达式求值、括号匹配等场景。C++中可通过STL的std::stack或手动数组实现栈操作,基本功能包括压栈、弹栈、判空等。例题B3758展示了栈在括号匹配中的典型应用——通过栈统计未匹配括号数量,动态补全成合法序列。栈的高效性使其成为算法和系

syshrgjdkdh的博客 2237

栈stack和队列

栈和队列一· 栈和队列二· 栈三.队列四.例题 一· 栈和队列 栈和队列是两种重要的线性结构。从数据结构来看,栈和队列也是线性表,其特殊性在于栈和队列的基本操作是线性表操作的子集(也具有顺序结构和链式结构),它们是操作受限的线性表,因此,可称为限定性的数据结构。 二· 栈 定义 :只能在表的一端(栈顶)进行插入和删除运算的线性表。 逻辑结构:与线性表相同,仍为一对一关系。 存储结构:用顺序栈或链栈存储均可,但以顺序栈更常见。 链栈: 运算规则:只能在栈顶运算,且访问结点时依照后进先出(LIFO)或先进后出

YWX 1万+

别再怕递归爆栈了!手把手教你用C语言栈数据结构重写快排(附完整代码)

本文详细介绍了如何用C语言栈数据结构重写快速排序算法,避免递归爆栈问题。通过手动实现栈结构和非递归快排方案,解决了系统栈空间限制的痛点,适用于处理大规模数据排序。文章包含完整代码示例和性能优化建议,帮助开发者掌握工业级排序算法实现。

weixin_29288313的博客 308

数据结构(栈Stack)

在本文中,我们详细探讨了如何实现一个简单的栈数据结构,并对栈的常见操作如pushpoppeek和size进行了深入的分析。通过这些方法,我们不仅了解了栈的基本功能,还能看到如何通过调整usedSize来有效管理栈的状态,保证其操作的高效性和安全性。栈作为一种基本的数据结构,具有广泛的应用场景,尤其在算法、数据解析、函数调用栈等方面扮演着重要角色。通过本文的学习,我们可以更好地理解栈的内在机制,并能够在实际编程中灵活运用栈。

eternal__day的博客 1328

必知必会的30个代码实现

必知必会的30个代码实现 < https://github.com/iostalks/Algorithms > 关于数组和链表的几个必知必会的代码实现 数组: 实现一个支持动态扩容的数组 实现一个大小固定的有序数组,支持动态增删改操作 实现两个有序数组合并为一个有序数组 链表: 实现单链表、循环链表、双向链表、支持增删操作 实现单链表反转 实现两个有序的链表合并为一个有序链表 实...

ywangjiyl的博客 298
上一篇: 机器学习——从线性回归到逻辑回归【附详细推导和代码】
下一篇: 高等数学——微分中值定理
TechFlow
博客等级 码龄7年 347粉丝 276原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值