纸上得来终觉浅, 绝知此事要躬行。
主页:June-Frost
专栏:C语言
该篇将带你了解 递归知识
程序调用自身的编程技巧称为递归( recursion)。简单的来说就是函数自己调用自己。
递归做为一种算法在程序设计语言中广泛应用。 一个过程或函数在其定义或说明中有直接或间接调用自身的一种方法,它通常把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。
递归的精髓在大事化小
下面将通过例子(递归和非递归)来帮助理解函数递归的思考方式。
接受一个无符号整型值,按照顺序打印它的每一位:
输入 :1234 打印:1 2 3 4
非递归:这个题很容易想到的一种思路就是放在数组中解决⟹
代码实现:
#include
int main()
{
unsigned int num = 0;
scanf("%d", &num);
int arr[100] = {0};
int count = 0;
//填充数组
while (num != 0)
{
arr[count] = num % 10;
num /= 10;
count++;
}
//打印
int i = 0;
for (i = count-1; i >= 0; i--)
{
printf("%d ", arr[i]);
}
return 0;
}
非递归的方式还是有些繁琐,我们尝试用更加简单的方法继续实现这个问题,递归就可以完美解决。
递归:
思路:假设一个函数为print,它的功能为:传入的参数可以按顺序打印每一位。假如我们传参1234,print(1234)就可以将其打印出来,我们将其大事化小:
- 将 打印 1234 的问题转化为 打印123 和 打印 4 的问题。
- 将 打印 123 的问题转化为 打印12 和 打印 3 的问题。
- 将 打印 12 的问题转化为 打印1 和打印 2 的问题。
- 打印 1 的问题无法继续拆分,那么 递归条件为:两位数以上。1位数则需要单独处理。
- 这样在处理该问题时只需要在该条件下,将这部分【相似】逻辑写出来即可。
代码实现:
#include
void print(unsigned int num)
{
if (num > 9)
{
print(num / 10);
printf("%d ", num % 10);
}
else
{
printf("%d ", num);
}
}
int main()
{
unsigned int num = 0;
scanf("%d", &num);
print(num);
return 0;
}
也可以直接写为:
void print(unsigned int num)
{
if (num > 9)
{
print(num / 10);
}
printf("%d ", num % 10);
}
该函数如何理解呢❓
该函数在栈区时怎样的❓
下图为函数递归到最后,在栈区的大概样子。
如果想要了解栈帧的相关细节,可以看博主的另一篇 ——> 函数栈帧。
通过这样处理,该问题就可以顺利解决。同时这样的解决方式也反应了递归的必要条件。
若是无法满足这样的条件,递归很可能会导致栈溢出等一系列问题。
非递归:只需要遍历一遍并计数即可。(需要创建临时变量)
size_t my_strlen(char* p)
{
int count = 0;//临时变量
while (*p++)
{
count++;
}
return count;
}
递归:这种方式不需要创建临时变量。
思路:与上一道题一样,我们假设my_strlen 可以直接计算出字符串的长度。假设传参:”abcd“
- 将计算 abcd 的长度拆分为 计算 bcd 的长度 后再加 1。
- 将计算bcd 的长度拆分为 计算 cd 的长度 再加 1。
- 将计算cd 的长度 拆分为 计算 d 的长度 再加 1 。
- 计算d 的长度拆分为 计算 空字符串 的长度 再加 1。
- 空字符串 无法像上面一样拆解,于是单独处理,返回 0 。
代码实现:
size_t my_strlen(char* p)
{
if (*p)
{
return my_strlen(++p) + 1;
}
return 0;
}
通过以上的例子,友友们了解了递归,但是在一些问题中,我们可以使用递归也可以使用 迭代 (循环就是迭代的一种)。
迭代:遍历累积 即可。
int Fun(int num)
{
int ret = 1;
int i = 0;
for (i = 1; i <= num; i++)
{
ret *= i;
}
return ret;
}
递归:
代码实现:
int Fun(int num)
{
if (num > 0)
{
return Fun(num - 1) * num;
}
return 1;
}
迭代:
int Fib(int n)
{
int a = 1;
int b = 1;
int c = 1;
while (n > 2)
{
c = a + b;
a = b;
b = c;
n--;
}
return c;
}
int Fib(int n)
{
if (n > 2)
{
return Fib(n - 1) + Fib(n - 2);
}
return 1;
}
注意:
处理方式:
汉诺塔问题是一个经典的数学问题,起源于印度。问题描述如下:有三个柱子,分别标记为A、B、C,A柱子上有n个不同大小的盘子,盘子按照从小到大的顺序摆放,要求将所有盘子从A柱子上移动到C柱子上,期间可以借助B柱子完成移动。但是在移动过程中,要求大盘子永远不能放在小盘子上面,如何去做?
思路: 递归的使用,其实将问题抽象化。汉诺塔是将n个在a上的盘子通过b移动至c,那么我们假设一个函数为:Hanoi,它的功能就是可以将起始柱上的盘子通过中转柱移位到目的柱。例如:Hanoi(n,a,b,c),就是a作为起始柱,b作为中转柱,c作为目的柱。这里将那么我们将这个汉诺塔问题通过这个函数功能不断拆分。
这样的关系就是递归的核心❗️
其中 将n-1个起始柱上的盘子移位到中转柱 这个步骤可以按照同样的逻辑拆分。
对其拆分时,目标柱为第二个柱子。
同样的 将n-1个中转柱的盘子转移到目标柱,也可以按照之前的逻辑拆分,这里,第三个柱子为目标柱,第二个为起始柱子。
在这里我只分析到了第二次拆分,这里可以继续拆分,直到当拆分到只有一个盘子时便无法继续拆分了(没有多余的盘子可以移动),只需要单独处理即可,同时这也是递归停止的条件。
代码实现:
#include
// 数量 起始柱 中转柱 目的柱
void Hanoi(int n, char pos1, char pos2, char pos3)
{
if (n > 1)
{
Hanoi(n - 1, pos1, pos3, pos2);
printf("%c->%c ", pos1, pos3);
Hanoi(n - 1, pos2, pos1, pos3);
}
else
{
printf("%c->%c ", pos1, pos3);
}
}
int main()
{
int n = 0;
scanf("%d", &n);
Hanoi(n,'A', 'B', 'C');
return 0;
}
青蛙跳台阶问题是一个经典的递归问题。假设有n 级台阶,一只青蛙可以跳 1级或 2级,问青蛙跳上这 n 级的台阶总共有多少种跳法?
思路: 因为青蛙可以一次跳两个或者一次跳一个,那么计算n级台阶的跳法,只需要知道跳上n-1级的跳法和n-2级的跳法,加起来就可以了,这个问题可以通过这样的逻辑不断拆分,所以使用递归将其解决。
这个问题可以转化为斐波那契数的问题。
代码实现:
#include
int Fun(int n)
{
if (n > 2)
{
return Fun(n - 1) + Fun(n - 2);
}
if (n == 2)
{
return 2;
}
if (n == 1)
{
return 1;
}
}
文章到这里就结束了,如果对你有帮助,你的点赞将会是我的最大动力,如果大家有什么问题或者不同的见解,欢迎大家的留言~