中序遍历的递归算法法中递归函数的参数+1和在函数里面定义变量+1有什么区别?(举个例子,就像图中这个中序遍历的递归算法法)

请登录查看
小白们在学习递归函数的时候可能无法搞清楚递归函数的原理和运行机制,递归函数是常用到的一类函数,最基本的特点是函数自身调用自身,但必须在调用自身前有条件判断,若满足条件,则调用函数本身,若不满足则终止本函数的自调用,然后把目前流程的主控权交回给上一层函数来执行,否则就会无限调用下去。下面先介绍php实现递归函数的3种基本方式。一、利用引用做参数先不管引用做不做参数,必须先明白引用到底是什么?引用不过是指两个不同名的变量指向同一块存储地址。本来每个变量有各自的存储地址,赋值删除各行其道。现在可好,两个变量共享一块存储地址。 $a=&$b; 。实际上指的是 $a 不管不顾自己原来的存储地址,非要和 $b 共享一室了。因而任何对存储地址数值的改变都会影响两个值。    函数之间本来也是各行其是,即便是同名函数。递归函数是考虑将引用作为参数,成为一个桥梁,形成两个函数间的数据共享。虽然两个函数见貌似操作的是不同地址,但是实际上操作的是一块儿内存地址。function test($a=0,&$result=array()){
if ($a&10) {
$result[]=$a;
test($a,$result);
上面的例子非常简答,以a&10作为判断条件,条件成立,则把a赋给result[];将result的引用传入函数,会将每一次递归产生的a添加到结果数组result。因而本例生成的$result数组是 Array ( [0] =& 1 [1] =& 2 [2] =& 3 [3] =& 4 [4] =& 5 [5] =& 6 [6] =& 7 [7] =& 8 [8] =& 9 ) 。本例比较有意思的是echo a的值。相信很多人认为是吧,其实不然,是。为什么呢?因为函数还没执行echoa前就进行了下一次的函数递归。真正执行echo a是当a&10条件不满足的时候,echo a,返回result,对于上一层而言,执行完递归函数,开始执行本层的echo $a,依次类推。二、利用全局变量利用全局变量完成递归函数,请确保你确实理解什么是全局变量。global在函数内申明变量不过是外部变量的同名引用。变量的作用范围仍然在本函数范围内。改变这些变量的值,外部同名变量的值自然也改变了。但一旦用了&,同名变量不再是同名引用。利用全局变量实现递归函数没必要理解到这么深的一层,还保持原有对全局变量的看法就可以顺理成章理解递归函数。function test($a=0,$result=array()){
if ($a&10) {
$result[]=$a;
test($a,$result);
三、利用静态变量我们常常在类中见到static,今天我们把它利用到递归函数中。请记住static的作用:仅在第一次调用函数的时候对变量进行初始化,并且保留变量值。  举个例子:function test(){
static $count=0;
这一段代码的执行结果是多少?是00000么?必然不是。是01234。首先第一次调用test(),static对 $count 进行初始化,其后每一次执行完都会保留 $count 的值,不再进行初始化,相当于直接忽略了 static $count=0; 这一句。  因而将static应用到递归函数作用可想而知。在将需要作为递归函数间作为“桥梁&的变量利用static进行初始化,每一次递归都会保留&桥梁变量&的值。function test($a=0){
static $result=array();
if ($a&10) {
$result[]=$a;
小结所谓递归函数,重点是如何处理函数调用自身是如何保证所需要的结果得以在函数间合理&传递&,当然也有不需要函数之间传值得递归函数,例如:function test($a=0){
if ($a&10) {
php函数递归调用实例function arrContentReplact($array)
  if(is_array($array))
  foreach($array as $k =& $v)
  $array[$k] = arrContentReplact($array[$k]);
  $array = str_replace(
  array('&', '&'),
  array('{', '}'),
  $array
  return $
  $arr = array(array(&& 小刚&&,&& 小晓&&,array(&&小强&&,array(&&浪人&&))),&& 小飞&&,&& 小李&&,&& 小红&&);
  $arr3 = arrContentReplact($arr);
  echo &
  print_r($arr3);
  echo &
PHP递归实现无限级分类在一些复杂的系统中,要求对信息栏目进行无限级的分类,以增强系统的灵活性。那么PHP是如何实现无限级分类的呢?下面介绍一下使用递归算法并结合mysql数据表实现无限级分类的方法。1、Mysql首先我们准备一张数据表class,记录商品分类信息。表中有三个字段,id:分类编号,主键自增长;title:分类名称;pid:所属上级分类id。class表结构:CREATE TABLE IF NOT EXISTS `class` (
`id` mediumint(6) NOT NULL AUTO_INCREMENT,
`title` varchar(30) NOT NULL,
`pid` mediumint(6) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=MyISAM
DEFAULT CHARSET=utf8;
插入数据后,如图:2、PHP根据不同的需求,我们提供两种不同格式的自定义函数,一种是返回字符串,一种是返回数组,两种函数都使用了递归方法。先看返回字符串格式的函数:function get_str($id = 0) {
$sql = &select id,title from class where pid= $id&;
$result = mysql_query($sql);//查询pid的子类的分类
if($result && mysql_affected_rows()){//如果有子类
$str .= '&ul&';
while ($row = mysql_fetch_array($result)) { //循环记录集
$str .= &&li&& . $row['id'] . &--& . $row['title'] . &&/li&&; //构建字符串
get_str($row['id']); //调用get_str(),将记录集中的id参数传入函数中,继续查询下级
$str .= '&/ul&';
以上函数get_str()通过递归,不断查询下级分类,并最终返回字符串,大家可以根据项目需求修改其中的str,最终生成一个无限分级列表:include_once('connect.php'); //连接数据库,connect.php文件自己写一个啊
echo get_str(0); //输出无限级分类
效果如图:接着我们来看返回数组格式的函数,一样要使用递归:function get_array($id=0){
$sql = &select id,title from class where pid= $id&;
$result = mysql_query($sql);//查询子类
$arr = array();
if($result && mysql_affected_rows()){//如果有子类
while($rows=mysql_fetch_assoc($result)){ //循环记录集
$rows['list'] = get_array($rows['id']); //调用函数,传入参数,继续查询下级
$arr[] = $ //组合数组
函数get_array()返回了数组,这是我们期待的,所以推荐使用get_array()得到数组,这样一来,我们可以对数组进行任意操作,比如我们可以将数组转换成json格式的数据传给前端页面,前端页面可以通过解析json数据灵活展示分类信息。比如树形结构的分类列表,下拉分类列表等。include_once('connect.php'); //连接数据库
$list = get_array(0); //调用函数
print_r($list); //输出数组
输出效果:如果要输出json格式的数据,则可使用:echo json_encode($list);
意见反馈:
联系方式:
广告等垃圾信息
不友善内容
违反法律法规的内容
不宜公开讨论的政治内容快速排序的优化 和关于递归的问题,说说我的想法 - 简书
快速排序的优化 和关于递归的问题,说说我的想法
James Gosling
贴吧习惯,先放张大佬照片骗骗流量镇楼。
本文有七千字,阅读大约需要占用你10分钟时间。
好吧。。随便写的,我也不知道会花多久看完。因为写的比较烂,而且只是结合我查的资料说说想法,可能有很多错误的地方,也许看着看着你就不想看了。
春招都讲究“金三银四”,三月份各种内推蜂拥而至,我的求职第一步却还迟迟没有迈出。我不想毫无准备就草草上阵,但由于复习的晚,时间上又特别紧张。最近感觉自己越来越焦虑了。面对向往的公司,就像面对喜欢的女生一样,渴望能在一起却又迟迟不敢迈出相遇的第一步。
最近忙着准备找工作,想赶在三月份结束前投一波内推。所以其实有挺多想分享的东西,却一直没时间更新博客。最近应该也不会写太多,毕竟这个还是挺花时间的。我想着这段时间憋个大招,等到过几个月如果拿到了满意的offer,就也像大神们一样写个面经,像什么《如果拿到XX公司offer》《我的面试经历》之类的文章供学弟学妹们膜拜一下,哈哈。当然这是求职成功的情况,我也不能保证你们肯定能看到我的这个大招。
写这篇文章的原因是今天对递归问题产生了一个疑惑,问了很多人,一直也没得到满意的答案。感觉这个问题挺有意思,就写出来和大家分享一下,也说说我的想法。因为是了解快排优化时候想到的问题,就连着快排优化也一起写了。
晚上写的,有点困,思路不太清晰。而且我也只是参考其他人的文章,分享下个人观点,可能有很多错误的想法或者没有说清楚的地方。如果有不同的观点欢迎讨论指正。下面进入正题。
1. 快排深入探索
快排是一个经典并且被广泛应用的排序算法,是很重要的知识点。之前简单介绍过快排,相信大家随手写一个快排已经没有问题了。但是光知道怎么写是远远不够的,所以今天我们先继续探索下快排。
1.1快排为什么快
之前分析过快排复杂度,当划分均衡时,平均时间复杂度O(nlogn),空间O(logn);当划分完全不均衡时,最坏时间O(n?),空间O(n)。而堆排序平均最坏时间复杂度都为O(nlogn),但为什么实际应用中快排效果好于堆排?
原因主要有三个:
虽然都是O(nlogn)级别,但是我们知道时间复杂度是近似得到的,快排前面的系数更小,所以性能更好些。
堆排比较交换次数更多。因为快排是枢轴(pivot)左边的元素都比pivot小,右边的元素都更大,比较交换次数会比堆排更少些。具体可以参考下这篇文章
第三个原因也是最主要的原因,和cpu缓存(cache)有关。学计算机组成原理时候我们了解过,cpu有一块高速缓存区(cache)。堆排序要经常处理距离很远的数,不符合局部性原理,会导致cache命中率降低,频繁读写内存。不了解cache的同学可以看一下下面这篇文章。
1.2快排如何优化
快排就一定快吗?当然不是的。考虑如下情况:
a.划分不均匀
对于分治算法,当每次划分时,算法若都能分成两个等长的子序列时,那么分治算法效率会达到最大。如果输入序列有序(当然正常情况算法应该判断当序列有序时直接返回),我们选最左边元素做枢轴点,即划分极度不均匀时,不仅时间复杂度变成O(n?),递归深度也变成了n,这样很容易令栈内存溢出。所以要尽量避免这种情况。
b.当有大量重复元素时
每趟排序对每个子序列只产生一个有序位置, 所以对数组中相等元素的处理效率不是很高。如果有大量重复元素,实际上快排做了很多无用工作。由于划分函数的特点,对于一个每个元素都完全相同的一个序列来讲,快速排序也会退化到 O(n^2)。
针对上面两种情况,提出对应的优化方式a、b。
a. 优化枢轴点选取方式
1.三数取中,选三个数(也可以更多)作为样本,取其中位数作为枢轴点,这样划分更均衡。
2.直接找到中位数作枢轴点。选取中位数的可以在 O(n)时间内完成。证明见《算法导论(第二版) 》 第九章中位数。
b. 三分序列
经典快排分成大于枢轴和小于枢轴两堆,而遇到等于枢轴的元素时根据具体实现不同全部在枢轴左边或右边。所以才会导致元素全部一样时退化到O(n^2)的问题。所以改进方法是在分区的时候,将序列分为 3 堆,一堆小于中轴元素,一堆等于中轴元素,一堆大于中轴元。次递归调用快速排序的时候,只需对小于和大于中轴元素的两堆数据进行排序。
实现方法和荷兰国旗问题完全一样(关于荷兰国旗问题可以参考)。
除了上面的两种优化方式,当序列很小时改用插排也是常用的优化方式。
c. 小数组变插入排序
为什么当数据很少时要用插排呢?其实有时候当规模较小时分治算法效率是不如普通算法的。插入排序中包含的常数因子较小,使得当 n 较小时,算法运行得更快。因此,当数组递归子序列的规模在一个固定阀值(阀值指数组的最大长度)以下时,采用插入排序对子序列进行排序,能够缩短排序时间。一般阀值选取在 7到10 个元素,我记得Java里的Arrays.sort()方法好像是用的7个元素(当元素是基本类型时sort用的是优化版快排,当元素是对象时用的是归并排序,因为对于对象需要稳定性)。
d. 多线程快排
首先要特别说明,多线程是个很重要的知识,无论在操作系统还是java语言里。面试中也是必问的东西,一定要好好掌握。
我们之前的优化都局限在单线程程序。快速排序对每个子序列的排序都是独立的,因此快排是比较适合用多线程实现的。那为什么要用多线程实现呢?
对于多核处理器,单线程只会映射到一个CPU上,而多线程会映射到多个CPU上,这样多线程实现的快排可以利用计算机的并行处理能力来提高排序的性能。这也是写多线程排序的实际应用场景。
对于单核处理器,多线程技术在一些情况如涉及大量IO,也可以起到最大限度地利用CPU资源的这个目的。不过对于快排这种情况,仅仅是做一种简单的计算,其间不涉及任何可能是使线程挂起的操作,如I/O读写,等待某种事件等等。多个线程与单个线程相比,增加了切换的开销,理论上应该更慢,所以在这种情况下,并没有必要使用什么多线程、并行的技术来优化快排,没什么实际意义,还不如多从别的角度想想有没有优化空间。
而我在网上看到的一些文章,他们写的多线程排序其实并不是考虑多核并行这个特性,各线程只是并发的(注意并行和并发区别)。这些代码可能双核的机器生成了有上千个线程,但是他们的测试结果却非常快,如果和单线程程序比较发现快了几个数量级。
图片来自该文章(注意这篇博客里写的利用计算机并行处理能力是不准确的,如果仅仅希望利用计算机的并行能力,双核的电脑写两个线程处理就可以达到)
这样做能大大提高运行时间的真正原因是,多个线程可以让操作系统分给程序更多的时间片来执行代码。这和操作系统的调度策略有关。对同样一个进程来讲,多一个线程就可以多分到CPU时间。举例说明,假如在你的程序启动前,系统中已经有50个线程在运行,那么当你的程序启动后,假如他只有一个线程,那么平均来讲,它将获得1/51的CPU时间,而如果他有两个线程,那么就会获得2/52的CPU时间(这里仅仅是举例,真实情况当然更复杂),就是说这样做只是提高了快排程序抢夺资源的能力,相应的,其他程序运行时间就要更久一点。
多线程版的快速排序,还没来得及亲自实现。正好最近在看《Thinking in Java》,等看到并发的时候一定要自己写一个多线程的快排。我也强烈推荐你这么做。因为我觉得如果面试时候聊到了快排,能把常用的优化方式说出来只能令面试官觉得你基础不错。但是如果能随手写出来一个多线程版的快排,聊一聊这么做的利弊,那一定能让人眼前一亮。
另外Java7里提供了一个用于并行执行任务的框架,Fork/Join框架。如果你能继续“不经意间”提起Java7的这个新框架,再用Fork/Join再写一个,然后聊聊Java7\8的新特性,那面试官的表情一定是下面这样的。
友情提示:装逼有风险,实践需谨慎
下面是我看的几篇文章,详细介绍了多线程快速排序的内容,大家可以好好看看。因为还没亲自实现,我就先不贴代码了。不过需要注意搞清楚到底是因为什么原因他们的代码提升了效率。就像我上面分析的一样,其实有些代码并不完全依靠多核并行的优势,更大程度是靠多线程获得更多CPU时间。这也可以解释为什么有的文章里分别实现了普通多线程版和Fork Join版,测试后却发现用Fork/Join的程序并没有提高效率。
参考文章:
e. 按位拆分快速排序并行算法
关于这个算法我也不了解。是我在网上搜索资料的时候偶然看到一篇论文提到的。摘要如下:
针对大数据量排序算法优化问题,提出一种基于Java的按位拆分的排序新算法。该排序算法按照位拆分数据,并结合Java的多线程对拆分的数据进行并行处理。数据实验结果表明,对于大数据量排序,该算法性能明显优于快速排序算法,而且算法具有很好的并行效率。
我也没有仔细看。这里把链接附上,有兴趣的同学可以看看,弄明白了记得给我讲一下是怎么回事哦。
f. 在知乎上看到的一个思路(存疑)
知乎上看到的一个答案里最后提了这么一句:
最后一个来自《算法导论》的丧心病狂的思路:选定一个 k,当序列长度小于 k 时,sort 函数直接不作处理返回原序列。整个序列经过这样一次 sort 之后当然不是有序的,此时对这个序列做一次插入排序(因为插入排序在处理 “几乎” 有序的序列时,运行非常快)。根据算导的结论,这样的复杂度是 O(nk + n log(n/k)) 的。作者:匿名用户链接:
因为答主说了是来自《算法导论》里提供的思路,所以我就把它也算上了。但是我个人认为这个方法并没什么卵用。。。
我是这样分析它的复杂度的。按照这个思路,第一次快排的时间复杂度是O((n/k)log(n/k))≈O(nlogn-nlogk)(常系数k比较小忽略了)。针对这个几乎有序的序列,插排的复杂度是O(nk),两次加起来就是O(nlogn-nlogk+nk),和答主给的复杂度是一样的,但问题是O(nlogn-nlogk+nk)&O(nlogn)啊。。。这样做的目的是什么。。
其实看过我之前文章的同学应该知道,处理几乎有序序列最快的原地排序方式并不是插排,而是堆排方式(参考)。这种方式能把几乎有序序列的排序优化到O(n*logk),但即使这样,加起来总的复杂度还是O(nlogn-nlogk+nlogk)=O(nlogn),这不是一样的吗?
也可能是我困了没想明白。。如果有不同意我想法的同学欢迎在评论区纠正。
刚刚说了这么多,那跟递归有什么关系呢?
好吧。。。并没有什么关系。
开个玩笑~当然有关系了。
因为快排就是一个递归算法,我们知道递归的问题是如果递归太深容易栈溢出。而针对快排的优化,还有一个角度是对递归的优化。网上很多文章都提到一个叫做尾递归优化的东西,比如下面这篇文章。
为了更好的理解,我了解了一下什么叫尾递归。
2.1 尾递归
尾递归是尾调用(Tail Call)的一种。(Tail Call)是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数(注意这里是最后一步,不是最后一行)。具体概念看这两篇文章吧,我就不复述了。还有这个漫谈递归系列,写的也非常好,介绍了关于递归的各种知识,有兴趣的同学可以看一下。我觉得主要是函数式语言的程序员用得到尾递归,对于我们java程序员了解即可(我们一般直接写迭代了)。
2.2 所谓的“快排尾递归优化”真的是尾递归优化吗?
了解完什么是尾递归后,我反而对“快速排序的尾递归优化”的方法产生了疑问。比如有博客里是这么写的:
QUICKSORT中的第二次递归调用并不是必须的,可以用迭代控制结构来代替它,这种技术叫做“尾递归”,大多数的编译器也使用了这项技术。来自:
关键代码如下:
void QuickSort(int *a,int p,int r)
while(p&r)//直到把右半边数组划分成最多只有一个元素为止,就排完了!
q=RandomPartition(a,p,r);
QuickSort(a,p,q-1);
首先上面代码中递归调用并不是最后一步,甚至最后一行都不是,和我们上面看到的尾递归的定义不太一样。如果我们暂且不考虑它到底是不是尾递归,那么用这样的方式到底能不能降低递归深度呢?这篇文章里分析了一下为什么可以降低递归深度。但是我个人认为这样做是完全不能起到优化作用的。我们可以自己举个具体例子,画个递归树。我得出的结果是普通递归的快排和所谓的“尾递归优化”的快排的递归过程是一样的,都是先对枢轴左边的子数组递归调用,再对枢轴右边的子数组递归调用。上面代码的只是把对枢轴右边的子数组的递归调用放到了下一次循环里。所以我觉得只是换了个写法而已,实质并么有改变。而且我们也举个极端点的例子,就是划分极度不均衡,每次枢轴点右面的子数组大小都为0,递归树此时所以节点都只有左孩子,退化成一条链表,此时递归深度为n。然后用上面代码模拟一下发现递归深度还是n,没有优化。所以对于很多博客里给出的这种思路我是不赞同的。
那么快排有没有办法写成真正的尾递归的形式呢?要弄明白这个问题,就要思考什么样递归可以改成尾递归,什么样的递归不能改,是不是所有递归都可以改?
2.3 深入尾递归
先给出我的结论,快排可以写成真正的尾递归形式,而且所以的递归都可以写成尾递归的形式。因为尾递归和循环本质上是一样的(参考上面从汇编角度讲尾递归优化那篇文章,可以理解成编译器会把尾递归优化成循环),我们可以用人工模拟系统栈的方式用循环改写所有递归,那么尾递归也同样可以这样做。但是这么做究竟有没有意义,就是另一个需要讨论的话题了。
通过阮一峰的这篇文章里的例子,我们可以发现,其实文章里给的例子可以很轻松的写成循环形式。因为尾递归实际上就是把每次变化的变量通过参数传递给递归函数了,跟我们平时写循环不断更新局部变量是一样的。而事实上java程序员们也是这么做的,因为java编译器不提供尾递归优化,所以遇到这样的能写成尾递归的问题大家一般都直接用循环写了。就像下面求阶乘的代码所示。
int factorial(int n,int total) {
if (n == 1)
return factorial(n - 1, n * total);
factorial(5, 1) // 120
int factorial2(n){
int total=1;
while(n&0){
total=total*n;
我们可以发现,其实上面尾递归函数和循环函数是一样的,都是通过变量total保存函数运算结果,然后通过参数传递给下层递归函数。
如果我们可以确切的知道下层函数需要从上层函数得到什么信息,就可以像上面的方法一样,把所有用到的内部变量改写成函数的参数。但是我们知道很多递归函数是不满足这种情况的。比如快排,比如二叉树的深度优先遍历。在回溯的过程我们需要知道所有上层函数的信息,而这些信息,也就是递归深度,是不确定的,不能通过固定的参数传递。
但是如果我们把一个栈结构(List)作为参数呢?这样不就可以把所有函数调用信息传递下去了。就像stack overflow里的这个问题里的做的一样。如果我写的不清楚,你可以看一下这个回答,里面有实现代码,不过是scala的,看着有点费劲。
我觉得主要是函数式语言的程序员会使用这样的方式把普通递归改写成尾递归,毕竟函数式语言没有循环结构。对于java程序员来说都是直接用循环写了。但是这两种方式本质上是一样的。都是用人工模拟系统栈的思路实现的。把系统栈转化成建在堆上的人工栈。就像上面stack overflow里回答的一样。
Any recursive function can be be converted to use the heap, rather than the stack, to track the context. The process is called.
Tail recursion requires you to pass work, both completed and work-to-do, forward on each step. So you just have to encapsulate your work-to-do on the heap instead of the stack. You can use a list as a stack, so that's easy enough.
关于那个trampolining,是函数式编程里的一个概念,也是为了优化递归(因为递归对于函数式编程非常关键)。跟我想表达的东西没什么联系,如果对它好奇,可以跳转到下面的链接。因为我也没有弄明白它。。。了解的同学可以给我讲讲。。
写到这里我的体会就是:
函数式编程有必要了解一点,尤其是现在scala这么火。
多上上国外的程序员论坛还是有好处的,有些东西英文资料更多一些。
要多思考,多学习。
2.4 递归与非递归
写到这里,关于快排能不能尾递归优化的问题已经解决了,我的回答就是快排可以写成尾递归,但这是函数式语言程序员喜欢做的事,和java程序员用循环改写递归是一样的。那么接下来的问题就是这么做有意义吗?或者说,有没有必要把所有递归改写成循环?快排改成非递归版速度能更快吗?
写到这里真的好困了。。。所以我简单点说吧,下次有机会再详细说一说递归与循环。
关于递归和非递归的问题,也分两个阵营,循环党和递归党。由于以前受到过爆菊爆栈的伤害,而且一直听到的观点就是递归调用有栈溢出问题,并且效率低,只是写着简单点,思路好理解。所以我之前一直是坚定的循环党,奉行循环至上,属于遇到递归算法就一定会想办法写个非递归版本出来那种,平时也喜欢动不动就拿个非递归版的算法来炫耀。
奉行白人至上的3k党
但如果递归真的一无是处,函数式语言为什么要用递归表示循环?Java里的Arrays.sort方法为什么不写成非递归的?于是信仰不坚定的我很快就改阵营了。。。
我的想法是,栈空间比较小,递归深度太深时容易栈溢出,用自己维护一个栈来模拟递归的方式,虽然空间复杂度一样,但可以把这部分内存转移到堆上,防止栈溢出。在时间效率上,递归在函数调用上也有一个时间开销。
但问题是,其实我们在写递归时候,如果遇到栈溢出情况,往往是我们自己的算法有问题,而不是递归本身的问题。比如快排没有随机化选取枢轴时递归深度可能为n,但如果合理设计算法,去掉冗余,logn的递归深度一般是不会导致栈溢出的。即使是TB级别的数据,递归深度也不过是40个栈帧(当然这么算是不准确的,只是为表达我的意思)。而且栈是不是会爆掉这个很大程度上是取决于程序员对程序的把控的。比如说如果程序写的不好,在栈帧内存放大的数据,比如递归函数里申请 1KB 的栈上空间(例如写一个 char s[1024]; ),这个递归函数的最大调用深度达到 100 以上,就会导致 stack overflow(假设栈空间100KB)。如果输入数据稍微大一点就爆栈,首先应该反思自己的算法设计和代码,而不是归罪于递归。有大量冗余过程的算法,即使通过在堆上维护人工栈避免了栈溢出,也会因为太久的运行时间令人难以忍受。
而关于时间效率问题,虽然函数调用有时间开销。但是函数调用的开销主要是参数信息的压栈出栈,跳转(jmp or call),其中压栈出栈在非递归算法里也要执行,而且由于自己维护的栈在堆上,涉及到开辟空间和垃圾回收,List容量不够扩容时也需要额外的时间开销,速度反而比系统栈更慢。另外现在编译器对递归也会进行优化,所以说递归时间效率上更慢我觉得也是不妥的。即使有差距,应该也小到可以说是效率几乎一样了。
所以我觉得,既然递归有这么大的表现力,而且效率也并不更低,为什么要牺牲掉简洁优雅的代码,去费劲改成循环呢?当然我这里指的是那种需要人工模拟系统栈去模拟递归过程的递归。而像上面提到的求阶乘的那种递归,我认为都是属于不必要的递归,因为可以很轻松的用循环实现。或者也可以称为是可以优化的递归。总之,优化算法,最重要的应该是结合具体情况对算法本身优化,而不是粗暴的把递归改成非递归。
最后放张图作为结尾吧。
早睡早起!加油加油!}

我要回帖

更多关于 ackerman函数递归算法 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信