递归和循环的区别
递归和循环在设计思路上是有区别的,例如计算 1 - n 的累加,利用 for 循环来做。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 5;
int i = 1;
int nSum = 0;
for (; i <= n; i++)
{
nSum = nSum + i;
}
system("pause");
return 0;
}
for 循环是一个线性迭代的思路,数值一个一个相加,1+2+3+4+5 ,求出 5 的累加。而递归的思路则不是一条线做迭代,思路如下,求 5 的累加 Sum(5),只需知道 4 的累加(Sum(4))是多少,然后结果加 5;这个时候问题就回到了 4 的累加 (Sum(4))是多少,只需知道 3 的累加 (Sum(3) )是多少,然后结果加 4;依次 Sum(3) 等于 2 的累加 (Sum(2)) 加 3 ;Sum(2) 等于 1 的累加 (Sum(1) )+ 2 。
这时,需要注意一点,递归的设计,必须在越推越深,越推越深……到最内层时,必须要有个常识性结果,这里就是 Sum(1) = 1,否则,在编写递归程序时,会出问题的。
根据上面推理过程,并且因为已知 Sum(1) = 1,所以 Sum(2) = 3,Sum(3) = 6,Sum(4) = 10,Sum(5) = 15,最后结果就等于 15。
由上图可以看出,递归的执行线路是一个类 U 型。在不计函数本身的开销上,行动线路就已经比循环多一倍了。所以递归的效率是低于循环的,用递归解决这一类问题很显然是不划算的。
根据上面的推理过程,用递归实现 5 的累加。首先写出口, n 等于 1 时返回 1,n 等于其他时,n 的累加等于 n-1 项的累加 (GetSum(n-1))加 n。
#include <stdio.h>
#include <stdlib.h>
int GetSum(int n)
{
if (n == 1)
{
return 1;
}
return GetSum(n - 1) + n;
}
int main()
{
int n = 5;
int i = 1;
int nSum = 0;
nSum = GetSum(n);
system("pause");
return 0;
}
深入理解递归的执行过程
通过调试观察递归的执行过程,按 F10 进行单步调试,此时在 main 函数里面, n 的值等于 5。按 F11 进入 GetSum 函数的内部。
因为 n 等于 5,不等于 1 ,直接跳到 return GetSum(n-1) + n 这里,因为要求出 GetSum(n-1) + n 的结果,就先要求出 GetSum(n-1) 的返回值。
继续按 f11 进入函数 GetSum 的内部 ,虽然和前一步函数名是一样的,但是传入的参数却是不同的,此时 n 等于 4,通过栈窗口可以看到,此时函数在 GetSum(int 0x00000004) 这个位置,而前一个函数在 GetSum(int 0x00000005) 这个位置。
依次按 F11 ,可以看到依次进入,GetSum(int 0x00000003),GetSum(int 0x00000002),GetSum(int 0x00000001) 的函数内部。
当执行到,n 等于 1,函数所处 GetSum(int 0x00000001) 时。
因为 n == 1,所以 return 1,根据函数栈可以看出,当退出 GetSum(int 0x00000001) 之后,进入GetSum(int 0x00000002),当退出 GetSum(int 0x00000002) 之后,进入GetSum(int 0x00000003)……依次退出函数栈,如下图,直到回到 main 函数为止。类似于盗梦空间的片段,普通生活(main 函数),如果结束了,人就死了(程序结束)。从第一层梦(第一次函数调用)进入第二层梦(函数被调用两次,由第一层函数调用),再进入第三层梦(函数被调用三次,由第二层函数调用)……依次可进入更深层次梦境。当满足一定条件时,退出梦境,也依次退出,由第三层梦退回第二层梦,第二层梦退回第一层梦,第一层梦退回到现实生活。
循环适合解决线性问题,递归适合解决非线性问题
由分析和执行过程可以看出,用递归解决累加问题,既耗费时间又耗费脑力,并且效率不高,也就是俗话说的费力不讨好。但是为什么老师还有一些教课书上都在说递归的好呢?这里的好,必须是要分场和的,在这之前,先介绍两种问题,分别是线性问题和分线性问题。
线性问题:有且只有一个前驱,有且只有一个后继,例如:累加问题,编程中的数组,火车的车厢。
非线性问题:人们常说,条条大路通罗马,就是一个非线性问题。如下图,A 为出发地点,A 可以经过 C 到达罗马,A 也可以经过 B 再经过 D 到达罗马。因为每个节点可以有多条路径,多种选择,这种结构就是非线性。
从分析和调试过程可以得出结论:递归不适合解决线性问题,循环适合解决线性问题。下面用大家都知道的斐波拉契数列,来再次理解递归,并证明递归不适合线性问题。
斐波拉契数列规定了数列的前两项是 1,从第 3 项开始,第 n 项等于 n-1 项加 n-2 项。所以,第 3 项是 2 ,第 4 项是 3,第 5 项是 5,第 6 项是 8,第7 项是 13。
在拉波拉契数列中 8 的前面一定是 5 ,后面一定是 13,所以斐波拉契数列是一个线性问题,适合使用循环解决问题。
用循环编写程序,打印斐波拉契数列 46 项:
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 斐波拉契数列
// 1 1 2 3 5 8 13
unsigned int f1 = 1;
unsigned int f2 = 1;
unsigned int f3 = 0;
int i = 0;
printf("第1项\t1\r\n");
printf("第2项\t1\r\n");
while (i < 44)
{
f3 = f1 + f2;
printf("第%d项\t%d\r\n", i + 3, f3);
f1 = f2;
f2 = f3;
i++;
}
system("pause");
return 0;
}
编译执行,秒出结果
用递归解决,打印斐波拉契数列 46 项。为了便于分析,假设只打印斐波拉契数列第 5 项的值,第 5 项等于第 4 项加第 3 项,第 4 项等于第 3 项加第 2 项,第 3 项等于第 2 项加第 1 项,又因为第 2 项和第 1 项都是 1 ,所以可得出第五项的结果。
由此,可写出程序:首先写出口, n 等于 1 或 n 等于 2 时返回 1,n 等于其他时,第 n 项等于第 n-1 项加 n-2 项。
#include <stdio.h>
#include <stdlib.h>
int fib(int n)
{
if (n == 1 || n == 2)
{
return 1;
}
return fib(n - 1) + fib(n - 2);
}
int main()
{
// 斐波拉契数列
// 1 1 2 3 5 8 13
unsigned int f1 = 1;
unsigned int f2 = 1;
unsigned int f3 = 0;
int i = 3;
printf("第1项\t1\r\n");
printf("第2项\t1\r\n");
while (i <= 46)
{
f3 = fib(i);
printf("第%d项\t%d\r\n", i, f3);
f1 = f2;
f2 = f3;
i++;
}
system("pause");
return 0;
}
编译执行,程序运行起来非常的慢,运行到 40 项就需要花费很长的时间。
递归实现走迷宫
递归非常适合解决非线性问题,例如走迷宫,走迷宫在十字路口时,可以有 4 个选择,这是一个非线性问题。用递归实现走下图迷宫。迷宫设置规则 1 位墙, 0 为路,2 行 2 列是迷宫的起始位置,$ 为迷宫的出口
11111111111111111111111
10101001111101111111111
10101010000000000100001
10101010111101110101101
10000000111100000101111
101001100000011101011$1
10110110110110110101101
10000000100110110000001
11111111111111111111111
首先用程序打印这个迷宫。思路是:定义迷宫,求出迷宫的行数,然后逐行打印。
#include <stdio.h>
#include <stdlib.h>
void ShowMaze(char szMaze[][24], int nCount)
{
int i = 0;
while (i < nCount)
{
printf("%s\r\n", szMaze[i]);
i++;
}
}
int main()
{
char szMaze[][24] = {
"11111111111111111111111",
"10101001111101111111111",
"10101010000000000100001",
"10101010111101110101101",
"10000000111100000101111",
"101001100000011101011$1",
"10110110110110110101101",
"10000000100110110000001",
"11111111111111111111111"
};
int nRaw = sizeof(szMaze) / sizeof(szMaze[0]);
ShowMaze(szMaze, nRaw);
system("pause");
return 0;
}
算法思想:逆时针探路,分别走的路线是 x-1,y 和 x,y+1 和 x+1,y 和 x,y-1,如果等于 $ 或者 0 就在往前走一步,然后调用函数本身,传的值是往前走一步的坐标,当前坐标被置为白脸。如果往前走一步的值,不等于 $ 或者 1 ,则返回调用的函数,当前的坐标被置为黑脸。直到坐标本身等于 $ 走出迷宫为止,程序如下:
#include <stdio.h>
#include <stdlib.h>
void ShowMaze(char szMaze[][24], int nCount)
{
int i = 0;
while (i < nCount)
{
printf("%s\r\n", szMaze[i]);
i++;
}
}
void Maze(char szMaze[][24], int x, int y, int nRaw)
{
system("cls");
ShowMaze(szMaze, nRaw);
system("pause");
if (szMaze[x][y] == '$')
{
system("cls");
ShowMaze(szMaze, nRaw);
printf("恭喜过关\r\n");
system("pause");
exit(0);
}
szMaze[x][y] = '\1';
if (szMaze[x - 1][y] == '$' || szMaze[x - 1][y] == '0')
{
Maze(szMaze, x - 1, y, nRaw);
}
if (szMaze[x][y + 1] == '$' || szMaze[x][y + 1] == '0')
{
Maze(szMaze, x, y + 1, nRaw);
}
if (szMaze[x + 1][y] == '$' || szMaze[x + 1][y] == '0')
{
Maze(szMaze, x + 1, y, nRaw);
}
if (szMaze[x][y - 1] == '$' || szMaze[x][y - 1] == '0')
{
Maze(szMaze, x, y - 1, nRaw);
}
szMaze[x][y] = '\2';
system("cls");
ShowMaze(szMaze, nRaw);
system("pause");
return;
}
int main()
{
char szMaze[][24] =
{
"11111111111111111111111",
"10101001111101111111111",
"10101010000000000100001",
"10101010111101110101101",
"10000000111100000101111",
"101001100000011101011$1",
"10110110110110110101101",
"10000000100110110000001",
"11111111111111111111111"
};
int nRaw = sizeof(szMaze) / sizeof(szMaze[0]);
ShowMaze(szMaze, nRaw);
Maze(szMaze, 1, 1, nRaw);
printf("本迷宫无解\r\n");
system("pause");
return 0;
}
白脸构成的线路,是完整的走迷宫路线