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

本文始发于个人公众号: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)

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

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

原文地址:https://www.cnblogs.com/techflow/p/12334261.html

时间: 2024-10-09 22:42:51

数据结构——30行代码实现栈和模拟递归的相关文章

30 行代码绘出你的微信朋友统计图

前言 大家好,这里是「brucepk」爬虫 系列教程.此文首发于「brucepk」公众号,欢迎大家关注.此系列教程以实例项目为材料进行分析,从项目中学习 python 爬虫,跟着我一起学习,每天进步一点点. 学编程是一件枯燥的事情,比较好的方法是在实际项目中学习成长.今天带来的是 30 行代码画出你的微信朋友的性别统计图. 最近发现一个有意思的库:itchat,itchat 是一个开源的微信个人号接口.今天就用 itchat 来统计自己微信好友性别的比例并用柱形图展示出来. 项目环境:pytho

Tensorflow快餐教程(1) - 30行代码搞定手写识别

去年买了几本讲tensorflow的书,结果今年看的时候发现有些样例代码所用的API已经过时了.看来自己维护一个保持更新的Tensorflow的教程还是有意义的.这是写这一系列的初心. 快餐教程系列希望能够尽可能降低门槛,少讲,讲透. 为了让大家在一开始就看到一个美好的场景,而不是停留在漫长的基础知识积累上,参考网上的一些教程,我们直接一开始就直接展示用tensorflow实现MNIST手写识别的例子.然后基础知识我们再慢慢讲. Tensorflow安装速成教程 由于Python是跨平台的语言,

用30行代码开发一个上传、下载文件的接口

分享一段代码,开发了3个接口: 1.上传文件 2.查看所有文件 3.下载文件 使用python开发,需要安装flask模块,使用pip install flask安装即可,代码量特别少,就能实现,加上注释总共35行代码,特别的简单,代码如下: import flask,os,time from flask import request,send_from_directory,jsonify app = flask.Flask(__name__)#创建一个app,代表这个web服务 @app.ro

30行代码构建javascript 的MVC模式

MVC模式是软件工程中一种软件架构模式,一般把软件模式分为三部分,模型(Model)+视图(View)+控制器(Controller); 很多讲解MVC的例子都从一个具体的框架的某个概念入手,比如Backbone的collection或AngularJS中model,这当然不失为一个好办法.但框架之所以是框架,而不是类库(jQuery)或者工具集(Underscore),就是因为它们的背后有着众多优秀的设计理念和最佳实践,这些设计精髓相辅相成,环环相扣,缺一不可,要想在短时间内透过复杂的框架而看

数据结构--书上代码用栈求解迷宫问题存在BUG(非最优解)

数据结构第四版p79页迷宫问题我觉得存在BUG,下图盗用贺老师就会的QAQ,也希望贺老师能看到帮忙解答一下啦. BUG:  程序从起始点(1,1)开始寻找路径,在当前点进行判断其上下左右是否存在可走点,如果从(1,1)点开始判断如图那么它的右(1,2)下(2,1)都是可走点那么将右边的格子坐标进栈呢还是将下边的格子坐标进栈?书本上给的代码是先判断上边格子再判断右边格子再判断下边格子再判断左边格子,这就造成了一个问题:(1,2)则个点会被进栈(因为(1,2)点位于(1,1)点的右边被先判断进栈),

30 行代码实现 JS 中的 MVC

一连串的名字走马观花式的出现和更迭,它们中一些已经渐渐淡出了大家的视野,一些还在迅速茁壮成长,一些则已经在特定的生态环境中独当一面舍我其谁.但不论如何,MVC已经并将持续深刻地影响前端工程师们的思维方式和工作方法. 很多讲解MVC的例子都从一个具体的框架的某个概念入手,比如Backbone的collection或AngularJS中model,这当然不失为一个好办法.但框架之所以是框架,而不是类库(jQuery)或者工具集(Underscore),就是因为它们的背后有着众多优秀的设计理念和最佳实

30行代码实现JavaScript中的MVC

从09年左右开始,MVC逐渐在前端领域大放异彩,并终于在刚刚过去的2015年随着React Native的推出而迎来大爆发:AngularJS.EmberJS.Backbone.ReactJS.RiotJS.VueJS…… 一连串的名字走马观花式的出现和更迭,它们中一些已经渐渐淡出了大家的视野,一些还在迅速茁壮成长,一些则已经在特定的生态环境中独当一面舍我其谁.但不论如何,MVC已经并将持续深刻地影响前端工程师们的思维方式和工作方法. 很多讲解MVC的例子都从一个具体的框架的某个概念入手,比如B

PowerShell:30行代码轻松实现SQL Server数据库容量监控

本文介绍如何用PowerShell脚本实现SQL Server数据库容量监控 闲话就不多说,直入主题 一.建表 为每台服务器创建一个表,用于记录服务器各个数据库的容量,以服务器名作为表名. CREATE TABLE table_name( [LOG_DATE] [varchar](20) NULL, [DB_NAME] [varchar](50) NULL, [TOTAL_SIZE_MB] [numeric](15, 2) NULL, [USE_SIZE_MB] [numeric](15, 2)

108、【特效】30行代码,打造一个垂直+横向的 双向 ViewPager (转载)

http://www.jianshu.com/p/b9a9717e98e8 https://github.com/kaelaela/VerticalViewPager