函数调用、理解递归

对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈。

  • 代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写
  • 数据段:保存初始化的全局变量和静态变量,可读可写不可执行
  • BSS:未初始化的全局变量和静态变量
  • 堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行
  • 栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要,可读可写可执行

来一张图:
[图片上传失败...(image-d902e7-1512804060985)]

上面这些对理解调用栈有什么用呢。其实想要彻底弄明白,还需要懂汇编才行。这里我们只需要知道栈会存放局部变量,函数参数,当前状态,函数调用信息对后面的理解就够了。

下面通过一个例子来理解递归调用的执行过程(Xcode)

void up_and_down(int n)
{
printf("before: Level %d:n location %p\n",n,&n); /* 1 */
if(n<4)
up_and_down(n+1);
printf("after: Level %d:n location %p\n",n,&n); /* 2 */
} int main(int argc, const char * argv[]) {
@autoreleasepool {
up_and_down(1);
}
return 0;
}

执行结果

before: Level 1:n location 0x7fff5fbff75c
before: Level 2:n location 0x7fff5fbff73c
before: Level 3:n location 0x7fff5fbff71c
before: Level 4:n location 0x7fff5fbff6fc
after: Level 4:n location 0x7fff5fbff6fc
after: Level 3:n location 0x7fff5fbff71c
after: Level 2:n location 0x7fff5fbff73c
after: Level 1:n location 0x7fff5fbff75c
Program ended with exit code: 0

分析过程: 
首先, main() 使用参数 1 调用了函数 up_and_down() ,于是 up_and_down() 中形式参数 n 的值是 1, 故打印语句 #1 输出了 Level1 。然后,由于 n 的数值小于 4 ,所以 up_and_down() (第 1 级)使用参数 n+1 即数值 2 调用了 up_and_down()( 第 2 级 ). 使得 n 在第 2级调用中被赋值 2, 打印语句 #1 输出的是 Level2 。与之类似,下面的两次调用分别打印出 Level3 和 Level4 。

当开始执行第 4 级调用时, n 的值是 4 ,因此 if 语句的条件不满足。这时候不再继续调用 up_and_down() 函数。第 4 级调用接着执行打印语句 /* 2 */,即输出 Level4 ,因为 n 的值是 4 。现在函数需要执行 return 语句,此时第 4 级调用结束,把控制权返回给该函数的调用函数,也就是第 3 级调用函数。第 3 级调用函数中前一个执行过的语句是在 if 语句中进行第 4 级调用。因此,它继续执行其后继代码,即执行打印语句 /* 2 */,这将会输出 Level3 .当第 3 级调用结束后,第 2 级调用函数开始继续执行,即输出Level2 .依次类推.

注意,每一级的递归都使用它自己的私有的变量 n .可以查看地址的值来证明。也就是栈保存了调用的参数。

如果还没看懂,没关系,我再用一种最为简单的方式在解释一下。完全可以简单就是把递归函数一层一层展开。比如上面的例子,如果展开就可以写成下面这样

void up_and_down_simple(int n)
{
printf("before: Level %d:n location %p\n",n,&n); /* 1 */
if(n<4)
up_and_down1(n+1);
printf("after: Level %d:n location %p\n",n,&n); /* 2 */
}
void up_and_down1(int n)
{
printf("before: Level %d:n location %p\n",n,&n); /* 1 */
if(n<4)
up_and_down2(n+1);
printf("after: Level %d:n location %p\n",n,&n); /* 2 */
} void up_and_down2(int n)
{
printf("before: Level %d:n location %p\n",n,&n); /* 1 */
if(n<4)
up_and_down3(n+1);
printf("after: Level %d:n location %p\n",n,&n); /* 2 */
}
void up_and_down3(int n)
{
printf("before: Level %d:n location %p\n",n,&n); /* 1 */
if(n<4)
up_and_down4(n+1);
printf("after: Level %d:n location %p\n",n,&n); /* 2 */
}
void up_and_down4(int n)
{
printf("before: Level %d:n location %p\n",n,&n); /* 1 */
if(n<4)
up_and_down(n+1);
printf("after: Level %d:n location %p\n",n,&n); /* 2 */
} int main(int argc, const char * argv[]) {
@autoreleasepool {
// up_and_down(1);
up_and_down_simple(1);
}
return 0;
}

打印的结果:

before: Level 1:n location 0x7fff5fbff75c
before: Level 2:n location 0x7fff5fbff73c
before: Level 3:n location 0x7fff5fbff71c
before: Level 4:n location 0x7fff5fbff6fc
after: Level 4:n location 0x7fff5fbff6fc
after: Level 3:n location 0x7fff5fbff71c
after: Level 2:n location 0x7fff5fbff73c
after: Level 1:n location 0x7fff5fbff75c
Program ended with exit code: 0

这样一对比二者的结果是一样的所以说,如果你对递归还是很难理解,就去用展开的思路理解吧。

总结一下

  • 每一次函数调用都会有一次返回.当程序流执行到某一级递归的结尾处时,它会转移到前一级递归继续执行.
  • 递归函数中,位于递归调用前的语句和各级被调函数具有相同的顺序.如打印语句 #1 位于递归调用语句前,它按照递归调用的顺序被执行了 4 次;位于递归调用语句后的语句的执行顺序和各个被调用函数的顺序相反.
  • 每一级的函数调用都有自己的私有变量.
  • 递归函数中必须包含可以终止递归调用的语句.

常见递归问题

有了上面的基础,现在开始来刷刷几道简单的题:

阶乘n!

按照递归的套路两个: 1. 递归公式: 有反复执行的过程(调用自身) 2. 退出条件: 有跳出反复执行过程的条件(递归出口)

  • 递归公式 n! = n * (n-1) * (n-2) * ...* 1(n>0)
  • 退出条件 n == 0
int recursive(int n) {
if (0 == n) {
return (1);
}
else {
return n * recursive(n - 1);
}
}

斐波那契数列

斐波纳契数列,又称黄金分割数列,指的是这样一个数列:1、1、2、3、5、8、13、21、……

  • 递归公式 Fib(n) = Fib(n-1) + Fib(n-2);
  • 退出条件 n == 0 ,n == 1
int Fib(int n) {
if (0 == n) {
return 1;
}
if (1 == n) {
return 1;
} return Fib(n -1) + Fib(n - 2);
}

全排列

从n个不同元素中任取m(m≤n)个元素,按照一定的顺序排列起来,叫做从n个不同元素中取出m个元素的一个排列。当m=n时所有的排列情况叫全排列。

如1,2,3三个元素的全排列为:

  1,2,3
1,3,2
2,1,3
2,3,1
3,1,2
3,2,1

这种问题递归公式和退出条件并不是那么明显,需要深入分析。如何去分析呢。一般思路就是总结归纳,先用最简单的例子找到规律,再提炼成公式。

把123的全排列可以看出三组,分别是1xx,2xx, 3xx。可以想成一个数列的全排列的公式 :n个元素的全排列=(一个元素作为前缀)+(其余n-1个元素的全排列);

退出条件:如果只有一个元素的全排列,则说明已经排完,则输出数组;

不断换排头通过for循环就可以实现。然后就是前缀需要交换。先把基本的写好

交换函数:

void Swap(char str[], int a, int b) {
char temp = str[a];
str[a] = str[b];
str[b] = temp;
}

主函数

//全排列
int sum = 0;
void Perm(char str[], int begin, int end) {
if (begin == end)
{
for (int i = 0; i <= end; i++)
{
cout << str[i];
}
cout << endl;
sum++;
return;
}
else
{
for (int j = begin; j <= end; j++)
{
printf("\n swap begin:%d j:%d \n", begin, j);
Swap(str, begin, j);//交换是第几个
Perm(str, begin + 1, end);
Swap(str, j, begin);//归位
}
}
}

为了看清整个交换流程,加了个日志

4
abcd swap begin:0 j:0 swap begin:1 j:1 swap begin:2 j:2
abcd swap begin:2 j:3
abdc swap begin:1 j:2 swap begin:2 j:2
acbd swap begin:2 j:3
acdb swap begin:1 j:3 swap begin:2 j:2
adcb swap begin:2 j:3
adbc swap begin:0 j:1 swap begin:1 j:1 swap begin:2 j:2
bacd swap begin:2 j:3
badc swap begin:1 j:2 swap begin:2 j:2
bcad swap begin:2 j:3
bcda swap begin:1 j:3 swap begin:2 j:2
bdca swap begin:2 j:3
bdac swap begin:0 j:2 swap begin:1 j:1 swap begin:2 j:2
cbad swap begin:2 j:3
cbda swap begin:1 j:2 swap begin:2 j:2
cabd swap begin:2 j:3
cadb swap begin:1 j:3 swap begin:2 j:2
cdab swap begin:2 j:3
cdba swap begin:0 j:3 swap begin:1 j:1 swap begin:2 j:2
dbca swap begin:2 j:3
dbac swap begin:1 j:2 swap begin:2 j:2
dcba swap begin:2 j:3
dcab swap begin:1 j:3 swap begin:2 j:2
dacb swap begin:2 j:3
dabc
24
Program ended with exit code: 0

根据日志结合代码来分析就很容易理解了。

河内塔问题

n个盘子和3根柱子:A(源)、B(备用)、C(目的),盘子的大小不同且中间有一孔,可以将盘子“串”在柱子上,每个盘子只能放在比它大的盘子上面。起初,所有盘子在A柱上,问题是将盘子一个一个地从A柱子移动到C柱子。移动过程中,可以使用B柱,但盘子也只能放在比它大的盘子上面。

从上面的分析得出:
该问题可以分解成以下子问题:
第一步:将n-1个盘子从A柱移动至B柱(借助C柱为过渡柱)
第二步:将A柱底下最大的盘子移动至C柱
第三步:将B柱的n-1个盘子移至C柱(借助A柱为过渡柱)

int i;    //记录步数
//i表示进行到的步数,将编号为n的盘子由from柱移动到to柱(目标柱)
void move(int n, char from, char to) {
printf("第%d步:将%d号盘子%c---->%c\n", i++, n, from, to);
} //汉诺塔递归函数
//n表示要将多少个"圆盘"从起始柱子移动至目标柱子
//start_pos表示起始柱子,tran_pos表示过渡柱子,end_pos表示目标柱子
void Hanio(int n, char start_pos, char tran_pos, char end_pos){
if(n == 1) { //很明显,当n==1的时候,我们只需要直接将圆盘从起始柱子移至目标柱子即可.
move(n,start_pos, end_pos);
}
else {
Hanio(n-1, start_pos, end_pos, tran_pos); //递归处理,一开始的时候,先将n-1个盘子移至过渡柱上
move(n, start_pos, end_pos); //然后再将底下的大盘子直接移至目标柱子即可
Hanio(n-1, tran_pos, start_pos, end_pos); //然后重复以上步骤,递归处理放在过渡柱上的n-1个盘子 此时借助原来的起始柱作为过渡柱(因为起始柱已经空了)
}
}

这个思考起来有点麻烦,所以注释写得很多。

更多

除了上面列举的几个例子,还有比较常见的,二分查找,快排也用到了递归的思想。先这样吧。脑子还是得多用才能更加灵活。

作者:纸简书生
链接:https://www.jianshu.com/p/99ca6dba3be6
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

理解C语言递归up_and_down的更多相关文章

  1. 【转载】理解C语言中的关键字extern

    原文:理解C语言中的关键字extern 最近写了一段C程序,编译时出现变量重复定义的错误,自己查看没发现错误.使用Google发现,自己对extern理解不透彻,我搜到了这篇文章,写得不错.我拙劣的翻 ...

  2. 深入理解c语言_从编译器的角度考虑问题_纪念Dennis Ritchie先生

    开源中国: Dennis Ritchie教授过世了,他发明了C语言,一个影响深远并彻底改变世界的计算机语言.一门经历40多年的到今天还长盛不训的语言,今天很多语言都受到C的影 响,C++,Java,C ...

  3. 深入理解C语言的函数调用过程 【转】

    转自:http://blog.chinaunix.net/uid-25909619-id-4240084.html 原文地址:深入理解C语言的函数调用过程 作者:wjlkoorey258     本文 ...

  4. "深入理解C语言" 指针

    本文对coolshell中的"深入理解C语言"这篇文章中提到的指针问题, 进行简要的分析. #include <stdio.h> int main(void){ ]; ...

  5. 理解C语言中指针的声明以及复杂声明的语法

    昨天刚把<C程序设计语言>中"指针与数组"章节读完,最终把心中的疑惑彻底解开了.如今记录下我对指针声明的理解.顺便说下怎样在C语言中创建复杂声明以及读懂复杂声明. 本文 ...

  6. 这样子来理解C语言中指针的指针

    友情提示:阅读本文前,请先参考我的之前的文章<从四个属性的角度来理解C语言的指针也许会更好理解>,若已阅读,请继续往下看. 我从4个属性的角度来总结了C语言中的指针概念.对于C语言的一个指 ...

  7. 深入理解C语言 - 指针使用的常见错误

    在C语言中,指针的重要性不言而喻,但在很多时候指针又被认为是一把双刃剑.一方面,指针是构建数据结构和操作内存的精确而高效的工具.另一方面,它们又很容易误用,从而产生不可预知的软件bug.下面总结一下指 ...

  8. 深入理解C语言

    语言只是一种工具,任何语言之间都是相通的,一通则百通,关键是要理解语言背后的思想,理解其思想,任何语言,拿来用就行了.语言没有好坏之分,任何语言既然存在自然有它存在的价值. 在一个到处是OOP的年代, ...

  9. 一套帮助你理解C语言的测试题(转)

    前言 原文链接:http://www.nowamagic.net/librarys/veda/detail/775 内容 在这个网站(http://stevenkobes.com/ctest.html ...

随机推荐

  1. Subversion 1.8.9 ( SVN Client ) 安装最新版本的svn客户端

    For CentOS7 Users: [WandiscoSVN] name=Wandisco SVN Repo baseurl=http://opensource.wandisco.com/cento ...

  2. java 之UDP编程

    大白话:每一台电脑都有自己的ip地址,向指定的ip地址发数据,数据就发送到了指定的电脑.UDP通信只是一种通信方式而已,其特点就不多说.有了ip地址数据就能发送到指定的电脑了,但是呢!我把数据发送到电 ...

  3. Asp.net中web.config配置文件详解(一)

    本文摘自Asp.net中web.config配置文件详解 web.config是一个XML文件,用来储存Asp.NET Web应用程序的配置信息,包括数据库连接字符.身份安全验证等,可以出现在Asp. ...

  4. C#中用HttpWebRequest中发送GET/HTTP/HTTPS请求 (转载)

    这个需求来自于我最近练手的一个项目,在项目中我需要将一些自己发表的和收藏整理的网文集中到一个地方存放,如果全部采用手工操作工作量大而且繁琐,因此周公决定利用C#来实现.在很多地方都需要验证用户身份才可 ...

  5. HDU 6165 FFF at Valentine

    题目大意:给出一个有向图,问你这个图中是否对于任意两点\(u,v\),都至少满足\(u\to v\)(\(u\)可到达\(v\),下同)或\(v\to u\)中的一个. 一看就是套路的图论题,我们先把 ...

  6. 案例学python——案例一:抓图

    最近项目不那么紧张,有时间来研究一下Python,先前断断续续的自学了一段时间,有些浅基础.刚好在码云上看到比较适合的案例,跟着案例再往前走一波. 案例一:爬虫抓图 开发工具:PyCharm    脚 ...

  7. BodeAbp前端介绍

    BodeAbp的前端可以根据自己的喜好选型,推荐React.js.angular2.js.vue.js,后续我会以react.js为例说明BodeAbp前端的一些设计思路. BodeAbp提供的前端d ...

  8. 行业干货-如何逆向解决QT程序汉化中乱码问题

    前言 “一款QT开发的国外软件,大概率是没有做中文支持的,所以你汉化中,不论怎么设置编码都一定是乱码.面对这个问题,你去互联网上找答案,答案却大多是复制粘贴的开发中解决乱码的文章,可是我们是要逆向中解 ...

  9. Oracle数据库冷备份与热备份操作梳理

    Oracle数据库的备份方式有冷备份和热备份两种,针对这两种备份的实施过程记录如下: 一.Oracle冷备份 概念数据库在关闭状态下完成所有物理系统文件拷贝的过程,也称脱机备份.适合于非归档模式(即n ...

  10. B. Forgery

    链接 [http://codeforces.com/contest/1059/problem/B] 题意 要伪造医生签名,先给你医生的签名nm的网格'.'表示空白',#'表示墨水,你的笔可以这么画以一 ...