用递归与分治策略求解网球循环赛日程表_递归解法:让程序员的代码更优雅

 一切都应该尽可能地简单,但不要太简单 ——阿尔伯特·爱因斯坦

将复杂的问题简单化,抓到本质,能够让我们的编程更愉快。分治策略、动态规划都是如此,将复杂的问题分解成规模小的简单问题。本文介绍的递归,是很多算法都使用的一种方法,让复杂问题简单化,同时能让你的代码更加优雅

调用栈

讲递归之前,不得不提到调用栈(Call Stack),因为调用栈:

  • 使得递归成为可能
  • 同时有助于理解递归的执行细节。

自定义函数,使得代码可以复用。从某种程度上说,一个程序的执行过程,就是无数个的函数相互调用,达到实现最终目标的旅程。而这些函数之间的调用关系,就用调用栈来描述,这也是“函数调用栈”前四个字的由来,最后一个字对应LIFO的数据结构

函数需要区分定义(defined)和执行(executed, called)两种情形,以python为例:

  • def语句定义一个函数,本质上和赋值语句没有区别,只是bind了一个name到一个value (使得定义递归函数成为可能)
  • 函数真正执行时(called), 会创建一个Local Frame, 这个Local Frame维护着该函数内部的局部变量。

函数每执行一次,创建的一个Local Frame,所有的Local Frame使用函数调用栈(Call Stack, LIFO)维护,每个Local Frame对应该栈中的一个元素,里面记录着某个函数在某次执行时的一些局部变量(Local Variable, 函数的入参及函数内部定义的变量)。调用栈,让不同执行函数间的局部变量互相不干扰,使得程序能够正确执行。

函数调用栈在每个编程语言内部实现,使用起来很方便,但每次的函数调用,都会占用一定的内存。如果函数调用次数很多(一直push入栈),并一直不返回结果(没有pop出栈),即Call Stack很大,会占用大量的内存,可能会导致栈溢出,也就是著名的stack overflow

很多编程比赛中,为追求速度的极致,很多选手采用C、C++语言,很多有经验的选手,通常都将大的数组放在main函数,用全局变量表示。如果放在某个函数内部做为局部变量,即使递归调用次数少,但也可能因为局部变量太大,产生栈溢出。

何为递归

递归,简单说就是一个函数,自己调用自己。

调用栈,使得递归成为可能。递归和其他函数调用并没有本质不同,都是执行到某行,建立新的栈帧,执行完后弹出返回原点继续执行。

递归函数实现中的一个常见模式,以基本情况开始(base case),后面跟着的是递归情况(recursive case)。

  • 基本情况:定义了在输入是最简单输入数据下的求解,此时函数不调用自身,避免形成死循环。
  • 递归情况:此时函数调用自身,将规模大的原问题(复杂)分解为规模小的子问题(简单)。

理解了调用栈,再理解递归就相当简单。举一个计算阶乘的例子:

a1f66276fb5378cc182a48e726c48582.png

可以这么理解:

  1. CEO说(main函数对应的栈帧):总监, 给我算下f(3)
  2. 总监(f(3)函数对应的栈帧): 经理,给我算下f(2)
  3. 经理(f(2)函数对应的栈帧): 组长,给我算下f(1)
  4. 组长(f(1)函数对应的栈帧): 程序员,给我算下f(0)
  5. 程序员(f(0)函数对应的栈帧):报告组长,f(0) = 1;(f(0)执行完毕,销毁f(0)栈帧)
  6. 组长:报告经理,f(1) = 1*f(0) = 1,f(1)执行完毕,销毁f(1)栈帧
  7. 经理: 报告总监, f(2) = 2*f(1) = 2,f(2)执行完毕,销毁f(2)栈帧
  8. 总监:报告老大, f(3) = 3*f(2) = 6,f(3)执行完毕,销毁f(3)栈帧
  9. CEO总结: 干得漂亮,main()执行完毕,销毁main()栈帧,退出。

注意上述步骤的粗体部分: f(3) -> f(2) -> f(1) -> f(0) -> f(0) -> f(1) -> f(2) -> f(3), 明显的LIFO的栈结构。

尾递归: 不只要优雅,也要性能

递归让人又爱又恨。爱他的人说:递归让我的代码更优雅;恨他的人说,递归让性能更差。

也有人说,使用递归,代码更加优雅;但使用循环,性能更优。

如果说非递归是普通人,递归是优雅的天使,那么尾递归就是战斗天使,好看又能打。尾递归,是递归的一种特殊形式,需满足下面两个条件:

  • 递归调用是整个函数体中最后执行的语句(即递归调用完成后,不需要再执行其他运算)
  • 它的返回值不属于表达式的一部分

编译器可以利用尾递归的特性,生成优化的代码,猜测内部用循环的形式实现,而不是建立一个又一个的栈帧。(注:之所以能够优化,是因为调用在尾部,没有必要保存任何局部变量,可以通过循环实现)。

以1+2+3+…+n求累积和为例(不用阶乘,是因为书太大,即使long long也会溢出)。

源码如下:

319706f2807384b5aaaf151c6e223405.png

Makefile如下:

7587d054efb848c36ec733716a75f79b.png

测试结果如下:

usng optimimized cumsumtco_cumsum(999000)=499000999500cumsum(999000)=499000999500usng default cumsumSegmentation fault
  • 默认编译选项: 即"g++ cumsum.cpp -o cumsum
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值