函数(入门——下篇)

目录

一.什么是递归

二.递归的两大基本条件

三.递归练习

四.递归与迭代

五.一些总结和思考


一.什么是递归

程序调用自身的编程技巧称为递归(recursion).

递归做为一种算法在程序设计语言中广泛应用.但值得注意的是,有些编程语言极度依赖递归,而有些编程语言甚至不允许递归的存在,C语言则处在两者之间.C中可以使用递归,但是大多数程序员并不会经常使用递归,不能沉迷在递归中无法自拔!!!
一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解.
递归策略
只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的主要思考方式在于:把大事化小

盗梦空间不知道大家看过没有,主角一行人为了完成一个任务的解决,逐步深入层层梦境,然后从外到里,再从里到外,层层梦境退出来.

当然两者不是完全相同,只是有一定相似性,递归还有限定条件,而且往往是进行相同的操作(也就是将一个大问题分解成小问题),这个需要我们做多点题来逐步加深理解.

二.递归的两大基本条件

1.存在限制条件,当满足这个限制条件的时候,递归便不再继续.
2.每次递归调用之后越来越接近这个限制条件.
在函数栈帧一文中,我们也提到过,每调用一次函数,实际就要在栈区开辟一块空间,因此递归可以简化程序,一定程度上是采取空间换取时间的策略.如果没有限制条件,从里到外再一步步跳出函数,则stack overflow(栈溢出)是无法避免的.

三.递归练习

(大事化小,牢记心中!!!)

Lg.1
接受一个整型值(无符号),按照顺序打印它的每一位。
例如:
输入: 1234 ,输出 1 2 3 4
思路分析
假如我要打印1234
那我先打印出1 2 3,再打印4,就能得到1 2 3 4
我想打印出1 2 3
那我先打印出1 2,再打印3
类似要打印出1 2
就先打印出1,再打印2
那剩下一个1,直接打印就好,从这里我们就可以得出递归的约束条件是什么.(小于10)
#include <stdio.h>
void Print(int x)
{
	if (x > 10)
	{
		Print(x / 10);
	}
	printf("%d ", x % 10);
}
int main()
{
	int x = 0;
	scanf("%d", &x);
	Print(x);
	return 0;
}
图解:
Lg.2
编写函数不允许创建临时变量,求字符串的长度(递归实现strlen函数)
思路分析:
假设我要统计“Breaking bad\0”这个字符串的长度
那我统计“reaking bad\n”的长度,再加上1就可以(B的长度是1)
我要统计“ reaking bad\n ”这个字符串的长度
那我统计“ eaking bad\n ”的长度,再加上1(r的长度为1)
...
直到字符串只剩下\0,已经没有字符了,我们便意识到递归要终止了,同时我们也找出了递归终止的条件.
#include <stdio.h>
int my_strlen(char* str)
{
	if (*str == '\0')
		return 0;
	else
		return my_strlen(str + 1) + 1;
}
int main()
{
	char str[] = "Breaking bad";
	int len = my_strlen(str);
	printf("%d", len);
	return 0;
}

Lg.3

汉诺塔问题

相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏.

该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘.

游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好.

操作规则:

1.每次只能移动一个盘子,操作过程中盘子可以置于A、B、C任一杆上.

2.在移动过程中三根杆上都始终保持大盘在下,小盘在上.

如图所示,这里只列举了只有三个金盘的情况,目标则是在保证移动过程中,小金盘始终在大金盘之上,然后将A杆上的金盘全部移动到C杆上.

思路分析: 

设金盘数目为n

当n = 1时,只需要直接从A杆将金盘移动到C杆
当n = 2时,将第一个金盘,先移动到B杆,再将第二个金盘移动到C杆,最后将第一个金盘从B杆移动到C杆.
当n = 3时, 我们意识到第三个金盘实际上我们可以把它当成空气,毕竟它是最大的金盘,不管我们对上面的金盘怎么操作,都不会违反规则.
于是,便出现一个很有趣的现象,假设我们把目标杆当成B杆(原本是C杆),那移动上面两个金盘到B杆,则完全和n = 2时的情况相同,再把第三个金盘移动到C杆,然后把目标杆定回C杆,此时和n = 2的情况也依旧完全相同.
此时,我们便意识到,这是一个递归问题,要解决第n个金盘应该如何移动.
就先将上面n - 1个金盘移动到B杆(借助C杆),再将第n个金盘从A杆移动到C杆,再将第n - 1个金盘从B杆移动到C杆(借助A杆).
抽象出来,更进一步说, 这是一个不断借助辅助杆,移动到目标杆的过程 ,期间辅助杆,目标杆在不同层级n中是不同的(即不一定C杆就是目标杆),但是步骤永远是上述提到的三个步骤.(是一个自相似的过程)

于是,我们便着手构造汉诺塔函数,设定四个参数,n为盘子的个数.

cur_stick————>当前杆
assist_stick———>辅助杆
target_stick———>目标杆
详细代码如下:
#include <stdio.h>

void move(int n ,char cur_stick, char target_stick)
{
	printf("将第%d个盘子从%c杆 ——> %c杆\n", n, cur_stick, target_stick);
}

void Hanoitower(int n, char cur_stick, char assist_stick, char target_stick)
{
	if (n == 1)
		move(1,cur_stick ,target_stick);
	else
	{
		Hanoitower(n - 1, cur_stick, target_stick, assist_stick);
		move(n,cur_stick, target_stick);
		Hanoitower(n - 1, assist_stick, cur_stick, target_stick);
	}
}

int main()
{
	int n;
	scanf("%d", &n);
	Hanoitower(n, 'A', 'B', 'C');
	return 0;
}

移动的总次数:

前面我们依旧提到过,想要将N个盘子全部移动到目标杆,关键就是将第N个(最底下的盘子移过去),仔细思考一下,我们可以比较轻松理解到,将N-1个盘子从当前杆移动到辅助杆,再从辅助杆移动到目标杆,两个的步骤绝对是完全相同的.

所以,F(n) = 2*F(n - 1) + 1,是一个递推公式

通过数学归纳法,我们可以轻松得到,F(n) = 2*n - 1.轻松就可以得到移动的总次数.

Lg.4

青蛙跳台阶问题

一只青蛙一次可以跳一级台阶,或者二级台阶,请问它跳k级台阶有多少种方法?

思路分析:

假设小青蛙已经跳到最后一步,就差临门一脚,就成功跳完所有台阶

这时候我们会发现,有两种情况

第一种,是还差一个台阶,和跳n - 1 个台阶没两样,就差多跳1个台阶

第二,是还差两个台阶,那和跳n - 2个台阶没两样,就差多跳2个台阶

举例说明:

我跳3级台阶的方法,实际上就是

跳1级台阶的方法个数(跳1个台阶,差2个台阶,再跳一次2个台阶,总共为1)

+

跳2级台阶的方法个数  (跳2个台阶,有2种方法,再跳一次1个台阶,总共为2)

=

3种方法

....类似下去,我们便可以得到这也是一道递归的问题,而且和斐波拉契数列问题相同.

详细代码如下:

#include <stdio.h>

int move_step(int x)
{
	if (x == 1)
		return 1;
	else if (x == 2)
		return 2;
	else
		return move_step(x - 1) + move_step(x - 2);
}

int main()
{
	int n;
	scanf("%d", &n);
	printf("%d", move_step(n));
	return 0;
}

四.递归与迭代

我们从一道题开始入手,即经典的斐波拉契数问题.

int fib ( int n )
{
if ( n <= 2 )        
   return 1 ;
else
    return fib ( n - 1 ) + fib ( n - 2 );
}
代码和刚刚跳台阶的问题很像,但当我们n取比较大的数字时,例如50,我们就会发现,运行的时间会非常久,甚至得不出答案.
为什么会出现这个问题呢?
 
假设n = 7,那我们需要计算fib(6)和fib(5),而计算fib(6),需要计算fib(5)和fib(4),我们会发现fib(5)需要进行重复计算,我们再仔细想想,就会发现,越往下走,实际上,需要重复计算的次数就越多,而每一次调用斐波拉契函数,需要开辟函数栈帧,这会很容易导致Stack overflow.(空间不够)
那我们需要怎么修改呢?
这时候就可以用到迭代的方法.(从上往下进行运算,不用重复计算数据,将会大大缩短程序运行的时间.)与循环很相似.
int fib ( int n )
{
    int result ;
    int pre_result ;
    int next_older_result ;
    result = pre_result = 1 ;
    while ( n > 2 )
    {
          n -= 1 ;
          next_older_result = pre_result ;
          pre_result = result ;
          result = pre_result + next_older_result ;
    }
    return result ;
}

五.一些总结和思考

通过上面几道练习,我们已经简单了解到什么是递归

1.有许多问题是以递归的形式进行解释的,这只是因为它比非递归的形式更为清晰,但是这些问题的迭代实现往往比递归实现效率更高,虽然代码的可读性稍微差些.因此我们也不需要过度沉迷于递归中,什么问题都思考一下递归.
2.很多时候,像是汉诺塔问题,如果对递归的细节,进行过度的深究,反而不容易理解和编写程序,实际上,进行递归程序的编写, 只需要考虑自相似的程序部分,和函数的出口(也就是什么时候停止)便可以尝试进行编写.它类似外包,你只需要负责你进行的这一部分,至于其余的部分,是由其他人负责,他们可能和你完成着相似的任务,但你不需要去了解别人应该如何具体完成这个任务.
3.现在做的题目还不多,对于递归了解还不够深入,但递归有点类似数学上的数学归纳法的感觉
假设n = 1时情况成立,我们只需要证明从n - 1 到 n 的部分也成立,就可以得到n 的递推式,其中所有的过程,我们也不必要深究,当然,多去思考一下具体如何实现,还是有利于我们加深对程序的理解.

4.递归实现的前提是函数栈帧开辟需要申请空间,通过向栈帧压进变量,返回函数栈帧的时候,自然就能重新获得原来这个变量.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值