一、递归式遍历:
递归十分适合于自相似(分形)类的数据结构。有些线性数据结构的递归可以改为尾递归使得递归栈帧不再累积。但非线性数据结构的递归由于每层的栈帧可能以不同的方式和参数调用自身,例如二叉树遍历的递归,每层函数栈里面有两次自我调用,不能该为尾递归,在极限情况下有爆栈可能。因此需要模仿函数栈的特性写出等效迭代代码。
二叉树后序递归逻辑结构如下:
void read(treenode *node) { if(node!=NULL) //① { read(node->lchild); //② read(node->rchild);// ③ display(node->val);// ④ } }
很明显,和线性数据结构,如链表的递归流程图有显著差别:
链表的递归删除过程流程图
那么,二叉树递归这种非线性数据结构递归过程就有如下几种:
分支——需要采用多重if else实现
标记——在递归过程中显然上一层执行的函数调用在结束返回后(出栈),便执行下一层的函数调用(入栈),不会反复调用。但是循环因为每次都会从开始重新执行,所以为了避免死循环读取左子树最左结点和它的父结点,需要标记好已读取过的数据。
栈——在递归中利用了函数栈的特性来做出栈入栈的操作。但在循环中,栈要自己手动编写。
流程推理:
图例:
黄色:当前已标记的结点
红色:当前已出栈(和显示)的结点
根据总结出来的特性和现象,把后序递归改为等效循环我需要:
1个栈
1个标记表
1个if else-if else结构和标记表结合达成递归中3句的功能
二、循环替代分析
那么,循环分析大致如下:
先看中间流程。从头结点的左子树的运作过程开始看:
很明显,这种递归的过程是非线性的,不是一路函数栈一路入栈和一路出栈,而是入栈和出栈交替进行。线性递归可以很方便地通过简单循环取代(例如链表的递归创建和循环创建都很简单),但对于这种非线性递归改成循环,必须在循环中通过多重分支语句进行等效流程控制。
根据总结出来的特性和现象,把后序递归改为等效循环我需要:
1个栈
1个标记表
1个if else-if else结构和标记表结合达成递归中3句的功能
对应的运作过程就是:
当前递归层结点为“2结点”:
结点的左子是否为空?否。因此左子进入下次递归(函数栈入栈一层)。
是。结点的右子是否为空?否。因此右子进入下次递归(函数栈入栈一层)。
是。显示该层结点结束该层递归(该层出函数栈)
对应的循环逻辑是:当前结点的左子是否为空?否。因此入栈“结点的左结点”。
否则,结点的右子是否为空?否。因此入栈“结点的左结点”。
否则,结点出栈并显示。设置栈顶为上一个结点
根据上述分析,很明显需要if_else if_else结构的判断语句,而且if中每次判断的应当是当前结点的左结点、右结点。
结点对应的不完全伪代码是:
if(当前结点的左结点非空)
{
}
else if(当前结点的右结点非空)
{
}
else
{
}
第一次:
while(结束条件) { if(结点有左子结点) { 入栈左子结点,标记为已读 } else if(结点有右子结点) { 入栈右子结点,标记为已读 } else { 没有左右子结点出栈并显示该结点 } }
思考后发现结点无法从头结点的左右子树开始一步步下移。那么继续深化:
第二次:
临时结点 = 头结点 while(结束条件) { if(临时结点 有左子结点) { 入栈 临时结点 的 左子结点,标记为已读 临时结点 重新被赋值为 临时结点的左结点 } else if(临时结点 有右子结点) { 入栈 临时结点 的右子结点,标记为已读 临时结点 重新被赋值为 临时结点的右结点 } else { 没有左右子结点,出栈,显示出栈结点 临时结点 = 再次出栈之后的结点 (局部分析:如栈当前状态为1 2 4,则出栈一次后为1 2,再出栈后出栈的2赋值给了临时结点,栈只剩下1。这样会导致回退后栈顶元素成了1,而不是该有的2,因此要补充回去2,使栈变会1 2 的同时,临时结点重新从2开始遍历) 重新入栈 临时结点 } }
思考后发现结点在最左的左子树最后一个结点和父结点之间会来回跳跃,需要标记已经入栈过的元素使得其不会反复死循环遍历2个结点。
第三次:
临时结点 = 头结点 while(结束条件) { if(临时结点 有左子结点 而且 已标记表 中没有 (临时结点 的 左子结点)) { 入栈 临时结点 的 左子结点,标记为已读 临时结点 重新被赋值为 临时结点的左结点 临时结点 的 左子结点 加入已标记表 } else if(临时结点 有右子结点 而且 已标记表 中没有 (临时结点 的 右子结点)) { 入栈 临时结点 的右子结点,标记为已读 临时结点 重新被赋值为 临时结点的右结点 临时结点 的 右子结点 加入已标记表 } else { 没有左右子结点,出栈,显示出栈结点 临时结点 = 再次出栈之后的结点 (局部分析:如栈当前状态为1 2 4,则出栈一次后为1 2,再出栈后出栈的2赋值给了临时结点,栈只剩下1。这样会导致回退后栈顶元素成了1,而不是该有的2,因此要补充回去2,使栈变会1 2 的同时,临时结点重新从2开始遍历) 重新入栈 临时结点 } }
思考后这次应能正常后序遍历二叉树了。最后加上循环结束条件即可。
第四次:
临时结点 = 头结点 while(头结点已经遍历了两次 这件事 为 还没发生) { if(临时结点 有左子结点 而且 已标记表 中没有 (临时结点 的 左子结点)) { 入栈 临时结点 的 左子结点,标记为已读 临时结点 重新被赋值为 临时结点的左结点 临时结点 的 左子结点 加入已标记表 } else if(临时结点 有右子结点 而且 已标记表 中没有 (临时结点 的 右子结点)) { 入栈 临时结点 的右子结点,标记为已读 临时结点 重新被赋值为 临时结点的右结点 临时结点 的 右子结点 加入已标记表 } else { 没有左右子结点 或 左右子结点都入过栈了,出栈,显示出栈结点 临时结点 = 再次出栈之后的结点 (局部分析:如栈当前状态为1 2 4,则出栈一次后为1 2,再出栈后出栈的2赋值给了临时结点,栈只剩下1。这样会导致回退后栈顶元素成了1,而不是该有的2,因此要补充回去2,使栈变会1 2 的同时,临时结点重新从2开始遍历) 重新入栈 临时结点 } }
三、具体算法和演示如下:
#include "stdio.h" #include "stdlib.h" #define new_tree_node (treenode*)malloc(sizeof(treenode)) typedef struct tnode { int val; struct tnode *leftchild,*rightchild; }treenode; treenode* create_tree() { treenode *head = new_tree_node; head->val = 1; head->leftchild = new_tree_node; head->leftchild->val = 2; head->rightchild = new_tree_node; head->rightchild->val = 3; head->leftchild->leftchild = new_tree_node; head->leftchild->leftchild->val = 4; head->leftchild->leftchild->leftchild = new_tree_node; head->leftchild->leftchild->leftchild->val = 8; head->leftchild->leftchild->leftchild->leftchild = NULL; head->leftchild->leftchild->leftchild->rightchild = NULL; head->leftchild->leftchild->rightchild = NULL; head->leftchild->rightchild = new_tree_node; head->leftchild->rightchild->val = 5; head->leftchild->rightchild->leftchild = NULL; head->leftchild->rightchild->rightchild = NULL; head->rightchild->leftchild = new_tree_node; head->rightchild->leftchild->val = 6; head->rightchild->leftchild->leftchild = NULL; head->rightchild->leftchild->rightchild = NULL; head->rightchild->rightchild = new_tree_node; head->rightchild->rightchild->val = 7; head->rightchild->rightchild->leftchild = NULL; head->rightchild->rightchild->rightchild = NULL; return head; } void print_node_val(treenode *t) { printf("%d\n",t->val); } void read(treenode *head) { treenode *llist[100]; treenode *fake_stack[100]; int ReadFinished = 0; int i,j=0,k=0; int node_lchild_is_read=0,node_rchild_is_read=0; int head_show_times = 0; treenode *temp = head; fake_stack[j++] = temp; llist[k++] = temp;//遍历标记 while(ReadFinished == 0) { for(i=0;i<100;i++) { if(llist[i]==temp->leftchild) node_lchild_is_read = 1;//检测结点的左结点是否在已遍历标记表中 if(llist[i]==temp->rightchild) node_rchild_is_read = 1;//检测结点的右结点是否在已遍历标记表中 } if(temp->leftchild!=NULL && node_lchild_is_read == 0) //如果结点有左结点而且在没在表中登记为已遍历,则入栈,对应递归语句第一条 { fake_stack[j++] = temp->leftchild; llist[k++] = temp->leftchild; temp = temp->leftchild; } else if(temp->rightchild!=NULL && node_rchild_is_read == 0) //否则,如果结点有右结点而且在没在表中登记为已遍历,则入栈,对应递归语句第二条 { fake_stack[j++] = temp->rightchild; llist[k++] = temp->rightchild; temp = temp->rightchild; } else //否则,左右结点都没有,则结点显示,把自己出栈,把遍历结点从自己的上一个开始——即再出栈一次被赋值给遍历结点, //对应递归的第三句——函数栈的本层结束,返回上层 { print_node_val(temp); fake_stack[--j]; temp = fake_stack[--j]; fake_stack[j++] = temp; if(temp==head) head_show_times++; if(head_show_times==2) ReadFinished = 1; //如果头结点在这里出现了2次,则代表头结点左右子树都完成遍历,结束循环 } node_lchild_is_read=0; //设置为刚开始的状态,准备下次遍历 node_rchild_is_read=0; } print_node_val(temp); //最后显示头结点 /*对比起递归,等效的循环需要把递归自身的特性手动实现,例如递归第一句结束后,会自动执行第二句。但是循环如果不设置 标志和搭配的判断句,将会引起出栈回到上一层数据点后又把已经遍历过的再遍历一次。所以要通过标记表把已经处理过的数据跳开。 简单来说:若自己要实现等效于递归函数栈的迭代,并且每层栈有多层语句,则自己要手动实现返回上层函数栈后跳过已执行语句这种现象*/ } int main() { treenode *head = create_tree(); read(head); return 0; }
由于标记表和栈是用有限数组写成,因此若树的规模很大可能会出问题,因此链表和链栈版本如下:
四、链表和链栈版本:
链表工具类(文件名CJZLinkList.c):
#define new_node (Linklist*)malloc(sizeof(Linklist)) typedef struct node { void *content; struct node *prev,*next; struct node *head,*last; int size; void* (*get)(struct node *head,int Count); void (*put)(struct node *head,void *content); void (*remove)(struct node *head,int Count); void (*removeall)(struct node *head); void (*clearalldata)(struct node *head); void (*insert)(struct node *head,int Count,void *Content); int (*getSize)(struct node *head); }Linklist; void* Linklist_get(Linklist *head,int Count) { int i; Linklist *temp = head; for(i=0;i<=Count;i++) { if(temp->next!=NULL) temp = temp->next; else return; } return temp->content; } void Linklist_put(Linklist *head,void *content) { Linklist *last = head->last; last->next = new_node; last->next->prev = last; last = last->next; last->content = content; last->get = Linklist_get; last->put = Linklist_put; last->next = head; head->last = last; head->prev = last; head->size++; } int getSize(Linklist *head) { return head->size; } void Linklist_insert(Linklist *head,int Count,void *Content) { int i; Linklist *break_node,*new_llnode,*temp = head; for(i=0;i<=Count-1;i++) { if(temp->next!=NULL) temp = temp->next; else return; } break_node = temp; new_llnode = new_node; new_llnode->content = Content; new_llnode->next = break_node->next; new_llnode->prev = break_node; break_node->next->prev = new_llnode; break_node->next = new_llnode; head->size++; } void Linklist_removeall(Linklist *head) //消灭整个链表,此时链表在内存中已经整个不存在,任何操作均已非法 { int i; Linklist *temp=head->next; for(i = head->size;i>0;i--) { //printf("delete node address %d\n",head); free(head); head = temp; temp = temp->next; } } void Linklist_clearalldata(Linklist *head) { Linklist *temp=head->last,*temp_prev; while( { temp_prev = temp->prev; //printf("delete node address %d\n",temp); free(temp); temp = temp_prev; } //printf("End\n"); //free(head->content); head->last = head; head->next = head; head->prev = head; head->head = head; head->size = 0; } void Linklist_remove(Linklist *head,int Count) { int i; Linklist *remove_node,*temp = head; for(i=0;i<=Count-1;i++) { if(temp->next!=NULL) temp = temp->next; else return; } if(temp == head->last) { head->last = head->last->prev; head->last->next = head; head->prev = head->last; } if(temp == head) //如果删除的是链表头,要把之前的链表头的下一个结点变成链表头,要初始化一下这个结点 { remove_node = head; head = head->next; head->prev = remove_node->prev; head->get = Linklist_get; head->put = Linklist_put; head->getSize = getSize; head->insert = Linklist_insert; head->remove = Linklist_remove; head->removeall = Linklist_removeall; head->clearalldata = Linklist_clearalldata; head->head = head; head->last = remove_node->last; head->size = remove_node->size; } remove_node = temp; temp->prev->next = temp->next; temp->next->prev = temp->prev; free(remove_node); head->size--; } Linklist* createLinklist() { Linklist *node = new_node; node->get = Linklist_get; node->put = Linklist_put; node->getSize = getSize; node->insert = Linklist_insert; node->remove = Linklist_remove; node->removeall = Linklist_removeall; node->clearalldata = Linklist_clearalldata; node->head = node; node->last = node; node->prev = node->next = NULL; node->size = 0; return node; }
栈工具类(文件名CJZStack.c):
#define new_stack_node (Stack*)malloc(sizeof(Stack)) typedef struct stack_node { void *val; struct stack_node *next,*prev,*top; void (*push)(struct stack_node *start_node,int val); void* (*pop)(struct stack_node *start_node,void (*fun)()); int (*IsEmpty)(struct stack_node *head); }Stack; int Stack_isempty(Stack *head) { if(head->next==NULL) return 1; else return 0; } void* Stack_pop(Stack *start_node,void (*fun)()) { Stack *temp = start_node->top; void *content; if(fun!=NULL) fun(temp->prev->val); if(temp->prev!=NULL && temp->prev->val!=NULL) content = temp->prev->val; start_node->top = temp->prev; free(temp->next); free(temp); start_node->top->next = NULL; start_node->top->IsEmpty = Stack_isempty; return content; } void Stack_push(Stack *start_node,void *val) { Stack *temp = start_node->top; temp->val = val; temp->push = Stack_push; temp->pop = Stack_pop; temp->IsEmpty = Stack_isempty; temp->next = new_stack_node; temp->next->prev = temp; temp->next->next = NULL; start_node->top = temp->next; } Stack* createStack() { Stack *stack = new_stack_node; stack->val = NULL; stack->next = NULL; stack->top = stack; stack->push = Stack_push; stack->pop = Stack_pop; stack->IsEmpty = Stack_isempty; return stack; }
主程序:
#include "stdio.h" #include "stdlib.h" #include "CJZLinkList.c" #include "CJZStack.c" #define new_tree_node (treenode*)malloc(sizeof(treenode)) typedef struct tnode { int val; struct tnode *leftchild,*rightchild; }treenode; treenode* create_tree() { treenode *head = new_tree_node; head->val = 1; head->leftchild = new_tree_node; head->leftchild->val = 2; head->rightchild = new_tree_node; head->rightchild->val = 3; head->leftchild->leftchild = new_tree_node; head->leftchild->leftchild->val = 4; head->leftchild->leftchild->leftchild = new_tree_node; head->leftchild->leftchild->leftchild->val = 8; head->leftchild->leftchild->leftchild->leftchild = NULL; head->leftchild->leftchild->leftchild->rightchild = NULL; head->leftchild->leftchild->rightchild = NULL; head->leftchild->rightchild = new_tree_node; head->leftchild->rightchild->val = 5; head->leftchild->rightchild->leftchild = NULL; head->leftchild->rightchild->rightchild = NULL; head->rightchild->leftchild = new_tree_node; head->rightchild->leftchild->val = 6; head->rightchild->leftchild->leftchild = NULL; head->rightchild->leftchild->rightchild = NULL; head->rightchild->rightchild = new_tree_node; head->rightchild->rightchild->val = 7; head->rightchild->rightchild->leftchild = NULL; head->rightchild->rightchild->rightchild = NULL; return head; } void print_node_val(treenode *t) { printf("%d\n",t->val); } void read(treenode *head,Linklist *llist,Stack *stack) { int ReadFinished = 0; int i/*,j=0*/; int node_lchild_is_read=0,node_rchild_is_read=0; int head_show_times = 0; //treenode *fake_stack[100]; treenode *temp = head; stack->push(stack,temp); //fake_stack[j++] = temp; llist->put(llist,temp);//遍历标记 while(ReadFinished == 0) { for(i=0;i<=llist->getSize(llist);i++) { if(llist->get(llist,i)==temp->leftchild) node_lchild_is_read = 1;//检测结点的左结点是否在已遍历标记表中 if(llist->get(llist,i)==temp->rightchild) node_rchild_is_read = 1;//检测结点的右结点是否在已遍历标记表中 } if(temp->leftchild!=NULL && node_lchild_is_read == 0) //如果结点有左结点而且在没在表中登记为已遍历,则入栈,对应递归语句第一条 { stack->push(stack,temp->leftchild); //fake_stack[j++] = temp->leftchild; llist->put(llist,temp->leftchild); temp = temp->leftchild; } else if(temp->rightchild!=NULL && node_rchild_is_read == 0) //否则,如果结点有右结点而且在没在表中登记为已遍历,则入栈,对应递归语句第二条 { stack->push(stack,temp->rightchild); //fake_stack[j++] = temp->rightchild; llist->put(llist,temp->rightchild); temp = temp->rightchild; } else //否则,左右结点都没有,则结点显示,把自己出栈,把遍历结点从自己的上一个开始——即再出栈一次被赋值给遍历结点, //对应递归的第三句——函数栈的本层结束,返回上层 { print_node_val(temp); stack->pop(stack,NULL); //fake_stack[--j]; temp = stack->pop(stack,NULL); //temp = fake_stack[--j]; stack->push(stack,temp); //fake_stack[j++]; //没有这句就会出事,用循环写自相似结构的东西,调试起来挺麻烦的 //printf("node is %d\n",temp->val); if(temp==head) head_show_times++; //printf("head_show_times %d\n",head_show_times); if(head_show_times==2) ReadFinished = 1; //如果头结点在这里出现了2次,则代表头结点左右子树都完成遍历,结束循环 } node_lchild_is_read=0; //设置为刚开始的状态,准备下次遍历 node_rchild_is_read=0; } print_node_val(temp); //最后显示头结点 /*对比起递归,等效的循环需要把递归自身的特性手动实现,例如递归第一句结束后,会自动执行第二句。但是循环如果不设置 标志和搭配的判断句,将会引起出栈回到上一层数据点后又把已经遍历过的再遍历一次。所以要通过标记表把已经处理过的数据跳开。 简单来说:若自己要实现等效于递归函数栈的迭代,并且每层栈有多层语句,则自己要手动实现返回上层函数栈后跳过已执行语句这种现象*/ } int main() { treenode *head = create_tree(); Linklist *llist = createLinklist(); //已遍历过的表 Stack *stack = createStack(); //模拟函数栈,实际只放数据 read(head,llist,stack); return 0; }