zoukankan      html  css  js  c++  java
  • 递归学习

    // 查询数组最大值 $r为count($arr)-1
    function findMax($arr,$r,$l = 0) {
        if($l == $r) {
            return $arr[$l];
        } else {
            $a = $arr[$l];
            $b = findMax($arr,$r,$l+1);
            if($a > $b) {
                return $a;
            } else {
                return $b;
            }
        }
    }

    // 数组从小到大排序 $r为count($arr)-1
    function sortArr(&$arr,$r,$l = 0) {
        if($r == $l) {
    
        } else {
            for($i = $l+1;$i <= $r;$i++) {
                $a = $arr[$i];
                $b = $arr[$l];
                if ($a < $b) {
                    $arr[$l] = $a;
                    $arr[$i] = $b;
                }
            }
            sortArr($arr,$r,$l+1);
        }
    }
    // 费波纳切数列 1 1 2 3 5 8 13
    function getFB($n) {
        if($n < 3) {
            return 1;
        } else {
            return getFB($n-1) + getFB($n-2);
        }
    }
    /* 汉诺塔
    *有三根柱子,原始装满大小不一的盘子的柱子我们称为A,还有两根空的柱子,我们分别称为B和C(任选)最终的目的就是将A柱子的盘子全部移到C柱子中移动的时候有个规则:一次只能移动一个盘子,小的盘子不能在大的盘子上面(反过来:大的盘子不能在小的盘子上面)
    */
    function getHNT($n,$start='A',$transfer='B',$target='C') {
        if($n == 1) {
            echo $start . '-->' . $target , '<br />';
        } else {
            getHNT($n-1,$start,$target,$transfer);
            echo $start . '-->' . $target . '<br />';
            getHNT($n-1,$transfer,$start,$target);
        }
    }
    每次执行完s指令,都会有一层递归调用终止,直到返回main函数。事实上,如果在递归调用初期查看调用栈,则会发现每次递归调用都会多一个栈帧——和普通的函数调用并没有什么不同。确实如此。由于使用了调用栈,C语言自然支持了递归。在C语言的函数中,调用自己和调用其它函数并没有什么本质区别,都是建立新栈帧,传递参数并修改当前代码行。在函数体执行完毕后删除栈帧,处理返回值并修改当前代码行。
    下面举个例子加强理解。
    皇帝(拥有main函数的栈帧):大臣,你给我算一下f(3)。
    大臣(拥有f(3)的栈帧):知府,你给我算一下f(2)。
    知府(拥有f(2)的栈帧):县令,你给我算一下f(1)。
    县令(拥有f(1)的栈帧):师爷,你给我算一下f(0)。
    师爷(拥有f(0)的栈帧):回老爷,f(0)=1。
    县令(心算f(1)=f(0)*1=1):回知府大人,f(1)=1。
    知府(心算f(2)=f(1)*2=2):回大人,f(2)=2。
    大臣(心算f(3)=f(2)*3=6):回皇上,f(3)=6。
    皇帝满意了。
    通过这个例子可以说明一些问题。递归调用时新建了一个栈帧,并且跳转到了函数开头处执行,就好比皇帝找大臣、大臣找知府这样的过程。尽管同一时刻可以有多个栈帧(皇帝、大臣、知府同时处于“等待下级回话”的状态),但“当前代码行”只有一个。
    最后补充:调用栈并不储存在可执行文件中,而是在运行时创建。调用栈所在的段称为堆栈段,它有自己的大小,如果调用次数多了,就会产生若干个栈帧,便会发生越界,这种情况称为栈溢出。由于局部变量也是放在堆栈段的,所以局部变量太大也会造成栈溢出,这就是为什么要把较大的数组放在main函数外的原因。
    最后搬运一篇别人的博客。
    内容主要是他学习递归的过程。
      递归真是个奇妙的思维方式。对一些简单的递归问题,我总是惊叹于递归描述问题和编写代码的简洁。但是总感觉没能融会贯通地理解递归,有时尝试用大脑去深入“递归”,层次较深时便常产生进不去,出不来的感觉。这种状态也导致我很难灵活地运用递归解决问题。有一天,我看到一句英文:“To Iterate is Human, to Recurse, Divine.”中文译为:“人理解迭代,神理解递归。”然后,我心安理得地放弃了对递归的深入理解。直到看到王垠谈程序语言最精华的原理时提到了递归,并说递归比循环表达能力强很多,而且效率几乎一样。再次唤醒了我对递归的理解探索。

    我首先在知乎上发现了下面两个例子,对比了递归和循环。

      递归:你打开面前这扇门,看到屋里面还有一扇门(这门可能跟前面打开的门一样大小(静),也可能门小了些(动)),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,你继续打开,..., 若干次之后,你打开面前一扇门,发现只有一间屋子,没有门了。你开始原路返回,每走回一间屋子,你数一次,走到入口的时候,你可以回答出你到底用这钥匙开了几扇门。

      循环:你打开面前这扇门,看到屋里面还有一扇门,(这门可能跟前面打开的门一样大小(静),也可能门小了些(动)),你走过去,发现手中的钥匙还可以打开它,你推开门,发现里面还有一扇门,(前面门如果一样,这门也是一样,第二扇门如果相比第一扇门变小了,这扇门也比第二扇门变小了(动静如一,要么没有变化,要么同样的变化)),你继续打开这扇门,...,一直这样走下去。 入口处的人始终等不到你回去告诉他答案。

    该用户这么总结到:递归就是有去(递去)有回(归来)。

    具体来说,为什么可以”有去“? 

      这要求递归的问题需要是可以用同样的解题思路来回答除了规模大小不同其他完全一样的问题。

    为什么可以”有回“?

      这要求这些问题不断从大到小,从近及远的过程中,会有一个终点,一个临界点,一个baseline,一个你到了那个点就不用再往更小,更远的地方走下去的点,然后从那个点开始,原路返回到原点。

    上面的解释几乎回答了我已久的疑问:为什么我老是有递归没有真的在解决问题的感觉?

      因为递是描述问题,归是解决问题。而我的大脑容易被递占据,只往远方去了,连尽头都没走到,何谈回的来。

    《漫谈递归:递归的思想》这篇文章将递归思想归纳为:

      递归的基本思想是把规模大的问题转化为规模小的相似的子问题来解决。在函数实现时,因为解决大问题的方法和解决小问题的方法往往是同一个方法,所以就产生了函数调用它自身的情况。另外这个解决问题的函数必须有明显的结束条件,这样就不会产生无限递归的情况了。

      需注意的是,规模大转化为规模小是核心思想,但递归并非是只做这步转化,而是把规模大的问题分解为规模小的子问题和可以在子问题解决的基础上剩余的可以自行解决的部分。而后者就是归的精髓所在,是在实际解决问题的过程。

    我试图把我理解到递归思想用递归用程序表达出来,确定了三个要素:递 + 结束条件 + 归。

    function recursion(大规模)
    {
        if(end_condition){
            end;    
        }
        else{     //先将问题全部描述展开,再由尽头“返回”依次解决每步中剩余部分的问题
            recursion(小规模);     //go;
            solve;                 //back;
        }
    }
    但是,我很容易发现这样描述遗漏了我经常会遇到的一种递归情况,比如递归遍历的二叉树的先序。
    我将这种情况用如下递归程序表达出来:
    function recursion(大规模)
    {
        if(end_condition){
            end;    
        }
        else{           //在将问题转换为子问题描述的每一步,都解决该步中剩余部分的问题。
            solve;                 //back;
            recursion(小规模);     //go;
        }
    }

    总结到这里,我突然发现递归是为了最能表达这种思想,所以用“递归”这个词,其实递归可以是“有去有回”,也可以是“有去无回”。但其根本是“由大往小地去,由近及远地去”。“递”是必需,“归”并非必需,依赖于要解决的问题,有的需要去的路上解决,有的需要回来的路上解决。有递无归的递归其实就是我们很容易理解的一种分治思想。

    其实理解递归可能没有“归”,只有去(分治)的情况后,我们应该想到递归也许可以既不需要在“去”的路上解决问题,也不需要在“归”的路上解决问题,只需在路的尽头解决问题,即在满足停止条件时解决问题。递归的分治思想不一定是要把问题规模递归到最小,还可以是将问题递归穷举其所有的情形,这时通常递归的表达力体现在将无法书写的嵌套循环(不确定数量的嵌套循环)通过递归表达出来。

    将这种递归情形用递归程序描述如下:

    recursion()
    {
        if(end_condition){
            solve;    
        }
        else{     //在将问题转换为子问题描述的每一步,都解决该步中剩余部分的问题。
            for(){
                recursion();        //go
            }
        }
    }

    由这个例子,可以发现这种递归对递归函数参数出现了设计要求,即便递归到尽头,组合的字符串规模(长度)也没有变小,规模变小的是递归函数的一个参数。可见,这种变化似乎一下将递归的灵活性大大地扩展了,所谓的大规模转换为小规模需要有一个更为广义的理解了。

    对递归的理解就暂时到这里了,可以看出文章中提到关于“打开一扇门”的递归例子来解释递归并不准确,例子只描述了递归的一种情况。而“递归就是有去(递去)有回(归来)”的论断同样不够准确。要为只读了文章前半部分的读者惋惜了。

    我也给出自己对递归思想的总结吧:

    递归的基本思想是广义地把规模大的问题转化为规模小的相似的子问题或者相似的子问题集合来解决。广义针对规模的,规模的缩小具体可以是指递归函数的参数,也可以是其参数之一。相似是指解决大问题的方法和解决小问题的方法往往是同一个方法,还可以是指解决子问题集的各子问题的方法是同一个方法。解决大问题的方法可以是由解决次规模问题的方法和解决剩余部分的方法组成,也可以是由一系列解决次规模问题的方法组成。

    评论区:
    提问:博主好,我认为博主的说法有些不妥,请指正!我认为,递归必然是有去有回的,这是由递归本身的性质所决定的,或者说,这是由递归的定义本身所决定的,因此不存在你所说的“有去无回”之类的情况。至于递归算法在解决问题的时机上,有可能是在递去过程中,也有可能是在归来过程中,还有可能在两个过程中一起解决。请指正~
    评论:从程序的角度来看,递归确实是有去有回的。但是从解决具体问题的角度来看,可能递归在递的过程中,已经解决了问题,从而导致了归的非必需。
  • 相关阅读:
    CSS之旅——第二站 如何更深入的理解各种选择器
    CSS之旅——第一站 为什么要用CSS
    记录一些在用wcf的过程中走过的泥巴路 【第一篇】
    asp.net mvc 之旅—— 第二站 窥探Controller下的各种Result
    asp.net mvc 之旅—— 第一站 从简单的razor入手
    Sql Server之旅——终点站 nolock引发的三级事件的一些思考
    Sql Server之旅——第十四站 深入的探讨锁机制
    Sql Server之旅——第十三站 对锁的初步认识
    Sql Server之旅——第十二站 sqltext的参数化处理
    Sql Server之旅——第十一站 简单说说sqlserver的执行计划
  • 原文地址:https://www.cnblogs.com/init-007/p/10216405.html
Copyright © 2011-2022 走看看