2.用基础知识实现递归转栈式访问
基于以上几点,我们怎么把所有的递归都用栈这个数据结构实现呢?
想必聪明的你已经有想法了吧!
例题 : (请先自己思考尝试一下怎么实现)
先给出二叉树节点定义 :
typedef struct BiTNode{
char data;
BiTNode* lchild,*rchild;
} *BiTree;
访问二叉树使用的函数 :
void visit(BiTNode * node)
如果觉得上述节点定义眼熟,那我们俩应该都是供液大学的。
题1.难度等级: C
使用栈实现二叉树的先序遍历
函数头:
void preOrderRead(BiTree tree);
题2.难度等级: B
使用栈实现二叉树的后续遍历
void postOrderRead(BiTree tree);
题3.难度等级: A
二叉树的节点含有成员变量M
找出二叉树中成员变量M为a 和 b 的节点的最小祖先节点(假设a b 只出现一次)
BiTNode* findNearestAncestor(BiTree tree, char a, char b);
题目1为例,思路开导与解析 :
首先我们需要写出大概的使用递归实现功能的代码。
题1:
void preOrderRead(BiTree tree){
if(tree == NULL){
return;
}
visit(tree);//3
preOrderRead(tree -> lchild); //1
preOrderRead(tree -> rchild);//2
}
怎么转换成栈实现呢?
从之前我们分析中知道,对函数的调用实际上是创建栈帧的过程,那么上图1,2处我们调用了两次函数,那么在这两处我们应该都要
用创建栈帧来代替。
问题是创建的栈帧里面应该包含什么内容呢?
应该包含这个栈帧创建到销毁过程中所有需要的信息。而这里的信息可能不是直接获得的,例如可能我们的栈帧中包含了一个指向父栈帧的指针,那么我们就可以和父栈帧
通信,而无需要把父栈帧中的某些变量之类的信息冗杂地包含到栈帧里来。
严谨一点地说,栈帧应该包含本栈帧创建到销毁过程中需要的所有信息的 “来源或者信息本身”。
还有更重要的一点,递归函数的方法体只有一个,也就是说,对说有的栈帧都要进行同一个操作,无论这个栈帧包含的信息有多么不一样!
所以,方法中对栈帧的处理至关重要,他将作用于所有栈帧。
要能够根据栈帧内包含的信息对信息不同的栈帧做出合理的操作。
返回我们的题目1。除去1和2这两个创建栈帧的过程。如果把当前方法的调用想成一个栈帧,那么我们在栈帧里需要执行的操作只是判断本栈帧的节点是否为空,不空就读取,仅此而已。
对应的,设计我们的函数实现.
在这里,我们把栈的元素直接设计为节点,因为节点的信息已经够我们完成所有操作(只有visit操作而已);
1.如果把栈帧的入栈想成函数调用,出栈想成函数返回,那么当栈为空的时候,函数调用就结束了。于是有了下面1处的判断栈是否是空的
2.你可能会问:子函数都没调用完,2处怎么就把父函数的栈帧出栈了呢?因为如果我们在把子函数栈帧入栈(调用子函数)前将父函数的所有操作都做了,并且子函数的栈帧不需要和父函数栈帧通信的话,那么父函数的栈帧没有存在在栈中的意义了,因为该执行的都执行完了,子函数也不需要他,子函数在栈中的顺序也不会变,好一可怜的老父亲。
在下面需要对栈帧做的所有操作只有visit,也就是访问他的节点,子函数栈帧入栈前(调用子函数)就可以把父函数的所有操作在3处完成了,没有其他操作要等待子函数栈帧出栈(返回)接着做,而且子函数的栈帧已经包含所有操作需要的信息了(BiTree),所以2处父栈帧直接出栈。
4,5两处子函数栈帧入栈,表示父函数递归调用子函数。注意要放右孩子 rchild先,因为栈是先入后访问,而且左孩子总是先于右孩子访问
void preOrderRead(BiTree tree){
Stack stack;
init(stack);
stack.push(tree);
while( ! empty(stack)){ // 1
BiTNode* node = stack.pop(); // 2
if(node == NULL){
continue;
}
visit(node); // 3
stack.push(tree -> rchild); // 4
stack.push(tree -> lchild); // 5
}
}