美文网首页
关于函数递归和迭代的转化, 及尾递归相关知识的接触和思考

关于函数递归和迭代的转化, 及尾递归相关知识的接触和思考

作者: wudimingwo | 来源:发表于2019-01-10 22:20 被阅读0次

    javascript实现数据结构: 树和二叉树,二叉树的遍历和基本操作

    js 二叉树

    【数据结构与算法】深入浅出递归和迭代的通用转换思想

    经典算法|递归和递归消除的迭代法

    我总是怀疑, 我是不是能学好编程.
    我似乎总是会跑到某种奇怪的地方上去,
    消耗很多时间, 像是在浪费, 又像是有价值.

    网上说, 大多数的迭代,和递归是可以互相转化的.
    从形式上这两者是非常不同的.

    我们试着去寻找, 这两者都需要的概念,在这两者身上是如何体现的.
    当然我最初的目的是,
    希望能够很建议,起码思路很清晰的能够把我遇到的,
    迭代能够转换成递归,
    同理能够把递归转换成 迭代.

    我们先看最简单的循环

                while(true) {
                    console.log(1)
                }
    这是个死循环, 这里没有一个变量,
    但这里拥有三个概念,
    1. 循环
    2. 循环条件, 即 true
    3. 执行代码 , 即 console.log(1)
    
    改成 递归 则是
                function go () {
                    console.log(1);
                    go();
                }
    我相信这是个递归函数, 并且效果和上面的while循环相似.
    把上面对应的概念在这里寻找一下
    1. 循环, 函数自身调用体现了循环
    2. 循环条件, 似乎在这里没有体现, 那我们可以稍微改一下
    3. 执行代码, 即 console.log(1)
    
    为了体现 循环条件 我们改一下
            function go () {
                    console.log(1);
                    if (true) {
                        go();
                    }
                }
    
    

    上面这两个代码, 语法上都没毛病, 概念也都没毛病.
    那么第二个问题, 上面的代码, 明显是死循环,
    我们大多时候希望的不是死循环.
    一个代码想要避开死循环, 需要什么?
    需要两个概念, 一个是开启循环, 一个是 关闭循环.
    而想要实现这个事情, 就必须要从循环条件入手.
    但这个条件本身要有两个状态,
    这很明显啊, 如果只有一个状态, 要么无法开启, 要么就一直循环.
    换言之, 条件必须是一个变量.

                var flag = true;
                
                    while(flag) {
                        console.log(1);
                    }
                
                
                function go () {
                    console.log(2);
                    if (flag) {
                        go();
                    }
                }
    
    

    理论上来讲, 这个flag, 可以在循环体外面更改,
    也可以在循环体内部更改.

    在外部

                var flag = true;
                
                    while(flag) {
                        console.log(1);
                    }
                
                
                function go () {
                    console.log(2);
                    if (flag) {
                        go();
                    }
                }
    flag = false;
    
    但可能会发现不会像预期那样停止循环.
    电脑会因为死循环 导致卡机, 无法接收执行 flag = false 的语句
    
    

    在内部

                var flag = true;
                
                    while(flag) {
                        console.log(1);
                                    flag = false;
                    }
                
                
                function go () {
                    console.log(2);
                            flag = false;
                    if (flag) {
                        go();
                    }
                }
    

    一般情况下(至少作为一个小白我遇到的大多数情况), 我们会设置成在内部控制循环条件.
    不过像上面这种, 刚开启就结束的循环, 似乎很难称得上循环.

    你可能会想, 这逼是有毛病吧, 净扯些没用的循环, 或者算不上循环.
    那我们开始说一些我们常见的循环.

    遍历输出一个数组

    var arr = [1,2,3];
    
    for(var i = 0; i < arr.length, i++){
      console.log(arr[i])
    }
    首先我们要理解, 所有的for 循环, 都可以改成 while循环
    var i = 0;
    while(i < arr.length){
      console.log(arr[i]);
      i++;
    }
    如果我们改成上面那种flag 方式, 也可以
    var flag = true;
    var i = 0;
    while(flag) {
      console.log(arr[i]);
      i++;
      if(i >= arr.length) {flag = false}
    }
    反正先有一个信心是, 所有的for循环,都可以转成 while + flag 这种形式
    换句话来讲, 作为循环的条件变量, 可以不是flag, 任何其他的变量都有可能.
    
    
    试着改成递归,然后发现, 有了变量,参数之后, 有两种基本选择
    第一种, 变量和参数放在全局当中
    var arr = [1,2,3];
    var i = 0;
                function diedai () {
                    console.log(arr[i])
                    i++;
                    if (i < arr.length) {
                        diedai();
                    }
                }
    
    第二种, 每次调用,都传递进去
                function diedai (arr,i) {
                    var arr = arr || [];
                    var i = i || 0;
                    console.log(arr[i])
                    i++;
                    if (i < arr.length) {
                        diedai(arr,i);
                    }
                }
    diedai(arr);// 我们要把数据传进去.
    
    

    其实这种情况, 我本人最快联想到的是arr.forEach() 方法
    我们模拟一下forEach

    var arr = [1,2,3];
    function cb (item) { console.log(item)};
                function forEach (arr,cb) {
                    var len = arr.length;
                    for(var i = 0; i < len ; i++) {
                        cb(arr[i])
                    }
                }
    forEach(arr,cb)
    
    我们试着改成 递归,
    根据上面的说法, 有两种方案
    
    第一种, 变量放在全局
    
                function forEach (arr,cb) {
                    var arr = arr;
                    var cb = cb;
                    var len = arr.length;
                    var i = 0;
                            / 实际上, 真正完成递归的是函数go() 只是所有的变量,都放在了父级上
                    function go () {
                        cb(arr[i]);
                        i++;
                        if (i < len) {
                            go();
                        }
                    }
                    go()
                }
    
    第二种, 变量传递
    
                function forEach (arr,cb,i) {
                    var i = i || 0;// 除了初始值, 其他都是传进来的值
                    cb(arr[i++]);
                    if (i < arr.length) {
                        forEach(arr,cb,i);
                    }
                }
    
    从某种角度来讲, 无论是递归,还是迭代,
    除了要解决算法问题之外, 其实要考虑的是, 如何存值的问题.
    递归方式之所以看似有两种, 是因为,
    1. 函数内部可以访问函数外部的变量,并改变函数外部的变量.
    2. 函数内部可以覆盖函数外部的变量.
    

    虽然上面的递归是递归, 因为确实是函数自身调用,
    但似乎感觉和我们常见的递归相比差点什么,
    主要是, 我们的递归虽然能够完成循环,
    但每个函数似乎都没有返回值,
    或者说, 没有利用返回值

    我们模拟下reduce 来感受一下

    迭代版本
              var arr = [1,2,3];  
              
              function add (a,b) {
                return a + b;
              }
              
              function reduce (arr,cb,init) {
                if (typeof init == "undefined") {
                    var i = 1;
                    var init = arr[0];
                }else{
                    var i = 0;
                    var init = init;
                }
                for(; i < arr.length; i++) {
                    init = cb(init,arr[i],i,arr)
                }
                return init
              }
              
              reduce(arr,add)/ 6
    
    递归版本1, 参数都放在 全局上
              function reduce (arr,cb,init) {
                if (typeof init == "undefined") {
                    var i = 1;
                    var init = arr[0];
                }else{
                    var i = 0;
                    var init = init;
                }
                
                function go () {
                    init = cb(init,arr[i],i,arr);
                    i++;
                    if (i < arr.length) {
                        go();
                    }
                }
                go()
                return init;
                
              }
              reduce(arr,add)/ 6
    
    递归版本2
              function reduce (arr,cb,init,i) {
                if (typeof init == "undefined") {
                    var i = i || 1;
                    init = arr[0];
                }else{
                    var i = i || 0;
                    init = init;
                }
                
                
                if (i < arr.length) {
                    init = cb(init,arr[i],i,arr);
                    i++;
                    init = reduce(arr,cb,init,i)
                    return init;
                }else {
                    return init
                }
              }
    
    稍微简化一下,尽管根本没有简化的效果
              
              function reduce (arr,cb,init,i) {
                if (typeof init == "undefined") {
                    var i = i || 1;
                    init = arr[0];
                }else{
                    var i = i || 0;
                    init = init;
                }
                
                if (i < arr.length) {
                    return reduce(arr,cb,cb(init,arr[i],i,arr),i + 1)
                }else {
                    return init
                }
              }
    
    
    

    在把迭代转换成 递归的时候, 我的思路究竟都发生了些什么?
    实际上,但从上面两个例子当中,感觉 递归版本1 跟 迭代基本没什么区别.
    主要就是递归版本2,区别比较明显.
    这要怎么描述呢?
    版本1 当中, 函数的感觉就是一段代码的执行,
    版本2 当中, 函数的感觉其实代表的是一个值的,
    我觉得例子举得不太恰当.

    还是累加问题

    迭代版本
              function add (n) {
                var sum = 0;
                for(var i = 0; i <= n ; i++) {
                    sum = sum + i;
                }
                return sum;
              }  
    
    递归版本          
              function add (n) {
                if (n == 0) {
                    return 0
                }
    //          add(n) = n + add(n - 1);
                 return (n + add(n - 1));
              }  
    
    

    感觉这么比较似乎还是不太好,
    因为从例子上来看, 迭代版本采取的是从0递增
    而递归版本是从n 递减,
    我们加一下

    迭代版
              function add (n) {
                var sum = 0;
                for(;n--;){
                    sum = sum + n
                }
              }
    递归版
              function add (n,i) {
                var i = i || 0;
                if (i == n) {
                    return n
                }
    //          add(n,i) = add(n,i + 1) + i
                return add(n,i + 1) + i
              }  
    
    哇塞, 我自己都被自己秀到了,有没有?
    

    当然如果我们套用之前递归版本还可以这样

    套用之前的递归版本1
              function add (n) {
                var sum = 0;
                var i = 0;
                function go () {
                    sum = sum + i;
                    i++;
                    if (i <= n) {
                        go()
                    }
                }
                    go();
                return sum
              }
    套用之前的递归版本2
              function add (n,sum,i) {
                var sum = sum || 0;
                var i = i || 0;
                    sum = sum + i;
                    i++;
                    if (i <= n) {
                        sum = add(n,sum,i)
                    }
                return sum
              }
    
    哈哈哈哈哈哈哈哈哈
    有没有觉得很搞笑?
    

    我感觉,自己整理不下去了, 感觉好难整理思路.每次都是这样.
    首先, 比较这个套用了递归版本2 和最上面的递归版本,
    能感觉两者的差异很大.
    从形式上来讲, 递归原版比这个版本2 要简洁非常多.

    为什么会这样呢?
    因为原版的思路是从关系入手的.
    即,n之累加和 等于 n + (n-1)之累加.
    重点体现的是这一关系.
    而这种思路, 应该算是数学上的归纳法思路的实现?

    而递归版本2 的思路其实是 迭代版本的 思路体现,
    或者概念体现.
    即, 迭代版本中, 我们需要的变量, 我们在递归时, 想要全部用上,
    所以才会造成这种代码.

    客观来讲, 递归版本2 是可以简化到 递归原版的.
    我们来试着简化一下

    从这里
              function add7 (n,sum,i) {
                var sum = sum || 0;
                var i = i || 0;
                    sum = sum + i;
                    i++;
                    if (i <= n) {
                        sum = add7(n,sum,i)
                    }
                return sum
              }
    到这里
              function add8 (n,i) {
                var i = i || 0;
                if (i < n) {
                    i = add8(n,i + 1) + i
                }
                return i
              }
    
    天哪, 你看到了没?
    我简直不敢想象.
    这肯定是一种简化, 一种合理的简化,
    根据等号进行的简化.
    我之所以不敢想象是因为, 对于递归结构的函数, 
    我压根生不起想要简化的念头, 因为很容易让我脑瓜疼
    
    这里的return 也可以看成是 = 
    因为与sum 有关的 = 都可以进行整理
    首先是
    sum = sum + i;和
    sum = add7(n,sum,i)
    
     可以直接合成 
    sum = add7(n,sum,i) + i;
    
    但问题是, 中间有个 i++, 即, 两个i 的值是不同的, 所以
    sum = sum + i;
    i++;
    sum = add7(n,sum,i)
    
    这三句可以合成为
    sum = add7(n,sum,i + 1) + i;
    
    此时的函数是这样的
    
              function add7 (n,sum,i) {
                var sum = sum || 0;
                var i = i || 0;
                if (i <= n) {
                    sum = add7(n,sum,i + 1) + i
                }
                return sum
              }
    从形式上来讲, i 完全可以代替 sum
    所以就变成了
              function add8 (n,i) {
                var i = i || 0;
                if (i < n) {
                    i = add8(n,i + 1) + i
                }
                return i
              }
    不过有个细节是, i <= n 要改成 i < n
    
    我试着简化成这样
              function add8 (n,i) {
                var i = i || 0;
                if (i < n) {
                    return add8(n,i + 1) + i
                }
              }
    结果不行, 
    因为当 i >= n 时 返回的是 undifined
    必然导致所有结果要么是 undifined, 要么是 NaN
    应该改成这样
              function add8 (n,i) {
                var i = i || 0;
                if (i < n) {
                    return add8(n,i + 1) + i
                }else{
                            return n
                    }
              }
    
    然后对if 条件进行 转换就变成
              function add8 (n,i) {
                var i = i || 0;
                if (i >= n) {
                    return n
                }else{
                            return add8(n,i + 1) + i
                    }
              }
    然后变成
              function add8 (n,i) {
                var i = i || 0;
                if (i >= n) {
                    return n
                }
                            return add8(n,i + 1) + i
              }
    然后就回到上面的那个递归了
    
    可是从这里怎么变成下面这样的?
               function add (n) {
                if (n == 0) {
                    return 0
                }
                 return (n + add(n - 1));
              } 
    
    这满满的都是很难掌握的东西, 我们就试着体验一下,
    要分析 i 和 n 的关系, 从明面上来讲, 
    i  和 n 似乎没有关系
    不存在 i = f(n) ,, 两者没有直接的运算关系.
    
    唯一明显的关系就是, i >=n return n
    即, add(n,n) = n
     i 的取值范围是 0 ~ n, 
    存在三个量,
    n,i,add()
    已知条件
    add(n,n) = n
    i = 0;
    核心关系
    add(n,i) = add(n,i + 1) + i
    把i 用n 替换一下,
    这里的替换方式很明显应该是数学上的运算
    我确实不甚了解
    或者真的存在这种运算关系吗?
    
    当 i = n 时,
    add(n,n) = add(n,n - 1) + n
    
    稍微整理一下就是
              function add (n,n) {
                if (n == 0) {/此处 i = 0 的初始值变成了 n 的一个边界, 又或者是一个出口
                    return 0
                }
               return add(n,n - 1) + n
              }
    可以看出, (也不怎么好看出),
    第一个参数位置的 n 似乎没有也不影响运算
    所以最终可以整理出
              function add (n) {
                if (n == 0) {
                    return 0
                }
                return add(n - 1) + n
              }
    
    哇塞, 鼓掌.
    虽然牵强的部分比较多,
    而且可能在专科眼里, 这种程度的数学运算是小儿科.
    但我还是给自己鼓一下掌,鼓励一下自己.
    
    

    从上面,其实我们能够得到很多信息.

    1. 从大的来讲,
      实现同一个功能, 即使用递归实现,
      形态可能也是多样的.
    2. 其次, 递归的形态 可以通过 = , return, 的关系
      以及变量和变量的关系的整理,
      进行转换.
    3. 对比一下累加,迭代方式的最简洁形态和递归的最简洁形态,
      可以看出,一般 迭代方式所需要的变量数量, 要比递归形态的数量要多.
      比如 按照上面的例子来讲 迭代方式, 似乎必须存在 一个变量sum
      sum = sum + n
      等式左右两侧的sum 值 明显是不同的,
      左边的sum 代表接收返回值, 右边的sum 表示运算中的参数
      即, 用 一个sum 完成了前一次和后一次的值传递?
      严格来讲应该是这样的
    var sum1 = 0;/ 作为参数的前一个sum
    var sum2 = 0;/ 作为接收值的sum
    for(;n--;){
      sum1 = sum2;/
      sum2 = sum1 + n;/ 
    }
    很明显, 可以把 sum1 和 sum2 合并成一个. 根据等式替换
    

    而在 递归当中, 没有这个sum , 那么这个sum 的概念是如何体现的? 或者由谁来承载的?
    答案是 add(n) 和 add(n - 1)
    在递归中, add(n) 和add(n - 1), 又或者 add (n - 2)
    并非只是代表函数的执行, 而是代表了一个值, 或者概念.
    在这道题中 add(n) 就相当于 sum1 , add(n - 1) 代表 sum2
    即, 在递归中, 函数的执行状态, 可以当做一个变量.
    不同的参数的执行状态, 即可代表不同变量, 或者 这个变量的不同值?

    我们再说一次 add(1) 是一个变量, 这个变量即不是 add 也不是 1, 但就是个变量.
    就好像
    a 是一个变量, b 是一个变量,
    a + b 相当于是一个新的变量
    我们可以理解为 var add(a,b) = a + b 即, 我们用 add(a,b) 来代表 a + b 这个变量.

    把一个简单至极的道理, 讲得这么让人不想看第二遍, 我也是服了自己了.

    但我们稍微注意一下就可以发现,
    在迭代方式中, sum = sum + 1 时, 我们始终只是占用了一个sum变量
    一旦前一个值完成使命, 就会被后一个值覆盖,内存,也就是从空间上来讲,
    很节省.
    但反观 递归, add(n),add(n - 1),add(n - 2),add(n - 3) 这些相当于都是不同的变量,
    在得到执行完成之前, 都会占用空间.

    换个正常点的说法就是,
    观察上面的递归可以发现, add(n) 只有在 add(n - 1) 返回确定值的时候, 才能执行完成这个函数.
    依次递推就是, 直到 add(0) 之前, 所有的函数会嵌套方式处于执行状态,无法完成
    我这个概念学得不是很扎实, 网上称之为压栈.
    即, 每执行一个函数,会开启一个栈, 这个栈里存着该函数里的变量, 当执行完的时候,
    这个栈可能会被释放,
    即,开栈会占用内存, 函数执行完会释放栈, 也就是释放内存,释放空间.
    压栈就是, 这个函数还没执行完, 在里面有执行了一个函数, 且只有里面的函数执行完的时候,
    外面的函数才能执行完,
    这句话的意思很明显, 会存在同时开两个栈,里面的栈关闭外面的才能关闭.
    想想, 多少次递归, 就会存在同时开多少个栈的情况, 太多栈,占用太多内存,
    必然就有可能出现电脑吃不消的情形 .

    这也就是为什么, 网上大多数人说, 递归虽然比迭代简单,
    但能用迭代替换递归就尽量替换递归的一个原因.
    另一个原因是说, 迭代比递归执行效率更高.
    可能也是作用域链的问题, 也就是压栈压多了,运行起来是不是会慢一点?
    关于这个不是很清楚, 我想知道的也不是这个.

    如果你跟我一样是个新手,
    你应该会和我有同样的一个疑问.
    即,
    人说递归比迭代简单, 或者简洁,我怎么时常没这个感觉?

    比如上面那个forEach,reduce 的 递归版本, 我就感觉非常的不舒服, 不好阅读, 难以理解.
    答案是, 我写得烂呗.
    我们不是得出一个结论, 同一个功能递归函数, 可以很复杂, 也可能很简洁嘛.
    所以第一个可能的答案是,我写得烂.

    但这就相当于是一句废话,我们想要的是递归的优势,
    你跟我讲, 你代码写得烂,体会不出, 这不是废话嘛.

    我猜测是这样的,
    有时候递归的方式, 也就是函数的方式,
    能够更好的表达或体现一些概念?
    特别是能够体现归纳法的概念?
    所以只要我们把一个问题,变成一个从归纳的角度分析的问题,
    在用递归翻译, 就会变得很简洁?

    比如累加法,中 累加1,100之间的数字.
    变成归纳的概念就是, 100 + 1,99的累加

    所以,
    我们上面完成的例子, 基本都是把迭代 改成 递归,
    这个过程当中, 我可能是走了一个比较难的路子,
    即, 我想把迭代时,遇到的概念(变量), 原封不动的去改成递归.
    可能存在另一种方式,
    即, 我可以从整体角度,看这到底干了什么? 想要解决什么问题?
    然后从概念上, 变成一个归纳的问题? 然后再翻译成 递归?
    当然我只是说说,,呵,,呵


    上面都是把迭代改成递归
    在我们接触把递归改成 迭代之前,
    我们再做几个例子,
    如果能够发现一些转换的通式自然是最好,

    下面是一个用双重循环写的排序

                var arr = [1,5,96,3,88,5,8,5,9];
                
                // 排序, 双重循环
                
                function sort (arr) {
                    var len = arr.length;
                    for(var i = 0; i < len - 1; i++) {
                        for(var j = i + 1; j < len; j++) {
                            var temp;
                            if (arr[i] <= arr[j]) {
                                // 交换数据
                                temp = arr[j];
                                arr[j] = arr[i];
                                arr[i] = temp;
                            }
                        }
                    }
                }
                sort(arr);
    

    我们试着改成递归,
    我这都还没开始想, 就有点头疼了.

    先改成 while
                function sort (arr) {
                    var len = arr.length;
                    // 首先改成 递减
                    while (len--){
                        var j = len
                        while (j--){
                            var temp;
                            if (arr[len] >= arr[j]) {
                                // 交换数据
                                temp = arr[j];
                                arr[j] = arr[len];
                                arr[len] = temp;
                            }
                        }
                    }
                    
                }
    
    再改成, 跟循环没区别的递归版本
                function sort (arr) {
                    var len = arr.length;
                    // 首先改成 递减
                    while (len--){
                        var j = len
                        while (j--){
                            var temp;
                            if (arr[len] >= arr[j]) {
                                // 交换数据
                                temp = arr[j];
                                arr[j] = arr[len];
                                arr[len] = temp;
                            }
                        }
                    }
                    
                }
    
    再把最外层, 给干掉, 所有参数尽量全传进去
    
                    function go1 (arr,len) {
                        var len = len || arr.length;
                        len--;
                            function go2 (len,j) {
                                    j--;
                                    var temp;
                                    if (arr[len] >= arr[j]) {
                                        // 交换数据
                                        temp = arr[j];
                                        arr[j] = arr[len];
                                        arr[len] = temp;
                                    }
                                if (j > 0) {
                                    go2(len,j);
                                }
                            }
                            go2(len,len);
                        if (len > 0) {
                            go1(arr,len);
                        }
                    }
                    go1(arr);
    
    然后考虑 变量之间的关系 以及等式替换
    搞不下去了.
    勉强能够做到把迭代改成递归,
    只是目前来讲有两个限定条件,
    一个是,  该题中的函数不涉及 返回值
    另一个是, 需要先改成 while循环, 以捋清思路.
    

    关于迭代改成递归,就先到这里,
    反正日后遇到一些迭代, 我们可以当做一些待做的题, 放到这里来, 试着慢慢改一改.

    明天, 我们就学习一下, 怎样把递归改成迭代,
    你会发现, 网上基本都是教你怎样把 递归改迭代.
    明天再说.

    相关文章

      网友评论

          本文标题:关于函数递归和迭代的转化, 及尾递归相关知识的接触和思考

          本文链接:https://www.haomeiwen.com/subject/ahmnrqtx.html