二叉树基本操作续二:前序、中序、后序遍历(非递归 迭代方式)
这里给出二叉树三种遍历方式的迭代实现代码。二叉树的递归实现使用系统栈入栈出栈,而非递归的迭代实现方法就是手动维护一个栈,来模拟递归的入栈出栈过程。
本文没有给出用户栈的代码,如果需要结合上篇的测试代码一起测试,则需要自己实现自己的栈,以及基本的pop、push等栈操作函数。
前序迭代遍历:
1 void iter_preorder(tree_pointer ptr) 2 { 3 //前序遍历:先遍历根节点,然后再分别遍历左右子树 4 int top = -1; 5 tree_pointer stack[MAXSIZE]; 6 for (;;) { 7 //从根节点开始,往左下方走,先输出根节点,然后把右儿子(即右子树的根节点)入栈 8 //对于任意节点N,当N输出后,它的左儿子也跟着输出了 9 //所以,我们只需要关心它的右儿子,右儿子这时已全部入栈 10 for (; ptr; ptr = ptr->left_child) { 11 printf("\t%d", ptr->data); 12 if (ptr->right_child) { 13 push(&top, ptr->right_child); 14 } 15 } 16 //这时N本身及其左儿子已经遍历过,于是从栈中取出右儿子,开始遍历 17 ptr = pop(&top); 18 if (!ptr) break; //栈pop出NULL时,表示已经遍历完成,跳出循环 19 } 20 }
中序迭代遍历:
1 void iter_inorder(tree_pointer ptr) 2 { 3 //中序遍历:先遍历根节点的左子树,然后根节点,然后右子树 4 int top = -1; 5 tree_pointer stack[MAXSIZE]; 6 for (;;) { 7 //从根节点开始,往左下方走,节点本身还有左儿子全部入栈 8 //对于任一节点Node,Node入栈后,它的左儿子必然入栈 9 for (; ptr; ptr = ptr->left_child) { 10 push(&top, ptr); //入栈 11 } 12 //取栈顶元素,根据上面的注释可知,栈顶节点要么没有左儿子,要么左儿子已经遍历过,所以就可以遍历这个节点 13 //如果当前指针不为空,就输出当前节点,然后把ptr指向当前节点的右儿子,继续循环以遍历以当前节点的右儿子为根节点-- 14 //的二叉树。 15 //所以,当栈为空的时候,就表示遍历完成 16 ptr = pop(&top); //出栈 17 if (!ptr) break; //栈空说明遍历完成 18 printf("\t%d", ptr->data); 19 ptr = ptr->right_child; 20 } 21 }
前序遍历和中序遍历比较简单,就像开始说的,代码中自己维护一个栈,模拟递归调用的入栈出栈过程。后序遍历相对要复杂一些,因为后序遍历是先遍历左右子树,最后再处理根节点。所以就需要保存一些额外的信息来记录当前节点的左右子树是否已经遍历完成。后序遍历的代码实现稍微纠结了一段时间,参考了网上一些实现,终于明白了。特别参考了这篇文章,在此向作者致谢。 由于比较偷懒,所以代码写的稍微简洁了点,如果看注释不太明白,可以看我连接的文章。
后序迭代遍历:
1 void iter_postorder(tree_pointer ptr) 2 { 3 //后序遍历:先遍历左右子树,然后输出根节点 4 //后序遍历最为复杂,需要记录当前节点的右子树是否遍历过,当右子树也遍历过之后,才能输出当前节点 5 //因此,使用了一个和栈一样大小的数组作为对应栈的元素的右子树是否遍历过的标志,0 没有,1 表示遍历过 6 int top = -1; 7 tree_pointer stack[MAXSIZE]; 8 int array_rvisited[MAXSIZE] = {}; 9 10 for (;;) { 11 //从根节点开始,往左下走,把遇到的所有节点都入栈,同时设置数组对应位置的标志为0 12 //同中序遍历一样,对于任意节点N,其入栈后,左儿子也跟着入栈 13 for (; ptr; ptr = ptr->left_child) { 14 push(&top, ptr); 15 array_rvisited[top] = 0; 16 } 17 //取栈顶元素(我在这里使用了pop,其实应该用get_top这样的函数,在if判断过了才真正pop 18 //我在这里没有实现栈的代码,默认用push,pop这两个函数表示这一操作,所以就不引入get_top函数了,而是在else中, 19 //又把pop出的元素push了回去) 20 ptr = pop(&top); 21 if (!ptr) break; //栈pop为NULL时,说明已经遍历完成,就跳出循环 22 //判断取出的元素,根据上面for循环的注释,可知当前节点的左儿子已经遍历完成 23 //如果当前元素没有右子树,或者右子树已经遍历过,就输出当前节点 24 if (!ptr->right_child || array_rvisited[top_rvisited]) { 25 printf("\t%d", ptr->data); 26 //这里说面当前节点及其左右子树都已经遍历完成,应该继续从栈里取元素处理 27 //所以把ptr设置为NULL,让内层for不再运行。从而到pop的地方 28 ptr = NULL; 29 } else { 30 //如果当前节点有右子树,并且右子树还没有遍历过,则把当前节点从新push回去,把数组对应位置设置为1 31 //然后开始处理右子树,当右子树处理完成了。下次当前节点再出栈,if判断就会通过(标志为1),那时就输出当前节点 32 push(&top, ptr); 33 array_rvisited[top] = 1; 34 ptr = ptr->right_child; 35 } 36 } 37 }
PS:这三个函数,因为太懒,不想实现栈的pop和push,我都没有加到前一篇文章的代码中测试,只是进行了纸笔推算,原理应该没错。所以仅供参考。