1.递归函数
一个直接调用自己或通过一系列的调用间接的调用自己的函数,称为递归函数。
2.递归调用执行流程
在整个递归函数执行期间,系统会创建一个“递归工作栈”,用来记录各层函数调用时的数据
* 每进入一层递归,都会将相关数据封装好压入栈,这里的相关数据包含函数实际参数,所有局部变量,上一层返回的地址(包括主函数的返回地址),称为“工作记录”
* 每进入一层递归,都会产生一条工作记录,将其压入栈中。
* 每退出一层递归,都会将栈顶的记录弹出栈,继续执行下一个栈顶的工作记录。
* 这保证了当前执行层的工作记录必是栈顶的工作记录,称这个记录为“活动记录”
3.递归实现中序遍历
//递归中序遍历
void InOrderTraverse(BiTree T){
if(T){
if(T->lchild)
InOrderTraverse(T->lchild);//1
printf("%c",T->data);
if(T->rchild)
InOrderTraverse(T->rchild);//2
}
}
4.非递归实现中序遍历
我们要做的是自己用栈模拟系统递归工作栈的工作流程:第一次调用中序遍历函数,系统会将函数调用的返回地址和根节点地址(函数参数)作为工作记录压入栈中,对于我们自己的栈,只需将根节点地址(函数参数)作为工作记录即可(因为我们的需求是将结点遍历到并将其data打印出来,不需要记住函数调用的返回地址,只需保证结点访问次序(这一点栈的先进后出特性就可保证))。
现在开始研究递归流程:给定一棵二叉树进行递归中序遍历,语句1会一直执行直到找到最左结点(此过程中会将遍历到的结点地址压入栈中),此时有两种情况:
对于第一种情况:当NULL作为根节点地址传入,此次调用函数会直接结束,开始返回,这是从左子树返回,由中序遍历可知下一步即可访问p,然后执行语句2,将p的右孩子作为根节点进行遍历。
对于第二种情况:当NULL作为根节点地址传入,此次调用函数会直接结束,开始返回,这是从左子树返回,由中序遍历可知下一步即可访问p,然后执行语句2,将p的右孩子NULL作为根节点传入,此次调用也直接返回,至此对p的访问这一层调用结束,栈顶记录弹出,开始访问p的父结点,即当前的栈顶记录。
自己用栈描述:
void NoneRecursionInOrderTraverse(BiTree T,Stack *stack){
BiTree p=T;//这里的p可以看做活动记录
BiTree e;
Push(stack, p);//需要先压入一条工作记录,以满足下面循环的起始条件,有个弊端是寻找最左孩子时,需要先找左孩子再压栈,最后会将最左结点的左孩子NULL压入
while (!StackEmpty(stack)) {
p= GetTop(*stack);//每次都要获取栈顶记录,栈顶的记录很特殊,是当前系统需要执行的函数的数据,将栈顶记录中的地址赋值给p模拟了将实参传递给递归函数的形参并开始函数中的代码逻辑
while (p) {//当p不为空时,持续访问p的左孩子,并将访问到的结点地址压入栈中
p = p->lchild;//注意这里需要先访问左孩子再压栈
Push(stack, p);//最后会将最左结点的左孩子即NULL压入栈;
}
Pop(stack, &e);//空指针出栈(模拟了:将NULL作为参数去调用遍历函数,会直接结束,开始返回),这是从左子树返回上一层,开始访问本层(pop后的栈顶记录)即根节点
if(!StackEmpty(stack)){
Pop(stack,&p);//栈顶弹出并访问,也可以先访问再弹出
printf("%c", p->data);
Push(stack,p->rchild);//每一次压栈都代表调用了一次递归函数,将右孩子作为根节点进行遍历
}
}
}
另一种实现方式:
void method1(BiTree T,Stack *stack){
BiTree p=T;
while(p){//直接找到最左孩子
Push(stack, p);
p=p->lchild;
}
while(!StackEmpty(stack)){
Pop(stack,&p);//
/*取出栈顶记录,开始访问,即使该记录的右孩子还未访问,这里的退栈操作不会影响递归的执行
* (因为我们目标是按一定顺序遍历二叉树的所有结点且每个结点只访问一次),该记录访问过后就可以退栈并访问其右孩子
* */
printf("%c",p->data);
p=p->rchild;
while(p){
Push(stack, p);
p=p->lchild;
}
}
}
对方式一还可以优化:
//优化1
void method(BiTree T,Stack *stack){
BiTree p=T;
while (!StackEmpty(stack)||p) {//此处 || 了一个p,这只在第一次进循环的时候起作用,巧妙的解决了栈空无法进循环的问题,就不用在循环前将根节点压入栈
while (p) {//当p不为空时,将p压入栈中,持续访问p的左孩子,直至找到最左边的叶子结点
Push(stack, p);//此处可以先将访问到的结点压入栈再访问其左孩子,最后避免了将最左孩子的左孩子NULL压入栈,可以简化一些操作
p = p->lchild;//
}
if(!StackEmpty(stack)){
Pop(stack,&p);//栈顶弹出并访问,也可以先访问再弹出
printf("%c", p->data);
p=p->rchild;
}
}
}
进一步简化:
//优化2
void method2(BiTree T,Stack *stack){
BiTree p=T;
while(!StackEmpty(stack)||p){
if(p){
Push(stack, p);
p = p->lchild;
}else{
Pop(stack,&p);
printf("%c", p->data);
p=p->rchild;
}
}
}