如何写递归程序 以及 递归和栈的联系
2014-05-13 13:35
197 查看
这几天看到一些递归算法的程序,有些不明白,虽然递归形式上很简洁,但是当你自己写的时候,递归本质上是栈,几乎所有的递归都可以写成循环,同理反之,在自己写递归程序的时候其实得非常清楚的指导整个程序的出栈入栈的过程,查阅了一些文献,做了一些总结,希望各位同仁寄予指点。
1 什么时递归?若在一个函数、过程、或者数据结构定义的内部,直接(或间接的)出现定义本身的应用
2 什么情况下用递归
当较大规模的问题可以分解成一个或者多个规模更小,但更具有类似原问题特性的子问题的时候,即较大问题可以递归的调用较小的子问题来描述。
3 怎么用
其实通俗的来说,递归最重要的就是要抓住一条原则:你默认当前递归调用的子函数已经帮你解决了子问题!不要去想它是如何解决的,然后考虑的就是边界条件了。
这个原则看上去很简单,其实很多递归算法的思想就是这么一回事,比如阶层f(n)=n!,我在递归的时候,我就当f(n-1)这个子问题已经给我算好了,f(n)=n*f(n-1)就很理所当然的就解决了,需要做的就是判断边界条件;类似的问题还有求一个二叉树的深度问题也是的,一个树的深度就是1+两个左右子树最大值,depth(root)=1+Math.max(depth(root.left)+depth(root.left));类似的问题还有前序中序后序遍历或者求一个单链表的逆序(思想是:reverse(head)和reverse(head.next)之间其实就是把head放到reverse(head.next)后面,思路很巧妙,比用三个指针移来移去,指来指去要好理解的多)等。也就是说在我只需要从最整体上考虑子问题,利用子问题来求解那个大问题。
但是问题在于:当我需要巧妙利用递归问题来求别的问题的时候,我们就需要深入到递归内部,比如说,这样一个问题,求解一个链表,求倒数第K个字符,怎么样来用递归来做,我觉得要深刻理解这个递归也非常清楚得知道其出栈入栈的原理,要不然你写程序的时候思路会很混乱。
我们先来看一下栈在递归算法的内部实现中所起到的作用
调用函数时:系统将为调用者构造一个由参数表和返回地址组成的活动记录,并将其压入到由系统提供的运行时刻栈的栈顶,然后将程序的控制权转移到被调用函数,若被调用函数有局部变量,在运行时刻栈的栈顶也要为其分配相应的空间。因此,活动记录和这些局部变量形成了一个可供被调函数使用的活动结构。
局部变量 |
返回地址(函数调用语句的下一个指令的位置) |
参数表 |
②被调函数执行完毕时:系统将运行时刻栈栈顶的活动结构退栈,并根据退栈的活动结构中所保存的返回地址将程序的控制权转移给调用者继续执行。
例如求f(n)=n!用递归的话它的入栈出栈过程如下:
public static long Factorial(int n){ if(n==0) return 1; else return n*Factorial(n-1); }这个是最简洁的写法,但是为了说明参数表,和返回地址出栈关系,我们换一种写法:
public static long Factorial_2(int n){ long temp; if(n==0) return 1; else{ temp=Factorial_2(n-1); temp=n*temp; return temp; } }
下面说明其参数表和返回地址出栈入栈的逻辑图(活动结构省略了局部变量)
它每递归调用一次就进行一次压栈
入栈过程:
参数表 返回地址
3 | (6) |
2 | (6) |
3 | (6) |
1 | (6) |
2 | (6) |
3 | (6) |
当我遇到n==0 return1;后此时返回的地址在(6)这行代码的地方,首先完成的运行时刻栈栈顶的退栈
2 | (6) |
3 | (6) |
3 | (6) |
具体的步骤大家可以用个设置断点的方式来模拟走一遍,走完就清楚整个过程了。
了解这些原理看,我们回头看刚才那个求一个链表中倒数第K个元素的递归算法,
一看这个好像不好用递归,因为我们好像找不到这个问题的子问题,子问题(以头结点开始的链表的倒数第K个字符和以第二个结点开始的链表的倒数第K个字符这个大问题并不是小问题的联系),但是,我们可以这样考虑,求倒数第K个字符,可以把所有的结点压入一个栈,然后当我弹出第K个的时候,那个结点不就是倒数第K个结点嘛,但是毫无疑问我们new出一个栈内存空间,无疑会以要牺牲空间,但是我们知道递归里面就有栈结构,为何不可以运用递归里面的栈呢?
根据刚才递归入栈出栈的原理,我们可以让链表递归自己(就是模拟入栈),当入到最后一个的时候,设置边界条件,开始出现return,一次return后,系统会在运行时刻栈将栈顶元素出栈,同时我们记录弹出了多少个元素了(注意用全局变量,因为局部变量会在每一次递归的时候重新赋值)
static int level = 0; public static void reGetKthNodeRec(Node head, int k) { if(head == null||k<=0) return; reGetKthNodeRec(head.next, k); level++; if(level == k) System.out.println(head.val); }虽然这个程序很短,但是还是非常巧妙的运用了递归,值得去深入研究一下。以后遇到类似需要栈的时候,可以不用额外开辟栈空间,保证了算法的高效。
类似的比如我们想一个节点在一棵二叉树中从根节点开始到它自己这条路径,
当然,用到树,递归肯定会派上用场,大致思路是这样的:
我们定义这样一个函数,求node在root这个树种的路径,用一个arraylist保存
private static boolean get
9934
NodePath(TreeNode root,TreeNode node,ArrayList<TreeNode> path)
先判断边界条件(任何函数的参数,动手写前,第一个要考虑的就是边界条件),这里我们需要判断 root为不为空
把这个根节点加入到队列中,然后看下这个根节点是不是和node相等,如果相等,说明,此时到头了,
然后关键是定义一个Boolean类型的标志,来判断根结点的左右结点往下存不存在要找的那个结点,左右结点都不存在的话,就把这个结点从当前链表删除
private static boolean getNodePath(TreeNode root, TreeNode node,ArrayList<TreeNode> path) { if(root==null) return false; path.add(root); if(root==node) return true; boolean found=false; found=getNodePath(root.left, node, path); // 先在左子树中找 if(!found) // 如果没找到,再在右子树找 found=getNodePath(root.right, node, path); if(!found) // 如果实在没找到证明这个节点不在路径中,说明刚才添加进去的不是路径上的节点,删掉! path.remove(root); return found; }
之前写程序的时候还发现有一个题目,我觉得是利用递归里面的栈的最佳的一个例子。
比如说要实现从尾到头打印一个单链表,最直接的思路就是,new一个栈空间,将链表的元素一次放入到栈中,然后逐个将栈中元素弹出;
看到栈,我们可以联想到递归里面的栈,每调用一次递归函数,就相当于入一次账,到递归的边界条件时,开始出栈
/** * 从尾到头打印单链表 * 对于这种颠倒顺序的问题,我们应该就会想到栈,后进先出。所以,这一题要么自己使用栈,要么让系统使用栈,也就是递归。注意链表为空的情况 * 。时间复杂度为O(n) */ public static void reversePrintListStack(Node head) { Stack<Node> s = new Stack<Node>(); Node cur = head; while (cur != null) { s.push(cur); cur = cur.next; } while (!s.empty()) { cur = s.pop(); System.out.print(cur.val + " "); } System.out.println(); }
看看用递归怎么实现
/** * 从尾到头打印链表,使用递归(优雅!) */ public static void reversePrintListRec(Node head) { if(head==null) return ; reversePrintListRec(head.next); System.out.println(head.val); }
总而言之,对于递归,一定要深入理解其中入栈出栈时的过程,这样当自己写的时候才有自己的思路
3(6)
相关文章推荐
- 如何获取某个进程的主窗口以及创建进程的程序名?
- STM32 I/O的耐压问题,中断问题,以及如何在Keil (RVMDK) 中观察程序的执行时间
- 递归程序如何实现及其原理?
- 如何制作Java可执行程序以及安装程序(补上了图片)
- Java程序、JSP以及JavaScript中如何判断某个字符串是否包含某个子串
- 利用PROGISP实现ARDUINO IDE编写的程序的下载以及如何把AVR单片机做成ARDUINO板
- (3)分布式下的爬虫Scrapy应该如何做-递归爬取方式,数据输出方式以及数据库链接
- 如何获取某个进程的主窗口以及创建进程的程序名?
- asp小偷程序如何利用xmlhttp实现表单的提交以及cookies或session的发送
- Ubuntu 如何查看端口使用情况以及停止使用该端口号的程序
- 如何使对话框程序启动以及主窗口最小化时不在任务栏上显示
- C语言系列(二):最近重拾C语言的想法,谈到C中易错点,难点;以及开源代码中C语言的一些常用技巧,以及如何利用define、typedef、const等写健壮的C程序
- 如何利用gdb调试程序之细节(info reg命令以及寄存器地址)
- VS2013如何生成exe文件以及如何更改exe程序图标
- 栈的理解以及如何计算程序所需栈的大小并在IAR中设置栈
- 使用ConfuserEx加密混淆程序以及如何脱壳反编译
- 使用ConfuserEx加密混淆程序以及如何脱壳反编译
- 【万字总结】探讨递归与迭代的区别与联系及如何求解10000的阶层
- 如何获取某个进程的主窗口以及创建进程的程序名(进程映像名)
- (4)用记事本写Java程序HelloWorld,以及用控制台如何执行程序