函数嵌套
一 嵌套定义
在Go语言中,定义在函数内部的函数必须是匿名函数
例1:
例2:
二 嵌套调用
在函数内又调用了其他函数,即函数的嵌套调用
三 嵌套调用之递归调用
3.1 递归调用
递归调用是嵌套调用一种特殊形式,如果嵌套调用到了函数自己,就是递归调用。
详细地说:在调用一个函数时,该函数内又直接或者间接地调用了函数自身,称之为递归调用
例1:直接调用自己
例2:间接调用自己
3.2 回溯与递推
递归其实就是用函数实现的循环,在3.1小节中,两个例子的递归调用都是一个无限循环的过程,而无限递归是没有意义的,因为
-
1、我们的问题通常都是在有限次数内就完成,不需要无限次递归
-
2、每调用一个函数就会在栈区新开辟一个栈、帧空间,这意味着无限递归会引发栈溢出问题fatal error: stack overflow
补充一个重要概念:栈、帧空间是什么?
所以,我们必须让递归调用在满足某个特定条件下终止,即一个正确的递归调用应该分为两个阶段
-
1、回溯:向下一层一层递归调用下去
-
2、递推:回溯阶段遇到结束条件则结束,开始向上一层一层递推。回溯与递推的分水岭就是结束条件
下面我们用一个浅显的例子,为了让读者阐释递归的原理和使用:
某公司四个员工坐在一起,问第四个人薪水,他说比第三个人多1000,问第三个人薪水,第他说比第二个人多1000,问第二个人薪水,他说比第一个人多1000,最后第一人说自己每月5000,请问第四个人的薪水是多少?
思路解析:
要知道第四个人的月薪,就必须知道第三个人的,第三个人的又取决于第二个人的,第二个人的又取决于第一个人的,而且每一个员工都比前一个多一千,数学表达式即:
很明显这是一个递归的过程,可以将该过程分为两个阶段:回溯和递推。
在回溯阶段,要求第n个员工的薪水,需要回溯得到(n-1)个员工的薪水,以此类推,直到得到第一个员工的薪水,此时,salary(1)已知,因而不必再向前回溯了。然后进入递推阶段:从第一个员工的薪水可以推算出第二个员工的薪水(6000),从第二个员工的薪水可以推算出第三个员工的薪水(7000),以此类推,一直推算出第第四个员工的薪水(8000)为止,递归结束。需要注意的一点是,递归一定要有一个结束条件,这里n=1就是结束条件。
代码实现
执行结果
程序分析:
在未满足n==1的条件时,一直进行递归调用,即一直回溯,见图的左半部分。而在满足n==1的条件时,终止递归调用,即结束回溯,从而进入递推阶段,依次推导直到得到最终的结果。
了解:
大部分编程语言使用固定大小的函数调用栈,常见的大小从64KB到2MB不等。固定大小栈会限制递归的深度(例如python),当你用递归处理大量数据时,需要避免栈溢出;除此之外,还会导致安全性问题。
而在Go语言中,使用的是可变栈,栈的大小按需增加(初始时很小),这意味着在go中使用递归时,大多数情况都不需要考虑溢出和安全问题,前提是你的递归有结束条件并且层级不是特别多,当然了,如果层级很多,也还是会栈溢出
3.3 了解尾递归优化
go不支持尾递归优化,下述概念了解即可
Golang编译器针对尾递归没有优化,也就是说即便你按照上文把递归优化为尾递归,也还是会栈溢出
3.4 递归的总结
递归本质就是在做重复的事情,所以理论上递归可以解决的问题循环也都可以解决,只不过在某些情况下,使用递归会更容易实现,具体来讲只要一个问题可以满足下边三个条件,那就可以考虑使用递归
-
1、一个问题的解可以分解成几个规模更小的子问题的解
-
2、子问题除了数据规模不一样,求解思路必须完全一样
-
3、存在递归终止条件,把问题分解为子问题,把子问题再分解为子子问题,一层一层分解下去,不能存在无限循环,这就需要有终止条件
3.5 递归案例
写递归的代码,最关键的就是「写出递归公式、找到递归终止条件」,剩下的将递归公式转化成代码就简单了
例1:斐波那契数
- 请使用递归的方式,求出斐波那契数 1,1,2,3,5,8,13…
- 给你一个整数 n,求出它的斐波那契数是多少?
例2:求函数值
- 已知 f(1)=3; f(n) = 2*f(n-1)+1;
- 请使用递归的思想编程,求出 f(n)的值?
例3:猴子吃桃子问题**
- 有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一半,然后再多吃一个。当到第十天时,想再吃时(还没吃),发现只有 1 个桃子了。问题:最初共多少个桃子?