• 递归算法设计


    基本概念
    在定义一个函数时,出现调用自身函数的,称为递归(recursion)。
    如果一个递归函数,最后一条语句是递归调用语句,则称这种递归调用为尾递归(tail recursion)。
    一个递归模型通常有两部分构成:初值(递归出口)和递归体。
    递归的使用条件
    递归的数学定义,比如斐波那契数列:F(1)=F(2)=1,F(n)=F(n−1)+F(n−2),n≥3F(1)=F(2)=1,F(n)=F(n−1)+F(n−2),n≥3。
    递归的数据结构,出现了指向自身的指针或者引用,如链表、树、图等。
    递归的求解方法。比如经典的汉诺塔问题。
    递归函数的时间空间
    求解递归函数的时间通常需要根据问题解出相应的递归式。
    对于形如归并排序的分治算法,其递归式通常形如T(n)=aT(bn)+f(n)T(n)=aT(bn)+f(n),通常可以使用主定理(《算法导论》 p53)来求解。
    一般情况的递归算法的时间分析可能比较困难,需要详细了解递归的执行过程。比如动态规划法和暴力算法都可以使用递归,但是他们的时间复杂度有显著差异。
    递归的空间复杂度除了要考虑分配的临时变量之外,还需要考虑递归的深度(虽然使用的是栈空间,也要将其计算在内。)

    递归程序非递归化
    对于递归的实现机理,需要理解现代CPU的栈帧模型。栈帧保存了当前函数状态的相关信息。当函数调用另一个函数时,它将保存临时变量等信息,同时为被调用的函数开辟另一个帧。因此递归函数调用,每一层是不会相互影响的。
    通常情况下递归是由编译器自动实现,然而系统的栈空间是固定的,对于递归深度较大的情况,可能会出现栈溢出(stack overflow),因此这时候必须用栈的数据结构手动模拟栈帧,来实现递归程序非递归化。具体操作来说,就是在原来递归出现之前,利用栈保存前一层的环境,然后切换到下一层;在原来递归返回到调用函数时,将栈顶元素出栈,得到被保存的环境。
    一个例子可以参考之前第3章综合练习的一道题目字符串解码(Decode String)。
    需要注意的是,编译器在自动实现递归的过程中,它能够自动将传值的参数进行恢复,但是不能将传地址(引用)的参数(尤其是定义在全局变量、堆空间的)进行恢复。这种情况下,需要手动将其恢复。这个问题可以思考DFS遍历迷宫时,用int [][]和vector<vector<int>>的异同:因为vector是一个类对象,每次自我调用的压栈都会将其复制一份,在返回时出栈也要调用析构函数销毁。这样保证了每次使用的vector<int>都是相互独立的,自然不需要手动恢复。而int [][]则是直接传地址,在一个地方修改了,其他地方也是修改的。虽然使用vector<vector<int>>能够省去恢复的麻烦,但是其反复复制元素造成的效率问题也是不容忽视的。

    递归算法的一般设计步骤
    对于一般的递归问题,通常需要先转化成一个包含有初始状态和递推状态的模型。
    递归一般是将较复杂的大问题,利用递推关系转化为一个或者多个相对较小的问题。直到最后每个子问题都满足初始条件(达到递归出口)。
    对于递归定义问题的求解,直接利用定义进行递推就可以了。某些较为复杂的,非简单的数学问题,就需要抽象出递推关系。
    抽象递推关系也是非常重要的,动态规划算法就是基于递推关系来确定最优解的。
    如何具体设计递归算法,包括简单的回溯法(back-tracking)、动态规划算法(dynamic programming),会在习题里结合具体的例子说明。
    ---------------------
    作者:_g63
    来源:CSDN
    原文:https://blog.csdn.net/jsxyg63/article/details/78306061
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    Bootstrap_警示框
    Bootstrap_标签
    Bootstrap_分页
    Bootstrap_导航条
    Bootstrap_导航
    Bootstrap_按钮工具栏
    Bootstrap_下拉菜单
    Bootstrap_网格系统
    Bootstrap_表单_图标
    统计学习方法 李航---第12章 统计学习方法总结
  • 原文地址:https://www.cnblogs.com/theEndOfSummer/p/10513860.html
Copyright © 2020-2023  润新知