继续深入学习递归,当我们说递归时,到底是在说什么。上一篇学习了:
漫谈递归、迭代、循环——人理解迭代,神理解递归
树这种数据结构就是用递归方式定义的,树的遍历用递归来实现简洁明了。谈到递归,我们不得不说的数据结构就是栈,它是一种容器,特点就是先入栈的数据后出栈。
![04c829b63c6b0a157b954339475e94d0.png](https://i-blog.csdnimg.cn/blog_migrate/a926a0f217d010f269e73b68a1aef0f4.jpeg)
栈操作示意图
在操作系统中,栈一般都是向下生长的,与我们的直观感觉相反。我们假设栈底地址保存在EBP寄存器中,栈顶地址保存在ESP寄存器中。执行push操作,栈顶指针就会向下移动;执行pop操作,栈顶指针就会向上移动。
当一个函数被调用时,会为其分配一个新的栈空间,栈里会压入这次函数调用时需要保存的信息。确切地说,该栈从栈底到栈顶保存了函数参数,函数的返回地址,调用者的栈信息,调用者的上下文信息,被调函数的局部变量。函数的返回地址保存的是调用者已经执行到哪一条指令,调用返回时,系统从所保存指令的下一条指令执行。被调用的函数栈里还保存了调用函数的栈顶和栈底寄存器EBP和ESP,这样函数返回到调用者时,可以正确使用调用者的栈。
用二叉树的前序遍历举例,探究一下递归程序在运行时,背后的栈是如何运行的。
- void pre_traverse(BTree pTree)
- {
- if(pTree)
- {
- printf("%c",pTree->data);
- if(pTree->pLchild)
- pre_traverse(pTree->pLchild);
- if(pTree->pRchild)
- pre_traverse(pTree->pRchild);
- }
- }
![48838e3bc63c43607beaa7614efec940.png](https://i-blog.csdnimg.cn/blog_migrate/310703dd71518b2e5a27b5b64aeee020.jpeg)
图1 节点1入栈示意图
如图1 所示,如果当前程序指针访问节点1,只有一个栈,函数执行到了第五行,用FIP:5表示。
当函数执行到第7行时,就会对自己进行递归调用。此时,系统会为这次调用自己新开辟一个栈。如图2。 这个栈里的当前指针指向节点2。在调用自己的新栈里保存了原调函数的栈底和栈顶寄存器和调用自己发生时的行号,EIP:7。
![a9fe44aebea2164444634651c71886a6.png](https://i-blog.csdnimg.cn/blog_migrate/7ee0293d6c5506800d69eebe01e444fb.jpeg)
图2 节点2入栈示意图
访问完节点2之后,执行到第7行,就会再产生一次递归调用,系统会再开辟一个新的栈,这个栈的当前指针指向节点3,如图3。
![12ea52f4873c80972bfce6b4bcfd0fd2.png](https://i-blog.csdnimg.cn/blog_migrate/4c4eef12bdfeada2f09be0eb98bde855.jpeg)
图3 节点3入栈示意图
在这次函数运行过程中,已判断发现,节点3的左右孩子都为空,所以不会再调用自己发起递归调用了。也不会再建立新的栈了。这次函数运行结束后,就会返回到调用者继续执行。通过当前栈中的old EBP和old ESP来返回调用者的堆栈,同时用old EIP:7返回到调用者的下一条指令,新的EIP:8。从宏观上看,节点3的栈被系统回收。当前栈的指针对应节点2。如图4。
![71fba5134ecdbce2a6e958555c87cb02.png](https://i-blog.csdnimg.cn/blog_migrate/b06d803e1c375037022acb77f4136d2a.jpeg)
图4 节点3的栈被系统回收
返回到节点2之后,程序执行到第9行时,再次产生递归调用,和上次不同,这次访问右孩子。系统会为节点4开辟新的栈。如图5。
![d8ba90b7212048b82b1a5765ebd8cc13.png](https://i-blog.csdnimg.cn/blog_migrate/304505c4f94aabbc9f4e1036cf7f5a03.jpeg)
图5 节点4开辟新的堆栈
从图5我们可看出,节点4的左右孩子都为空,不进行递归调用。返回上一级节点2对应的栈,系统回收节点4对应的栈。由于old EIP :9,返回节点2后,程序会从第10行开始执行。但是第10行程序就结束了,程序会继续返回节点1对应的栈,并从程序的第9行继续执行。如图6。
![1183c3ff174ce0102a6505a4a28084f8.png](https://i-blog.csdnimg.cn/blog_migrate/fb76b9188afd8b627d601184021138d0.jpeg)
图6 连续返回到节点1的栈
当程序执行第9行时,又会产生递归调用,为节点5开辟新的栈,如图7,新的EIP:7。
![d89f52c7d1bacece41aeabbc50d7b029.png](https://i-blog.csdnimg.cn/blog_migrate/94a11ac23aab527847e0d2c1198c6e2f.jpeg)
图7 为结点5开辟新的栈
程序运行到第7行之后,发现节点5有左孩子,发起递归调用,为节点6开辟新的栈,如图8。
![f84330ea9409f9a169503abb4e45f7f9.png](https://i-blog.csdnimg.cn/blog_migrate/5a15d865d6c7652e9dad6c0f92b10065.jpeg)
图8 为节点6开辟的栈
程序运行到第5行,发现节点6左右孩子都为空,系统回收节点6的栈,退到节点5的栈。由于old EIP :7,到节点5之后,执行程序第8行。第8行判断右孩子为空,程序继续返回,系统回收节点5的栈。节点5的old EIP:9,程序从第10行开始运行,第10行为程序结束的地方。继续回收节点1的栈,程序结束。完成了前序树的遍历。
最先开辟的栈,最后回收。漫谈递归、迭代、循环——人理解迭代,神理解递归