【递归调用陷阱】| 递归调用分析、三种递归传值方式、打印树形结构图 ...
递归函数:一个含直接或间接调用本函数语句的函数被称之为递归函数。
在很多的编程实例中都会用到递归的方式解决问题,在许多问题上使用递归的思想解题也会非常方便。使用递归调用的代码可读性高,易于理解。就拿斐波那契数列为列,在初学编程时应该都使用过递归求斐波那契数列吧。递归的实例还有很多,如我们的汉诺塔问题,构建二叉树等等。相信现在让大家用递归写一个递归求阶乘什么的都分分钟可以解决,递归嘛,简单的很。可是…你真的有了解过递归吗?
今天我们的主题是,递归调用的几种传值方式,以及几个简单的递归调用图分析。
下面进入正题。
文章目录
1. 函数形参的几种传值方式比较
在我们C/C++语言中,有几种特殊的传值方式。例如一个 int fun(int n)
的函数,我们就有以下几种常用的传值方式。定义:int n = 10;
。
- fun(n);普通的实参传形参。
fun(n) ==》 fun(10) - fun(n-1); 或 fun(n+1); 一般用于递归函数中,缩小计算规模。
fun(n+1) ==》 fun(11) - fun(n–); 或 fun(n++);是 fun(n);n++;的简写。
fun(n++) ==》 fun(10) - fun(–n); 或 fun(++n);是 n=n+1;fun(n);的简写。
fun(++n) ==》fun(11)
此外,还有一种特殊的传值方式,那就是C++中的引用传递。int fun(int& n)
、int n = 10
。
- fun(n) ==》fun(10)
这几种传值方式,在一般的函数中都基本不会出现什么问题。但是,如果把他们用在了递归函数中那可就不一样了。
2. 参考以下代码,请认真思考其调用过程,并画出他们的递归调用图。
示范:如斐波那契数列递归式
int Fib(int n)
{
if (n <= 2) return 1;
else return Fib(n - 1) + Fib(n-2);
}
int main() { Fib(5); return 0; }
其五次的递归调用图为:
请参考示范完成下列各示例。
2.1 示例:使用 fun(i+1) 递归调用
参考代码:
void fun1(int i, int n)
{
if (i >= n)
{
}
else
{
fun1(i + 1, n);
fun1(i + 1, n);
}
}
int main()
{
fun1(0,3);
return 0;
}
请认真思考其调用过程,并画出他们的递归调用图。
2.2 示例:使用 fun(++i) 递归调用
参考代码:
void fun2(int i, int n)
{
if (i >= n)
{
}
else
{
fun2(++i, n);
fun2(++i, n);
}
}
int main()
{
fun2(0,3);
return 0;
}
请认真思考其调用过程,并画出他们的递归调用图。
2.3 示例:使用 fun(++i)的引用传递的递归调用
参考代码:
void fun3(int &i, int n)
{
if (i >= n)
{
}
else
{
fun3(++i, n);
fun3(++i, n);
}
}
int main()
{
int a = 0;
fun3(a,3);
return 0;
}
请认真思考其调用过程,并画出他们的递归调用图。
2.4 示例:使用 fun(i++) 的递归调用
参考代码:
void fun4(int i, int n)
{
if (i >= n)
{
}
else
{
fun4(i++, n);
fun4(i++, n);
}
}
int main()
{
fun4(0,3);
return 0;
}
请认真思考其调用过程,并画出他们的递归调用图。
3. 答案:调用过程分析
通过分析递归结构我们可以知道该段代码在运行时的调用可以看做是一个二叉树。
这里我们在每次递归调用时,打印 i 的值,分析调用过程。这里输出的 i 的序列是对其进行前序二叉遍历的结果。
3.1 示例递归调用图解:使用 fun(i+1) 递归调用
void fun1(int i, int n)
{
cout << i << " " ;
if (i >= n)
{
}
else
{
fun1(i + 1, n);
fun1(i + 1, n);
}
}
// 输出:0 1 2 3 3 2 3 3 1 2 3 3 2 3 3
// 以下用每层调用时i的值,分析递归调用过程
// 0
// / \
// 1 1
// / \ / \
// 2 2 2 2
// /\ /\ /\ /\
// 3 3 3 3 3 3 3 3
3.2 示例递归调用图解:使用 fun(++i) 递归调用
void fun2(int i, int n)
{
cout << i << " " ;
if (i >= n)
{
}
else
{
fun2(++i, n);
fun2(++i, n);
}
}
// 输出:0 1 2 3 4 3 2 3 4
// 以下用每层调用时i的值,分析递归调用过程
// 0
// / \
// 1 2
// / \ / \
// 2 3 3 4
// /\
// 3 4
3.3 示例递归调用图解:使用 fun(++i)的引用传递的递归调用
void fun3(int &i, int n)
{
cout << i << " " ;
if (i >= n)
{
}
else
{
fun3(++i, n);
fun3(++i, n);
}
}
// 输出:0 1 2 3 4 5 6
// 以下用每层调用时i的值,分析递归调用过程
// 0
// / \
// 1 6
// / \
// 2 5
// /\
// 3 4
3.4 示例递归调用图解:使用 fun(i++) 的递归调用
void fun4(int i, int n)
{
cout << i << " ";
if (i >= n)
{
}
else
{
fun4(i++, n);
fun4(i++, n);
}
}
// 输出:0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 ... ...
// 以下用每层调用时i的值,分析递归调用过程
// 0
// /
// 0
// /
// 0
// /
// 0
// /
// 0
// ...
分析:引用传递 和 i++ 传递对程序造成的影响
这里主要对引用传递 和 i++ 传递进行分析。
void fun3(int &i, int n)
引用传递的参数与原参数实际上是一个变量。可以认为引用传递的参数对整个函数是全局作用域的,在函数内参数改变时,将会对整个函数产生影响。
fun4(i++)
,这种传值方式等同于为fun4(n)
。这就形成了死递归,而递归函数严格意义上来说并没有无限递归、死递归的说法。因为,递归调用时在栈上开辟空间,而栈的大小是有限制的,当我们程序说使用的栈空间超过了栈的容量时就会发生栈溢出错误。我这里使用的 vs 编译器,经测试栈空间大小约为 1M左右,在 Centos 7 下测试栈空间约为 8M 左右。
4. 输出调用过程,打印树形结构
既然我们已经分析出了他的调用过程,不妨再设计程序打印出其调用过程的树形结构图。
4.1 设计程序打印以上三种递归的递归过程
首先,定义一个结点,保存树的结点深度和结点值
struct node // 保存树的结点
{
node(int _dep, int _val) :dep(_dep), val(_val)
{}
int dep; // 根节点的深度
int val; // 根节点的数值
};
其次,定义“打印树”类,主要数据成员有 left和right ,分别保存树的左右子树结点。以及增加左右子树的方法和一个输出树结构的方法。
class PrintTree
{
public:
void addLeft(node tmp)
{
left.push_back(tmp);
}
void addRight(node tmp)
{
right.push_back(tmp);
}
void showTree(int depth);
private:
vector<node> left;
vector<node> right;
};
实现输出树结构层次的方法,这里要借助一个“数的深度”参数来实现,打印相同深度的结点。另外,为了让输出的“树形”更直观,我们再根据树的深度不同打印数量不同的空格。
void PrintTree::showTree(int depth)
{
cout << "\t0" << endl; // 根节点
int i = 1;
while (i <= depth)
{
vector<node>::const_iterator lt = left.begin();
vector<node>::const_iterator rt = right.begin();
int sp = depth - i; /* 打印空格 */
while (sp-- ){ cout << " ";}
while (lt<left.end())
{
if ((*lt).dep == i) /* 输出深度为 i 的左结点 */
{
cout << (*lt).val ;
int s = depth - i+1; /* 打印空格 */
while (s--) { cout << " "; }
}
lt++;
}
while (rt < right.end()) /* 输出深度为 i 的右结点 */
{
if ((*rt).dep == i)
{
cout << (*rt).val ;
int s = depth - i +1; /* 打印空格 */
while (s--) { cout << " "; }
}
rt++;
}
cout << endl;
i++;
}
}
最后,改造三个递归函数,使之在递归调用时把结点信息存入“打印树”中。
void fun11(int i, int n, PrintTree& t)
{
if (i >= n)
{
}
else
{
t.addLeft(node(i + 1, i + 1));
fun11(i + 1, n, t);
t.addRight(node(i + 1, i + 1));
fun11(i + 1, n, t);
}
}
void fun22(int i, int n,PrintTree& t,int d)
{
if (i >= n)
{
}
else
{
int k = i;
t.addLeft(node(d + 1, ++k));
fun22(++i, n, t, d + 1);
t.addRight(node(d + 1, ++k));
fun22(++i, n, t, d + 1);
}
}
void fun33(int& i, int n, PrintTree& t, int d)
{
if (i >= n)
{
}
else
{
t.addLeft(node(d + 1, i+1));
fun33(++i, n,t,d+1);
t.addRight(node(d + 1, i+1));
fun33(++i, n,t,d+1);
}
}
4.2 运行测试,分别打印出fun1()、 fun2()、 fun3() 的递归过程。
run:fun11();
int main()
{
PrintTree tr;
fun11(0, 3, tr);
tr.showTree(3);
return 0;
}
// 输出:
0
1 1
2 2 2 2
3 3 3 3 3 3 3 3
run:fun22();
int main()
{
PrintTree tr;
fun22(0, 3, tr, 0);
tr.showTree(3);
return 0;
}
// 输出
0
1 2
2 3 3 4
3 4
run:fun33();
int main()
{
PrintTree tr;
int a = 0;
fun33(a, 3, tr, 0);
tr.showTree(3);
return 0;
}
// 输出
0
1 6
2 5
3 4
最后,如果觉得我的文章对你有帮助的话请帮忙点个赞,你的鼓励就是我学习的动力。如果文章中有错误的地方欢迎指正,有不同意见的同学也欢迎在评论区留言,互相学习。