• 递归转非递归


    递归转非递归

    在这个文章中,将模拟递归在系统中运行的过程,一来帮助大家对递归理解更深入一些,二来在关键时候,可以规避递归栈的限制。

    上次文章说过,递归就是多重函数调用,函数将自己的运行状态保存起来,然后转而去调用其他函数去了,等到函数返回继续在原地执行。

    假设说你要修电脑,先要用螺丝刀把机箱给打开,然后。。。
    等等,螺丝刀呢?没有螺丝刀咋办,买呗,于是你就把电脑丢下,跑出去买了一个螺丝刀
    买回来之后,继续把机箱打开,发现内存条坏了,于是你就把电脑丢下,跑出去买了一个内存条(这里有个细节,机箱你是打开的,有两种做法,一种是打开着的机箱丢在地上,等买完内存条再回来继续,一种是把打开着的机箱装回去,等买完内存条再回来重新拆)

    我们来看一个真实的递归例子,后序优先遍历(leetcode-145)

    1
    2
    3
    4
    5
    6
    7
    8
    void dfs(TreeNode* root) {
    if (root == NULL) {
    return;
    }
    dfs(root->left);
    dfs(root->right);
    visit(root); //前中后序的代码就区别于visit(root)的位置,放在两个dfs前面就是前序,放在两个dfs之间就是中序,本质上没什么区别
    }

    我们从代码层面上来解释上图的代码执行过程:

    • 执行节点1,需要先执行左节点,挂起,节点2进入待执行栈
    • 执行节点2,需要先执行左节点,挂起,节点2的左节点进入待执行栈
    • 栈顶元素是个null,弹出
    • 继续执行节点2,先前执行到左节点那个地方,继续,需要先执行右节点,挂起,节点2的右节点进入待执行栈
    • 栈顶元素是个null,弹出
    • 继续执行节点2,先前执行到右节点那个地方,继续,访问节点2,结束,弹出
    • 继续执行节点1,先前执行到左节点那个地方,继续,需要先执行右节点,挂起,节点3进入待执行栈
    • 执行节点3,需要先执行左节点,挂起,节点3的左节点进入待执行栈
    • 栈顶元素是个null,弹出
    • 执行执行节点3,先前执行到左节点那个地方,继续,需要先执行右节点,挂起,节点3的右节点进入待执行栈
    • 栈顶元素是个null,弹出
    • 继续执行节点3,先前执行到右节点那个地方,继续,访问节点3,结束,弹出
    • 继续执行节点1,先前执行到右节点那个地方,继续,访问节点1,结束,弹出
    • 栈为空,执行完毕

    理解完这个递归的过程后,我们的非递归代码就呼之欲出了

    • 定义一个数据类型,用来保存递归过程中的上下文,比如,各种临时变量,在递归的过程中,这些变量语言层面上帮我们解决了上下文保留,非递归版本我们需要自己保存
    • 分解递归函数代码,将需要递归调用函数的部分分开,变成步骤1,步骤2,。。。例如,上面的后续优先遍历,就分成3个步骤,1是从开头到递归调用左节点之前,2是调用左节点后,调用右节点之前,3是调用右节点之后。

    于是非递归版本的代码就是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    struct Context {
    TreeNode* root;
    int step;
    Process(TreeNode* _root) {
    root = _root;
    step = 0;
    }
    };

    void artidfs(TreeNode* root) {
    vector<Context> stack;
    stack.push_back(Context(root));
    while (!stack.empty()) {
    int last = stack.size() - 1;
    if (stack[last].root == NULL) {
    //对标dfs函数中第一行,root为空,直接返回
    stack.pop_back();
    } else if (stack[last].step == 0) {
    //处于第一个阶段中
    stack[last].step++;
    stack.push_back(Context(stack[last].root->left));
    } else if (stack[last].step == 1) {
    //处于第二个阶段
    stack[last].step++;
    stack.push_back(Context(stack[last].root->right));
    } else {
    //处于第三个阶段
    visit(Context(stack[last].root));
    stack.pop_back();
    }
    }
    }

    这个代码基本上是通用的,大家可以尝试着改写其他递归代码,可以加深对递归的理解。

    大家理解完这个代码后,再思考下为啥语言中有递归栈层数和递归中内存使用的限制。

     
    如有错误,恳求读者指出,发送到wu13213786609@outlook.com。
  • 相关阅读:
    create-react-app搭建的项目中添加bootstrap
    用es6的Array.reduce()方法计算一个字符串中每个字符出现的次数
    为Docker配置阿里加速器,系统为Debian8
    基于Spring Boot,使用JPA动态调用Sql查询数据
    基于Spring Boot,使用JPA调用Sql Server数据库的存储过程并返回记录集合
    基于Spring Boot,使用JPA操作Sql Server数据库完成CRUD
    ES6,Array.includes()函数的用法
    【编程风格】c++命名约定
    【转】吴恩达的视频课程做成了文字版 ~~~
    【专业学习】常用的技术网站
  • 原文地址:https://www.cnblogs.com/WLCYSYS/p/15262529.html
Copyright © 2020-2023  润新知