数据结构—递归 Ⅷ

数据结构-递归(第五章)的整理笔记,若有错误,欢迎指正。
函数递归

递归的定义

  • 在定义一个过程或函数时出现调用本过程或本函数的成分称为递归( recursion)。
  • 若调用自身,称为直接递归(direct recursion)。
  • 若过程或函数p调用过程或函数q,而q又调用,称为间接递归(indirect recursion)。
  • 在算法设计中,任何间接递归算法都可以转换为直接递归算法来实现
  • 在计算技术中,与递归有关的概念有递归数列、递归过程、递归算法、递归程序和递归方法等。
  • (1)递归数列指的是由递归关系所确定的数列。
  • (2)递归过程指的是直接或间接调用自身的过程。
  • (3)递归算法指的是包含递归过程的算法。
  • (4)递归程序指的是直接或间接调用自身的程序。
  • (5)递归方法指的是一种在有限步骤内根据特定的法则或公式对一个或多个前面的元素进行运算,以确定一系列元素(如数或函数)的方法。
  • 如果一个递归过程或递归函数中的递归调用语句是最后一条执行语句,则称这种递归调用为尾递归(tail recursion)。
  • 一般情况下,尾递归算法可以通过循环或者迭代方式转换为等价的非递归算法;对于不是尾递归的复杂递归算法,在理解递归调用实现过程的基础之上可以用栈来模拟递归执行过程,从而将其转换为等价的非递归算法。
    例: 求n!(既是直接递归函数,又是尾递归)
long fac(int n)
{	
	long fact;
	if (n == 0 || n == 1) //0!和1!等于1
		fact = 1;
	else
		fact = n*fac(n-1); //递归调用fac函数
	return fact;
}
  • 递归算法通常把一个大的复杂问题层层转化为ー个或多个与原问题相似的规模较小的问题来求解,递归策略只需少量的代码就可以描述出解题过程中所需要的多次重复计算,大大减少了算法的代码量。
  • 一般来说,能够用递归解决的问题应该满足以下3个条件:
    (1)需要解决的问题可以转化为一个或多个子问题来求解,而这些子问题的求解方法与原问题完全相同,只是在数量规模上不同;
    (2)递归调用的次数必须是有限的;
    (3)必须有结结递归的条件来终止递归。
  • 递归算法的优点是结构简单、清晰,易于阅读,方便其正确性证明;缺点是算法执行中占用的内存空间较多,执行效率低,不容易优化。


何时使用递归

  • 在以下3种情况下常常要用到递归方法:
  1. 定义是递归的
  • 有许多数学公式、数列等的定义是递归的。例如,求n!和 Fibonacci数列等。对于这些问题的求解可以将其递归定义直接转化为对应的递归算法。
    例: 斐波那契数列
    F i b o n a c c i ( n ) = { F ( 0 ) = 0          n = 0 F ( 1 ) = 1          n = 1 F ( n ) = F ( n − 1 ) + F ( n − 2 )          n ≥ 2 Fibonacci(n)=\begin{cases} F(0)=0\;\;\;\;n=0 \\ F(1)=1\;\;\;\;n=1 \\ F(n)=F(n - 1)+F(n - 2)\;\;\;\;n ≥ 2 \\ \end{cases} Fibonacci(n)=F(0)=0n=0F(1)=1n=1F(n)=F(n1)+F(n2)n2
  1. 数据结构是递归的
  • 有些数据结构是递归的,对于递归数据结构,采用递归的方法编写算法既方便又有效。
    例: 单链表就是一种递归数据结构,其结点类型声明如下:
typedef struct LNode
{ 
	ElemType data; //存放结点数据
	struct LNode *next; //指向下一个同类型结点的指针
}LinkNode; //单链表的结点类型
  • 其中,结构体LNode的声明用到了它自身,即指针域next是一种指向自身类型结点的指针,所以它是一种递归数据结构。
  1. 问题的求解方法是递归的
  • 有些问题的解法是递归的,典型的有Hanoi(汉诺)塔问题求解。


汉诺塔问题

  • Hanoi(汉诺)塔问题是一个古典的数学问题,是用递归方法解题的典型例子。
  • 古印度有一个梵塔,塔内有3个柱子A、B、C,开始时A柱上套有64个盘子,盘子大小不等,大的在下,小的在上。有一个老和尚想把这64个盘子从A柱移到C柱,但规定每次只能移动一个盘,且在任何时候3个柱上的盘子都是大盘在下,小盘在上,在移动过程中可以利用B柱。
  • 为便于理解,先分析将A柱上3个盘子移到C柱上的过程:
    (1) 将A柱上2个盘子移到B柱上(借助C)
    (2) 将A柱上1个盘子移到C柱上
    (3) 将B柱上2个盘子移到C柱上(借助A)。
    其中第(2)步可以直接实现,第(1)步又可用递归方法分解为:
    1.1将A上1个盘子从A移到C;
    1.2将A上1个盘子从A移到B;
    1.3将C上1个盘子从C移到B。
    第(3)步可以分解为
    3.1将B上1个盘子从B移到A上;
    3.2将B上1个盘子从B移到C上;
    3.3将A上1个盘子从A移到C上。
    将以上综合起来,可得到移动3个盘子的步骤为 A → C , A → B , C → B , A → C , B → A , B → C , A → C A\rightarrow C,A\rightarrow B,C\rightarrow B,A\rightarrow C,B\rightarrow A,B\rightarrow C,A\rightarrow C ACABCBACBABCAC
    在这里插入图片描述
  • 完成三个盘共经历7步,由此可以推出:移动n个盘子要经历 2 n − 1 2^{n-1} 2n1步。

  • 由上面的分析可知:将n个盘子从A柱移到C柱可以分解为以下3个步骤:
    (1)将A柱上n-1个盘借助C柱先移到B柱上;
    (2)把A柱上剩下的一个盘移到C柱上;
    (3)将nー1个盘从B柱借助于A柱移到C柱上。

  • 上面第(1)步和第(3)步,都是把nー1个盘从一个柱移到另一个柱上,采取的办法是样的,只是柱的名字不同而已。为使之一般化,可以将第(1)步和第(3)步表示为:将x柱上nー1个盘移到y柱(借助z柱)。只是在第(1)步和第(3)步中,x、y、z和
    A、B、C的对应关系不同。

  • 对第(1)步,对应关系是:x对应A,y对应B,z对应C。

  • 对第(3)步,对应关系是:x对应B,y对应C,z对应A。

  • 因此,可以把上面3个步骤分成两类操作:
    (1)将nー1个盘从一个柱移到另一个柱上(n>1)。
    (2)将1个盘子从一个柱上移到另一柱上。

代码实现

#include<stdio.h>
void move(char a, char b)
{
	printf("%c-->%c\n", a, b);
}

void hanoi(int n,char x,char y,char z)
{
	if (n == 1) move(x, z); //只有一个盘,直接从x塔座移到z塔座上
	else
	{
		hanoi(n - 1, x, z, y); //将x塔座上的n-1个盘片借助z塔座移动到y塔座上
		move(x, z);
		hanoi(n - 1, y, x, z); //将y塔座上的n-1个盘片借助x塔座移动到z塔座上
	}
}

int main()
{
	int m; //m是需要移到的盘子数
	printf("input the number of diskes:");
	scanf("%d", &m); //输入盘子数
	printf("The step to moving %d diskes:\n", m);
	hanoi(m, 'A', 'B', 'C'); //执行移动盘子
	return 0;
}

运行结果

在这里插入图片描述

双递归

例1:

#include<stdio.h>
void fun(int n)
{
	if (n > 0)
	{
		printf("%d\n", n);
		fun(n - 1); //fun1
		fun(n - 1); //fun2
	}
}

int main()
{
	fun(3);
	return 0;
}

运行结果

  • 运行结果:3 2 1 1 2 1 1
  • 第七、八行虽然递归调用的是其自身的fun(),但为了分析每次调用时具体执行的函数,将第七行的fun()函数称为fun1();将第八行的fun()函数称为fun2()。
    在这里插入图片描述
    在这里插入图片描述

例2:

#include<stdio.h>
void fun(int n)
{
	if (n > 0)
	{
		
		fun(n - 1);
		fun(n - 1);
		printf("%d\n", n);
	}
}

int main()
{
	fun(3);
	return 0;
}

运行结果

  • 运行结果:1 1 2 1 1 2 3
    在这里插入图片描述
    在这里插入图片描述

分析

按照树形结构的样式,先一层一层把需要调用的函数写出来,如:

  • 主函数调用f(3),则把f(3)当作是第一层。
  • f(3)函数体里需要调用f1(2)和f2(2),则第二层有f1(2)和f2(2)。
  • 第二层的f1(2)函数体里需要调用f1(1)和f2(1),f2(2)函数体里同样需要调用f1(1)和f2(1),则第三层有f1(1)和f2(1)、f1(1)和f2(1),虽然第三层f1(1)和f2(1)是重复的,但它们在第一层中的主调函数不一样。前一个f1(1)和f2(1)是第二层中f1(1)调用的,而后一个f1(1)和f2(1)是第二层中f1(2)调用的!!
  • 第四层同理。
  • 当按照这种方式把需要调用的函数都列出来之后,在按照从上到下,先计算最左边再计算最右边的原则。即: f ( 3 ) → f 1 ( 2 ) → f 1 ( 1 ) → f 1 ( 0 ) → f 2 ( 0 ) → f 1 ( 1 ) → f 1 ( 2 ) → f 2 ( 1 ) → f 1 ( 0 ) → f 2 ( 0 ) → f 2 ( 1 ) → f 1 ( 2 ) → f ( 3 ) → f 2 ( 2 ) . . . . . . f(3)\rightarrow f1(2) \rightarrow f1(1) \rightarrow f1(0) \rightarrow f2(0) \rightarrow f1(1) \rightarrow f1(2) \rightarrow f2(1) \rightarrow f1(0) \rightarrow f2(0) \rightarrow f2(1) \rightarrow f1(2) \rightarrow f(3) \rightarrow f2(2)... ... f(3)f1(2)f1(1)f1(0)f2(0)f1(1)f1(2)f2(1)f1(0)f2(0)f2(1)f1(2)f(3)f2(2)......
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值