深入理解递归及尾递归-03

1.递归
  递归是一种强大的方法,在计算机科学领域中,递归是通过递归函数来实现的,也就是用函数调用自身。

举例:编写一个程序使用递归求n的阶乘

int fac(int n){
	
	if (n < 0){
		return 0;
	}	
	else if (n == 0){
		return 1;
	}
	else if (n == 1){
		return 1;
	}
	else
		return n * fac(n-1);
	 
}

int main(void){
	int m; // 输入n
	printf("输入数字m=");
	scanf("%d", &m);
	printf("%d的阶乘是%d\n",m,fac(m));
	return 0;
	
}

这就是递归的方式,可以正式定义为:
递归描述
递归的过程分为两个基本阶段:递推回归。如下图所示:
递归过程
  在递推阶段,每一个递归调用通过进一步调用自己来记住这次递归过程。当其中有调用满足终止条件时,递推结束。比如,在计算n的阶乘时,终止条件是当n=1和n=0。每一个递归函数都必须拥有至少一个终止条件;否则,递推阶段将永远不会结束了。一旦递推阶段结束,处理过程就进入回归阶段,在这之前的函数调用以逆序的方式回归,直到最初调用的函数返回为止,此时递归过程结束。
  
  为了理解递归究竟是如何工作的,有必要先看看C语言中函数的执行方式。为此,还需要了解一点关于C程序在内存中的组织方式。基本上来说一个可执行程序由4个区域组成:代码段静态数据区

  • 代码段包含程序运行时所执行的机器指令
  • 静态数据区包含在程序生命周期内一直持久的数据,比如全局变量和静态局部变量。
  • 包含程序运行时动态分配的存储空间,比如用malloc分配的内存。
  • 包含函数调用的信息

当程序调用一个函数时,栈中会分配一块空间来保存与这个调用相关的信息,每一个调用被当做是活跃的。栈上的那块存储空间为活跃记录,或者称为栈帧。栈帧由5个部分组成:输入参数、返回值空间、计算表达式时用到的临时存储空间、函数调用时保存的状态信息以及输出参数
   一个活跃记录中的输出参数就成为栈中下一个活跃记录的输入参数。函数调用产生的活跃记录将一直存在于栈中直到这个函数调用结束。例如,计算4!时栈中记录的情况:
  
初始调用fac会在栈中产生一个活跃记录,输入参数n=4;由于这个调用没有满足函数的终止条件,因此fac将继续以n=3为参数递归调用,这将在栈上创建另一个活跃记录,输入参数n=3;n=3也是第一个活跃期中的输出参数,因为正是在第一个活跃期内调用fac产生了第二个活跃期。这个过程将一直继续,直到n的值变为1,此时满足终止条件,fac将返回1。

一旦当n=1时的活跃期结束,n=2时的递归计算结果就是2×1=2,因而n=2时的活跃期结束,返回值为2。结果就是n=3时的递归计算结果表示为3×2=6…,直到函数已经从最初的调用中返回,递归过程结束。
  
  栈是用来存储函数调用信息的绝好方案,这正是由于其后进先出的特点精确满足了函数调用和返回的顺序。然而,使用栈也有一些缺点。栈维护了每个函数调用的信息直到函数返回后才释放,这需要占用相当大的空间(开销变得很大)。幸运的是,可以采用尾递归的特殊递归方式来避免前面提到的这些缺点。

2.尾递归
一个函数中所有递归形式的调用都出现在函数的末尾。由于函数自身调用次数很多,递归层级很深,尾递归优化则使原本 O(n) 的调用栈空间只需要 O(1)。(空间复杂度)

以python为例,主要区分普通递归和尾递归对栈空间的使用:

def recsum(x):
  if x == 1:
    return x
  else:
    return x + recsum(x - 1)

调用recsum(5)为例,描述相应的栈空间变化:

recsum(5)
5 + recsum(4)
5 + (4 + recsum(3))
5 + (4 + (3 + recsum(2)))
5 + (4 + (3 + (2 + recsum(1))))
5 + (4 + (3 + (2 + 1)))
5 + (4 + (3 + 3))
5 + (4 + 6)
5 + 10
15

尾递归的方式

def tailrecsum(x, running_total=0):
  if x == 0:
    return running_total
  else:
    return tailrecsum(x - 1, running_total + x)

对内存的消耗

tailrecsum(5, 0) 
tailrecsum(4, 5) 
tailrecsum(3, 9)
tailrecsum(2, 12) 
tailrecsum(1, 14) 
tailrecsum(0, 15) 
15

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。

这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数5和1?

两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

function factorial(n) {
  return tailFactorial(n, 1);
}

factorial(5) // 120

函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值