C,是一种信仰

递归的运行:从一道“读程序写运行结果”的题目说开来

在今年暑假的算法竞赛练习题中,有这样一个“读程序写运行结果”的题目(第8题),test.c

0001 #include<stdio.h>
0002 #define MAX 100
0003 void solve(char first[], int spos_f, int epos_f, char mid[], int spos_m, int epos_m)
0004 {
0005     int i, root_m;
0006     if(spos_f > epos_f)
0007         return;
0008     for(i = spos_m; i <= epos_m; i++)
0009         if(first[spos_f] == mid[i])
0010         {
0011             root_m = i;
0012             break;
0013         }
0014     solve(first, spos_f + 1, spos_f + (root_m - spos_m), mid, spos_m, root_m - 1);
0015     solve(first, spos_f + (root_m - spos_m) + 1, epos_f, mid, root_m + 1, epos_m);
0016     printf("%c", first[spos_f]);
0017 }
0018 int main()
0019 {
0020     char first[MAX], mid[MAX];
0021     int len;
0022     scanf("%d", &len);
0023     scanf("%s", first);
0024     scanf("%s", mid);
0025     solve(first, 0, len - 1, mid , 0, len - 1);    
0026     printf("\n");
0027     return 0;
0028 }

这个函数的调用关系如下:

image

注意到solve(…)本身进行了递归。

当输入数据为:

7

ABDCEGF

BDAGECF

的时候,输出的结果是:

DBGEFCA

那么,这个结果是如何产生的呢?为了更清楚地看出程序的运行状况,我为程序加入了一些注释和调试代码,把它改成了下面这个样子,test1.c

0001 #include<stdio.h>
0002 #define MAX 100
0003 void solve(char first[], int spos_f, int epos_f, char mid[], int spos_m, int epos_m)
0004 {
0005     int i, root_m;
0006 
0007     printf("\n执行函数:solve(first,%d,%d,mid,%d,%d);\n",spos_f,epos_f,spos_m,epos_m);
0008     printf("传入的first序列: \t");
0009     for (i=spos_f; i<=epos_f; i=i+1) printf("%c",first[i]);
0010     printf("\n传入的mid序列: \t");
0011     for (i=spos_m; i<=epos_m; i=i+1) printf("%c",mid[i]);
0012     printf("\n");
0013 
0014     if(spos_f > epos_f)    {/*当传入的first区间不存在字符的时候,函数返回*/
0015         printf("该函数执行完毕:solve(first,%d,%d,mid,%d,%d);\n",spos_f,epos_f,spos_m,epos_m);
0016         return;
0017     }
0018     for(i = spos_m; i <= epos_m; i++)    /*从左向右,用root_m在mid中标出第一个和first[spos_f]相同的字符*/
0019         if(first[spos_f] == mid[i])
0020         {
0021             root_m = i;
0022             printf("root_m=%d\n",root_m);
0023             break;
0024         }
0025 
0026     printf("呼叫下列函数:\n"); 
0027     printf("solve: %d\t%d\t%d\t%d\n",spos_f + 1, spos_f + (root_m - spos_m),spos_m, root_m - 1);
0028     printf("solve: %d\t%d\t%d\t%d\n",spos_f + (root_m - spos_m) + 1, epos_f,root_m + 1, epos_m);
0029 
0030     solve(first, spos_f + 1, spos_f + (root_m - spos_m), mid, spos_m, root_m - 1);
0031     solve(first, spos_f + (root_m - spos_m) + 1, epos_f, mid, root_m + 1, epos_m);
0032     printf("%c", first[spos_f]);    /*当整个过程结束的时候,会打印first区间的开始字符*/
0033     printf("该函数执行完毕:solve(first,%d,%d,mid,%d,%d);\n",spos_f,epos_f,spos_m,epos_m);
0034 }
0035 int main()
0036 {
0037     char first[MAX], mid[MAX];
0038     int len;
0039     scanf("%d", &len);
0040     scanf("%s", first);
0041     scanf("%s", mid);
0042     solve(first, 0, len - 1, mid , 0, len - 1);    
0043     printf("\n");
0044     return 0;
0045 }

如果我们使用相同的输入数据,将会得到更加详细的结果,该结果反映了solve(…)函数的具体运行状况。具体结果如下:

执行函数:solve(first,0,6,mid,0,6);
传入的first序列:     ABDCEGF
传入的mid序列:     BDAGECF
root_m=2
呼叫下列函数:
solve: 1    2    0    1
solve: 3    6    3    6

执行函数:solve(first,1,2,mid,0,1);
传入的first序列:     BD
传入的mid序列:     BD
root_m=0
呼叫下列函数:
solve: 2    1    0    -1
solve: 2    2    1    1

执行函数:solve(first,2,1,mid,0,-1);
传入的first序列:    
传入的mid序列:    
该函数执行完毕:solve(first,2,1,mid,0,-1);

执行函数:solve(first,2,2,mid,1,1);
传入的first序列:     D
传入的mid序列:     D
root_m=1
呼叫下列函数:
solve: 3    2    1    0
solve: 3    2    2    1

执行函数:solve(first,3,2,mid,1,0);
传入的first序列:    
传入的mid序列:    
该函数执行完毕:solve(first,3,2,mid,1,0);

执行函数:solve(first,3,2,mid,2,1);
传入的first序列:    
传入的mid序列:    
该函数执行完毕:solve(first,3,2,mid,2,1);
D该函数执行完毕:solve(first,2,2,mid,1,1);
B该函数执行完毕:solve(first,1,2,mid,0,1);

执行函数:solve(first,3,6,mid,3,6);
传入的first序列:     CEGF
传入的mid序列:     GECF
root_m=5
呼叫下列函数:
solve: 4    5    3    4
solve: 6    6    6    6

执行函数:solve(first,4,5,mid,3,4);
传入的first序列:     EG
传入的mid序列:     GE
root_m=4
呼叫下列函数:
solve: 5    5    3    3
solve: 6    5    5    4

执行函数:solve(first,5,5,mid,3,3);
传入的first序列:     G
传入的mid序列:     G
root_m=3
呼叫下列函数:
solve: 6    5    3    2
solve: 6    5    4    3

执行函数:solve(first,6,5,mid,3,2);
传入的first序列:    
传入的mid序列:    
该函数执行完毕:solve(first,6,5,mid,3,2);

执行函数:solve(first,6,5,mid,4,3);
传入的first序列:    
传入的mid序列:    
该函数执行完毕:solve(first,6,5,mid,4,3);
G该函数执行完毕:solve(first,5,5,mid,3,3);

执行函数:solve(first,6,5,mid,5,4);
传入的first序列:    
传入的mid序列:    
该函数执行完毕:solve(first,6,5,mid,5,4);
E该函数执行完毕:solve(first,4,5,mid,3,4);

执行函数:solve(first,6,6,mid,6,6);
传入的first序列:     F
传入的mid序列:     F
root_m=6
呼叫下列函数:
solve: 7    6    6    5
solve: 7    6    7    6

执行函数:solve(first,7,6,mid,6,5);
传入的first序列:    
传入的mid序列:    
该函数执行完毕:solve(first,7,6,mid,6,5);

执行函数:solve(first,7,6,mid,7,6);
传入的first序列:    
传入的mid序列:    
该函数执行完毕:solve(first,7,6,mid,7,6);
F该函数执行完毕:solve(first,6,6,mid,6,6);
C该函数执行完毕:solve(first,3,6,mid,3,6);
A该函数执行完毕:solve(first,0,6,mid,0,6);

我们针对test1.c,进行评论。请读者注意以下几点:

1、在solove(…)中,first[]和mid[]中的数据都不会被改动。变动的只是solve(…)所传入的4个参数。其中 spos_f 意指first区间的开始位置(start position of first),epos_f 意指first区间的结束位置(end position of first);同样地 spos_m 是指mid区间的开始位置,epos_m是指mid区间的结束位置。

2、在solove(…)不断递归调用的过程中,其传入的参数在不断地变化。在计算新的传入参数的时候,solove(…)还利用到了root_m。这一计算过程本身并不复杂,但却令我非常费解:这种递归调用和计算的目的究竟何在?排序?查找?实在是不明白。虽然我并不理解整个程序的意义,但是这并不影响对程序运行结果的推算。在比赛的时候,列出一张类似下面这样的一张表,就可以推算出程序的运行结果:

第一行
spos_f

epos_f

spos_m
epos_m
root_m
第二行

0

6

0

6

2

第三行
spos_f+1
spos_f+(root_m-spos_m)
spos_m
root_m-1
 
第四行
spos_f+(root_m-spos_m)+1
epos_f
root_m+1
epos_m
 
其中,
第一行,是控制着程序运行的5个关键的变量。
第二行,是这五个变量的当前值。
第三、四行,是solove(…)进行递归调用时,会派生出的2个solove(…)函数的参数。其中第三行所派生出的solve(…)是要先于第四行的solve(…)执行的。
第三、四行所对应的solove(…),在执行的过程中,会计算出各自的root_m,该值是由test1.c的18~24行所测算出来的。

3、只有在函数返回之前的最后一句话,第32行,才会有打印的数据输出。而输出的数据,是这个将要返回的函数的first[]的区间中最左侧的字符。因此,我们关心的问题有两个:
  ①函数在调用的时候,其最左侧的字符下标(spos_f)是多少?
  ②函数在什么时候返回?

困扰学生的另一个问题就是:1个solove(…)会派生出2个solove(…),这就是所谓的递归,那么他们的执行先后顺序是怎样的呢?

仔细看test1.c的运行结果,就可以知道这个问题的答案。我把她们之间的调用关系图画在下面:(黑箭头表示调用,红箭头表示返回

image

有人会觉得,电脑是如何实现这种调用和返回的过程的呢?

电脑用到了一个被称为“栈”的数据结构,这是一个一维线性的数据结构,通过它,能够完成上述图中的调用过程。

栈:http://zh.wikipedia.org/wiki/%E5%A0%86%E6%A0%88
http://en.wikipedia.org/wiki/Stack_%28data_structure%29

具体展示如下:

阅读时,请注意:

1、在栈顶的,都是正在执行的函数

2、上层的函数,是由下层的函数所调用(派生)出来的。上层是下层的儿子。这点很重要,看图的过程中要认真体会。

3、通过这些调用过程,我们用栈,就能组织出上图树状的那种调用/返回结构。

4、入栈,意味着栈顶的函数调用新的函数,产生了新的栈顶;出栈,意味着栈顶的函数执行完毕(消亡),栈顶下移了。

image          image

 

image        image

 

image      image

 

image        image

 

image      image

 

image     image

 

image      image

 

image      image

 

image     image

 

image     image

 

image    image

 

image      image

 

image    image

 

image     image

 

image   image

 

哦,最后的结果是:

DBGEFCA

后记:这种题目令人十分费解。几乎每次算法竞赛,都有这样一个“读程序写运行结果”的题目,令我不明所以。有谁能知道这段代码究竟是什么目的或者用途呢?或者算法竞赛的目的,只是为了比赛一下选手的动手计算速度?

posted @ 2011-08-26 17:16  胖乎乎的王老师  Views(1593)  Comments(5Edit  收藏  举报
Email 联系我:fzd19zx@qq.com