递归 订阅
程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。 展开全文
程序调用自身的编程技巧称为递归( recursion)。递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。一般来说,递归需要有边界条件、递归前进段和递归返回段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回。
信息
外文名
recursion
分    类
计算机算法
中文名
递归
属    性
编程技巧
递归递归定义
递归,就是在运行的过程中调用自己。构成递归需具备的条件: 1. 子问题须与原始问题为同样的事,且更为简单;2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理。在数学和计算机科学中,递归指由一种(或多种)简单的基本情况定义的一类对象或方法,并规定其他所有情况都能被还原为其基本情况。例如,下列为某人祖先的递归定义:某人的双亲是他的祖先(基本情况)。某人祖先的双亲同样是某人的祖先(递归步骤)。斐波纳契数列(Fibonacci Sequence),又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21..... I [1]  斐波纳契数列是典型的递归案例:递归关系就是实体自己和自己建立关系。Fib(0) = 1 [基本情况] Fib(1) = 1 [基本情况] 对所有n > 1的整数:Fib(n) = (Fib(n-1) + Fib(n-2)) [递归定义] 尽管有许多数学函数均可以递归表示,但在实际应用中,递归定义的高开销往往会让人望而却步。例如:阶乘(1) = 1 [基本情况] 对所有n > 1的整数:阶乘(n) = (n * 阶乘(n-1)) [递归定义] 一种便于理解的心理模型,是认为递归定义对对象的定义是按照“先前定义的”同类对象来定义的。例如:你怎样才能移动100个箱子?答案:你首先移动一个箱子,并记下它移动到的位置,然后再去解决较小的问题:你怎样才能移动99个箱子?最终,你的问题将变为怎样移动一个箱子,而这时你已经知道该怎么做的。如此的定义在数学中十分常见。例如,集合论对自然数的正式定义是:1是一个自然数,每个自然数都有一个后继,这一个后继也是自然数。 德罗斯特效应是递归的一种视觉形式。图中女性手持的物体中有一幅她本人手持同一物体的小图片,进而小图片中还有更小的一幅她手持同一物体的图片,依此类推。又例如,我们在两面相对的镜子之间放一根正在燃烧的蜡烛,我们会从其中一面镜子里看到一根蜡烛,蜡烛后面又有一面镜子,镜子里面又有一根蜡烛……这也是递归的表现。
收起全文
精华内容
下载资源
问答
  • 为什么你学不会递归?告别递归,谈谈我的经验

    万次阅读 多人点赞 2019-10-27 16:01:40
    可能很多人在大一的时候,就已经接触了递归了,不过,我敢保证很多人初学者刚开始接触递归的时候,是一脸懵逼的,我当初也是,给我的感觉就是,递归太神奇了! 可能也有一大部分人知道递归,也能看的懂递归,但在...

    可能很多人在大一的时候,就已经接触了递归了,不过,我敢保证很多人初学者刚开始接触递归的时候,是一脸懵逼的,我当初也是,给我的感觉就是,递归太神奇了!

    可能也有一大部分人知道递归,也能看的懂递归,但在实际做题过程中,却不知道怎么使用,有时候还容易被递归给搞晕。也有好几个人来问我有没有快速掌握递归的捷径啊。说实话,哪来那么多捷径啊,不过,我还是想写一篇文章,谈谈我的一些经验,或许,能够给你带来一些帮助。

    为了兼顾初学者,我会从最简单的题讲起!

    递归的三大要素

    第一要素:明确你这个函数想要干什么

    对于递归,我觉得很重要的一个事就是,这个函数的功能是什么,他要完成什么样的一件事,而这个,是完全由你自己来定义的。也就是说,我们先不管函数里面的代码什么,而是要先明白,你这个函数是要用来干什么。

    例如,我定义了一个函数

    // 算 n 的阶乘(假设n不为0)
    int f(int n){
        
    }
    

    这个函数的功能是算 n 的阶乘。好了,我们已经定义了一个函数,并且定义了它的功能是什么,接下来我们看第二要素。

    第二要素:寻找递归结束条件

    所谓递归,就是会在函数内部代码中,调用这个函数本身,所以,我们必须要找出递归的结束条件,不然的话,会一直调用自己,进入无底洞。也就是说,我们需要找出当参数为啥时,递归结束,之后直接把结果返回,请注意,这个时候我们必须能根据这个参数的值,能够直接知道函数的结果是什么。

    例如,上面那个例子,当 n = 1 时,那你应该能够直接知道 f(n) 是啥吧?此时,f(1) = 1。完善我们函数内部的代码,把第二要素加进代码里面,如下

    // 算 n 的阶乘(假设n不为0)
    int f(int n){
        if(n == 1){
            return 1;
        }
    }
    

    有人可能会说,当 n = 2 时,那我们可以直接知道 f(n) 等于多少啊,那我可以把 n = 2 作为递归的结束条件吗?

    当然可以,只要你觉得参数是什么时,你能够直接知道函数的结果,那么你就可以把这个参数作为结束的条件,所以下面这段代码也是可以的。

    // 算 n 的阶乘(假设n>=2)
    int f(int n){
        if(n == 2){
            return 2;
        }
    }
    

    注意我代码里面写的注释,假设 n >= 2,因为如果 n = 1时,会被漏掉,当 n <= 2时,f(n) = n,所以为了更加严谨,我们可以写成这样:

    // 算 n 的阶乘(假设n不为0)
    int f(int n){
        if(n <= 2){
            return n;
        }
    }
    

    第三要素:找出函数的等价关系式

    第三要素就是,我们要不断缩小参数的范围,缩小之后,我们可以通过一些辅助的变量或者操作,使原函数的结果不变。

    例如,f(n) 这个范围比较大,我们可以让 f(n) = n * f(n-1)。这样,范围就由 n 变成了 n-1 了,范围变小了,并且为了原函数f(n) 不变,我们需要让 f(n-1) 乘以 n。

    说白了,就是要找到原函数的一个等价关系式,f(n) 的等价关系式为 n * f(n-1),即

    f(n) = n * f(n-1)。

    这个等价关系式的寻找,可以说是最难的一步了,如果你不大懂也没关系,因为你不是天才,你还需要多接触几道题,我会在接下来的文章中,找 10 道递归题,让你慢慢熟悉起来

    找出了这个等价,继续完善我们的代码,我们把这个等价式写进函数里。如下:

    // 算 n 的阶乘(假设n不为0)
    int f(int n){
        if(n <= 2){
            return n;
        }
        // 把 f(n) 的等价操作写进去
        return f(n-1) * n;
    }
    

    至此,递归三要素已经都写进代码里了,所以这个 f(n) 功能的内部代码我们已经写好了。

    这就是递归最重要的三要素,每次做递归的时候,你就强迫自己试着去寻找这三个要素。

    还是不懂?没关系,我再按照这个模式讲一些题。

    有些有点小基础的可能觉得我写的太简单了,没耐心看?少侠,请继续看,我下面还会讲如何优化递归。当然,大佬请随意,可以直接拉动最下面留言给我一些建议,万分感谢!

    案例1:斐波那契数列

    斐波那契数列的是这样一个数列:1、1、2、3、5、8、13、21、34…,即第一项 f(1) = 1,第二项 f(2) = 1…,第 n 项目为 f(n) = f(n-1) + f(n-2)。求第 n 项的值是多少。

    1、第一递归函数功能

    假设 f(n) 的功能是求第 n 项的值,代码如下:

    int f(int n){
        
    }
    

    2、找出递归结束的条件

    显然,当 n = 1 或者 n = 2 ,我们可以轻易着知道结果 f(1) = f(2) = 1。所以递归结束条件可以为 n <= 2 时,f(n= = 1。代码如下:

    int f(int n){
        if(n <= 2){
            return 1;
        }
    }
    

    第三要素:找出函数的等价关系式

    题目已经把等价关系式给我们了,所以我们很容易就能够知道 f(n) = f(n-1) + f(n-2)。我说过,等价关系式是最难找的一个,而这个题目却把关系式给我们了,这也太容易,好吧,我这是为了兼顾几乎零基础的读者。

    所以最终代码如下:

    int f(int n){
        // 1.先写递归结束条件
        if(n <= 2){
            return 1;
        }
        // 2.接着写等价关系式
        return f(n-1) + f(n - 2);
    }
    

    搞定,是不是很简单?

    零基础的可能还是不大懂,没关系,之后慢慢按照这个模式练习!好吧,有大佬可能在吐槽太简单了。

    案例2:小青蛙跳台阶

    一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。

    1、第一递归函数功能

    假设 f(n) 的功能是求青蛙跳上一个n级的台阶总共有多少种跳法,代码如下:

    int f(int n){
        
    }
    

    2、找出递归结束的条件

    我说了,求递归结束的条件,你直接把 n 压缩到很小很小就行了,因为 n 越小,我们就越容易直观着算出 f(n) 的多少,所以当 n = 1时,你知道 f(1) 为多少吧?够直观吧?即 f(1) = 1。代码如下:

    int f(int n){
        if(n == 1){
            return 1;
        }
    }
    

    第三要素:找出函数的等价关系式

    每次跳的时候,小青蛙可以跳一个台阶,也可以跳两个台阶,也就是说,每次跳的时候,小青蛙有两种跳法。

    第一种跳法:第一次我跳了一个台阶,那么还剩下n-1个台阶还没跳,剩下的n-1个台阶的跳法有f(n-1)种。

    第二种跳法:第一次跳了两个台阶,那么还剩下n-2个台阶还没,剩下的n-2个台阶的跳法有f(n-2)种。

    所以,小青蛙的全部跳法就是这两种跳法之和了,即 f(n) = f(n-1) + f(n-2)。至此,等价关系式就求出来了。于是写出代码:

    int f(int n){
        if(n == 1){
            return 1;
        }
        ruturn f(n-1) + f(n-2);
    }
    

    大家觉得上面的代码对不对?

    答是不大对,当 n = 2 时,显然会有 f(2) = f(1) + f(0)。我们知道,f(0) = 0,按道理是递归结束,不用继续往下调用的,但我们上面的代码逻辑中,会继续调用 f(0) = f(-1) + f(-2)。这会导致无限调用,进入死循环

    这也是我要和你们说的,关于递归结束条件是否够严谨问题,有很多人在使用递归的时候,由于结束条件不够严谨,导致出现死循环。也就是说,当我们在第二步找出了一个递归结束条件的时候,可以把结束条件写进代码,然后进行第三步,但是请注意,当我们第三步找出等价函数之后,还得再返回去第二步,根据第三步函数的调用关系,会不会出现一些漏掉的结束条件。就像上面,f(n-2)这个函数的调用,有可能出现 f(0) 的情况,导致死循环,所以我们把它补上。代码如下:

    int f(int n){
        //f(0) = 0,f(1) = 1,f(2) = 2等价于 n<=2时,f(n) = n。
        if(n <= 2){
            return n;
        }
        ruturn f(n-1) + f(n-2);
    }
    

    有人可能会说,我不知道我的结束条件有没有漏掉怎么办?别怕,多练几道就知道怎么办了。

    看到这里有人可能要吐槽了,这两道题也太容易了吧??能不能被这么敷衍。少侠,别走啊,下面出道难一点的。

    下面其实也不难了,就比上面的题目难一点点而已,特别是第三步等价的寻找。

    案例3:反转单链表。

    反转单链表。例如链表为:1->2->3->4。反转后为 4->3->2->1

    链表的节点定义如下:

    class Node{
        int date;
        Node next;
    }
    

    虽然是 Java语言,但就算你没学过 Java,我觉得也是影响不大,能看懂。

    还是老套路,三要素一步一步来。

    1、定义递归函数功能

    假设函数 reverseList(head) 的功能是反转但链表,其中 head 表示链表的头节点。代码如下:

    Node reverseList(Node head){
        
    }
    

    2. 寻找结束条件

    当链表只有一个节点,或者如果是空表的话,你应该知道结果吧?直接啥也不用干,直接把 head 返回呗。代码如下:

    Node reverseList(Node head){
        if(head == null || head.next == null){
            return head;
        }
    }
    

    3. 寻找等价关系

    这个的等价关系不像 n 是个数值那样,比较容易寻找。但是我告诉你,它的等价条件中,一定是范围不断在缩小,对于链表来说,就是链表的节点个数不断在变小,所以,如果你实在找不出,你就先对 reverseList(head.next) 递归走一遍,看看结果是咋样的。例如链表节点如下

    我们就缩小范围,先对 2->3->4递归下试试,即代码如下

    Node reverseList(Node head){
        if(head == null || head.next == null){
            return head;
        }
        // 我们先把递归的结果保存起来,先不返回,因为我们还不清楚这样递归是对还是错。,
        Node newList = reverseList(head.next);
    }
    

    我们在第一步的时候,就已经定义了 reverseLis t函数的功能可以把一个单链表反转,所以,我们对 2->3->4反转之后的结果应该是这样:

    我们把 2->3->4 递归成 4->3->2。不过,1 这个节点我们并没有去碰它,所以 1 的 next 节点仍然是连接这 2。

    接下来呢?该怎么办?

    其实,接下来就简单了,我们接下来只需要把节点 2 的 next 指向 1,然后把 1 的 next 指向 null,不就行了?,即通过改变 newList 链表之后的结果如下:

    也就是说,reverseList(head) 等价于 ** reverseList(head.next)** + 改变一下1,2两个节点的指向。好了,等价关系找出来了,代码如下(有详细的解释):

    //用递归的方法反转链表
    public static Node reverseList2(Node head){
        // 1.递归结束条件
        if (head == null || head.next == null) {
                 return head;
             }
             // 递归反转 子链表
             Node newList = reverseList2(head.next);
             // 改变 1,2节点的指向。
             // 通过 head.next获取节点2
             Node t1  = head.next;
             // 让 2 的 next 指向 2
             t1.next = head;
             // 1 的 next 指向 null.
            head.next = null;
            // 把调整之后的链表返回。
            return newList;
        }
    
    

    这道题的第三步看的很懵?正常,因为你做的太少了,可能没有想到还可以这样,多练几道就可以了。但是,我希望通过这三道题,给了你以后用递归做题时的一些思路,你以后做题可以按照我这个模式去想。通过一篇文章是不可能掌握递归的,还得多练,我相信,只要你认真看我的这篇文章,多看几次,一定能找到一些思路!!

    我已经强调了好多次,多练几道了,所以呢,后面我也会找大概 10 道递归的练习题供大家学习,不过,我找的可能会有一定的难度。不会像今天这样,比较简单,所以呢,初学者还得自己多去找题练练,相信我,掌握了递归,你的思维抽象能力会更强!

    接下来我讲讲有关递归的一些优化。

    有关递归的一些优化思路

    1. 考虑是否重复计算

    告诉你吧,如果你使用递归的时候不进行优化,是有非常非常非常多的子问题被重复计算的。

    啥是子问题? f(n-1),f(n-2)…就是 f(n) 的子问题了。

    例如对于案例2那道题,f(n) = f(n-1) + f(n-2)。递归调用的状态图如下:

    (img-adCaaEyJ-1572163241563)(https://user-gold-cdn.xitu.io/2019/3/12/169722f31645ef25?w=729&h=444&f=png&s=88214)]

    看到没有,递归计算的时候,重复计算了两次 f(5),五次 f(4)。。。。这是非常恐怖的,n 越大,重复计算的就越多,所以我们必须进行优化。

    如何优化?一般我们可以把我们计算的结果保证起来,例如把 f(4) 的计算结果保证起来,当再次要计算 f(4) 的时候,我们先判断一下,之前是否计算过,如果计算过,直接把 f(4) 的结果取出来就可以了,没有计算过的话,再递归计算。

    用什么保存呢?可以用数组或者 HashMap 保存,我们用数组来保存把,把 n 作为我们的数组下标,f(n) 作为值,例如 arr[n] = f(n)。f(n) 还没有计算过的时候,我们让 arr[n] 等于一个特殊值,例如 arr[n] = -1。

    当我们要判断的时候,如果 arr[n] = -1,则证明 f(n) 没有计算过,否则, f(n) 就已经计算过了,且 f(n) = arr[n]。直接把值取出来就行了。代码如下:

    // 我们实现假定 arr 数组已经初始化好的了。
    int f(int n){
        if(n <= 1){
            return n;
        }
        //先判断有没计算过
        if(arr[n] != -1){
            //计算过,直接返回
            return arr[n];
        }else{
            // 没有计算过,递归计算,并且把结果保存到 arr数组里
            arr[n] = f(n-1) + f(n-1);
            reutrn arr[n];
        }
    }
    

    也就是说,使用递归的时候,必要
    须要考虑有没有重复计算,如果重复计算了,一定要把计算过的状态保存起来。

    2. 考虑是否可以自底向上

    对于递归的问题,我们一般都是从上往下递归的,直到递归到最底,再一层一层着把值返回。

    不过,有时候当 n 比较大的时候,例如当 n = 10000 时,那么必须要往下递归10000层直到 n <=1 才将结果慢慢返回,如果n太大的话,可能栈空间会不够用。

    对于这种情况,其实我们是可以考虑自底向上的做法的。例如我知道

    f(1) = 1;

    f(2) = 2;

    那么我们就可以推出 f(3) = f(2) + f(1) = 3。从而可以推出f(4),f(5)等直到f(n)。因此,我们可以考虑使用自底向上的方法来取代递归,代码如下:

    public int f(int n) {
           if(n <= 2)
               return n;
           int f1 = 1;
           int f2 = 2;
           int sum = 0;
    
           for (int i = 3; i <= n; i++) {
               sum = f1 + f2;
               f1 = f2;
               f2 = sum;
           }
           return sum;
       }
    

    这种方法,其实也被称之为递推

    最后总结

    其实,递归不一定总是从上往下,也是有很多是从下往上的,例如 n = 1 开始,一直递归到 n = 1000,例如一些排序组合。对于这种从下往上的,也是有对应的优化技巧,不过,我就先不写了,后面再慢慢写。这篇文章写了很久了,脖子有点受不了了,,,,颈椎病?害怕。。。。

    说实话,对于递归这种比较抽象的思想,要把他讲明白,特别是讲给初学者听,还是挺难的,这也是我这篇文章用了很长时间的原因,不过,只要能让你们看完,有所收获,我觉得值得!有些人可能觉得讲的有点简单,没事,我后面会找一些不怎么简单的题。最后如果觉得不错,还请给我转发 or 点赞一波!

    另外,推荐一份计算机类书单,只为让大家更加方便找到自己想要的书籍,目前已经收集了几百本了,贡献给需要的人,地址:GitHub 6K,一份覆喊各类编程书籍的书单下载

    这里帅地也整理了一些不错的算法/计算机基础的资料,送给大家:

    翻遍全网,计网、操作系统、计组相关视频被我找到了

    还有一位朋友整理的设计模式资料

    字节跳动总结的设计模式 PDF 火了,完整版开放下载!

    还有一份很不错的算法笔记

    两个月斩获 70k star,前字节大神刷题笔记

    最后,献上我备战校招的思维导图 + 提升内功的 PDF 吧

    九大思维导图助你拿到心仪的 offer

    在这里插入图片描述

    原创 PDF 助你提升基础内容,里面也有我的个人经历

    在这里插入图片描述

    下载链接:https://pan.baidu.com/s/1thH7vqBEosgRrESTUcFL2Q 密码:9x30

    如果觉得有帮助,也要记得来个赞啊,只收藏不点赞都是刷流氓,嘻嘻

    展开全文
  • 递归

    万次阅读 2019-05-10 07:38:32
    递归是一种编程模式,用于一个任务可以被分割为多个相似的更简单的任务的场景。或者用于一个任务可以被简化为一个容易的行为上更简单的任务变体。或者像我们随后会看到的,用来处理特定类型的数据结构。 当一个函数...

    递归是一种编程模式,用于一个任务可以被分割为多个相似的更简单的任务的场景。或者用于一个任务可以被简化为一个容易的行为上更简单的任务变体。或者像我们随后会看到的,用来处理特定类型的数据结构。
    当一个函数解决一个任务时,在该过程中它可以调用很多其他函数。那么当 一个函数调用自身时 ,就称其为递归

    递归的俩种思考方式:

    简单起见,我们写一个函数pow(x, n),它可以计算x的n次方,即用x乘以自身n次。

    pow(2, 2) = 4
    pow(2, 3) = 8
    pow(2, 4) = 16
    

    有俩种实现方式。
    1.迭代思路:for循环:

    function pow(x, n) {
    	let result = 1;
    	//在循环中用x乘以result
    	for (let i = 0; i < n; i++) {
    		result *= x;
    	}
    	return result;
    }
    alert(pow(2, 3));  //8
    

    2.递归思路:简化任务,调用自身:

    function pow(x, n) {
    	if (n == 1) {
    		return x;
    	} else {
    		return x * pow(x, n - 1);
    	}
    }
    alert(pow(2, 3));   //8
    

    注意递归方式完全不相同。
    当pow(x, n)被调用时,执行分为俩个分支:
    在这里插入图片描述
    1.如果n == 1 ,所有事情都会变得很简单,这叫递归的基础,因为它立即得到显而易见的结果:pow(x, 1)等于x。
    2.否者,我们可以用x * pow(x, n - 1)表示pow(x, n)。这叫做一个递归步骤:我们将任务转变为更简单的行为(x的乘法)和更简单的同类任务调用(更小的n给pow)。后面步骤继续简化直到n等于1。
    我们也可以说pow递归的调用自身直到 n == 1。
    在这里插入图片描述
    比如,为了计算pow(2, 4),递归变体经过了下面几个步骤:

    1. pow(2, 4) = 2 * pow(2, 3)
    2. pow(2, 3) = 2 * pow(2, 2)
    3. pow(2, 2) = 2 * pow(2, 1)
    4. pow(2, 1) = 2

    所以,递归生成了更简单的函数调用。

    递归一般更简洁

    递归解决方案一般比迭代更简洁。
    这里我们可以使用三元运算符?来替换if语句,从而让pow(x, n)更简洁并且可读性依然很高:

    function pow(x, n) {
    	return (n == 1) ? x : (x * pow(x, n - 1));
    }
    

    最大的嵌套调用次数(包括首次)称为递归深度。在我们的例子中,它正好等于n。
    最大递归深度受限于JavaScript引擎。我们可以确信基本是10000,有些引擎可能允许更大,但是100000很可能就超过了限制。有一些自动优化能够缓解这个([尾部调用优化]),但是它们还没有被完全支持,只能用于简单场景。
    这就限制了递归的应用,但是递归仍然被广泛使用。有很多任务使用递归思路会让代码更简单,更容易维护。

    展开全文
  • 对斐波那契数列递归求解的时间复杂度分析

    一、斐波那契数列

    • 所谓斐波那契数列,是指【当前项】的值等于【前两项】之和的数列:
    ii 0 1 2 3 4 5 6 7 8 9
    f(i)f(i) 1 1 2 3 5 8 13 21 34 55
    • 该数列有递推公式如下:

    f(n)={1(n=0)1(n=1)f(n1)+f(n2)(n>2)f(n) = \begin{cases} 1 & (n = 0) \\ 1 & (n = 1) \\ f(n-1) + f(n-2) & (n > 2) \end{cases}

    二、递归求解斐波那契数列

    • C++ 代码实现如下:
    int f(unsigned int n) {
    	if(n <= 1) {
    		return 1;
    	}
    	return f(n-1) + f(n-2);
    }
    
    • 直接根据公式进行递归求解;

    三、时间复杂度分析

    • 图解求解的过程如下:
    n
    n-1
    n-2
    n-2
    n-3
    n-3
    n-4
    n-3
    n-4
    n-4
    n-5
    n-4
    n-5
    n-5
    n-6
    • 这是一棵二叉树,树的高度为 n,所以粗看深度优先搜索时访问的结点数为 2n2^n,但是仔细看,最左边的结点的高度一定比右边结点的高度大,所以不是一棵严格的完全二叉树。为了探究它实际的时间复杂度,我们改下代码:
    int f(unsigned int n) {
    	++c[n];
    	if(n <= 1) {
    		return 1;
    	}
    	return f(n-1) + f(n-2);
    }
    
    • 加了一句代码 ++c[n];,引入一个计数器,来看下在不同的 nn 的情况下, f(n)f(n) 这个函数会被调用几次;
    • 实测下来的表格如下:
    nn f(n)f(n) c[n]c[n]
    0 1 1
    1 1 1
    2 2 3
    3 3 5
    4 5 9
    5 8 15
    6 13 25
    7 21 41
    8 34 67
    9 55 109
    10 89 177
    11 144 287
    12 233 465
    13 377 753
    14 610 1219
    15 987 1973
    16 1597 3193
    17 2584 5167
    18 4181 8361
    19 6765 13529
    20 10946 21891
    21 17711 35421
    22 28657 57313
    23 46368 92735
    24 75025 150049
    25 121393 242785
    26 196418 392835
    27 317811 635621
    28 514229 1028457
    • 观察 c[n]c[n] 的增长趋势,首先排除等差数列,然后再来看是否符合等比数列,我们来尝试求下 c[n]/c[n1]c[n] / c[n-1] 的值,列出表格如下:
    nn f(n)f(n) c[n]c[n] c[n]/c[n1]c[n]/c[n-1]
    1 1 1 1.000000
    2 2 3 3.000000
    3 3 5 1.666667
    4 5 9 1.800000
    5 8 15 1.666667
    6 13 25 1.666667
    7 21 41 1.640000
    8 34 67 1.634146
    9 55 109 1.626866
    10 89 177 1.623853
    11 144 287 1.621469
    12 233 465 1.620209
    13 377 753 1.619355
    14 610 1219 1.618858
    15 987 1973 1.618540
    16 1597 3193 1.618348
    17 2584 5167 1.618227
    18 4181 8361 1.618154
    19 6765 13529 1.618108
    20 10946 21891 1.618080
    21 17711 35421 1.618062
    22 28657 57313 1.618051
    23 46368 92735 1.618045
    24 75025 150049 1.618041
    25 121393 242785 1.618038
    26 196418 392835 1.618037
    27 317811 635621 1.618036
    28 514229 1028457 1.618035
    • 观察发现,随着 nn 的不断增大,c[n]/c[n1]c[n]/c[n-1] 越来越接近一个常数,而这个常数就是:
      2511.618034\frac {2}{ \sqrt 5 - 1} \approx 1.618034
    • 当 n 趋近于无穷大的时候,满足如下公式:
      c[n]=251c[n1]c[n] = \frac {2}{ \sqrt 5 - 1} c[n-1]
    • 对等比数列化解后累乘得到:
      c[n]=251c[n1]=(251)2c[n2]=...=(251)nc[n] = \frac {2}{ \sqrt 5 - 1} c[n-1] = (\frac {2}{ \sqrt 5 - 1})^2 c[n-2] = ... = (\frac {2}{ \sqrt 5 - 1})^n
    • 所以,斐波那契数列 递归求解的时间复杂度就是 :
      O((251)n)O((\frac {2}{ \sqrt 5 - 1})^n)
    展开全文
  • 快速排序(三种算法实现和非递归实现)

    万次阅读 多人点赞 2017-11-29 18:41:44
    快速排序(Quick Sort)是对冒泡排序的一种改进,基本思想是选取一个记录作为枢轴,经过一趟排序,将整段序列分为...递归实现:void QuickSort(int* array,int left,int right) { assert(array); if(left &amp;gt;=

    快速排序(Quick Sort)是对冒泡排序的一种改进,基本思想是选取一个记录作为枢轴,经过一趟排序,将整段序列分为两个部分,其中一部分的值都小于枢轴,另一部分都大于枢轴。然后继续对这两部分继续进行排序,从而使整个序列达到有序。

    递归实现:

    void QuickSort(int* array,int left,int right)
    {
    	assert(array);
    	if(left >= right)//表示已经完成一个组
    	{
    		return;
    	}
    	int index = PartSort(array,left,right);//枢轴的位置
    	QuickSort(array,left,index - 1);
    	QuickSort(array,index + 1,right);
    }
    

    PartSort()函数是进行一次快排的算法。
    对于快速排序的一次排序,有很多种算法,我这里列举三种。

    左右指针法

    1. 选取一个关键字(key)作为枢轴,一般取整组记录的第一个数/最后一个,这里采用选取序列最后一个数为枢轴。
    2. 设置两个变量left = 0;right = N - 1;
    3. 从left一直向后走,直到找到一个大于key的值,right从后至前,直至找到一个小于key的值,然后交换这两个数。
    4. 重复第三步,一直往后找,直到left和right相遇,这时将key放置left的位置即可。

    这里写图片描述

    当left >= right时,一趟快速排序就完成了,这时将Key和array[left]的值进行一次交换。
    一次快排的结果:4 1 3 0 2 5 8 6 7 9

    基于这种思想,可以写出代码:

    int PartSort(int* array,int left,int right)
    {
    	int& key = array[right];
    	while(left < right)
    	{
    		while(left < right && array[left] <= key)
    		{
    			++left;
    		}
    		while(left < right && array[right] >= key)
    		{
    			--right;
    		}
    		swap(array[left],array[right]);
    	}
    	swap(array[left],key);
    	return left;
    }
    

    问题:下面的代码为什么还要判断left < right?

    while(left < right && array[left] <= key)
    

    key是整段序列最后一个,right是key前一个位置,如果array[right]这个位置的值和key相等,满足array[left] <= key,然后++left,这时候left会走到key的下标处。

    ###挖坑法

    1. 选取一个关键字(key)作为枢轴,一般取整组记录的第一个数/最后一个,这里采用选取序列最后一个数为枢轴,也是初始的坑位。
    2. 设置两个变量left = 0;right = N - 1;
    3. 从left一直向后走,直到找到一个大于key的值,然后将该数放入坑中,坑位变成了array[left]。
    4. right一直向前走,直到找到一个小于key的值,然后将该数放入坑中,坑位变成了array[right]。
    5. 重复3和4的步骤,直到left和right相遇,然后将key放入最后一个坑位。

    这里写图片描述

    当left >= right时,将key放入最后一个坑,就完成了一次排序。
    注意,left走的时候right是不动的,反之亦然。因为left先走,所有最后一个坑肯定在array[right]。

    写出代码:

    int PartSort(int* array,int left,int right)
    {
    	int key = array[right];
    	while(left < right)
    	{
    		while(left < right && array[left] <= key)
    		{
    			++left;
    		}
    		array[right] = array[left];
    		while(left < right && array[right] >= key)
    		{
    			--right;
    		}
    		array[left] = array[right];	 
    	}
    	array[right] = key;
    	return right;
    }
    

    ###前后指针法

    1. 定义变量cur指向序列的开头,定义变量pre指向cur的前一个位置。
    2. 当array[cur] < key时,cur和pre同时往后走,如果array[cur]>key,cur往后走,pre留在大于key的数值前一个位置。
    3. 当array[cur]再次 < key时,交换array[cur]和array[pre]。

    通俗一点就是,在没找到大于key值前,pre永远紧跟cur,遇到大的两者之间机会拉开差距,中间差的肯定是连续的大于key的值,当再次遇到小于key的值时,交换两个下标对应的值就好了。

    带着这种思想,看着图示应该就能理解了。
    这里写图片描述

    下面是实现代码:

    int PartSort(int* array,int left,int right)
    {
    	if(left < right){
    		int key = array[right];
    		int cur = left;
    		int pre = cur - 1;
    		while(cur < right)
    		{
    			while(array[cur] < key && ++pre != cur)//如果找到小于key的值,并且cur和pre之间有距离时则进行交换。注意两个条件的先后位置不能更换,可以参照评论中的解释
    			{
    				swap(array[cur],array[pre]);
    			}
    			++cur;
    		}
    		swap(array[++pre],array[right]);
    		return pre;
    	}
    	return -1;
    }
    

    最后的前后指针法思路有点绕,多思考一下就好了。它最大的特点就是,左右指针法和挖坑法只能针对顺序序列进行排序,如果是对一个链表进行排序, 就无用武之地了。

    所以记住了,前后指针这个特点!


    ###快速排序的优化

    首先快排的思想是找一个枢轴,然后以枢轴为中介线,一遍都小于它,另一边都大于它,然后对两段区间继续划分,那么枢轴的选取就很关键。

    1、三数取中法
    上面的代码思想都是直接拿序列的最后一个值作为枢轴,如果最后这个值刚好是整段序列最大或者最小的值,那么这次划分就是没意义的。
    所以当序列是正序或者逆序时,每次选到的枢轴都是没有起到划分的作用。快排的效率会极速退化。

    所以可以每次在选枢轴时,在序列的第一,中间,最后三个值里面选一个中间值出来作为枢轴,保证每次划分接近均等。

    2、直接插入
    由于是递归程序,每一次递归都要开辟栈帧,当递归到序列里的值不是很多时,我们可以采用直接插入排序来完成,从而避免这些栈帧的消耗。

    整个代码:

    //三数取中
    int GetMid(int* array,int left,int right)
    {
        assert(array);
        int mid = left + ((right - left)>>1);
        if(array[left] <= array[right])
        {
            if(array[mid] <  array[left])
                return left;
            else if(array[mid] > array[right])
                return right;
            else
                return mid;
        }
        else
        {
            if(array[mid] < array[right])
                return right;
            else if(array[mid] > array[left])
                return left;
            else
                return mid;
        }
    
    }
    
    //左右指针法
    int PartSort1(int* array,int left,int right)
    {
        assert(array);
        int mid = GetMid(array,left,right);
        swap(array[mid],array[right]);
        
        int& key = array[right];
        while(left < right)
        {
            while(left < right && array[left] <= key)//因为有可能有相同的值,防止越界,所以加上left < right
                ++left;
            while(left < right && array[right] >= key)
                --right;
    
            swap(array[left],array[right]);
        }
    
        swap(array[left],key);
        return left;
    }
    
    //挖坑法
    int PartSort2(int* array,int left,int right)
    {
        assert(array);
        int mid = GetMid(array,left,right);
        swap(array[mid],array[right]);
        
        int key = array[right];
        while(left < right)
        {
            while(left < right && array[left] <= key)
                ++left;
            array[right] = array[left];
           
            while(left < right && array[right] >= key)
                --right;
            array[left] = array[right];
        }
        array[right] = key;
        return right;
    }
    
    //前后指针法
    int PartSort3(int* array,int left,int right)
    {
        assert(array);
        int mid = GetMid(array,left,right);
    	swap(array[mid],array[right]);
        if(left < right){
    	    int key = array[right];
    	    int cur = left;
    	    int pre = left - 1;
    	    while(cur < right)
    	    {
    	         while(array[cur] < key && ++pre != cur)
    	         {
    	             swap(array[cur],array[pre]);
    	         }
    	            ++cur;
    	    }
    	        swap(array[++pre],array[right]);
    	        return pre;
    	}
    	return -1;
    }
    
    void QuickSort(int* array,int left,int right)
    {
        assert(array);
        if(left >= right)
            return;
    
    	//当序列较短时,采用直接插入
        if((right - left) <= 5)
        InsertSort(array,right-left+1);
        
        int index = PartSort3(array,left,right);
        QuickSort(array,left,index-1);
        QuickSort(array,index+1,right);
    }
    
    int main()
    {
    	int array[] = {4,1,7,6,9,2,8,0,3,5};
    	QuickSort(array,0,sizeof(array)/sizeof(array[0]) -1);//因为传的是区间,所以这里要 - 1;
    }
    

    非递归实现

    递归的算法主要是在划分子区间,如果要非递归实现快排,只要使用一个栈来保存区间就可以了。
    一般将递归程序改成非递归首先想到的就是使用栈,因为递归本身就是一个压栈的过程。

    void QuickSortNotR(int* array,int left,int right)
    {
    	assert(array);
    	stack<int> s;
    	s.push(left);
    	s.push(right);//后入的right,所以要先拿right
    	while(!s.empty)//栈不为空
    	{
    		int right = s.top();
    		s.pop();
    		int left = s.top();
    		s.pop();
    		
    		int index = PartSort(array,left,right);
    		if((index - 1) > left)//左子序列
    		{
    			s.push(left);
    			s.push(index - 1);
    		}
    		if((index + 1) < right)//右子序列
    		{
    			s.push(index + 1);
    			s.push(right);
    		}
    	}
    }
    

    上面就是关于快速排序的一些知识点,如果哪里有错误,还望指出。

    展开全文
  • 递归递归递归递归

    千次阅读 多人点赞 2015-06-07 18:35:38
    用扩展欧几里德算法求乘法逆元的算法过程,就是对递归调用的深层理解,花了老长时间才搞懂。。 下面是我看的别人博客上的讲解 C通过运行时堆栈支持递归函数的实现。递归函数就是直接或间接调用自身的函数 ...
  • Java实现简单的递归操作

    万次阅读 多人点赞 2017-04-03 11:22:48
    在数据结构算法设计中,或者一个方法的具体实现的时候,有一种方法叫做“递归”,这种方法在思想上并不是特别难,但是实现起来还是有一些需要注意的。虽然对于很多递归算法都可以由相应的循环迭代来代替,但是对于...
  • 递归算法讲解

    万次阅读 多人点赞 2017-06-15 22:11:51
    原作者:书呆子Rico 《递归的内涵与经典应用》 http://my.csdn.net/justloveyou_摘要: 大师 L. Peter Deutsch 说过:To Iterate is Human, to ...对一些简单的递归问题,我们总是惊叹于递归描述问题的能力和编写代
  • 10张动图学会python循环与递归

    万次阅读 多人点赞 2020-12-19 08:43:00
    图像(包括动图)是传递信息的一种高效方式,往往能增强表象、记忆与思维等方面的反应强度。所谓一图胜千言,说的就是这个道理。今天为大家整理了十张动图GIFS,有助于认识循环、递归、二分检索等...
  • 彻底理解递归,从递归的本质说起!

    万次阅读 多人点赞 2018-05-12 14:28:35
    比较简单地方式就是用递归去遍历,鉴于递归这种调用方法有一定的特殊性,今天还是想来讲讲怎么去理解递归遍历。本文针对想理解递归的过程的朋友,因为本人在学到这一部分的时候也纠结了很久,其实只要理解了过程,那...
  • python 递归-递归可视化

    万次阅读 2020-07-10 13:44:54
    python 递归-递归可视化 文章目录python 递归-递归可视化1、实现2、递归可视化-Coding 1、实现 2、递归可视化-Coding
  • 10张 GIF 动图让你弄懂递归等概念

    万次阅读 多人点赞 2019-07-27 12:15:00
    点击蓝色“五分钟学算法”关注我哟加个“星标”,一起学算法来源 | 编程派今天为大家整理了十张动图GIFS,有助于认识循环、递归、二分检索等概念的具体运行情况。一、循环GI...
  • 递归算法

    万次阅读 多人点赞 2017-02-08 00:20:28
    1.简单递归定义 2.递归与循环的区别与联系 3.递归的经典应用 1.简单递归定义 什么叫递归?(先定义一个比较简单的说法,为了理解,不一定对) 递归:无限调用自身这个函数,每次调用总会改动一个关键...
  • 汉诺塔递归调用(C语言实现)

    万次阅读 多人点赞 2018-11-03 14:17:09
    1.递归算法 递归算法:是一种直接或者间接地调用自身的算法。在计算机编写程序中,递归算法对解决一大类问题是十分有效的,它往往使算法的描述简洁而且易于理解。 递归过程一般通过函数或子过程来实现。 递归算法...
  • C语言 递归算法及简单递归练习总结

    万次阅读 多人点赞 2018-12-14 22:56:48
    递归 :大师 L. Peter Deutsch 说过:To Iterate is Human, to Recurse, Divine.中文译为:人理解迭代,神理解递归。 简单理解: 递归:你打开面前这扇门,看到屋里面还有一扇门。你走过去,发现手中的钥匙还可以...
  • 递归函数在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函 数。(1) 递归就是在过程或函数里调用自身。(2) 在使用递归策略时,必须有一个明确的递归结束条件,称为递归出口。 递归...
  • java从数据库读取菜单,递归生成菜单树

    万次阅读 多人点赞 2016-10-29 13:44:34
    java从数据库读取菜单,递归生成菜单树
  • 1.递归的线性递归,这里有一个最经典...每个目录的递归互不影响2.2分叉递归2这里是一个迷宫的递归,他会从起点开始向四个方向递归查找,便会停止当前坐标的当前方向的递归,而其他坐标还是会继续查找2.3分叉递归3与...
  • python递归

    千次阅读 2020-08-17 14:31:46
    什么是递归 递归是一种解决问题的方法,将问题分解为更小的子问题,直到得到一个足够小的问题可以被很简单的解决。通常递归涉及函数调用自身。递归允许我们编写优雅的解决方案,解决可能很难编程的问题。 递归的三...
  • JAVA递归算法及经典递归例子

    万次阅读 2019-05-03 14:52:16
    前言:递归(recursion):递归满足2个条件 1)有反复执行的过程(调用自身) 2)有跳出反复执行过程的条件(递归出口) 第一题:汉诺塔 对于这个汉诺塔问题,在写递归时,我们只需要确定两个条件: 1....
  • 递归转非递归

    千次阅读 2019-01-13 09:46:09
    能用递归解决的问题,必须满足以下两个条件: 一个问题能够分解成规模更小,且与原问题有着相同解的问题; 存在一个能让递归调用退出的简单出口。 存在一个能让递归调用退出的简单出口。 用递归实现判断回文串 #...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 294,646
精华内容 117,858
关键字:

递归