递归算法设计
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
递归算法设计
基本概念
在定义⼀个函数时,出现调⽤⾃⾝函数的,称为递归(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>>能够省去恢复的⿇烦,但是其反复复制元素造成的效率问题也是不容忽视的。