学习:算法图解第三章-递归
在看算法图解的时候看到递归时讲到了与栈的关系,递归是靠栈来实现的,书上讲的原理都能看懂,实践下吧。结果一写代码就出现各种问题,在解决这些问题时也加深了递归与栈的关系。
现来看个简单的程序吧,计算数组N个数据的和,这里计算1-100的和,用递归来实现,贴上关键部分吧:
//int data[100] = {1, 2, 3, ...... 100};
//int len = 100;
int n = 0;
int sum(int data[], int len)
{
if(n == len){ //基线条件
return 0;
}else{
n++;
return data[n-1] + sum(data, len); //递归调用
}
}
大家给下sum的结果?(自己先认真思考下结果)
其实这是我最开始写的代码(大神们一看肯定想,我操,还有这么2逼的写法,小学生都不如啊,没办法,本人能写出这么惊叹的代码,我自己都很佩服自己啊)
期待运行结果:5050(大家是不是也认为这个结果),VS上测试结果:10000,尼玛啥情况,与预期结果不符啊,程序有问题还是我理解的有问题,开始分析吧。
在设计N个数据和的本意是:
这里递归的条件是由n去控制的,计划实现的是data[0]+data[1]+…+data[99],那为什么结果是10000呢?前面说了,递归是栈来实现的,本来认为每进行一次递归调用时会把data[]首地址,n,len,以及data[n-1]的值都压人栈里,但是通过汇编代码发现实际上只有data[]首地址和len压人的栈中,n和data[n-1]并没有压人栈中,我们看下汇编代码。
if(n == len){ //基线条件
009D14D5 mov eax,dword ptr [n (9D7164h)]
009D14DA cmp eax,dword ptr [len]
009D14DD jne sum+35h (9D14E5h)
return 0;
009D14DF xor eax,eax
009D14E1 jmp sum+5Fh (9D150Fh)
}else{
009D14E3 jmp sum+5Fh (9D150Fh)
n++;
009D14E5 mov eax,dword ptr [n (9D7164h)]
009D14EA add eax,1
009D14ED mov dword ptr [n (9D7164h)],eax
return data[n-1] + sum(data, len); //递归调用
009D14F2 mov eax,dword ptr [len]
009D14F5 push eax
009D14F6 mov ecx,dword ptr [data]
009D14F9 push ecx //将n和data首地址压入栈中
009D14FA call sum (9D11FEh)
009D14FF add esp,8 //递归所有压栈完成后,下面开始出栈操作
009D1502 mov edx,dword ptr [n (9D7164h)] //此刻出栈时,n为100
009D1508 mov ecx,dword ptr [data]
009D150B add eax,dword ptr [ecx+edx*4-4] //所以,每次出栈执行的都是+data[99]
}
}
这里n是个全局变量,所以递归调用时n当前的值并不会压人栈中,并且data[n-1]的值也没有压人栈中,在递归所以压栈操作完成后进行出栈操作时,n为100,所以实际上每次执行的都是data[99]+sum(上一次的返回值)。所以最终是进行了100次data[99]的相加,所以运行结果是:10000而不是5050。
如果我们将程序稍微改下,结果会不一样:
//int data[100] = {1, 2, 3, ...... 100};
//int len = 100;
int n = 0;
int sum(int data[], int len)
{
int local_n = 0;
if(n == len){ //基线条件
return 0;
}else{
n++;
local_n = data[n-1];
return local_n + sum(data, len); //递归调用
//return data[n-1] + sum(data, len); //递归调用
}
}
这样运行后运行结果是:5050,我们来分析下为什么,看看汇编代码:
if(n == len){ //基线条件
002714D5 mov eax,dword ptr [n (277164h)]
002714DA cmp eax,dword ptr [len]
002714DD jne sum+35h (2714E5h)
return 0;
002714DF xor eax,eax
002714E1 jmp sum+64h (271514h)
}else{
002714E3 jmp sum+64h (271514h)
n++;
002714E5 mov eax,dword ptr [n (277164h)]
002714EA add eax,1
002714ED mov dword ptr [n (277164h)],eax
local_n = data[n-1];
002714F2 mov eax,dword ptr [n (277164h)]
002714F7 mov ecx,dword ptr [data]
002714FA mov edx,dword ptr [ecx+eax*4-4] //将data[n-1]的值给edx
002714FE mov dword ptr [local_n],edx //将edx的值给local_n,local_n是局部变量,所有local_n的当前值会压人到栈中
return local_n + sum(data, len); //递归调用
00271501 mov eax,dword ptr [len]
00271504 push eax
00271505 mov ecx,dword ptr [data]
00271508 push ecx
00271509 call sum (2711FEh)
0027150E add esp,8 //递归所有压栈完成后,下面开始出栈操作
00271511 add eax,dword ptr [local_n] //每次出栈执行的都是+local_n(local_n的值为之前压人栈时的值)
}
}
这里local_n是个局部变量,所以递归压入栈时也会把local_n的当前的值压人到栈中,在递归所以压栈操作完成后进行出栈操作时执行的都是local_n(入栈时的值)+sum(上一次的返回值),所以实际上是1+2+3…+100,结果为5050。
我们来通过内存和寄存器的值来看看是不是这样的:
这里local_n分配的地址是0x004EF558,在ESP-EBP之间,最有local_n的值也会压人栈中。
至此我们学习了递归用栈实现方式以及递归中全局变量和局部变量的存储问题,在以后的设计中需注意其区别,避免出现不必要的bug。
后来又将程序又稍微改了下,两种方式,均可以得到预期结果:
//递归,求和 N个元素和
//int data[100] = {1, 2, 3, ...... 100};
//int len = 100;
--------------------------------------------------------------
//sum(data[0-98])+ data[99] 先算前面的和
int sum(int data[], int len)
{
if(len == 0){ //基线条件
return 0;
}else{
return data[len-1] + sum(data, len-1); //递归调用
}
}
-------------------------------------------------------------
//data[0] + sum(data[1-99]) 先算后面的和
int primary_len = 100;
int sum(int data[], int len)
{
if(len == 0){ //基线条件
return 0;
}else{
return data[primary_len-len] + sum(data, len-1); //递归调用
}
}
总结:本文研究了递归与栈的关系,以及全局变量与局部变量对栈的不同影响,从而深刻的认识了递归。
注:前面的测试环境都是在win7 vs2008上进行的,前面结果是10000的同样代码在linux下测试结果则为5050,由此linux与windows在某些处理上可能稍有不同,带后面进行linux研究后在进行总结!
本人能力与理解力有限,如有有理解错的希望大家一起交流。